task = TaskManager.run("dummy/task", {}, self._dummy)
return task.wait(5) # wait for five seconds
+
+How to deal with asynchronous tasks in the front-end?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+All executing and most recently finished asynchronous tasks are displayed on the
+"Backgroud-Tasks" menu.
+
+The front-end developer should provide a description, success message and error
+messages for each task on ``TaskManagerMessageService.messages``.
+This messages can make use of the task metadata to provide more personalized messages.
+
+When submitting an asynchronous task, the developer should provide a callback
+that will be automatically triggered after the execution of that task.
+This can be done by using the ``TaskManagerService.subscribe``.
+
+Most of the times, all we want to do after a task completes the execution, is
+displaying a notification message based on the execution result. The
+``NotificationService.notifyTask`` will use the messages from
+``TaskManagerMessageService`` to display a success / error message based on the
+execution result of a task.
+
+Usage example:
+
+.. code-block:: javascript
+
+ export class TaskManagerMessageService {
+
+ messages = {
+ // Messages for 'rbd/create' task
+ 'rbd/create': new TaskManagerMessage(
+ // Description
+ (metadata) => `Create RBD '${metadata.pool_name}/${metadata.image_name}'`,
+ // Success message
+ (metadata) => `RBD '${metadata.pool_name}/${metadata.image_name}'
+ have been created successfully`,
+ // Error messages
+ (metadata) => {
+ return {
+ '17': `Name '${metadata.pool_name}/${metadata.image_name}' is already
+ in use.`
+ };
+ }
+ ),
+ // ...
+ };
+
+ // ...
+ }
+
+ export class RBDFormComponent {
+ // ...
+
+ submit() {
+ // ...
+ this.rbdService.create(request).then((resp) => {
+ // Subscribe the submitted task
+ this.taskManagerService.subscribe('rbd/create',
+ {'pool_name': request.pool_name, 'rbd_name': request.name},
+ // Callback that will be invoked after task is finished
+ (finishedTask: FinishedTask) => {
+ // Will display a notification message (success or error)
+ this.notificationService.notifyTask(finishedTask, finishedTask.ret_value.success);
+ });
+ // ...
+ })
+ }
+ }
import { AuthModule } from '../auth/auth.module';
import { NavigationComponent } from './navigation/navigation.component';
import { NotificationsComponent } from './notifications/notifications.component';
+import { TaskManagerComponent } from './task-manager/task-manager.component';
@NgModule({
imports: [
SharedModule,
RouterModule
],
- declarations: [NavigationComponent, NotificationsComponent],
+ declarations: [NavigationComponent, NotificationsComponent, TaskManagerComponent],
exports: [NavigationComponent]
})
export class NavigationModule {}
<!-- /.navbar-primary -->
<ul class="nav navbar-nav navbar-utility">
+ <li>
+ <cd-task-manager class="oa-navbar"></cd-task-manager>
+ </li>
<li>
<cd-notifications class="oa-navbar"></cd-notifications>
</li>
-
<li class="tc_logout">
<cd-logout class="oa-navbar"></cd-logout>
</li>
import { SharedModule } from '../../../shared/shared.module';
import { LogoutComponent } from '../../auth/logout/logout.component';
import { NotificationsComponent } from '../notifications/notifications.component';
+import { TaskManagerComponent } from '../task-manager/task-manager.component';
import { NavigationComponent } from './navigation.component';
describe('NavigationComponent', () => {
let component: NavigationComponent;
let fixture: ComponentFixture<NavigationComponent>;
- const fakeService = new NotificationService(null);
+ const fakeService = new NotificationService(null, null);
beforeEach(
async(() => {
HttpClientTestingModule,
PopoverModule.forRoot()
],
- declarations: [NavigationComponent, NotificationsComponent, LogoutComponent],
+ declarations: [
+ NavigationComponent,
+ NotificationsComponent,
+ LogoutComponent,
+ TaskManagerComponent
+ ],
providers: [{ provide: NotificationService, useValue: fakeService }]
}).compileComponents();
})
let component: NotificationsComponent;
let fixture: ComponentFixture<NotificationsComponent>;
- const fakeService = new NotificationService(null);
+ const fakeService = new NotificationService(null, null);
beforeEach(
async(() => {
--- /dev/null
+<ng-template #popTemplate>
+ <!-- Executing -->
+ <div *ngIf="executingTasks.length > 0">
+ <div class="separator">
+ EXECUTING
+ </div>
+ <hr>
+ <div *ngFor="let executingTask of executingTasks">
+ <table>
+ <tr>
+ <td class="icon-col">
+ <i class="fa fa-spinner fa-spin fa-fw" aria-hidden="true"></i>
+ </td>
+ <td>{{ executingTask.description }}</td>
+ <td class="text-right italic" *ngIf="executingTask.progress"><span>{{ executingTask.progress }} %</span></td>
+ </tr>
+ <tr>
+ <td colspan="3" class="text-right">
+ <span class="date">{{ executingTask.begin_time | cdDate }}</span></td>
+ </tr>
+ </table>
+ <hr>
+ </div>
+ </div>
+ <!-- Finished -->
+ <div *ngIf="finishedTasks.length > 0">
+ <div class="separator">
+ FINISHED
+ </div>
+ <hr>
+ <div *ngFor="let finishedTask of finishedTasks">
+ <table>
+ <tr>
+ <td class="icon-col">
+ <span *ngIf="!finishedTask.errorMessage">
+ <i class="fa fa-check text-success"></i>
+ </span>
+ <span *ngIf="finishedTask.errorMessage">
+ <i class="fa fa-exclamation-triangle text-danger"></i>
+ </span>
+ </td>
+ <td colspan="2">
+ {{ finishedTask.description }}
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td>
+ <span *ngIf="finishedTask.errorMessage" class="text-danger">
+ {{ finishedTask.errorMessage }}
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2" class="text-right">
+ <span class="date">{{ finishedTask.end_time | cdDate }}</span>
+ </td>
+ </tr>
+ </table>
+ <hr>
+ </div>
+ </div>
+ <!-- Empty -->
+ <div *ngIf="executingTasks.length === 0 && finishedTasks.length === 0">
+ <div class="message">
+ There are no background tasks.
+ </div>
+ </div>
+</ng-template>
+<a [popover]="popTemplate"
+ placement="bottom"
+ container="body"
+ outsideClick="true">
+ <i class="fa"
+ [ngClass]="icon"></i>
+ Background-Tasks<span *ngIf="executingTasks.length > 0"> ({{ executingTasks.length }})</span>
+</a>
--- /dev/null
+@import '../../../../styles/popover.scss';
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PopoverModule } from 'ngx-bootstrap';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { TaskManagerComponent } from './task-manager.component';
+
+describe('TaskManagerComponent', () => {
+ let component: TaskManagerComponent;
+ let fixture: ComponentFixture<TaskManagerComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ SharedModule,
+ PopoverModule.forRoot(),
+ HttpClientTestingModule
+ ],
+ declarations: [ TaskManagerComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TaskManagerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+
+import { ExecutingTask } from '../../../shared/models/executing-task';
+import { FinishedTask } from '../../../shared/models/finished-task';
+import { SummaryService } from '../../../shared/services/summary.service';
+import { TaskManagerMessageService } from '../../../shared/services/task-manager-message.service';
+
+@Component({
+ selector: 'cd-task-manager',
+ templateUrl: './task-manager.component.html',
+ styleUrls: ['./task-manager.component.scss']
+})
+export class TaskManagerComponent implements OnInit {
+
+ executingTasks: Array<ExecutingTask> = [];
+ finishedTasks: Array<FinishedTask> = [];
+
+ icon = 'fa-hourglass-o';
+
+ constructor(private summaryService: SummaryService,
+ private taskManagerMessageService: TaskManagerMessageService) {
+ }
+
+ ngOnInit() {
+ const icons = ['fa-hourglass-o', 'fa-hourglass-start', 'fa-hourglass-half', 'fa-hourglass-end'];
+ let iconIndex = 0;
+ this.summaryService.summaryData$.subscribe((data: any) => {
+ this.executingTasks = data.executing_tasks;
+ this.finishedTasks = data.finished_tasks;
+ for (const excutingTask of this.executingTasks) {
+ excutingTask.description = this.taskManagerMessageService.getDescription(excutingTask);
+ }
+ for (const finishedTask of this.finishedTasks) {
+ finishedTask.description = this.taskManagerMessageService.getDescription(finishedTask);
+ if (finishedTask.success === false) {
+ finishedTask.errorMessage = this.taskManagerMessageService.getErrorMessage(finishedTask);
+ }
+ }
+ if (this.executingTasks.length > 0) {
+ iconIndex = (iconIndex + 1) % icons.length;
+ } else {
+ iconIndex = 0;
+ }
+ this.icon = icons[iconIndex];
+ });
+ }
+
+}
--- /dev/null
+import { Task } from './task';
+
+export class ExecutingTask extends Task {
+ begin_time: number;
+ progress: number;
+}
--- /dev/null
+import { Task } from './task';
+import { TaskException } from './task-exception';
+
+export class FinishedTask extends Task {
+ begin_time: number;
+ end_time: number;
+ exception: TaskException;
+ latency: number;
+ progress: number;
+ ret_value: any;
+ success: boolean;
+
+ errorMessage: string;
+}
--- /dev/null
+export class TaskException {
+ status: number;
+ errno: number;
+ component: string;
+ detail: string;
+}
--- /dev/null
+export class Task {
+ name: string;
+ metadata: object;
+
+ description: string;
+}
public notificationService: NotificationService
) {}
+ _notify (resp) {
+ this.notificationService.show(
+ NotificationType.error,
+ resp.error.detail || '',
+ `${resp.status} - ${resp.statusText}`
+ );
+ }
+
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).catch(resp => {
if (resp instanceof HttpErrorResponse) {
case 401:
this.authStorageService.remove();
this.router.navigate(['/login']);
- // falls through
- default:
- this.notificationService.show(
- NotificationType.error,
- resp.error.detail || '',
- `${resp.status} - ${resp.statusText}`
- );
+ this._notify(resp);
+ break;
+ case 500:
+ this._notify(resp);
+ break;
}
}
// Return the error to the method that called it.
import { ToastOptions, ToastsManager } from 'ng2-toastr';
import { NotificationService } from './notification.service';
+import { TaskManagerMessageService } from './task-manager-message.service';
+import { TaskManagerService } from './task-manager.service';
describe('NotificationService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
- providers: [NotificationService, ToastsManager, ToastOptions]
+ providers: [
+ NotificationService,
+ ToastsManager,
+ ToastOptions,
+ TaskManagerService,
+ TaskManagerMessageService
+ ]
});
});
import { NotificationType } from '../enum/notification-type.enum';
import { CdNotification } from '../models/cd-notification';
+import { FinishedTask } from '../models/finished-task';
+import { TaskManagerMessageService } from './task-manager-message.service';
@Injectable()
export class NotificationService {
KEY = 'cdNotifications';
- constructor(public toastr: ToastsManager) {
+ constructor(public toastr: ToastsManager,
+ private taskManagerMessageService: TaskManagerMessageService) {
const stringNotifications = localStorage.getItem(this.KEY);
let notifications: CdNotification[] = [];
break;
}
}
+
+ notifyTask(finishedTask: FinishedTask, success: boolean = true) {
+ if (finishedTask.success && success) {
+ this.show(NotificationType.success,
+ this.taskManagerMessageService.getSuccessMessage(finishedTask));
+ } else {
+ this.show(NotificationType.error,
+ this.taskManagerMessageService.getErrorMessage(finishedTask),
+ this.taskManagerMessageService.getDescription(finishedTask));
+ }
+ }
}
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
+import { ConfigurationService } from '../api/configuration.service';
+import { RbdMirroringService } from '../api/rbd-mirroring.service';
+import { TcmuIscsiService } from '../api/tcmu-iscsi.service';
import { AuthGuardService } from './auth-guard.service';
import { AuthStorageService } from './auth-storage.service';
import { FormatterService } from './formatter.service';
import { NotificationService } from './notification.service';
import { SummaryService } from './summary.service';
+import { TaskManagerMessageService } from './task-manager-message.service';
+import { TaskManagerService } from './task-manager.service';
@NgModule({
imports: [CommonModule],
AuthStorageService,
FormatterService,
SummaryService,
- NotificationService
+ NotificationService,
+ TcmuIscsiService,
+ ConfigurationService,
+ RbdMirroringService,
+ TaskManagerService,
+ TaskManagerMessageService
]
})
export class ServicesModule {}
--- /dev/null
+import { Injectable } from '@angular/core';
+import { FinishedTask } from '../models/finished-task';
+import { Task } from '../models/task';
+
+class TaskManagerMessage {
+ descr: (metadata) => string;
+ success: (metadata) => string;
+ error: (metadata) => object;
+
+ constructor(descr: (metadata) => string,
+ success: (metadata) => string,
+ error: (metadata) => object) {
+ this.descr = descr;
+ this.success = success;
+ this.error = error;
+ }
+}
+
+@Injectable()
+export class TaskManagerMessageService {
+
+ messages = {
+ };
+
+ defaultMessage = new TaskManagerMessage(
+ (metadata) => 'Unknown Task',
+ (metadata) => 'Task executed successfully',
+ () => {
+ return {
+ };
+ }
+ );
+
+ constructor() { }
+
+ getSuccessMessage(finishedTask: FinishedTask) {
+ const taskManagerMessage = this.messages[finishedTask.name] || this.defaultMessage;
+ return taskManagerMessage.success(finishedTask.metadata);
+ }
+
+ getErrorMessage(finishedTask: FinishedTask) {
+ const taskManagerMessage = this.messages[finishedTask.name] || this.defaultMessage;
+ return taskManagerMessage.error(finishedTask.metadata)[finishedTask.exception.errno] ||
+ finishedTask.exception.detail;
+ }
+
+ getDescription(task: Task) {
+ const taskManagerMessage = this.messages[task.name] || this.defaultMessage;
+ return taskManagerMessage.descr(task.metadata);
+ }
+}
--- /dev/null
+import { Injectable } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { ExecutingTask } from '../models/executing-task';
+import { FinishedTask } from '../models/finished-task';
+import { Task } from '../models/task';
+import { SummaryService } from './summary.service';
+
+class TaskSubscription {
+ name: string;
+ metadata: object;
+ onTaskFinished: (finishedTask: FinishedTask) => any;
+
+ constructor(name, metadata, onTaskFinished) {
+ this.name = name;
+ this.metadata = metadata;
+ this.onTaskFinished = onTaskFinished;
+ }
+}
+
+@Injectable()
+export class TaskManagerService {
+
+ subscriptions: Array<TaskSubscription> = [];
+
+ constructor(private summaryService: SummaryService) {
+ summaryService.summaryData$.subscribe((data: any) => {
+ const executingTasks = data.executing_tasks;
+ const finishedTasks = data.finished_tasks;
+ const newSubscriptions: Array<TaskSubscription> = [];
+ for (const subscription of this.subscriptions) {
+ const finishedTask = <FinishedTask>this._getTask(subscription, finishedTasks);
+ const executingTask = <ExecutingTask>this._getTask(subscription, executingTasks);
+ if (finishedTask !== null && executingTask === null) {
+ subscription.onTaskFinished(finishedTask);
+ }
+ if (executingTask !== null) {
+ newSubscriptions.push(subscription);
+ }
+ this.subscriptions = newSubscriptions;
+ }
+ });
+ }
+
+ subscribe(name, metadata, onTaskFinished: (finishedTask: FinishedTask) => any) {
+ this.subscriptions.push(new TaskSubscription(name, metadata, onTaskFinished));
+ }
+
+ _getTask(subscription: TaskSubscription, tasks: Array<Task>): Task {
+ for (const task of tasks) {
+ if (task.name === subscription.name &&
+ _.isEqual(task.metadata, subscription.metadata)) {
+ return task;
+ }
+ }
+ return null;
+ }
+}