]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Hide progress bar in case of an error 22419/head
authorVolker Theile <vtheile@suse.com>
Mon, 23 Jul 2018 09:53:51 +0000 (11:53 +0200)
committerVolker Theile <vtheile@suse.com>
Mon, 23 Jul 2018 09:53:51 +0000 (11:53 +0200)
- Enhance datatable to handle loading errors.
- Add warning panel component
- Modify the icon of the error panel component
- Use cd-[info|warning|error]-panel in the cd-view-cache component

Signed-off-by: Volker Theile <vtheile@suse.com>
28 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/error-panel/error-panel.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/warning-panel/warning-panel.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/warning-panel/warning-panel.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/warning-panel/warning-panel.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/warning-panel/warning-panel.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-fetch-data-context.ts [new file with mode: 0644]

index 5d915a49feac1d398f6bb25e1a16ec3d48f6a7fc..2ca8b9c58569c4013f0d9cc77dfd8e34134d94d7 100644 (file)
@@ -12,7 +12,8 @@
                [status]="viewCacheStatus.status"
                [statusFor]="viewCacheStatus.statusFor"></cd-view-cache>
 
-<cd-table [data]="images"
+<cd-table #table
+          [data]="images"
           columnMode="flex"
           [columns]="columns"
           identifier="id"
index 17819e54a7fea1e34a8a6dd8c50a5547a386a621..6c61b09084414e7bc312a571f558287d56a55f01 100644 (file)
@@ -6,6 +6,7 @@ import { BsModalRef, BsModalService } from 'ngx-bootstrap';
 import { RbdService } from '../../../shared/api/rbd.service';
 import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
 import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component';
+import { TableComponent } from '../../../shared/datatable/table/table.component';
 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';
@@ -27,6 +28,7 @@ import { RbdModel } from './rbd-model';
   styleUrls: ['./rbd-list.component.scss']
 })
 export class RbdListComponent implements OnInit, OnDestroy {
+  @ViewChild(TableComponent) table: TableComponent;
   @ViewChild('usageTpl') usageTpl: TemplateRef<any>;
   @ViewChild('parentTpl') parentTpl: TemplateRef<any>;
   @ViewChild('nameTpl') nameTpl: TemplateRef<any>;
@@ -119,6 +121,10 @@ export class RbdListComponent implements OnInit, OnDestroy {
       this.summaryDataSubscription = this.summaryService.summaryData$.subscribe((data: any) => {
         this.loadImages(data.executing_tasks);
       });
+    },
+    () => {
+      this.table.reset(); // Disable loading indicator.
+      this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }];
     });
   }
 
@@ -166,6 +172,7 @@ export class RbdListComponent implements OnInit, OnDestroy {
         this.executingTasks = executingTasks;
       },
       () => {
+        this.table.reset(); // Disable loading indicator.
         this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }];
       }
     );
index 6494b2f14058944636ddc1d272d23b1c7270f291..726deba82d287864cfe2db166dc79eab39373d21 100644 (file)
@@ -8,7 +8,7 @@
 <cd-table [data]="filesystems"
           columnMode="flex"
           [columns]="columns"
-          (fetchData)="loadFilesystems()"
+          (fetchData)="loadFilesystems($event)"
           identifier="id"
           forceIdentifier="true"
           selectionType="single"
