]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Improve Summary's subscribe methods
authorTiago Melo <tmelo@suse.com>
Tue, 12 May 2020 19:29:16 +0000 (19:29 +0000)
committerTiago Melo <tmelo@suse.com>
Mon, 22 Jun 2020 13:41:48 +0000 (13:41 +0000)
Now subscribe only emits if the value is not undefined,
removing the need to do the validation after each subscribe.

Add a new subscribeOnce method, which replaces getCurrentSummary.
Like the previous one, this will only emit when there is a non undefined value,
and will only emit once.
After it emits, the subscription is closed automatically.

Fixes: https://tracker.ceph.com/issues/36563
Signed-off-by: Tiago Melo <tmelo@suse.com>
(cherry picked from commit e41860412ebf7ebd1b1a03a9a0a822f1b56320b6)

23 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/monitoring-list/monitoring-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/about/about.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/grafana/grafana.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/orchestrator-doc-panel/orchestrator-doc-panel.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/finished-task.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/summary.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager.service.ts

index 026cf343240ef74e84b0deecf4d5cf7bbe412309..40265dd6bc0eb8d853cbe8648cd9f3fc2ba6f7fe 100644 (file)
@@ -146,10 +146,11 @@ export class IscsiTargetListComponent extends ListWithDetails implements OnInit,
           this.settings = settings;
         });
       } else {
-        const summary = this.summaryservice.getCurrentSummary();
-        const releaseName = this.cephReleaseNamePipe.transform(summary.version);
-        this.docsUrl = `http://docs.ceph.com/docs/${releaseName}/mgr/dashboard/#enabling-iscsi-management`;
-        this.status = result.message;
+        this.summaryservice.subscribeOnce((summary) => {
+          const releaseName = this.cephReleaseNamePipe.transform(summary.version);
+          this.docsUrl = `http://docs.ceph.com/docs/${releaseName}/mgr/dashboard/#enabling-iscsi-management`;
+          this.status = result.message;
+        });
       }
     });
   }
