heading="Snapshots">
<cd-rbd-snapshot-list [snapshots]="selectedItem.snapshots"
[poolName]="selectedItem.pool_name"
- [rbdName]="selectedItem.name"
- [executingTasks]="selectedItem.executingTasks"></cd-rbd-snapshot-list>
+ [rbdName]="selectedItem.name"></cd-rbd-snapshot-list>
</tab>
</tabset>
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<RbdListComponent>;
+ 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: [
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', () => {
});
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');
+ });
+ });
});
-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';
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';
@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')
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;
}
}
];
- 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 ') +
- '<strong>' +
- value.join('</strong>, <strong>') +
- '</strong>'
- });
- });
- 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 ') +
+ '<strong>' +
+ value.join('</strong>, <strong>') +
+ '</strong>'
});
- 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) {
export class RbdModel {
+ id: string;
name: string;
pool_name: string;
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<RbdSnapshotListComponent>;
+ let summaryService: SummaryService;
const fakeAuthStorageService = {
isLoggedIn: () => {
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();
});
null,
rbdService,
null,
- notificationService
+ notificationService,
+ null,
+ null
);
spyOn(rbdService, 'deleteSnapshot').and.returnValue(observableThrowError({ status: 500 }));
spyOn(notificationService, 'notifyTask').and.stub();
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');
+ });
+ });
});
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';
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';
@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()
poolName: string;
@Input()
rbdName: string;
- @Input()
- executingTasks: ExecutingTask[] = [];
-
@ViewChild('nameTpl')
nameTpl: TemplateRef<any>;
@ViewChild('protectTpl')
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,
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;
}
}
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) {
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();
});
}
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,
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(
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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<object>;
+ 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<object>} 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<object>,
+ 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();
+ }
+ }
+}
getRunningTitle(task: Task) {
return this._getTaskTitle(task).running(task.metadata);
}
+
+ getRunningText(task: Task) {
+ return this._getTaskTitle(task).operation.running;
+ }
}