index b95b7a5469d4790d4d59e366e3b3ab7b80ccbd5a..346d90f75d10353a9fb7ca14e5bd330252feab99 100644 (file)
@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
 
 import { CephfsService } from '../../../shared/api/cephfs.service';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 
 @Component({
@@ -36,10 +37,15 @@ export class CephfsListComponent implements OnInit {
     ];
   }
 
-  loadFilesystems() {
-    this.cephfsService.list().subscribe((resp: any[]) => {
-      this.filesystems = resp;
-    });
+  loadFilesystems(context: CdTableFetchDataContext) {
+    this.cephfsService.list().subscribe(
+      (resp: any[]) => {
+        this.filesystems = resp;
+      },
+      () => {
+        context.error();
+      }
+    );
   }
 
   updateSelection(selection: CdTableSelection) {
index 834a21479d04ec6a53534526b4bec00e5a43ead9..f0255f6a2f491e54b1fdf17c304ebce484532695 100644 (file)
@@ -6,7 +6,7 @@
   </ol>
 </nav>
 <cd-table [data]="data | filter:filters"
-          (fetchData)="getConfigurationList()"
+          (fetchData)="getConfigurationList($event)"
           [columns]="columns"
           selectionType="single"
           (updateSelection)="updateSelection($event)">
index 5a6dc0eac9e7b342a7eb2da126d58e5c3a7cdc02..ee6b16a311870d369fc55940d8d0276797e4b0e7 100644 (file)
@@ -2,6 +2,7 @@ import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
 
 import { ConfigurationService } from '../../../shared/api/configuration.service';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 
 @Component({
@@ -114,10 +115,15 @@ export class ConfigurationComponent implements OnInit {
     this.selection = selection;
   }
 
-  getConfigurationList() {
-    this.configurationService.getConfigData().subscribe((data: any) => {
-      this.data = data;
-    });
+  getConfigurationList(context: CdTableFetchDataContext) {
+    this.configurationService.getConfigData().subscribe(
+      (data: any) => {
+        this.data = data;
+      },
+      () => {
+        context.error();
+      }
+    );
   }
 
   updateFilter() {
index 9948dded8c3f478163ea266d787baa3ccae8775a..192f060329fed0b98f9e792a70088fa1df0288c0 100644 (file)
@@ -10,7 +10,7 @@
 <cd-table [data]="hosts"
           [columns]="columns"
           columnMode="flex"
-          (fetchData)="getHosts()">
+          (fetchData)="getHosts($event)">
   <ng-template #servicesTpl let-value="value">
     <span *ngFor="let service of value; last as isLast">
       <a [routerLink]="[service.cdLink]"
index 6ce752d6edc752260f8c1388e4fda91070e2e572..f41772f18f7caded92e33962d3c5ae0f27822a7f 100644 (file)
@@ -2,6 +2,7 @@ import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
 
 import { HostService } from '../../../shared/api/host.service';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
 import { Permissions } from '../../../shared/models/permissions';
 import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
@@ -49,7 +50,7 @@ export class HostsComponent implements OnInit {
     ];
   }
 
-  getHosts() {
+  getHosts(context: CdTableFetchDataContext) {
     if (this.isLoadingHosts) {
       return;
     }
@@ -62,23 +63,21 @@ export class HostsComponent implements OnInit {
       mgr: 'manager'
     };
     this.isLoadingHosts = true;
-    this.hostService
-      .list()
-      .then((resp) => {
-        resp.map((host) => {
-          host.services.map((service) => {
-            service.cdLink = `/perf_counters/${service.type}/${service.id}`;
-            const permissionKey = typeToPermissionKey[service.type];
-            service.canRead = this.permissions[permissionKey].read;
-            return service;
-          });
-          return host;
+    this.hostService.list().then((resp) => {
+      resp.map((host) => {
+        host.services.map((service) => {
+          service.cdLink = `/perf_counters/${service.type}/${service.id}`;
+          const permissionKey = typeToPermissionKey[service.type];
+          service.canRead = this.permissions[permissionKey].read;
+          return service;
         });
-        this.hosts = resp;
-        this.isLoadingHosts = false;
-      })
-      .catch(() => {
-        this.isLoadingHosts = false;
+        return host;
       });
+      this.hosts = resp;
+      this.isLoadingHosts = false;
+    }).catch(() => {
+      this.isLoadingHosts = false;
+      context.error();
+    });
   }
 }
index 5bda82ef1000d0021f7f7515c20e37be1444288c..96f4442bfe9e166eb476c04fa3b91a31d40a9242 100644 (file)
@@ -4,7 +4,7 @@
   </ol>
 </nav>
 <cd-table [data]="pools"
-          (fetchData)="getPoolList()"
+          (fetchData)="getPoolList($event)"
           [columns]="columns"
           selectionType="single"
           (updateSelection)="updateSelection($event)">
index 303eca25d29ffa0d008ae91c87b14eabc80f2a41..0adbe9c19b65e83184aee65777adc6e138bbdaee 100644 (file)
@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
 
 import { PoolService } from '../../../shared/api/pool.service';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 
 @Component({
@@ -14,9 +15,7 @@ export class PoolListComponent {
   columns: CdTableColumn[];
   selection = new CdTableSelection();
 
-  constructor(
-    private poolService: PoolService,
-  ) {
+  constructor(private poolService: PoolService) {
     this.columns = [
       {
         prop: 'pool_name',
@@ -68,10 +67,14 @@ export class PoolListComponent {
     this.selection = selection;
   }
 
-  getPoolList() {
-    this.poolService.getList().subscribe((pools: any[]) => {
-      this.pools = pools;
-    });
+  getPoolList(context: CdTableFetchDataContext) {
+    this.poolService.getList().subscribe(
+      (pools: any[]) => {
+        this.pools = pools;
+      },
+      () => {
+        context.error();
+      }
+    );
   }
-
 }
index fd5388573844a7dd143f29cef9480b4642a4d475..0f6afe3eff225207f99c120ef3560172c63d1883 100644 (file)
@@ -15,7 +15,7 @@
           selectionType="multi"
           (updateSelection)="updateSelection($event)"
           identifier="bucket"
-          (fetchData)="getBucketList()">
+          (fetchData)="getBucketList($event)">
   <div class="table-actions">
     <div class="btn-group"
          dropdown>
index 4136225e5033e267b48a299857b2a3c6b0791a75..d03ea84fd43bb6b727a3d5828edfb0e217315a8d 100644 (file)
@@ -7,6 +7,7 @@ import { RgwBucketService } from '../../../shared/api/rgw-bucket.service';
 import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component';
 import { TableComponent } from '../../../shared/datatable/table/table.component';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { Permission } from '../../../shared/models/permissions';
 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
@@ -44,15 +45,13 @@ export class RgwBucketListComponent {
     ];
   }
 
-  getBucketList() {
+  getBucketList(context: CdTableFetchDataContext) {
     this.rgwBucketService.list().subscribe(
       (resp: object[]) => {
         this.buckets = resp;
       },
       () => {
-        // Force datatable to hide the loading indicator in
-        // case of an error.
-        this.buckets = [];
+        context.error();
       }
     );
   }
index f480f5548d64e7e1d932d48f3244c12737936c62..c93bef352bd8dcaf27c82ce5b423b94fea7800cd 100644 (file)
@@ -13,7 +13,7 @@
           columnMode="flex"
           selectionType="single"
           (updateSelection)="updateSelection($event)"
-          (fetchData)="getDaemonList()">
+          (fetchData)="getDaemonList($event)">
   <cd-rgw-daemon-details cdTableDetail
                          [selection]="selection">
   </cd-rgw-daemon-details>
index 848d1b24c4913d2a6fd74c3d076403addcf5cb8a..281421f788650c4bc3a97bf758e616d4d9ba81cd 100644 (file)
@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
 
 import { RgwDaemonService } from '../../../shared/api/rgw-daemon.service';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
 
@@ -39,15 +40,13 @@ export class RgwDaemonListComponent {
     ];
   }
 
-  getDaemonList() {
+  getDaemonList(context: CdTableFetchDataContext) {
     this.rgwDaemonService.list().subscribe(
       (resp: object[]) => {
         this.daemons = resp;
       },
       () => {
-        // Force datatable to hide the loading indicator in
-        // case of an error.
-        this.daemons = [];
+        context.error();
       }
     );
   }
index 85a1427b4920d833087b2a5bf01b2046be7559e9..3cb0bfd83582a073cadbd52d4cca93e6b7688278 100644 (file)
@@ -15,7 +15,7 @@
           selectionType="multi"
           (updateSelection)="updateSelection($event)"
           identifier="user_id"
-          (fetchData)="getUserList()">
+          (fetchData)="getUserList($event)">
   <div class="table-actions">
     <div class="btn-group" dropdown>
       <button type="button"
index 48832b9a14ca8c23652ac8e9ae2cb4ea89fe4456..fea045834e2f2443712167eb79dc41cc3c728fd7 100644 (file)
@@ -8,6 +8,7 @@ import { DeletionModalComponent } from '../../../shared/components/deletion-moda
 import { TableComponent } from '../../../shared/datatable/table/table.component';
 import { CellTemplate } from '../../../shared/enum/cell-template.enum';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { Permission } from '../../../shared/models/permissions';
 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
@@ -61,15 +62,13 @@ export class RgwUserListComponent {
     ];
   }
 
-  getUserList() {
+  getUserList(context: CdTableFetchDataContext) {
     this.rgwUserService.list().subscribe(
       (resp: object[]) => {
         this.users = resp;
       },
       () => {
-        // Force datatable to hide the loading indicator in
-        // case of an error.
-        this.users = [];
+        context.error();
       }
     );
   }
index 62d4876a5a141d0439f0d8348ce069567308c8a7..e4fb9b2b29c3c7d5dcda8a6950bf189bbb432024 100644 (file)
@@ -17,6 +17,7 @@ import { SparklineComponent } from './sparkline/sparkline.component';
 import { SubmitButtonComponent } from './submit-button/submit-button.component';
 import { UsageBarComponent } from './usage-bar/usage-bar.component';
 import { ViewCacheComponent } from './view-cache/view-cache.component';
+import { WarningPanelComponent } from './warning-panel/warning-panel.component';
 
 @NgModule({
   imports: [
@@ -42,7 +43,8 @@ import { ViewCacheComponent } from './view-cache/view-cache.component';
     InfoPanelComponent,
     ModalComponent,
     DeletionModalComponent,
-    ConfirmationModalComponent
+    ConfirmationModalComponent,
+    WarningPanelComponent
   ],
   providers: [],
   exports: [
@@ -54,7 +56,8 @@ import { ViewCacheComponent } from './view-cache/view-cache.component';
     LoadingPanelComponent,
     InfoPanelComponent,
     UsageBarComponent,
-    ModalComponent
+    ModalComponent,
+    WarningPanelComponent
   ],
   entryComponents: [ModalComponent, DeletionModalComponent, ConfirmationModalComponent]
 })
index c1c6f163db210449ec75f9dc09215414b1426583..375f3a95459c624e1cdb2e5111c66202e61b10fd 100644 (file)
@@ -2,7 +2,7 @@
   <table>
     <tr>
       <td rowspan="2" class="error-panel-alert-icon">
-        <i class="fa fa-3x fa-exclamation-triangle alert-danger"
+        <i class="fa fa-3x fa-times-circle alert-danger"
            aria-hidden="true"></i>
       </td>
       <td class="error-panel-alert-title">
index 0e2ac0f88c804a51d1bd6713ad497def0dcb53eb..ce98cad09efa271fadb662a68bed62f8a35040dd 100644 (file)
@@ -1,17 +1,14 @@
-<alert i18n
-       type="info"
-       *ngIf="status === vcs.ValueNone">
+<cd-info-panel i18n
+               *ngIf="status === vcs.ValueNone">
   Retrieving data<span *ngIf="statusFor"> for <span [innerHtml]="statusFor"></span></span>. Please wait...
-</alert>
+</cd-info-panel>
 
-<alert i18n
-       type="warning"
-       *ngIf="status === vcs.ValueStale">
+<cd-warning-panel i18n
+                  *ngIf="status === vcs.ValueStale">
   Displaying previously cached data<span *ngIf="statusFor"> for <span [innerHtml]="statusFor"></span></span>.
-</alert>
+</cd-warning-panel>
 
-<alert i18n
-       type="danger"
-       *ngIf="status === vcs.ValueException">
+<cd-error-panel i18n
+                *ngIf="status === vcs.ValueException">
   Could not load data<span *ngIf="statusFor"> for <span [innerHtml]="statusFor"></span></span>. Please check the cluster health.
-</alert>
+</cd-error-panel>
index f1a3228511f80cd7da3a531bf299f09dd13cd104..5ecd0c9eed3bbe67f60218ac0bba3bf4ce67fd89 100644 (file)
@@ -3,6 +3,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { AlertModule } from 'ngx-bootstrap';
 
 import { configureTestBed } from '../../unit-test-helper';
+import { ErrorPanelComponent } from '../error-panel/error-panel.component';
+import { InfoPanelComponent } from '../info-panel/info-panel.component';
+import { WarningPanelComponent } from '../warning-panel/warning-panel.component';
 import { ViewCacheComponent } from './view-cache.component';
 
 describe('ViewCacheComponent', () => {
@@ -10,7 +13,12 @@ describe('ViewCacheComponent', () => {
   let fixture: ComponentFixture<ViewCacheComponent>;
 
   configureTestBed({
-    declarations: [ViewCacheComponent],
+    declarations: [
+      ErrorPanelComponent,
+      InfoPanelComponent,
+      ViewCacheComponent,
+      WarningPanelComponent
+    ],
     imports: [AlertModule.forRoot()]
   });
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/warning-panel/warning-panel.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/warning-panel/warning-panel.component.html
new file mode 100644 (file)
index 0000000..cf9bc8c
--- /dev/null
@@ -0,0 +1,18 @@
+<alert type="warning">
+  <table>
+    <tr>
+      <td rowspan="2" class="warning-panel-alert-icon">
+        <i class="fa fa-3x fa-warning alert-warning"
+           aria-hidden="true"></i>
+      </td>
+      <td class="warning-panel-alert-title">
+        {{ title }}
+      </td>
+    </tr>
+    <tr>
+      <td class="warning-panel-alert-text">
+        <ng-content></ng-content>
+      </td>
+    </tr>
+  </table>
+</alert>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/warning-panel/warning-panel.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/warning-panel/warning-panel.component.scss
new file mode 100644 (file)
index 0000000..b47e386
--- /dev/null
@@ -0,0 +1,9 @@
+.warning-panel-alert-icon {
+  vertical-align: top;
+  padding-right: 15px; // See @alert-padding in bootstrap/less/variables.less
+}
+.warning-panel-alert-title {
+  font-weight: bold;
+}
+.warning-panel-alert-text {
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/warning-panel/warning-panel.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/warning-panel/warning-panel.component.spec.ts
new file mode 100644 (file)
index 0000000..ddf918e
--- /dev/null
@@ -0,0 +1,26 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AlertModule } from 'ngx-bootstrap';
+
+import { configureTestBed } from '../../unit-test-helper';
+import { WarningPanelComponent } from './warning-panel.component';
+
+describe('WarningPanelComponent', () => {
+  let component: WarningPanelComponent;
+  let fixture: ComponentFixture<WarningPanelComponent>;
+
+  configureTestBed({
+    declarations: [WarningPanelComponent],
+    imports: [AlertModule.forRoot()]
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(WarningPanelComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/warning-panel/warning-panel.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/warning-panel/warning-panel.component.ts
new file mode 100644 (file)
index 0000000..5dada0a
--- /dev/null
@@ -0,0 +1,14 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+  selector: 'cd-warning-panel',
+  templateUrl: './warning-panel.component.html',
+  styleUrls: ['./warning-panel.component.scss']
+})
+export class WarningPanelComponent {
+  /**
+   * The title to be displayed. Defaults to 'Warning'.
+   * @type {string}
+   */
+  @Input() title = 'Warning';
+}
index f835f8e033f32a35518f6052cbe7ab9707ac7f22..6f32e1d618d39dafc2088c990ea0687d1c199c5a 100644 (file)
@@ -1,3 +1,7 @@
+<cd-error-panel i18n
+                *ngIf="loadingError">
+  Failed to load data.
+</cd-error-panel>
 <div class="dataTables_wrapper">
   <div class="dataTables_header clearfix"
        *ngIf="toolHeader">
index e5e7d54566b6f54a6414c09202c4f8038f26c263..ef57fac28067fe64f550ad78628a15925c0d8a4a 100644 (file)
@@ -5,6 +5,7 @@ import { RouterTestingModule } from '@angular/router/testing';
 import { NgxDatatableModule } from '@swimlane/ngx-datatable';
 
 import { ComponentsModule } from '../../components/components.module';
+import { CdTableFetchDataContext } from '../../models/cd-table-fetch-data-context';
 import { configureTestBed } from '../../unit-test-helper';
 import { TableComponent } from './table.component';
 
@@ -245,4 +246,49 @@ describe('TableComponent', () => {
       clearLocalStorage();
     });
   });
+
+  describe('reload data', () => {
+    beforeEach(() => {
+      component.ngOnInit();
+      component.data = [];
+      component.updating = false;
+    });
+
+    it('should call fetchData callback function', () => {
+      component.fetchData.subscribe((context) => {
+        expect(context instanceof CdTableFetchDataContext).toBeTruthy();
+      });
+      component.reloadData();
+    });
+
+    it('should call error function', () => {
+      component.data = createFakeData(5);
+      component.fetchData.subscribe((context) => {
+        context.error();
+        expect(component.loadingError).toBeTruthy();
+        expect(component.data.length).toBe(0);
+        expect(component.loadingIndicator).toBeFalsy();
+        expect(component.updating).toBeFalsy();
+      });
+      component.reloadData();
+    });
+
+    it('should call error function with custom config', () => {
+      component.data = createFakeData(10);
+      component.fetchData.subscribe((context) => {
+        context.errorConfig.resetData = false;
+        context.errorConfig.displayError = false;
+        context.error();
+        expect(component.loadingError).toBeFalsy();
+        expect(component.data.length).toBe(10);
+        expect(component.loadingIndicator).toBeFalsy();
+        expect(component.updating).toBeFalsy();
+      });
+      component.reloadData();
+    });
+
+    afterEach(() => {
+      clearLocalStorage();
+    });
+  });
 });
index d196125e8517b2d9879288162ffd79ff67aa23c2..4fadc365694bb547efddd327e0feda1e64ab23d5 100644 (file)
@@ -23,6 +23,7 @@ import { Observable, timer as observableTimer } from 'rxjs';
 
 import { CellTemplate } from '../../enum/cell-template.enum';
 import { CdTableColumn } from '../../models/cd-table-column';
+import { CdTableFetchDataContext } from '../../models/cd-table-fetch-data-context';
 import { CdTableSelection } from '../../models/cd-table-selection';
 import { CdUserConfig } from '../../models/cd-user-config';
 
@@ -111,6 +112,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   search = '';
   rows = [];
   loadingIndicator = true;
+  loadingError = false;
   paginationClasses = {
     pagerLeftArrow: 'i fa fa-angle-double-left',
     pagerRightArrow: 'i fa fa-angle-double-right',
@@ -301,7 +303,19 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
 
   reloadData() {
     if (!this.updating) {
-      this.fetchData.emit();
+      this.loadingError = false;
+      const context = new CdTableFetchDataContext(() => {
+        // Do we have to display the error panel?
+        this.loadingError = context.errorConfig.displayError;
+        // Force data table to show no data?
+        if (context.errorConfig.resetData) {
+          this.data = [];
+        }
+        // Stop the loading indicator and reset the data table
+        // to the correct state.
+        this.useData();
+      });
+      this.fetchData.emit(context);
       this.updating = true;
     }
   }
@@ -329,13 +343,22 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     if (this.search.length > 0) {
       this.updateFilter(true);
     }
-    this.loadingIndicator = false;
-    this.updating = false;
+    this.reset();
     if (this.updateSelectionOnRefresh) {
       this.updateSelected();
     }
   }
 
+  /**
+   * Reset the data table to correct state. This includes:
+   * - Disable loading indicator
+   * - Reset 'Updating' flag
+   */
+  reset() {
+    this.loadingIndicator = false;
+    this.updating = false;
+  }
+
   /**
    * After updating the data, we have to update the selected items
    * because details may have changed,
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-fetch-data-context.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-fetch-data-context.ts
new file mode 100644 (file)
index 0000000..ec9fea4
--- /dev/null
@@ -0,0 +1,17 @@
+export class CdTableFetchDataContext {
+  errorConfig = {
+    resetData: true,   // Force data table to show no data
+    displayError: true // Show an error panel above the data table
+  };
+
+  /**
+   * The function that should be called from within the error handler
+   * of the 'fetchData' function to display the error panel and to
+   * reset the data table to the correct state.
+   */
+  error: Function;
+
+  constructor(error: () => void) {
+    this.error = error;
+  }
+}