index 68dc6303f46befa8208ba8250fb7558d7e571918..c51d5f7035546213ba8085fb33ba592b0ac9d96c 100644 (file)
@@ -16,6 +16,7 @@ import {
 import { RbdService } from '../../../shared/api/rbd.service';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { ExecutingTask } from '../../../shared/models/executing-task';
+import { Summary } from '../../../shared/models/summary.model';
 import { SummaryService } from '../../../shared/services/summary.service';
 import { TaskListService } from '../../../shared/services/task-list.service';
 import { SharedModule } from '../../../shared/shared.module';
@@ -56,7 +57,7 @@ describe('RbdTrashListComponent', () => {
   it('should load trash images when summary is trigged', () => {
     spyOn(rbdService, 'listTrash').and.callThrough();
 
-    summaryService['summaryDataSource'].next({ executingTasks: null });
+    summaryService['summaryDataSource'].next(new Summary());
     expect(rbdService.listTrash).toHaveBeenCalled();
   });
 
@@ -90,7 +91,7 @@ describe('RbdTrashListComponent', () => {
       addImage('1');
       addImage('2');
       component.images = images;
-      summaryService['summaryDataSource'].next({ executingTasks: [] });
+      summaryService['summaryDataSource'].next(new Summary());
       spyOn(rbdService, 'listTrash').and.callFake(() =>
         of([{ pool_name: 'rbd', status: 1, value: images }])
       );
@@ -124,7 +125,7 @@ describe('RbdTrashListComponent', () => {
     };
 
     beforeEach(() => {
-      summaryService['summaryDataSource'].next({ executingTasks: [] });
+      summaryService['summaryDataSource'].next(new Summary());
       spyOn(rbdService, 'listTrash').and.callFake(() => {
         of([{ pool_name: 'rbd', status: 1, value: images }]);
       });
index 5e96d126fdda2bd7bccbf8fd50ab9cb0ba9f7b0a..e08e235fe751fcf6f6ebebad99add9f672aeaf97 100644 (file)
@@ -38,17 +38,9 @@ export class MonitoringListComponent implements OnInit {
       this.isPrometheusConfigured = true;
     });
 
-    const subs = this.summaryService.subscribe((summary: any) => {
-      if (!summary) {
-        return;
-      }
-
+    this.summaryService.subscribeOnce((summary) => {
       const releaseName = this.cephReleaseNamePipe.transform(summary.version);
       this.docsUrl = `https://docs.ceph.com/docs/${releaseName}/mgr/dashboard/#enabling-prometheus-alerting`;
-
-      setTimeout(() => {
-        subs.unsubscribe();
-      }, 0);
     });
 
     // Activate tab according to given fragment
index 2ac3ef6b2811d815d4d9aa449b0fe208791998eb..97dfe0d03a8ac1cf7db91b694e816e6ea6d5302a 100644 (file)
@@ -18,7 +18,15 @@ describe('ServiceDetailsComponent', () => {
   configureTestBed({
     imports: [HttpClientTestingModule, RouterTestingModule, TabsModule.forRoot(), SharedModule],
     declarations: [ServiceDetailsComponent, ServiceDaemonListComponent],
-    providers: [i18nProviders, { provide: SummaryService, useValue: { subscribe: jest.fn() } }]
+    providers: [
+      i18nProviders,
+      {
+        provide: SummaryService,
+        useValue: {
+          subscribeOnce: jest.fn()
+        }
+      }
+    ]
   });
 
   beforeEach(() => {
index 77b3b649332a9b53d1a60308687d2b8bce05e65e..985b3b3f531de656c49d10b758f53b8e8eca8e02 100644 (file)
@@ -24,19 +24,11 @@ export class Nfs501Component implements OnInit, OnDestroy {
   ) {}
 
   ngOnInit() {
-    const subs = this.summaryService.subscribe((summary: any) => {
-      if (!summary) {
-        return;
-      }
-
+    this.summaryService.subscribeOnce((summary) => {
       const releaseName = this.cephReleaseNamePipe.transform(summary.version);
       this.docsUrl =
         `http://docs.ceph.com/docs/${releaseName}/mgr/dashboard/` +
         `#configuring-nfs-ganesha-in-the-dashboard`;
-
-      setTimeout(() => {
-        subs.unsubscribe();
-      }, 0);
     });
 
     this.routeParamsSubscribe = this.route.params.subscribe((params: { message: string }) => {
index 4b5ec0c909981dcdc1fa4a27ee00aa2fa2d950e3..ec8ffe0f032c1f1dfe67e5f1f71dd401dbf471e8 100644 (file)
@@ -6,6 +6,7 @@ import { RouterTestingModule } from '@angular/router/testing';
 
 import { TypeaheadModule } from 'ngx-bootstrap/typeahead';
 import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
 
 import { ActivatedRouteStub } from '../../../../testing/activated-route-stub';
 import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
@@ -48,11 +49,11 @@ describe('NfsFormComponent', () => {
   beforeEach(() => {
     const summaryService = TestBed.get(SummaryService);
     spyOn(summaryService, 'refresh').and.callFake(() => true);
-    spyOn(summaryService, 'getCurrentSummary').and.callFake(() => {
-      return {
+    spyOn(summaryService, 'subscribeOnce').and.callFake(() =>
+      of({
         version: 'master'
-      };
-    });
+      })
+    );
 
     fixture = TestBed.createComponent(NfsFormComponent);
     component = fixture.componentInstance;
index d152eaa96e611de847ba0e76fc8aa64e9d9141da..50616fb2d9408c0314e66918b48ea6cff0fea5ec 100644 (file)
@@ -123,9 +123,10 @@ export class NfsFormComponent implements OnInit {
       this.getData(promises);
     }
 
-    const summary = this.summaryservice.getCurrentSummary();
-    const releaseName = this.cephReleaseNamePipe.transform(summary.version);
-    this.docsUrl = `http://docs.ceph.com/docs/${releaseName}/radosgw/nfs/`;
+    this.summaryservice.subscribeOnce((summary) => {
+      const releaseName = this.cephReleaseNamePipe.transform(summary.version);
+      this.docsUrl = `http://docs.ceph.com/docs/${releaseName}/radosgw/nfs/`;
+    });
   }
 
   getData(promises: Observable<any>[]) {
index 36039aa4d97e85ea97191d943ef1b788699a5a37..8ad7e0d4e249d2d0814a2803128dc2eff1f1c68a 100644 (file)
@@ -16,6 +16,7 @@ import {
 import { NfsService } from '../../../shared/api/nfs.service';
 import { TableActionsComponent } from '../../../shared/datatable/table-actions/table-actions.component';
 import { ExecutingTask } from '../../../shared/models/executing-task';
+import { Summary } from '../../../shared/models/summary.model';
 import { SummaryService } from '../../../shared/services/summary.service';
 import { TaskListService } from '../../../shared/services/task-list.service';
 import { SharedModule } from '../../../shared/shared.module';
@@ -29,7 +30,7 @@ describe('NfsListComponent', () => {
   let nfsService: NfsService;
   let httpTesting: HttpTestingController;
 
-  const refresh = (data: object) => {
+  const refresh = (data: Summary) => {
     summaryService['summaryDataSource'].next(data);
   };
 
@@ -73,7 +74,7 @@ describe('NfsListComponent', () => {
     });
 
     it('should load exports on init', () => {
-      refresh({});
+      refresh(new Summary());
       httpTesting.expectOne('api/nfs-ganesha/export');
       expect(nfsService.list).toHaveBeenCalled();
     });
@@ -130,7 +131,7 @@ describe('NfsListComponent', () => {
       addExport('b');
       addExport('c');
       component.exports = exports;
-      refresh({ executing_tasks: [], finished_tasks: [] });
+      refresh(new Summary());
       spyOn(nfsService, 'list').and.callFake(() => of(exports));
       fixture.detectChanges();
 
index 73d0925bc58e1a70b11eef111bd6809f190c1546..dbe4ec6f209194af3a9501bf774574559017875c 100644 (file)
@@ -199,7 +199,10 @@ describe('PoolListComponent', () => {
 
     beforeEach(() => {
       summaryService = TestBed.get(SummaryService);
-      summaryService['summaryDataSource'].next({ executing_tasks: [], finished_tasks: [] });
+      summaryService['summaryDataSource'].next({
+        executing_tasks: [],
+        finished_tasks: []
+      });
     });
 
     it('gets all pools without executing pools', () => {
index 67fb868acb453c0bf8470c7b7629c9b2929baa4c..02fbc18cdc6077b55e1323d8d67bcbfd42335b08 100644 (file)
@@ -21,19 +21,11 @@ export class Rgw501Component implements OnInit, OnDestroy {
   ) {}
 
   ngOnInit() {
-    const subs = this.summaryService.subscribe((summary: any) => {
-      if (!summary) {
-        return;
-      }
-
+    this.summaryService.subscribeOnce((summary) => {
       const releaseName = this.cephReleaseNamePipe.transform(summary.version);
       this.docsUrl =
         `http://docs.ceph.com/docs/${releaseName}/mgr/dashboard/` +
         `#enabling-the-object-gateway-management-frontend`;
-
-      setTimeout(() => {
-        subs.unsubscribe();
-      }, 0);
     });
 
     this.routeParamsSubscribe = this.route.params.subscribe((params: { message: string }) => {
index c22507eebed17d9af418e49cdef9b7bde6ebea2c..231929933fe8512004cc67c76eb1fab83b023a3d 100644 (file)
@@ -40,10 +40,7 @@ export class AboutComponent implements OnInit, OnDestroy {
     this.projectConstants = AppConstants;
     this.hostAddr = window.location.hostname;
     this.modalVariables = this.setVariables();
-    this.subs = this.summaryService.subscribe((summary: any) => {
-      if (!summary) {
-        return;
-      }
+    this.subs = this.summaryService.subscribe((summary) => {
       const version = summary.version.replace('ceph version ', '').split(' ');
       this.hostAddr = summary.mgr_host.replace(/(^\w+:|^)\/\//, '').replace(/\/$/, '');
       this.versionNumber = version[0];
index cb6d0085dc91c514cd7ab7babd75999187aa6730..e6821ddf960ff137a01487745975e21023ecb7ac 100644 (file)
@@ -28,17 +28,9 @@ export class DashboardHelpComponent implements OnInit {
   ) {}
 
   ngOnInit() {
-    const subs = this.summaryService.subscribe((summary: any) => {
-      if (!summary) {
-        return;
-      }
-
+    this.summaryService.subscribeOnce((summary) => {
       const releaseName = this.cephReleaseNamePipe.transform(summary.version);
       this.docsUrl = `http://docs.ceph.com/docs/${releaseName}/mgr/dashboard/`;
-
-      setTimeout(() => {
-        subs.unsubscribe();
-      }, 0);
     });
   }
 
index e21c9eeb0887c1c5304aca834eb8a8724366760f..c184d0cda5fcdad9712570276ffb7817a1530430 100644 (file)
@@ -46,11 +46,8 @@ export class NavigationComponent implements OnInit, OnDestroy {
 
   ngOnInit() {
     this.subs.add(
-      this.summaryService.subscribe((data: any) => {
-        if (!data) {
-          return;
-        }
-        this.summaryData = data;
+      this.summaryService.subscribe((summary) => {
+        this.summaryData = summary;
       })
     );
     this.subs.add(
index aaedd546eff48a33d20136f97958840d2c3b9df8..b6cee7649d3b1a9967caf8a94bf13e91c2289dc9 100644 (file)
@@ -23,11 +23,8 @@ export class NotificationsComponent implements OnInit, OnDestroy {
 
   ngOnInit() {
     this.subs.add(
-      this.summaryService.subscribe((data: any) => {
-        if (!data) {
-          return;
-        }
-        this.hasRunningTasks = data.executing_tasks.length > 0;
+      this.summaryService.subscribe((summary) => {
+        this.hasRunningTasks = summary.executing_tasks.length > 0;
       })
     );
   }
index 4c4b5ecedfa2d0e462eca0de00bb90f1cff0f8f0..afb4136189005b68b0e982cd2aea5952b541ec20 100644 (file)
@@ -179,20 +179,13 @@ export class GrafanaComponent implements OnInit, OnChanges {
       three: 'grafana_three'
     };
 
-    const subs = this.summaryService.subscribe((summary: any) => {
-      if (!summary) {
-        return;
-      }
-
+    this.summaryService.subscribeOnce((summary) => {
       const releaseName = this.cephReleaseNamePipe.transform(summary.version);
       this.docsUrl =
         `http://docs.ceph.com/docs/${releaseName}/mgr/dashboard/` +
         `#enabling-the-embedding-of-grafana-dashboards`;
-
-      setTimeout(() => {
-        subs.unsubscribe();
-      }, 0);
     });
+
     this.settingsService.ifSettingConfigured('api/grafana/url', (url) => {
       this.grafanaExist = true;
       this.loading = false;
index 0fdde25b6dc88f60e7db6c837157b05c11756541..5a0f9b603e9e82044a03a436bd4bf0be84d9e350 100644 (file)
@@ -108,15 +108,12 @@ export class NotificationsSidebarComponent implements OnInit, OnDestroy {
     );
 
     this.subs.add(
-      this.summaryService.subscribe((data: any) => {
-        if (!data) {
-          return;
-        }
-        this._handleTasks(data.executing_tasks);
+      this.summaryService.subscribe((summary) => {
+        this._handleTasks(summary.executing_tasks);
 
         this.mutex.acquire().then((release) => {
           _.filter(
-            data.finished_tasks,
+            summary.finished_tasks,
             (task: FinishedTask) => !this.last_task || moment(task.end_time).isAfter(this.last_task)
           ).forEach((task) => {
             const config = this.notificationService.finishedTaskToNotification(task, task.success);
index 484c422314adbf59a50ac18245f553f9641e8b1f..d4728bce702367edc7f71457e7ccf93094cf33e4 100644 (file)
@@ -17,17 +17,9 @@ export class OrchestratorDocPanelComponent implements OnInit {
   ) {}
 
   ngOnInit() {
-    const subs = this.summaryService.subscribe((summary: any) => {
-      if (!summary) {
-        return;
-      }
-
+    this.summaryService.subscribeOnce((summary) => {
       const releaseName = this.cephReleaseNamePipe.transform(summary.version);
       this.docsUrl = `http://docs.ceph.com/docs/${releaseName}/mgr/orchestrator/`;
-
-      setTimeout(() => {
-        subs.unsubscribe();
-      }, 0);
     });
   }
 }
index 9dc780963ad418b5fa4b43700ddcd994cb528a1b..9e7dd5f98ed583a417bb0ae96f144f677b551dc1 100644 (file)
@@ -2,8 +2,8 @@ import { Task } from './task';
 import { TaskException } from './task-exception';
 
 export class FinishedTask extends Task {
-  begin_time: number;
-  end_time: number;
+  begin_time: string;
+  end_time: string;
   exception: TaskException;
   latency: number;
   progress: number;
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/summary.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/summary.model.ts
new file mode 100644 (file)
index 0000000..f2854a0
--- /dev/null
@@ -0,0 +1,15 @@
+import { ExecutingTask } from './executing-task';
+import { FinishedTask } from './finished-task';
+
+export class Summary {
+  executing_tasks?: ExecutingTask[];
+  filesystems?: any[];
+  finished_tasks?: FinishedTask[];
+  have_mon_connection?: boolean;
+  health_status?: string;
+  mgr_host?: string;
+  mgr_id?: string;
+  rbd_mirroring?: any;
+  rbd_pools?: any[];
+  version?: string;
+}
index 100beefd2fb2516bb29d905600f76e0521e6a7b0..14144921ef1819156a6227d351fd6558ee7d0636 100644 (file)
@@ -6,6 +6,7 @@ import { of as observableOf, Subscriber, Subscription } from 'rxjs';
 
 import { configureTestBed } from '../../../testing/unit-test-helper';
 import { ExecutingTask } from '../models/executing-task';
+import { Summary } from '../models/summary.model';
 import { AuthStorageService } from './auth-storage.service';
 import { SummaryService } from './summary.service';
 
@@ -14,7 +15,7 @@ describe('SummaryService', () => {
   let authStorageService: AuthStorageService;
   let subs: Subscription;
 
-  const summary: Record<string, any> = {
+  const summary: Summary = {
     executing_tasks: [],
     health_status: 'HEALTH_OK',
     mgr_id: 'x',
@@ -29,6 +30,8 @@ describe('SummaryService', () => {
     get: () => observableOf(summary)
   };
 
+  const nextSummary = (newData: any) => summaryService['summaryDataSource'].next(newData);
+
   configureTestBed({
     imports: [RouterTestingModule],
     providers: [
@@ -66,23 +69,71 @@ describe('SummaryService', () => {
     subs.unsubscribe();
   }));
 
-  describe('Should test methods after first refresh', () => {
+  describe('Should test subscribe without initial value', () => {
+    let result: Summary;
+    let i: number;
+
+    const callback = (response: Summary) => {
+      i++;
+      result = response;
+    };
+
     beforeEach(() => {
-      authStorageService.set('foobar', undefined, undefined);
-      summaryService.refresh();
+      i = 0;
+      result = undefined;
+      nextSummary(undefined);
     });
 
-    it('should call getCurrentSummary', () => {
-      expect(summaryService.getCurrentSummary()).toEqual(summary);
+    it('should call subscribeOnce', () => {
+      const subscriber = summaryService.subscribeOnce(callback);
+
+      expect(subscriber).toEqual(jasmine.any(Subscriber));
+      expect(i).toBe(0);
+      expect(result).toEqual(undefined);
+
+      nextSummary(undefined);
+      expect(i).toBe(0);
+      expect(result).toEqual(undefined);
+      expect(subscriber.closed).toBe(false);
+
+      nextSummary(summary);
+      expect(result).toEqual(summary);
+      expect(i).toBe(1);
+      expect(subscriber.closed).toBe(true);
+
+      nextSummary(summary);
+      expect(result).toEqual(summary);
+      expect(i).toBe(1);
     });
 
     it('should call subscribe', () => {
-      let result;
-      const subscriber = summaryService.subscribe((data) => {
-        result = data;
-      });
+      const subscriber = summaryService.subscribe(callback);
+
       expect(subscriber).toEqual(jasmine.any(Subscriber));
+      expect(i).toBe(0);
+      expect(result).toEqual(undefined);
+
+      nextSummary(undefined);
+      expect(i).toBe(0);
+      expect(result).toEqual(undefined);
+      expect(subscriber.closed).toBe(false);
+
+      nextSummary(summary);
+      expect(result).toEqual(summary);
+      expect(i).toBe(1);
+      expect(subscriber.closed).toBe(false);
+
+      nextSummary(summary);
       expect(result).toEqual(summary);
+      expect(i).toBe(2);
+      expect(subscriber.closed).toBe(false);
+    });
+  });
+
+  describe('Should test methods after first refresh', () => {
+    beforeEach(() => {
+      authStorageService.set('foobar', undefined, undefined);
+      summaryService.refresh();
     });
 
     it('should call addRunningTask', () => {
@@ -92,7 +143,11 @@ describe('SummaryService', () => {
           image_name: 'someImage'
         })
       );
-      const result = summaryService.getCurrentSummary();
+      let result: any;
+      summaryService.subscribeOnce((response) => {
+        result = response;
+      });
+
       expect(result.executing_tasks.length).toBe(1);
       expect(result.executing_tasks[0]).toEqual({
         metadata: { image_name: 'someImage', pool_name: 'somePool' },
@@ -101,19 +156,23 @@ describe('SummaryService', () => {
     });
 
     it('should call addRunningTask with duplicate task', () => {
-      let result = summaryService.getCurrentSummary();
+      let result: any;
+      summaryService.subscribe((response) => {
+        result = response;
+      });
+
       const exec_task = new ExecutingTask('rbd/delete', {
         pool_name: 'somePool',
         image_name: 'someImage'
       });
 
       result.executing_tasks = [exec_task];
-      summaryService['summaryDataSource'].next(result);
-      result = summaryService.getCurrentSummary();
+      nextSummary(result);
+
       expect(result.executing_tasks.length).toBe(1);
 
       summaryService.addRunningTask(exec_task);
-      result = summaryService.getCurrentSummary();
+
       expect(result.executing_tasks.length).toBe(1);
     });
   });
index 2603841921409c4171f50ff39b5312d947937144..a0783402773751cd8056019723a949a9a33a60d7 100644 (file)
@@ -3,8 +3,10 @@ import { Injectable } from '@angular/core';
 
 import * as _ from 'lodash';
 import { BehaviorSubject, Observable, Subscription } from 'rxjs';
+import { filter, first } from 'rxjs/operators';
 
 import { ExecutingTask } from '../models/executing-task';
+import { Summary } from '../models/summary.model';
 import { TimerService } from './timer.service';
 
 @Injectable({
@@ -13,7 +15,7 @@ import { TimerService } from './timer.service';
 export class SummaryService {
   readonly REFRESH_INTERVAL = 5000;
   // Observable sources
-  private summaryDataSource = new BehaviorSubject(null);
+  private summaryDataSource = new BehaviorSubject<Summary>(null);
   // Observable streams
   summaryData$ = this.summaryDataSource.asObservable();
 
@@ -29,29 +31,35 @@ export class SummaryService {
     return this.retrieveSummaryObservable().subscribe(this.retrieveSummaryObserver());
   }
 
-  private retrieveSummaryObservable(): Observable<Object> {
-    return this.http.get('api/summary');
+  private retrieveSummaryObservable(): Observable<Summary> {
+    return this.http.get<Summary>('api/summary');
   }
 
-  private retrieveSummaryObserver(): (data: any) => void {
-    return (data: Object) => {
+  private retrieveSummaryObserver(): (data: Summary) => void {
+    return (data: Summary) => {
       this.summaryDataSource.next(data);
     };
   }
 
   /**
-   * Returns the current value of summaryData
+   * Subscribes to the summaryData and receive only the first, non undefined, value.
    */
-  getCurrentSummary(): { [key: string]: any; executing_tasks: object[] } {
-    return this.summaryDataSource.getValue();
+  subscribeOnce(next: (summary: Summary) => void, error?: (error: any) => void): Subscription {
+    return this.summaryData$
+      .pipe(
+        filter((value) => !!value),
+        first()
+      )
+      .subscribe(next, error);
   }
 
   /**
    * Subscribes to the summaryData,
    * which is updated periodically or when a new task is created.
+   * Will receive only non undefined values.
    */
-  subscribe(next: (summary: any) => void, error?: (error: any) => void): Subscription {
-    return this.summaryData$.subscribe(next, error);
+  subscribe(next: (summary: Summary) => void, error?: (error: any) => void): Subscription {
+    return this.summaryData$.pipe(filter((value) => !!value)).subscribe(next, error);
   }
 
   /**
index 6d1f74b9fb1f06961e5c50e77d42515d89b2968f..9e5a7aa7327aff615ed374ec725021fdd81270da 100644 (file)
@@ -56,12 +56,10 @@ export class TaskListService implements OnDestroy {
     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.summaryDataSubscription = this.summaryService.subscribe((summary) => {
+      this.getUpdate().subscribe((resp: any) => {
+        this.updateData(resp, summary['executing_tasks'].filter(this.taskFilter));
+      }, this.onFetchError);
     }, this.onFetchError);
   }
 
index 2ef8da4136c51a873adafba3076e2185965e5f0f..7e99e5aad9000327797b76912b354ab68394d38d 100644 (file)
@@ -26,12 +26,9 @@ export class TaskManagerService {
   subscriptions: Array<TaskSubscription> = [];
 
   init(summaryService: SummaryService) {
-    return summaryService.subscribe((data: any) => {
-      if (!data) {
-        return;
-      }
-      const executingTasks = data.executing_tasks;
-      const finishedTasks = data.finished_tasks;
+    return summaryService.subscribe((summary) => {
+      const executingTasks = summary.executing_tasks;
+      const finishedTasks = summary.finished_tasks;
       const newSubscriptions: Array<TaskSubscription> = [];
       for (const subscription of this.subscriptions) {
         const finishedTask = <FinishedTask>this._getTask(subscription, finishedTasks);