From: Tiago Melo Date: Mon, 13 Aug 2018 15:36:32 +0000 (+0100) Subject: mgr/dashboard: Extract/Refactor Task merge X-Git-Tag: v14.0.1~443^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=2614805706a28df3bf0753b392f3eebf73aa442a;p=ceph.git mgr/dashboard: Extract/Refactor Task merge Signed-off-by: Stephan Müller Signed-off-by: Tiago Melo --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html index 636be3ae7d702..c0c5463fe6450 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html @@ -130,7 +130,6 @@ heading="Snapshots"> + [rbdName]="selectedItem.name"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts index 33a3a2d6a6e66..24b28a9dfcaa6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts @@ -10,31 +10,30 @@ import { TabsModule, TooltipModule } from 'ngx-bootstrap'; +import { BehaviorSubject, of } from 'rxjs'; import { configureTestBed } from '../../../../testing/unit-test-helper'; +import { RbdService } from '../../../shared/api/rbd.service'; import { ComponentsModule } from '../../../shared/components/components.module'; import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum'; +import { ExecutingTask } from '../../../shared/models/executing-task'; import { SummaryService } from '../../../shared/services/summary.service'; +import { TaskListService } from '../../../shared/services/task-list.service'; import { SharedModule } from '../../../shared/shared.module'; import { RbdDetailsComponent } from '../rbd-details/rbd-details.component'; import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component'; import { RbdListComponent } from './rbd-list.component'; +import { RbdModel } from './rbd-model'; describe('RbdListComponent', () => { - let component: RbdListComponent; let fixture: ComponentFixture; + let component: RbdListComponent; + let summaryService: SummaryService; + let rbdService: RbdService; - class SummaryServiceMock extends SummaryService { - data: any; - - raiseError() { - this.summaryDataSource.error(undefined); - } - - refresh() { - this.summaryDataSource.next(this.data); - } - } + const refresh = (data) => { + summaryService['summaryDataSource'].next(data); + }; configureTestBed({ imports: [ @@ -50,12 +49,18 @@ describe('RbdListComponent', () => { HttpClientTestingModule ], declarations: [RbdListComponent, RbdDetailsComponent, RbdSnapshotListComponent], - providers: [{ provide: SummaryService, useClass: SummaryServiceMock }] + providers: [SummaryService, TaskListService, RbdService] }); beforeEach(() => { fixture = TestBed.createComponent(RbdListComponent); component = fixture.componentInstance; + summaryService = TestBed.get(SummaryService); + rbdService = TestBed.get(RbdService); + + // this is needed because summaryService isn't being reseted after each test. + summaryService['summaryDataSource'] = new BehaviorSubject(null); + summaryService['summaryData$'] = summaryService['summaryDataSource'].asObservable(); }); it('should create', () => { @@ -63,32 +68,111 @@ describe('RbdListComponent', () => { }); describe('after ngOnInit', () => { - let summaryService: SummaryServiceMock; - beforeEach(() => { - summaryService = TestBed.get(SummaryService); - summaryService.data = undefined; fixture.detectChanges(); + spyOn(rbdService, 'list').and.callThrough(); }); it('should load images on init', () => { - spyOn(component, 'loadImages'); - summaryService.data = {}; - summaryService.refresh(); - expect(component.loadImages).toHaveBeenCalled(); + refresh({}); + expect(rbdService.list).toHaveBeenCalled(); }); it('should not load images on init because no data', () => { - spyOn(component, 'loadImages'); - summaryService.refresh(); - expect(component.loadImages).not.toHaveBeenCalled(); + refresh(undefined); + expect(rbdService.list).not.toHaveBeenCalled(); }); it('should call error function on init when summary service fails', () => { spyOn(component.table, 'reset'); - summaryService.raiseError(); + summaryService['summaryDataSource'].error(undefined); expect(component.table.reset).toHaveBeenCalled(); expect(component.viewCacheStatusList).toEqual([{ status: ViewCacheStatus.ValueException }]); }); }); + + describe('handling of executing tasks', () => { + let images: RbdModel[]; + + const addImage = (name) => { + const model = new RbdModel(); + model.id = '-1'; + model.name = name; + model.pool_name = 'rbd'; + images.push(model); + }; + + const addTask = (name: string, image_name: string) => { + const task = new ExecutingTask(); + task.name = name; + task.metadata = { + pool_name: 'rbd', + image_name: image_name, + child_pool_name: 'rbd', + child_image_name: 'd', + dest_pool_name: 'rbd', + dest_image_name: 'd' + }; + summaryService.addRunningTask(task); + }; + + const expectImageTasks = (image: RbdModel, executing: string) => { + expect(image.cdExecuting).toEqual(executing); + }; + + beforeEach(() => { + images = []; + addImage('a'); + addImage('b'); + addImage('c'); + component.images = images; + refresh({ executing_tasks: [], finished_tasks: [] }); + spyOn(rbdService, 'list').and.callFake(() => + of([{ poool_name: 'rbd', status: 1, value: images }]) + ); + fixture.detectChanges(); + }); + + it('should gets all images without tasks', () => { + expect(component.images.length).toBe(3); + expect(component.images.every((image) => !image.cdExecuting)).toBeTruthy(); + }); + + it('should add a new image from a task', () => { + addTask('rbd/create', 'd'); + expect(component.images.length).toBe(4); + expectImageTasks(component.images[0], undefined); + expectImageTasks(component.images[1], undefined); + expectImageTasks(component.images[2], undefined); + expectImageTasks(component.images[3], 'Creating'); + }); + + it('should show when a image is being cloned', () => { + addTask('rbd/clone', 'd'); + expect(component.images.length).toBe(4); + expectImageTasks(component.images[0], undefined); + expectImageTasks(component.images[1], undefined); + expectImageTasks(component.images[2], undefined); + expectImageTasks(component.images[3], 'Cloning'); + }); + + it('should show when a image is being copied', () => { + addTask('rbd/copy', 'd'); + expect(component.images.length).toBe(4); + expectImageTasks(component.images[0], undefined); + expectImageTasks(component.images[1], undefined); + expectImageTasks(component.images[2], undefined); + expectImageTasks(component.images[3], 'Copying'); + }); + + it('should show when an existing image is being modified', () => { + addTask('rbd/edit', 'a'); + addTask('rbd/delete', 'b'); + addTask('rbd/flatten', 'c'); + expect(component.images.length).toBe(3); + expectImageTasks(component.images[0], 'Updating'); + expectImageTasks(component.images[1], 'Deleting'); + expectImageTasks(component.images[2], 'Flattening'); + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts index e93f5f6402096..1c14a8b6ebdba 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts @@ -1,8 +1,7 @@ -import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import * as _ from 'lodash'; import { BsModalRef, BsModalService } from 'ngx-bootstrap'; -import { Subscription } from 'rxjs'; import { RbdService } from '../../../shared/api/rbd.service'; import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component'; @@ -12,13 +11,12 @@ import { CellTemplate } from '../../../shared/enum/cell-template.enum'; import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum'; import { CdTableColumn } from '../../../shared/models/cd-table-column'; import { CdTableSelection } from '../../../shared/models/cd-table-selection'; -import { ExecutingTask } from '../../../shared/models/executing-task'; import { FinishedTask } from '../../../shared/models/finished-task'; import { Permission } from '../../../shared/models/permissions'; import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; import { DimlessPipe } from '../../../shared/pipes/dimless.pipe'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; -import { SummaryService } from '../../../shared/services/summary.service'; +import { TaskListService } from '../../../shared/services/task-list.service'; import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; import { RbdParentModel } from '../rbd-form/rbd-parent.model'; import { RbdModel } from './rbd-model'; @@ -26,9 +24,10 @@ import { RbdModel } from './rbd-model'; @Component({ selector: 'cd-rbd-list', templateUrl: './rbd-list.component.html', - styleUrls: ['./rbd-list.component.scss'] + styleUrls: ['./rbd-list.component.scss'], + providers: [TaskListService] }) -export class RbdListComponent implements OnInit, OnDestroy { +export class RbdListComponent implements OnInit { @ViewChild(TableComponent) table: TableComponent; @ViewChild('usageTpl') @@ -47,18 +46,33 @@ export class RbdListComponent implements OnInit, OnDestroy { viewCacheStatusList: any[]; selection = new CdTableSelection(); - summaryDataSubscription: Subscription; - modalRef: BsModalRef; + builders = { + 'rbd/create': (metadata) => + this.createRbdFromTask(metadata['pool_name'], metadata['image_name']), + 'rbd/clone': (metadata) => + this.createRbdFromTask(metadata['child_pool_name'], metadata['child_image_name']), + 'rbd/copy': (metadata) => + this.createRbdFromTask(metadata['dest_pool_name'], metadata['dest_image_name']) + }; + + private createRbdFromTask(pool: string, name: string): RbdModel { + const model = new RbdModel(); + model.id = '-1'; + model.name = name; + model.pool_name = pool; + return model; + } + constructor( private authStorageService: AuthStorageService, private rbdService: RbdService, private dimlessBinaryPipe: DimlessBinaryPipe, private dimlessPipe: DimlessPipe, - private summaryService: SummaryService, private modalService: BsModalService, - private taskWrapper: TaskWrapperService + private taskWrapper: TaskWrapperService, + private taskListService: TaskListService ) { this.permission = this.authStorageService.getPermissions().rbdImage; } @@ -119,132 +133,62 @@ export class RbdListComponent implements OnInit, OnDestroy { } ]; - this.summaryDataSubscription = this.summaryService.subscribe( - (data: any) => { - if (data) { - this.loadImages(data.executing_tasks); - } - }, - () => { - this.table.reset(); // Disable loading indicator. - this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }]; - } + this.taskListService.init( + () => this.rbdService.list(), + (resp) => this.prepareResponse(resp), + (images) => (this.images = images), + () => this.onFetchError(), + this.taskFilter, + this.itemFilter, + this.builders ); } - ngOnDestroy() { - if (this.summaryDataSubscription) { - this.summaryDataSubscription.unsubscribe(); - } + onFetchError() { + this.table.reset(); // Disable loading indicator. + this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }]; } - loadImages(executingTasks) { - this.rbdService.list().subscribe( - (resp: any[]) => { - let images = []; - const viewCacheStatusMap = {}; - resp.forEach((pool) => { - if (_.isUndefined(viewCacheStatusMap[pool.status])) { - viewCacheStatusMap[pool.status] = []; - } - viewCacheStatusMap[pool.status].push(pool.pool_name); - images = images.concat(pool.value); - }); - const viewCacheStatusList = []; - _.forEach(viewCacheStatusMap, (value: any, key) => { - viewCacheStatusList.push({ - status: parseInt(key, 10), - statusFor: - (value.length > 1 ? 'pools ' : 'pool ') + - '' + - value.join(', ') + - '' - }); - }); - this.viewCacheStatusList = viewCacheStatusList; - images.forEach((image) => { - image.executingTasks = this._getExecutingTasks( - executingTasks, - image.pool_name, - image.name - ); - }); - this.images = this.merge(images, executingTasks); - }, - () => { - this.table.reset(); // Disable loading indicator. - this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }]; - } - ); - } - - _getExecutingTasks(executingTasks: ExecutingTask[], poolName, imageName): ExecutingTask[] { - const result: ExecutingTask[] = []; - executingTasks.forEach((executingTask) => { - if ( - executingTask.name === 'rbd/snap/create' || - executingTask.name === 'rbd/snap/delete' || - executingTask.name === 'rbd/snap/edit' || - executingTask.name === 'rbd/snap/rollback' - ) { - if ( - poolName === executingTask.metadata['pool_name'] && - imageName === executingTask.metadata['image_name'] - ) { - result.push(executingTask); - } + prepareResponse(resp: any[]): any[] { + let images = []; + const viewCacheStatusMap = {}; + resp.forEach((pool) => { + if (_.isUndefined(viewCacheStatusMap[pool.status])) { + viewCacheStatusMap[pool.status] = []; } + viewCacheStatusMap[pool.status].push(pool.pool_name); + images = images.concat(pool.value); }); - return result; - } - - private merge(rbds: RbdModel[], executingTasks: ExecutingTask[] = []) { - const resultRBDs = _.clone(rbds); - executingTasks.forEach((executingTask) => { - const rbdExecuting = resultRBDs.find((rbd) => { - return ( - rbd.pool_name === executingTask.metadata['pool_name'] && - rbd.name === executingTask.metadata['image_name'] - ); + const viewCacheStatusList = []; + _.forEach(viewCacheStatusMap, (value: any, key) => { + viewCacheStatusList.push({ + status: parseInt(key, 10), + statusFor: + (value.length > 1 ? 'pools ' : 'pool ') + + '' + + value.join(', ') + + '' }); - if (rbdExecuting) { - if (executingTask.name === 'rbd/delete') { - rbdExecuting.cdExecuting = 'deleting'; - } else if (executingTask.name === 'rbd/edit') { - rbdExecuting.cdExecuting = 'updating'; - } else if (executingTask.name === 'rbd/flatten') { - rbdExecuting.cdExecuting = 'flattening'; - } - } else if (executingTask.name === 'rbd/create') { - const rbdModel = new RbdModel(); - rbdModel.name = executingTask.metadata['image_name']; - rbdModel.pool_name = executingTask.metadata['pool_name']; - rbdModel.cdExecuting = 'creating'; - this.pushIfNotExists(resultRBDs, rbdModel); - } else if (executingTask.name === 'rbd/clone') { - const rbdModel = new RbdModel(); - rbdModel.name = executingTask.metadata['child_image_name']; - rbdModel.pool_name = executingTask.metadata['child_pool_name']; - rbdModel.cdExecuting = 'cloning'; - this.pushIfNotExists(resultRBDs, rbdModel); - } else if (executingTask.name === 'rbd/copy') { - const rbdModel = new RbdModel(); - rbdModel.name = executingTask.metadata['dest_image_name']; - rbdModel.pool_name = executingTask.metadata['dest_pool_name']; - rbdModel.cdExecuting = 'copying'; - this.pushIfNotExists(resultRBDs, rbdModel); - } }); - return resultRBDs; + this.viewCacheStatusList = viewCacheStatusList; + return images; } - private pushIfNotExists(resultRBDs: RbdModel[], rbdModel: RbdModel) { - const exists = resultRBDs.some((resultRBD) => { - return resultRBD.name === rbdModel.name && resultRBD.pool_name === rbdModel.pool_name; - }); - if (!exists) { - resultRBDs.push(rbdModel); - } + itemFilter(entry, task) { + return ( + entry.pool_name === task.metadata['pool_name'] && entry.name === task.metadata['image_name'] + ); + } + + taskFilter(task) { + return [ + 'rbd/clone', + 'rbd/copy', + 'rbd/create', + 'rbd/delete', + 'rbd/edit', + 'rbd/flatten' + ].includes(task.name); } updateSelection(selection: CdTableSelection) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts index 8be8dafdb1a51..92a77bdd6eaa4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts @@ -1,4 +1,5 @@ export class RbdModel { + id: string; name: string; pool_name: string; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts index 09420cca7161e..a2afd1e58f18d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts @@ -11,16 +11,21 @@ import { ApiModule } from '../../../shared/api/api.module'; import { RbdService } from '../../../shared/api/rbd.service'; import { ComponentsModule } from '../../../shared/components/components.module'; import { DataTableModule } from '../../../shared/datatable/datatable.module'; +import { ExecutingTask } from '../../../shared/models/executing-task'; import { Permissions } from '../../../shared/models/permissions'; import { PipesModule } from '../../../shared/pipes/pipes.module'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; import { NotificationService } from '../../../shared/services/notification.service'; import { ServicesModule } from '../../../shared/services/services.module'; +import { SummaryService } from '../../../shared/services/summary.service'; +import { TaskListService } from '../../../shared/services/task-list.service'; import { RbdSnapshotListComponent } from './rbd-snapshot-list.component'; +import { RbdSnapshotModel } from './rbd-snapshot.model'; describe('RbdSnapshotListComponent', () => { let component: RbdSnapshotListComponent; let fixture: ComponentFixture; + let summaryService: SummaryService; const fakeAuthStorageService = { isLoggedIn: () => { @@ -44,12 +49,17 @@ describe('RbdSnapshotListComponent', () => { RouterTestingModule, PipesModule ], - providers: [{ provide: AuthStorageService, useValue: fakeAuthStorageService }] + providers: [ + { provide: AuthStorageService, useValue: fakeAuthStorageService }, + SummaryService, + TaskListService + ] }); beforeEach(() => { fixture = TestBed.createComponent(RbdSnapshotListComponent); component = fixture.componentInstance; + summaryService = TestBed.get(SummaryService); fixture.detectChanges(); }); @@ -76,7 +86,9 @@ describe('RbdSnapshotListComponent', () => { null, rbdService, null, - notificationService + notificationService, + null, + null ); spyOn(rbdService, 'deleteSnapshot').and.returnValue(observableThrowError({ status: 500 })); spyOn(notificationService, 'notifyTask').and.stub(); @@ -93,4 +105,71 @@ describe('RbdSnapshotListComponent', () => { expect(called).toBe(true); })); }); + + describe('handling of executing tasks', () => { + let snapshots: RbdSnapshotModel[]; + + const addSnapshot = (name) => { + const model = new RbdSnapshotModel(); + model.id = 1; + model.name = name; + snapshots.push(model); + }; + + const addTask = (task_name: string, snapshot_name: string) => { + const task = new ExecutingTask(); + task.name = task_name; + task.metadata = { + pool_name: 'rbd', + image_name: 'foo', + snapshot_name: snapshot_name + }; + summaryService.addRunningTask(task); + }; + + const expectImageTasks = (snapshot: RbdSnapshotModel, executing: string) => { + expect(snapshot.cdExecuting).toEqual(executing); + }; + + const refresh = (data) => { + summaryService['summaryDataSource'].next(data); + }; + + beforeEach(() => { + snapshots = []; + addSnapshot('a'); + addSnapshot('b'); + addSnapshot('c'); + component.snapshots = snapshots; + component.poolName = 'rbd'; + component.rbdName = 'foo'; + refresh({ executing_tasks: [], finished_tasks: [] }); + component.ngOnChanges(); + fixture.detectChanges(); + }); + + it('should gets all snapshots without tasks', () => { + expect(component.snapshots.length).toBe(3); + expect(component.snapshots.every((image) => !image.cdExecuting)).toBeTruthy(); + }); + + it('should add a new image from a task', () => { + addTask('rbd/snap/create', 'd'); + expect(component.snapshots.length).toBe(4); + expectImageTasks(component.snapshots[0], undefined); + expectImageTasks(component.snapshots[1], undefined); + expectImageTasks(component.snapshots[2], undefined); + expectImageTasks(component.snapshots[3], 'Creating'); + }); + + it('should show when an existing image is being modified', () => { + addTask('rbd/snap/edit', 'a'); + addTask('rbd/snap/delete', 'b'); + addTask('rbd/snap/rollback', 'c'); + expect(component.snapshots.length).toBe(3); + expectImageTasks(component.snapshots[0], 'Updating'); + expectImageTasks(component.snapshots[1], 'Deleting'); + expectImageTasks(component.snapshots[2], 'Rolling back'); + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts index af27a265b88aa..14b1168bae735 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts @@ -2,6 +2,7 @@ import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@an import * as _ from 'lodash'; import { BsModalRef, BsModalService } from 'ngx-bootstrap'; +import { of } from 'rxjs'; import { RbdService } from '../../../shared/api/rbd.service'; import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component'; @@ -16,6 +17,8 @@ import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe'; import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; import { NotificationService } from '../../../shared/services/notification.service'; +import { SummaryService } from '../../../shared/services/summary.service'; +import { TaskListService } from '../../../shared/services/task-list.service'; import { TaskManagerService } from '../../../shared/services/task-manager.service'; import { RbdSnapshotFormComponent } from '../rbd-snapshot-form/rbd-snapshot-form.component'; import { RbdSnapshotModel } from './rbd-snapshot.model'; @@ -23,7 +26,8 @@ import { RbdSnapshotModel } from './rbd-snapshot.model'; @Component({ selector: 'cd-rbd-snapshot-list', templateUrl: './rbd-snapshot-list.component.html', - styleUrls: ['./rbd-snapshot-list.component.scss'] + styleUrls: ['./rbd-snapshot-list.component.scss'], + providers: [TaskListService] }) export class RbdSnapshotListComponent implements OnInit, OnChanges { @Input() @@ -32,9 +36,6 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { poolName: string; @Input() rbdName: string; - @Input() - executingTasks: ExecutingTask[] = []; - @ViewChild('nameTpl') nameTpl: TemplateRef; @ViewChild('protectTpl') @@ -52,6 +53,14 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { selection = new CdTableSelection(); + builders = { + 'rbd/snap/create': (metadata) => { + const model = new RbdSnapshotModel(); + model.name = metadata['snapshot_name']; + return model; + } + }; + constructor( private authStorageService: AuthStorageService, private modalService: BsModalService, @@ -59,7 +68,9 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { private cdDatePipe: CdDatePipe, private rbdService: RbdService, private taskManagerService: TaskManagerService, - private notificationService: NotificationService + private notificationService: NotificationService, + private summaryService: SummaryService, + private taskListService: TaskListService ) { this.permission = this.authStorageService.getPermissions().rbdImage; } @@ -103,40 +114,29 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { } ngOnChanges() { - this.data = this.merge(this.snapshots, this.executingTasks); - } + const itemFilter = (entry, task) => { + return entry.name === task.metadata['snapshot_name']; + }; - private merge(snapshots: RbdSnapshotModel[], executingTasks: ExecutingTask[] = []) { - const resultSnapshots = _.clone(snapshots); - executingTasks.forEach((executingTask) => { - const snapshotExecuting = resultSnapshots.find((snapshot) => { - return snapshot.name === executingTask.metadata['snapshot_name']; - }); - if (snapshotExecuting) { - if (executingTask.name === 'rbd/snap/delete') { - snapshotExecuting.cdExecuting = 'deleting'; - } else if (executingTask.name === 'rbd/snap/edit') { - snapshotExecuting.cdExecuting = 'updating'; - } else if (executingTask.name === 'rbd/snap/rollback') { - snapshotExecuting.cdExecuting = 'rolling back'; - } - } else if (executingTask.name === 'rbd/snap/create') { - const rbdSnapshotModel = new RbdSnapshotModel(); - rbdSnapshotModel.name = executingTask.metadata['snapshot_name']; - rbdSnapshotModel.cdExecuting = 'creating'; - this.pushIfNotExists(resultSnapshots, rbdSnapshotModel); - } - }); - return resultSnapshots; - } + const taskFilter = (task) => { + return ( + ['rbd/snap/create', 'rbd/snap/delete', 'rbd/snap/edit', 'rbd/snap/rollback'].includes( + task.name + ) && + this.poolName === task.metadata['pool_name'] && + this.rbdName === task.metadata['image_name'] + ); + }; - private pushIfNotExists(resultSnapshots: RbdSnapshotModel[], rbdSnapshotModel: RbdSnapshotModel) { - const exists = resultSnapshots.some((resultSnapshot) => { - return resultSnapshot.name === rbdSnapshotModel.name; - }); - if (!exists) { - resultSnapshots.push(rbdSnapshotModel); - } + this.taskListService.init( + () => of(this.snapshots), + null, + (items) => (this.data = items), + () => (this.data = this.snapshots), + taskFilter, + itemFilter, + this.builders + ); } private openSnapshotModal(taskName: string, oldSnapshotName: string = null) { @@ -149,8 +149,12 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { this.modalRef.content.onSubmit.subscribe((snapshotName: string) => { const executingTask = new ExecutingTask(); executingTask.name = taskName; - executingTask.metadata = { snapshot_name: snapshotName }; - this.executingTasks.push(executingTask); + executingTask.metadata = { + image_name: this.rbdName, + pool_name: this.poolName, + snapshot_name: snapshotName + }; + this.summaryService.addRunningTask(executingTask); this.ngOnChanges(); }); } @@ -180,7 +184,7 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { const executingTask = new ExecutingTask(); executingTask.name = finishedTask.name; executingTask.metadata = finishedTask.metadata; - this.executingTasks.push(executingTask); + this.summaryService.addRunningTask(executingTask); this.ngOnChanges(); this.taskManagerService.subscribe( finishedTask.name, @@ -206,7 +210,7 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { const executingTask = new ExecutingTask(); executingTask.name = finishedTask.name; executingTask.metadata = finishedTask.metadata; - this.executingTasks.push(executingTask); + this.summaryService.addRunningTask(executingTask); this.modalRef.hide(); this.ngOnChanges(); this.taskManagerService.subscribe( diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.spec.ts new file mode 100644 index 0000000000000..fa7966e9b56fd --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.spec.ts @@ -0,0 +1,129 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { of } from 'rxjs'; + +import { configureTestBed } from '../../../testing/unit-test-helper'; +import { ExecutingTask } from '../models/executing-task'; +import { SummaryService } from './summary.service'; +import { TaskListService } from './task-list.service'; +import { TaskMessageService } from './task-message.service'; + +describe('TaskListService', () => { + let service: TaskListService; + let summaryService: SummaryService; + let taskMessageService: TaskMessageService; + + let reset: boolean; + let list: any[]; + let apiResp: any; + let tasks: any[]; + + const addItem = (name) => { + apiResp.push({ name: name }); + }; + + configureTestBed({ + providers: [TaskListService, TaskMessageService, SummaryService], + imports: [HttpClientTestingModule, RouterTestingModule] + }); + + beforeEach(() => { + service = TestBed.get(TaskListService); + summaryService = TestBed.get(SummaryService); + taskMessageService = TestBed.get(TaskMessageService); + summaryService['summaryDataSource'].next({ executing_tasks: [] }); + + taskMessageService.messages['test/create'] = taskMessageService.messages['rbd/create']; + taskMessageService.messages['test/edit'] = taskMessageService.messages['rbd/edit']; + taskMessageService.messages['test/delete'] = taskMessageService.messages['rbd/delete']; + + reset = false; + tasks = []; + apiResp = []; + list = []; + addItem('a'); + addItem('b'); + addItem('c'); + + service.init( + () => of(apiResp), + undefined, + (updatedList) => (list = updatedList), + () => (reset = true), + (task) => task.name.startsWith('test'), + (item, task) => item.name === task.metadata['name'], + { + default: (task) => ({ name: task.metadata['name'] }) + } + ); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + const addTask = (name: string, itemName: string) => { + const task = new ExecutingTask(); + task.name = name; + task.metadata = { name: itemName }; + tasks.push(task); + summaryService.addRunningTask(task); + }; + + const expectItemTasks = (item: any, executing: string) => { + expect(item.cdExecuting).toBe(executing); + }; + + it('gets all items without any executing items', () => { + expect(list.length).toBe(3); + expect(list.every((item) => !item.cdExecuting)).toBeTruthy(); + }); + + it('gets an item from a task during creation', () => { + addTask('test/create', 'd'); + expect(list.length).toBe(4); + expectItemTasks(list[3], 'Creating'); + }); + + it('gets all items with one executing items', () => { + addTask('test/create', 'a'); + expect(list.length).toBe(3); + expectItemTasks(list[0], 'Creating'); + expectItemTasks(list[1], undefined); + expectItemTasks(list[2], undefined); + }); + + it('gets all items with multiple executing items', () => { + addTask('test/create', 'a'); + addTask('test/edit', 'a'); + addTask('test/delete', 'a'); + addTask('test/edit', 'b'); + addTask('test/delete', 'b'); + addTask('test/delete', 'c'); + expect(list.length).toBe(3); + expectItemTasks(list[0], 'Creating, Updating, Deleting'); + expectItemTasks(list[1], 'Updating, Deleting'); + expectItemTasks(list[2], 'Deleting'); + }); + + it('gets all items with multiple executing tasks (not only item tasks', () => { + addTask('rbd/create', 'a'); + addTask('rbd/edit', 'a'); + addTask('test/delete', 'a'); + addTask('test/edit', 'b'); + addTask('rbd/delete', 'b'); + addTask('rbd/delete', 'c'); + expect(list.length).toBe(3); + expectItemTasks(list[0], 'Deleting'); + expectItemTasks(list[1], 'Updating'); + expectItemTasks(list[2], undefined); + }); + + it('should call ngOnDestroy', () => { + expect(service.summaryDataSubscription.closed).toBeFalsy(); + service.ngOnDestroy(); + expect(service.summaryDataSubscription.closed).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.ts new file mode 100644 index 0000000000000..6b9e355b042f4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.ts @@ -0,0 +1,101 @@ +import { Injectable, OnDestroy } from '@angular/core'; + +import { Observable, Subscription } from 'rxjs'; + +import { ExecutingTask } from '../models/executing-task'; +import { SummaryService } from './summary.service'; +import { TaskMessageService } from './task-message.service'; + +@Injectable() +export class TaskListService implements OnDestroy { + summaryDataSubscription: Subscription; + + getUpdate: () => Observable; + preProcessing: (_: any) => any[]; + setList: (_: any[]) => void; + onFetchError: (error: any) => void; + taskFilter: (task: ExecutingTask) => boolean; + itemFilter: (item, task: ExecutingTask) => boolean; + builders: object; + + constructor( + private taskMessageService: TaskMessageService, + private summaryService: SummaryService + ) {} + + /** + * @param {() => Observable} getUpdate Method that calls the api and + * returns that without subscribing. + * @param {(_: any) => any[]} preProcessing Method executed before merging + * Tasks with Items + * @param {(_: any[]) => void} setList Method used to update array of item in the component. + * @param {(error: any) => void} onFetchError Method called when there were + * problems while fetching data. + * @param {(task: ExecutingTask) => boolean} taskFilter callback used in tasks_array.filter() + * @param {(item, task: ExecutingTask) => boolean} itemFilter callback used in + * items_array.filter() + * @param {object} builders + * object with builders for each type of task. + * You can also use a 'default' one. + * @memberof TaskListService + */ + init( + getUpdate: () => Observable, + preProcessing: (_: any) => any[], + setList: (_: any[]) => void, + onFetchError: (error: any) => void, + taskFilter: (task: ExecutingTask) => boolean, + itemFilter: (item, task: ExecutingTask) => boolean, + builders: object + ) { + this.getUpdate = getUpdate; + this.preProcessing = preProcessing; + this.setList = setList; + this.onFetchError = onFetchError; + this.taskFilter = taskFilter; + this.itemFilter = itemFilter; + this.builders = builders; + + this.summaryDataSubscription = this.summaryService.subscribe((tasks: any) => { + if (tasks) { + this.getUpdate().subscribe((resp: any) => { + this.updateData(resp, tasks.executing_tasks.filter(this.taskFilter)); + }, this.onFetchError); + } + }, this.onFetchError); + } + + private updateData(resp: any, tasks: ExecutingTask[]) { + const data: any[] = this.preProcessing ? this.preProcessing(resp) : resp; + this.addMissing(data, tasks); + data.forEach((item) => { + const executingTasks = tasks.filter((task) => this.itemFilter(item, task)); + item.cdExecuting = this.getTaskAction(executingTasks); + }); + this.setList(data); + } + + private addMissing(data: any[], tasks: ExecutingTask[]) { + const defaultBuilder = this.builders['default']; + tasks.forEach((task) => { + const existing = data.find((item) => this.itemFilter(item, task)); + const builder = this.builders[task.name]; + if (!existing && (builder || defaultBuilder)) { + data.push(builder ? builder(task.metadata) : defaultBuilder(task)); + } + }); + } + + private getTaskAction(tasks: ExecutingTask[]): string { + if (tasks.length === 0) { + return; + } + return tasks.map((task) => this.taskMessageService.getRunningText(task)).join(', '); + } + + ngOnDestroy() { + if (this.summaryDataSubscription) { + this.summaryDataSubscription.unsubscribe(); + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 444f274cc78ce..cb4308da0b04c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -152,4 +152,8 @@ export class TaskMessageService { getRunningTitle(task: Task) { return this._getTaskTitle(task).running(task.metadata); } + + getRunningText(task: Task) { + return this._getTaskTitle(task).operation.running; + } }