]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Forms now wait for all data to load until displayed 34746/head
authorTiago Melo <tmelo@suse.com>
Fri, 24 Apr 2020 11:20:51 +0000 (11:20 +0000)
committerTiago Melo <tmelo@suse.com>
Wed, 6 May 2020 16:56:28 +0000 (16:56 +0000)
Add directive to handle the display of the form.
It will display the form only when loading is finished.
Otherwise it will show a loading message or error message.

Add a new class that should be extended by all form components.
For now it only has methods for dealing with loading,
but this could be improved later.

Fixes: https://tracker.ceph.com/issues/44912
Signed-off-by: Tiago Melo <tmelo@suse.com>
38 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.ts [new file with mode: 0644]

index 217ec33a32d179ffca918d6b7af2e5e4bac2cbd5..dc432a095fa833c31c6f843dbcaf6260c64832af 100644 (file)
@@ -1,9 +1,9 @@
-<div class="cd-col-form">
+<div class="cd-col-form"
+     *cdFormLoading="loading">
   <form name="targetForm"
         #formDir="ngForm"
         [formGroup]="targetForm"
-        novalidate
-        *ngIf="targetForm">
+        novalidate>
     <div class="card">
       <div i18n="form title|Example: Create Pool@@formTitle"
            class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
index 5c4d362bae080f90df94e43cba5574c47cbabd5f..a5d2acf60d32941c1b901fbbcaf93aca911205d2 100644 (file)
@@ -13,6 +13,7 @@ import { SelectMessages } from '../../../shared/components/select/select-message
 import { SelectOption } from '../../../shared/components/select/select-option.model';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { Icons } from '../../../shared/enum/icons.enum';
+import { CdForm } from '../../../shared/forms/cd-form';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 import { CdValidators } from '../../../shared/forms/cd-validators';
 import { FinishedTask } from '../../../shared/models/finished-task';
@@ -25,7 +26,7 @@ import { IscsiTargetIqnSettingsModalComponent } from '../iscsi-target-iqn-settin
   templateUrl: './iscsi-target-form.component.html',
   styleUrls: ['./iscsi-target-form.component.scss']
 })
-export class IscsiTargetFormComponent implements OnInit {
+export class IscsiTargetFormComponent extends CdForm implements OnInit {
   cephIscsiConfigVersion: number;
   targetForm: CdFormGroup;
   modalRef: BsModalRef;
@@ -97,6 +98,7 @@ export class IscsiTargetFormComponent implements OnInit {
     private taskWrapper: TaskWrapperService,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.resource = this.i18n('target');
   }
 
@@ -182,6 +184,8 @@ export class IscsiTargetFormComponent implements OnInit {
       if (data[5]) {
         this.resolveModel(data[5]);
       }
+
+      this.loadingReady();
     });
   }
 
index 3b4315df1b139e8bfe9b19d826c65098b14a3f00..75f86642df3ef3d1059d67241d70db786a3446c0 100644 (file)
@@ -1,4 +1,5 @@
-<div class="cd-col-form">
+<div class="cd-col-form"
+     *cdFormLoading="loading">
   <form name="rbdForm"
         #formDir="ngForm"
         [formGroup]="rbdForm"
index 5556a847a6b7d02268f6062b515adc297ae7e86d..b19b50776a76630b5d687dae53911ed5cc1f0928 100644 (file)
@@ -53,6 +53,8 @@ describe('RbdFormComponent', () => {
     fixture = TestBed.createComponent(RbdFormComponent);
     component = fixture.componentInstance;
     activatedRoute = TestBed.get(ActivatedRoute);
+
+    component.loadingReady();
   });
 
   it('should create', () => {
index 7820ad7d8ee510a91b279fabae2dbe7519c698d0..112e2de300548a025310133315319bf47e49c7d0 100644 (file)
@@ -12,6 +12,7 @@ import { PoolService } from '../../../shared/api/pool.service';
 import { RbdService } from '../../../shared/api/rbd.service';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { Icons } from '../../../shared/enum/icons.enum';
+import { CdForm } from '../../../shared/forms/cd-form';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 import {
   RbdConfigurationEntry,
@@ -38,7 +39,7 @@ import { RbdFormResponseModel } from './rbd-form-response.model';
   templateUrl: './rbd-form.component.html',
   styleUrls: ['./rbd-form.component.scss']
 })
-export class RbdFormComponent implements OnInit {
+export class RbdFormComponent extends CdForm implements OnInit {
   poolPermission: Permission;
   rbdForm: CdFormGroup;
   getDirtyConfigurationValues: (
@@ -105,6 +106,7 @@ export class RbdFormComponent implements OnInit {
     public actionLabels: ActionLabelsI18n,
     public router: Router
   ) {
+    super();
     this.poolPermission = this.authStorageService.getPermissions().pool;
     this.resource = this.i18n('RBD');
     this.features = {
@@ -293,6 +295,8 @@ export class RbdFormComponent implements OnInit {
         this.setResponse(resp, this.snapName);
         this.rbdImage.next(resp);
       }
+
+      this.loadingReady();
     });
 
     _.each(this.features, (feature) => {
index eff515223017c5f8567a720ebd3cfce52dff8c58..ab23e58c59f59cbfdb31f6f97ede0d7e7128709f 100644 (file)
@@ -1,4 +1,5 @@
-<div class="cd-col-form">
+<div class="cd-col-form"
+     *cdFormLoading="loading">
   <form name="configForm"
         #formDir="ngForm"
         [formGroup]="configForm"
index b7458f6a85be378adf6ff5986f3db423a24d7996..771180f1394a7d031611659d2d08a2f5bdba2f92 100644 (file)
@@ -9,6 +9,7 @@ import { ConfigurationService } from '../../../../shared/api/configuration.servi
 import { ConfigFormModel } from '../../../../shared/components/config-option/config-option.model';
 import { ConfigOptionTypes } from '../../../../shared/components/config-option/config-option.types';
 import { NotificationType } from '../../../../shared/enum/notification-type.enum';
+import { CdForm } from '../../../../shared/forms/cd-form';
 import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
 import { NotificationService } from '../../../../shared/services/notification.service';
 import { ConfigFormCreateRequestModel } from './configuration-form-create-request.model';
@@ -18,7 +19,7 @@ import { ConfigFormCreateRequestModel } from './configuration-form-create-reques
   templateUrl: './configuration-form.component.html',
   styleUrls: ['./configuration-form.component.scss']
 })
-export class ConfigurationFormComponent implements OnInit {
+export class ConfigurationFormComponent extends CdForm implements OnInit {
   configForm: CdFormGroup;
   response: ConfigFormModel;
   type: string;
@@ -36,6 +37,7 @@ export class ConfigurationFormComponent implements OnInit {
     private notificationService: NotificationService,
     private i18n: I18n
   ) {
+    super();
     this.createForm();
   }
 
@@ -62,6 +64,7 @@ export class ConfigurationFormComponent implements OnInit {
       const configName = params.name;
       this.configService.get(configName).subscribe((resp: ConfigFormModel) => {
         this.setResponse(resp);
+        this.loadingReady();
       });
     });
   }
index c224c713180467b6a829d81e6dd57cbae5a43b41..0fcf0cd1a5fa8b9ba03c423116e39d15d5c9ce91 100644 (file)
@@ -1,9 +1,6 @@
-<cd-loading-panel *ngIf="loading"
-                  i18n>Loading...</cd-loading-panel>
-
-<div class="cd-col-form">
+<div class="cd-col-form"
+     *cdFormLoading="loading">
   <form name="hostForm"
-        *ngIf="!loading"
         #formDir="ngForm"
         [formGroup]="hostForm"
         novalidate>
index bfa2987107a4ceddb2a658a6821a08fb99b4a0a2..1949338594a2af57e94b434deab99836099cae5d 100644 (file)
@@ -4,6 +4,7 @@ import { Router } from '@angular/router';
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import { HostService } from '../../../../shared/api/host.service';
 import { ActionLabelsI18n, URLVerbs } from '../../../../shared/constants/app.constants';
+import { CdForm } from '../../../../shared/forms/cd-form';
 import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
 import { CdValidators } from '../../../../shared/forms/cd-validators';
 import { FinishedTask } from '../../../../shared/models/finished-task';
@@ -14,11 +15,10 @@ import { TaskWrapperService } from '../../../../shared/services/task-wrapper.ser
   templateUrl: './host-form.component.html',
   styleUrls: ['./host-form.component.scss']
 })
-export class HostFormComponent implements OnInit {
+export class HostFormComponent extends CdForm implements OnInit {
   hostForm: CdFormGroup;
   action: string;
   resource: string;
-  loading = true;
   hostnames: string[];
 
   constructor(
@@ -28,6 +28,7 @@ export class HostFormComponent implements OnInit {
     private hostService: HostService,
     private taskWrapper: TaskWrapperService
   ) {
+    super();
     this.resource = this.i18n('host');
     this.action = this.actionLabels.CREATE;
     this.createForm();
@@ -38,7 +39,7 @@ export class HostFormComponent implements OnInit {
       this.hostnames = resp.map((host) => {
         return host['hostname'];
       });
-      this.loading = false;
+      this.loadingReady();
     });
   }
 
index f246824f084470873de9c68e63ae1e62f950ff34..aef2f59679f022342800fcc0e91640d84a971933 100644 (file)
@@ -1,11 +1,5 @@
-<cd-loading-panel *ngIf="loading && !error"
-                  i18n>Loading configuration...</cd-loading-panel>
-<cd-alert-panel type="error"
-                *ngIf="loading && error"
-                i18n>The configuration could not be loaded.</cd-alert-panel>
-
 <div class="cd-col-form"
-     *ngIf="!loading && !error">
+     *cdFormLoading="loading">
   <form name="mgrModuleForm"
         #frm="ngForm"
         [formGroup]="mgrModuleForm"
index 4810c5f8f8fbdb4da9ba7fdcdada4e8a10d7f518..f8474b1f04ae018ef0206ced7c67232980b57724 100644 (file)
@@ -8,6 +8,7 @@ import { forkJoin as observableForkJoin } from 'rxjs';
 
 import { MgrModuleService } from '../../../../shared/api/mgr-module.service';
 import { NotificationType } from '../../../../shared/enum/notification-type.enum';
+import { CdForm } from '../../../../shared/forms/cd-form';
 import { CdFormBuilder } from '../../../../shared/forms/cd-form-builder';
 import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
 import { CdValidators } from '../../../../shared/forms/cd-validators';
@@ -18,10 +19,8 @@ import { NotificationService } from '../../../../shared/services/notification.se
   templateUrl: './mgr-module-form.component.html',
   styleUrls: ['./mgr-module-form.component.scss']
 })
-export class MgrModuleFormComponent implements OnInit {
+export class MgrModuleFormComponent extends CdForm implements OnInit {
   mgrModuleForm: CdFormGroup;
-  error = false;
-  loading = false;
   moduleName = '';
   moduleOptions: any[] = [];
 
@@ -32,27 +31,28 @@ export class MgrModuleFormComponent implements OnInit {
     private mgrModuleService: MgrModuleService,
     private notificationService: NotificationService,
     private i18n: I18n
-  ) {}
+  ) {
+    super();
+  }
 
   ngOnInit() {
     this.route.params.subscribe((params: { name: string }) => {
       this.moduleName = decodeURIComponent(params.name);
-      this.loading = true;
       const observables = [
         this.mgrModuleService.getOptions(this.moduleName),
         this.mgrModuleService.getConfig(this.moduleName)
       ];
       observableForkJoin(observables).subscribe(
         (resp: object) => {
-          this.loading = false;
           this.moduleOptions = resp[0];
           // Create the form dynamically.
           this.createForm();
           // Set the form field values.
           this.mgrModuleForm.setValue(resp[1]);
+          this.loadingReady();
         },
         (_error) => {
-          this.error = true;
+          this.loadingError();
         }
       );
     });
index b3f02dcf16099092902031c81778289b9602c0e3..998d5d9856b059a17f60f97c82a14a6788ca0e84 100644 (file)
@@ -1,6 +1,7 @@
 <cd-orchestrator-doc-panel *ngIf="!hasOrchestrator"></cd-orchestrator-doc-panel>
+
 <div class="cd-col-form"
-     *ngIf="!loading && hasOrchestrator">
+     *cdFormLoading="loading">
   <form name="form"
         #formDir="ngForm"
         [formGroup]="form"
index e6be9f0ea5c9f92aadc365d0bb8c8b286a59ef51..62a22a0c4b298146b13a80db535083536eb50f5b 100644 (file)
@@ -10,6 +10,7 @@ import { OrchestratorService } from '../../../../shared/api/orchestrator.service
 import { SubmitButtonComponent } from '../../../../shared/components/submit-button/submit-button.component';
 import { ActionLabelsI18n } from '../../../../shared/constants/app.constants';
 import { Icons } from '../../../../shared/enum/icons.enum';
+import { CdForm } from '../../../../shared/forms/cd-form';
 import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
 import { CdTableColumn } from '../../../../shared/models/cd-table-column';
 import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
@@ -26,7 +27,7 @@ import { OsdFeature } from './osd-feature.interface';
   templateUrl: './osd-form.component.html',
   styleUrls: ['./osd-form.component.scss']
 })
-export class OsdFormComponent implements OnInit {
+export class OsdFormComponent extends CdForm implements OnInit {
   @ViewChild('dataDeviceSelectionGroups', { static: false })
   dataDeviceSelectionGroups: OsdDevicesSelectionGroupsComponent;
 
@@ -44,7 +45,6 @@ export class OsdFormComponent implements OnInit {
   form: CdFormGroup;
   columns: Array<CdTableColumn> = [];
 
-  loading = false;
   allDevices: InventoryDevice[] = [];
 
   availDevices: InventoryDevice[] = [];
@@ -60,7 +60,7 @@ export class OsdFormComponent implements OnInit {
   features: { [key: string]: OsdFeature };
   featureList: OsdFeature[] = [];
 
-  hasOrchestrator = false;
+  hasOrchestrator = true;
   docsUrl: string;
 
   constructor(
@@ -71,6 +71,7 @@ export class OsdFormComponent implements OnInit {
     private router: Router,
     private bsModalService: BsModalService
   ) {
+    super();
     this.resource = this.i18n('OSDs');
     this.action = this.actionLabels.CREATE;
     this.features = {
@@ -86,8 +87,10 @@ export class OsdFormComponent implements OnInit {
   ngOnInit() {
     this.orchService.status().subscribe((status) => {
       this.hasOrchestrator = status.available;
-      if (this.hasOrchestrator) {
+      if (status.available) {
         this.getDataDevices();
+      } else {
+        this.loadingNone();
       }
     });
 
@@ -122,20 +125,16 @@ export class OsdFormComponent implements OnInit {
   }
 
   getDataDevices() {
-    if (this.loading) {
-      return;
-    }
-    this.loading = true;
     this.orchService.inventoryDeviceList().subscribe(
       (devices: InventoryDevice[]) => {
         this.allDevices = _.filter(devices, 'available');
         this.availDevices = [...this.allDevices];
-        this.loading = false;
+        this.loadingReady();
       },
       () => {
         this.allDevices = [];
         this.availDevices = [];
-        this.loading = false;
+        this.loadingError();
       }
     );
   }
index 8d02a031a24a8c39c9227287e6b1873dc1a456ee..6aba5b50430ffe94d3f2e96e824091d528f8dd3e 100644 (file)
@@ -1,11 +1,5 @@
-<cd-loading-panel *ngIf="loading && !error"
-                  i18n>Loading configuration...</cd-loading-panel>
-<cd-alert-panel type="error"
-                *ngIf="loading && error"
-                i18n>The configuration could not be loaded.</cd-alert-panel>
-
 <div class="cd-col-form"
-     *ngIf="!loading && !error">
+     *cdFormLoading="loading">
   <ng-container [ngSwitch]="step">
     <!-- Configuration step -->
     <div *ngSwitchCase="1">
index b258d2c18696d3713b9d7c2cc95e0d2d428e31f1..c1d1640b53d62373dda737e55632b6df3c40346d 100644 (file)
@@ -4,12 +4,12 @@ import { Router } from '@angular/router';
 
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import * as _ from 'lodash';
-import { BlockUI, NgBlockUI } from 'ng-block-ui';
 import { forkJoin as observableForkJoin } from 'rxjs';
 
 import { MgrModuleService } from '../../../shared/api/mgr-module.service';
 import { TelemetryService } from '../../../shared/api/telemetry.service';
 import { NotificationType } from '../../../shared/enum/notification-type.enum';
+import { CdForm } from '../../../shared/forms/cd-form';
 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 import { CdValidators } from '../../../shared/forms/cd-validators';
@@ -21,14 +21,9 @@ import { TextToDownloadService } from '../../../shared/services/text-to-download
   templateUrl: './telemetry.component.html',
   styleUrls: ['./telemetry.component.scss']
 })
-export class TelemetryComponent implements OnInit {
-  @BlockUI()
-  blockUI: NgBlockUI;
-
-  error = false;
+export class TelemetryComponent extends CdForm implements OnInit {
   configForm: CdFormGroup;
   licenseAgrmt = false;
-  loading = false;
   moduleEnabled: boolean;
   options: Object = {};
   previewForm: CdFormGroup;
@@ -54,10 +49,11 @@ export class TelemetryComponent implements OnInit {
     private telemetryService: TelemetryService,
     private i18n: I18n,
     private textToDownloadService: TextToDownloadService
-  ) {}
+  ) {
+    super();
+  }
 
   ngOnInit() {
-    this.loading = true;
     const observables = [
       this.mgrModuleService.getOptions('telemetry'),
       this.mgrModuleService.getConfig('telemetry')
@@ -69,10 +65,10 @@ export class TelemetryComponent implements OnInit {
         const configs = _.pick(resp[1], this.requiredFields);
         this.createConfigForm();
         this.configForm.setValue(configs);
-        this.loading = false;
+        this.loadingReady();
       },
       (_error) => {
-        this.error = true;
+        this.loadingError();
       }
     );
   }
@@ -120,17 +116,18 @@ export class TelemetryComponent implements OnInit {
   }
 
   private getReport() {
-    this.loading = true;
+    this.loadingStart();
+
     this.telemetryService.getReport().subscribe(
       (resp: object) => {
         this.report = resp;
         this.reportId = resp['report']['report_id'];
         this.createPreviewForm();
-        this.loading = false;
+        this.loadingReady();
         this.step++;
       },
       (_error) => {
-        this.error = true;
+        this.loadingError();
       }
     );
   }
index ad2cab02246985621f48e471f2faccdbea850579..f351ea9f059c2c27642a11e492f39c1cb4731af1 100644 (file)
@@ -1,4 +1,5 @@
-<div class="cd-col-form">
+<div class="cd-col-form"
+     *cdFormLoading="loading">
   <form name="nfsForm"
         #formDir="ngForm"
         [formGroup]="nfsForm"
index d152eaa96e611de847ba0e76fc8aa64e9d9141da..0df8b002be6c776a3217339c1f75aadaccef3741 100644 (file)
@@ -13,6 +13,7 @@ import { SelectMessages } from '../../../shared/components/select/select-message
 import { SelectOption } from '../../../shared/components/select/select-option.model';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { Icons } from '../../../shared/enum/icons.enum';
+import { CdForm } from '../../../shared/forms/cd-form';
 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 import { CdValidators } from '../../../shared/forms/cd-validators';
@@ -29,7 +30,7 @@ import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.compo
   templateUrl: './nfs-form.component.html',
   styleUrls: ['./nfs-form.component.scss']
 })
-export class NfsFormComponent implements OnInit {
+export class NfsFormComponent extends CdForm implements OnInit {
   @ViewChild('nfsClients', { static: true })
   nfsClients: NfsFormClientComponent;
 
@@ -92,6 +93,7 @@ export class NfsFormComponent implements OnInit {
     private i18n: I18n,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.permission = this.authStorageService.getPermissions().pool;
     this.resource = this.i18n('NFS export');
     this.createForm();
@@ -137,6 +139,8 @@ export class NfsFormComponent implements OnInit {
       if (data[4]) {
         this.resolveModel(data[4]);
       }
+
+      this.loadingReady();
     });
   }
 
index aeb5d438184358a8cf6ef0a80e4fd9545a448330..c49c710e13fd62279c98e418dca536a62ac40048 100644 (file)
@@ -1,9 +1,6 @@
-<cd-loading-panel *ngIf="!(info && ecProfiles)"
-                  i18n>Loading...</cd-loading-panel>
-
-<div class="cd-col-form">
+<div class="cd-col-form"
+     *cdFormLoading="loading">
   <form name="form"
-        *ngIf="info && ecProfiles"
         #formDir="ngForm"
         [formGroup]="form"
         novalidate>
index c4ee052ae1f555d9c9fd7cc2cb0083b253e5da81..d172c7c45635208e87372ae1923741e77d2598b6 100644 (file)
@@ -201,6 +201,8 @@ describe('PoolFormComponent', () => {
     navigationSpy = spyOn(router, 'navigate').and.stub();
 
     setUpPoolComponent();
+
+    component.loadingReady();
   });
 
   it('should create', () => {
index 47519c0a0a34007aab3c4d81e6f6844676337631..5197e5e7023db247e5b8034f312913cbc082c401 100644 (file)
@@ -16,6 +16,7 @@ import { CriticalConfirmationModalComponent } from '../../../shared/components/c
 import { SelectOption } from '../../../shared/components/select/select-option.model';
 import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
 import { Icons } from '../../../shared/enum/icons.enum';
+import { CdForm } from '../../../shared/forms/cd-form';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 import { CdValidators } from '../../../shared/forms/cd-validators';
 import {
@@ -51,7 +52,7 @@ interface FormFieldDescription {
   templateUrl: './pool-form.component.html',
   styleUrls: ['./pool-form.component.scss']
 })
-export class PoolFormComponent implements OnInit {
+export class PoolFormComponent extends CdForm implements OnInit {
   @ViewChild('crushInfoTabs', { static: false }) crushInfoTabs: TabsetComponent;
   @ViewChild('crushDeletionBtn', { static: false }) crushDeletionBtn: TooltipDirective;
   @ViewChild('ecpInfoTabs', { static: false }) ecpInfoTabs: TabsetComponent;
@@ -99,6 +100,7 @@ export class PoolFormComponent implements OnInit {
     private i18n: I18n,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.editing = this.router.url.startsWith(`/pool/${URLVerbs.EDIT}`);
     this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
     this.resource = this.i18n('pool');
@@ -190,6 +192,7 @@ export class PoolFormComponent implements OnInit {
         this.initEditMode();
       } else {
         this.setAvailableApps();
+        this.loadingReady();
       }
       this.listenToChanges();
       this.setComplexValidators();
@@ -240,6 +243,7 @@ export class PoolFormComponent implements OnInit {
       this.poolService.get(param.name).subscribe((pool: Pool) => {
         this.data.pool = pool;
         this.initEditFormData(pool);
+        this.loadingReady();
       })
     );
   }
index df2d3ac369d08320b451f5e2c4f212352727e920..07aa631bf350bcbef08317a5143c002fdf870a61 100644 (file)
@@ -1,8 +1,5 @@
-<cd-loading-panel *ngIf="editing && loading && !error"
-                  i18n>Loading bucket data...</cd-loading-panel>
-
 <div class="cd-col-form"
-     *ngIf="!loading && !error">
+     *cdFormLoading="loading">
   <form name="bucketForm"
         #frm="ngForm"
         [formGroup]="bucketForm"
index b4013fff39ea42520ee95c31a3ee49209fa0f70b..52a7f22cef3768a4e9df3a3c8372cc6bf4bd1297 100644 (file)
@@ -11,6 +11,7 @@ import { of as observableOf } from 'rxjs';
 import { configureTestBed, FormHelper, i18nProviders } from '../../../../testing/unit-test-helper';
 import { RgwBucketService } from '../../../shared/api/rgw-bucket.service';
 import { RgwSiteService } from '../../../shared/api/rgw-site.service';
+import { RgwUserService } from '../../../shared/api/rgw-user.service';
 import { NotificationType } from '../../../shared/enum/notification-type.enum';
 import { NotificationService } from '../../../shared/services/notification.service';
 import { SharedModule } from '../../../shared/shared.module';
@@ -24,6 +25,7 @@ describe('RgwBucketFormComponent', () => {
   let rgwBucketService: RgwBucketService;
   let getPlacementTargetsSpy: jasmine.Spy;
   let rgwBucketServiceGetSpy: jasmine.Spy;
+  let enumerateSpy: jasmine.Spy;
   let formHelper: FormHelper;
 
   configureTestBed({
@@ -44,6 +46,7 @@ describe('RgwBucketFormComponent', () => {
     rgwBucketService = TestBed.get(RgwBucketService);
     rgwBucketServiceGetSpy = spyOn(rgwBucketService, 'get');
     getPlacementTargetsSpy = spyOn(TestBed.get(RgwSiteService), 'getPlacementTargets');
+    enumerateSpy = spyOn(TestBed.get(RgwUserService), 'enumerate');
     formHelper = new FormHelper(component.bucketForm);
   });
 
@@ -156,6 +159,7 @@ describe('RgwBucketFormComponent', () => {
         ]
       };
       getPlacementTargetsSpy.and.returnValue(observableOf(payload));
+      enumerateSpy.and.returnValue(observableOf([]));
       fixture.detectChanges();
 
       expect(component.zonegroup).toBe(payload.zonegroup);
@@ -226,6 +230,7 @@ describe('RgwBucketFormComponent', () => {
       component['route'].params = observableOf({ bid: 'bid' });
       component.editing = true;
       rgwBucketServiceGetSpy.and.returnValue(observableOf(fakeResponse));
+      enumerateSpy.and.returnValue(observableOf([]));
       component.ngOnInit();
       component.bucketForm.patchValue({
         versioning: versioningChecked,
index 6ee152f5781bd7ae773a4aebd26dba86054af113..3c9cba3d49bdae9590d060dda7196621b684c0d3 100644 (file)
@@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
 
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import * as _ from 'lodash';
+import { forkJoin } from 'rxjs';
 
 import { RgwBucketService } from '../../../shared/api/rgw-bucket.service';
 import { RgwSiteService } from '../../../shared/api/rgw-site.service';
@@ -11,6 +12,7 @@ import { RgwUserService } from '../../../shared/api/rgw-user.service';
 import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
 import { Icons } from '../../../shared/enum/icons.enum';
 import { NotificationType } from '../../../shared/enum/notification-type.enum';
+import { CdForm } from '../../../shared/forms/cd-form';
 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 import { CdValidators } from '../../../shared/forms/cd-validators';
@@ -23,11 +25,9 @@ import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
   templateUrl: './rgw-bucket-form.component.html',
   styleUrls: ['./rgw-bucket-form.component.scss']
 })
-export class RgwBucketFormComponent implements OnInit {
+export class RgwBucketFormComponent extends CdForm implements OnInit {
   bucketForm: CdFormGroup;
   editing = false;
-  error = false;
-  loading = false;
   owners: string[] = null;
   action: string;
   resource: string;
@@ -55,6 +55,7 @@ export class RgwBucketFormComponent implements OnInit {
     private i18n: I18n,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.editing = this.router.url.startsWith(`/rgw/bucket/${URLVerbs.EDIT}`);
     this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
     this.resource = this.i18n('bucket');
@@ -89,62 +90,69 @@ export class RgwBucketFormComponent implements OnInit {
   }
 
   ngOnInit() {
-    // Get the list of possible owners.
-    this.rgwUserService.enumerate().subscribe((resp: string[]) => {
-      this.owners = resp.sort();
-    });
+    const promises = {
+      owners: this.rgwUserService.enumerate()
+    };
 
     if (!this.editing) {
-      // Get placement targets:
-      this.rgwSiteService.getPlacementTargets().subscribe((placementTargets: any) => {
-        this.zonegroup = placementTargets['zonegroup'];
-        _.forEach(placementTargets['placement_targets'], (placementTarget) => {
-          placementTarget['description'] = `${placementTarget['name']} (${this.i18n('pool')}: ${
-            placementTarget['data_pool']
-          })`;
-          this.placementTargets.push(placementTarget);
-        });
-
-        // If there is only 1 placement target, select it by default:
-        if (this.placementTargets.length === 1) {
-          this.bucketForm.get('placement-target').setValue(this.placementTargets[0]['name']);
-        }
-      });
+      promises['getPlacementTargets'] = this.rgwSiteService.getPlacementTargets();
     }
 
     // Process route parameters.
     this.route.params.subscribe((params: { bid: string }) => {
-      if (!params.hasOwnProperty('bid')) {
-        return;
+      if (params.hasOwnProperty('bid')) {
+        const bid = decodeURIComponent(params.bid);
+        promises['getBid'] = this.rgwBucketService.get(bid);
       }
-      const bid = decodeURIComponent(params.bid);
-      this.loading = true;
 
-      this.rgwBucketService.get(bid).subscribe((resp: object) => {
-        this.loading = false;
+      forkJoin(promises).subscribe((data: any) => {
+        // Get the list of possible owners.
+        this.owners = (<string[]>data.owners).sort();
 
-        // Get the default values (incl. the values from disabled fields).
-        const defaults = _.clone(this.bucketForm.getRawValue());
+        // Get placement targets:
+        if (data['getPlacementTargets']) {
+          const placementTargets = data['getPlacementTargets'];
+          this.zonegroup = placementTargets['zonegroup'];
+          _.forEach(placementTargets['placement_targets'], (placementTarget) => {
+            placementTarget['description'] = `${placementTarget['name']} (${this.i18n('pool')}: ${
+              placementTarget['data_pool']
+            })`;
+            this.placementTargets.push(placementTarget);
+          });
 
-        // Get the values displayed in the form. We need to do that to
-        // extract those key/value pairs from the response data, otherwise
-        // the Angular react framework will throw an error if there is no
-        // field for a given key.
-        let value: object = _.pick(resp, _.keys(defaults));
-        value['placement-target'] = resp['placement_rule'];
-        value['versioning'] = resp['versioning'] === RgwBucketVersioning.ENABLED;
-        value['mfa-delete'] = resp['mfa_delete'] === RgwBucketMfaDelete.ENABLED;
+          // If there is only 1 placement target, select it by default:
+          if (this.placementTargets.length === 1) {
+            this.bucketForm.get('placement-target').setValue(this.placementTargets[0]['name']);
+          }
+        }
+
+        if (data['getBid']) {
+          const bidResp = data['getBid'];
+          // Get the default values (incl. the values from disabled fields).
+          const defaults = _.clone(this.bucketForm.getRawValue());
 
-        // Append default values.
-        value = _.merge(defaults, value);
+          // Get the values displayed in the form. We need to do that to
+          // extract those key/value pairs from the response data, otherwise
+          // the Angular react framework will throw an error if there is no
+          // field for a given key.
+          let value: object = _.pick(bidResp, _.keys(defaults));
+          value['placement-target'] = bidResp['placement_rule'];
+          value['versioning'] = bidResp['versioning'] === RgwBucketVersioning.ENABLED;
+          value['mfa-delete'] = bidResp['mfa_delete'] === RgwBucketMfaDelete.ENABLED;
 
-        // Update the form.
-        this.bucketForm.setValue(value);
-        if (this.editing) {
-          this.isVersioningAlreadyEnabled = this.isVersioningEnabled;
-          this.isMfaDeleteAlreadyEnabled = this.isMfaDeleteEnabled;
-          this.setMfaDeleteValidators();
+          // Append default values.
+          value = _.merge(defaults, value);
+
+          // Update the form.
+          this.bucketForm.setValue(value);
+          if (this.editing) {
+            this.isVersioningAlreadyEnabled = this.isVersioningEnabled;
+            this.isMfaDeleteAlreadyEnabled = this.isMfaDeleteEnabled;
+            this.setMfaDeleteValidators();
+          }
         }
+
+        this.loadingReady();
       });
     });
   }
index a34d3f9499028f34b7868fb27388b5572f6869b5..92888df49f45f9333775849b02493980093927f4 100644 (file)
@@ -1,12 +1,5 @@
-<cd-loading-panel *ngIf="editing && loading && !error"
-                  i18n>Loading user data...</cd-loading-panel>
-<cd-alert-panel type="error"
-                *ngIf="editing && error"
-                (backAction)="goToListView()"
-                i18n>The user data could not be loaded.</cd-alert-panel>
-
 <div class="cd-col-form"
-     *ngIf="!loading && !error">
+     *cdFormLoading="loading">
   <form #frm="ngForm"
         [formGroup]="userForm"
         novalidate>
index 77321d6578b8ba572ba31b78941c9695a7fb781b..19cdf6ad77960af9ba5cfde9e3d6d37336268c3b 100644 (file)
@@ -11,6 +11,7 @@ import { RgwUserService } from '../../../shared/api/rgw-user.service';
 import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
 import { Icons } from '../../../shared/enum/icons.enum';
 import { NotificationType } from '../../../shared/enum/notification-type.enum';
+import { CdForm } from '../../../shared/forms/cd-form';
 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 import { CdValidators, isEmptyInputValue } from '../../../shared/forms/cd-validators';
@@ -31,11 +32,9 @@ import { RgwUserSwiftKeyModalComponent } from '../rgw-user-swift-key-modal/rgw-u
   templateUrl: './rgw-user-form.component.html',
   styleUrls: ['./rgw-user-form.component.scss']
 })
-export class RgwUserFormComponent implements OnInit {
+export class RgwUserFormComponent extends CdForm implements OnInit {
   userForm: CdFormGroup;
   editing = false;
-  error = false;
-  loading = false;
   submitObservables: Observable<Object>[] = [];
   icons = Icons;
   subusers: RgwUserSubuser[] = [];
@@ -59,6 +58,7 @@ export class RgwUserFormComponent implements OnInit {
     private i18n: I18n,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.resource = this.i18n('user');
     this.subuserLabel = this.i18n('subuser');
     this.s3keyLabel = this.i18n('S3 Key');
@@ -155,17 +155,16 @@ export class RgwUserFormComponent implements OnInit {
     // Process route parameters.
     this.route.params.subscribe((params: { uid: string }) => {
       if (!params.hasOwnProperty('uid')) {
+        this.loadingReady();
         return;
       }
       const uid = decodeURIComponent(params.uid);
-      this.loading = true;
       // Load the user and quota information.
       const observables = [];
       observables.push(this.rgwUserService.get(uid));
       observables.push(this.rgwUserService.getQuota(uid));
       observableForkJoin(observables).subscribe(
         (resp: any[]) => {
-          this.loading = false;
           // Get the default values.
           const defaults = _.clone(this.userForm.value);
           // Extract the values displayed in the form.
@@ -223,9 +222,11 @@ export class RgwUserFormComponent implements OnInit {
             }
           });
           this.capabilities = resp[0].caps;
+
+          this.loadingReady();
         },
-        (error) => {
-          this.error = error;
+        () => {
+          this.loadingError();
         }
       );
     });
index e7a6ee4a7e57d6849b94e1e0a56dbf0b36139e26..00260011115ba5f396bc4fb2f3cea694cd0b2df5 100644 (file)
@@ -1,4 +1,5 @@
-<div class="cd-col-form">
+<div class="cd-col-form"
+     *cdFormLoading="loading">
   <form name="roleForm"
         #formDir="ngForm"
         [formGroup]="roleForm"
index a5b5fb7a9123d2f4440d6344db7a7605908418ae..24e38b2d6b96c9a4906c30b39f0f020f1455d9fb 100644 (file)
@@ -11,6 +11,7 @@ import { RoleService } from '../../../shared/api/role.service';
 import { ScopeService } from '../../../shared/api/scope.service';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { NotificationType } from '../../../shared/enum/notification-type.enum';
+import { CdForm } from '../../../shared/forms/cd-form';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 import { CdValidators } from '../../../shared/forms/cd-validators';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
@@ -23,7 +24,7 @@ import { RoleFormModel } from './role-form.model';
   templateUrl: './role-form.component.html',
   styleUrls: ['./role-form.component.scss']
 })
-export class RoleFormComponent implements OnInit {
+export class RoleFormComponent extends CdForm implements OnInit {
   @ViewChild('headerPermissionCheckboxTpl', { static: true })
   headerPermissionCheckboxTpl: TemplateRef<any>;
   @ViewChild('cellScopeCheckboxTpl', { static: true })
@@ -55,6 +56,7 @@ export class RoleFormComponent implements OnInit {
     private i18n: I18n,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.resource = this.i18n('role');
     this.createForm();
     this.listenToChanges();
@@ -131,6 +133,8 @@ export class RoleFormComponent implements OnInit {
     this.scopeService.list().subscribe((scopes: Array<string>) => {
       this.scopes = scopes;
       this.roleForm.get('scopes_permissions').setValue({});
+
+      this.loadingReady();
     });
   }
 
@@ -147,6 +151,8 @@ export class RoleFormComponent implements OnInit {
         ['name', 'description', 'scopes_permissions'].forEach((key) =>
           this.roleForm.get(key).setValue(resp[1][key])
         );
+
+        this.loadingReady();
       });
     });
   }
index e0a27c9a0c233235b9931268d007124f35478c7d..abdcaffea9f4df87d991358db51f218e35819116 100644 (file)
@@ -1,11 +1,8 @@
-<cd-loading-panel *ngIf="!pwdExpirationSettings"
-                  i18n>Loading...</cd-loading-panel>
-
-<div class="cd-col-form">
+<div class="cd-col-form"
+     *cdFormLoading="loading">
   <form name="userForm"
         #formDir="ngForm"
         [formGroup]="userForm"
-        *ngIf="pwdExpirationSettings"
         novalidate>
     <div class="card">
       <div i18n="form title|Example: Create Pool@@formTitle"
index 952508ca0d018665b93eb24d4a956cb7c1123735..9f2f4de895c3442470be1a5ff4c2df1c9933c64e 100644 (file)
@@ -16,6 +16,7 @@ import { SelectMessages } from '../../../shared/components/select/select-message
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { Icons } from '../../../shared/enum/icons.enum';
 import { NotificationType } from '../../../shared/enum/notification-type.enum';
+import { CdForm } from '../../../shared/forms/cd-form';
 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 import { CdValidators } from '../../../shared/forms/cd-validators';
@@ -32,7 +33,7 @@ import { UserFormModel } from './user-form.model';
   templateUrl: './user-form.component.html',
   styleUrls: ['./user-form.component.scss']
 })
-export class UserFormComponent implements OnInit {
+export class UserFormComponent extends CdForm implements OnInit {
   @ViewChild('removeSelfUserReadUpdatePermissionTpl', { static: true })
   removeSelfUserReadUpdatePermissionTpl: TemplateRef<any>;
 
@@ -73,6 +74,7 @@ export class UserFormComponent implements OnInit {
     private formBuilder: CdFormBuilder,
     private settingsService: SettingsService
   ) {
+    super();
     this.resource = this.i18n('user');
     this.createForm();
     this.messages = new SelectMessages({ empty: this.i18n('There are no roles.') }, this.i18n);
@@ -149,6 +151,8 @@ export class UserFormComponent implements OnInit {
             pwdExpirationDateField.setValue(expirationDate);
             pwdExpirationDateField.setValidators([Validators.required]);
           }
+
+          this.loadingReady();
         }
       }
     );
@@ -161,6 +165,8 @@ export class UserFormComponent implements OnInit {
       this.userService.get(username).subscribe((userFormModel: UserFormModel) => {
         this.response = _.cloneDeep(userFormModel);
         this.setResponse(userFormModel);
+
+        this.loadingReady();
       });
     });
   }
index a1b4c9a04b501f243d92dce51707730f65eb5169..f216ac6d6b80b30f06dbb6d0c4610d618a283866 100644 (file)
 <ng-template #content>
   <ng-content></ng-content>
 </ng-template>
-
-<div class="text-right"
-     *ngIf="backAction.observers.length > 0">
-  <button class="btn btn-light tc_backButton"
-          type="button"
-          (click)="backAction.emit()"
-          autofocus
-          i18n>Back</button>
-</div>
index 5798d24df11a37671bcfa85b3935b696345d0ec8..10e0801e8ea076ed22aeb424343d0d1402a5bdc8 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { Component, Input, OnInit } from '@angular/core';
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import { Icons } from '../../enum/icons.enum';
 
@@ -12,8 +12,6 @@ export class AlertPanelComponent implements OnInit {
   title = '';
   @Input()
   bootstrapClass = '';
-  @Output()
-  backAction = new EventEmitter();
   @Input()
   type: 'warning' | 'error' | 'info' | 'success';
   @Input()
index 65a028883396675d1616628e426cfad778a62d4b..82db2629053339862aac709b83aa3808e09d77da 100644 (file)
@@ -8,6 +8,8 @@ import { Observable, Subscriber, timer as observableTimer } from 'rxjs';
 
 import { configureTestBed, modalServiceShow } from '../../../../testing/unit-test-helper';
 import { DirectivesModule } from '../../directives/directives.module';
+import { AlertPanelComponent } from '../alert-panel/alert-panel.component';
+import { LoadingPanelComponent } from '../loading-panel/loading-panel.component';
 import { CriticalConfirmationModalComponent } from './critical-confirmation-modal.component';
 
 @NgModule({
@@ -94,7 +96,12 @@ describe('CriticalConfirmationModalComponent', () => {
   let fixture: ComponentFixture<CriticalConfirmationModalComponent>;
 
   configureTestBed({
-    declarations: [MockComponent, CriticalConfirmationModalComponent],
+    declarations: [
+      MockComponent,
+      CriticalConfirmationModalComponent,
+      LoadingPanelComponent,
+      AlertPanelComponent
+    ],
     schemas: [NO_ERRORS_SCHEMA],
     imports: [ModalModule.forRoot(), ReactiveFormsModule, MockModule, DirectivesModule],
     providers: [BsModalRef]
index a07b9f91f31654e3686e190eaaa64495d8c7053e..785f08606b73acb41c429d0f21f925d2b43cc8c2 100644 (file)
@@ -1,7 +1,8 @@
 <alert type="info">
   <strong>
     <i [ngClass]="[icons.spinner, icons.spin]"
-       aria-hidden="true"></i>
+       aria-hidden="true"
+       class="mr-2"></i>
   </strong>
   <ng-content></ng-content>
 </alert>
index 1ed763052b2e8547a93294d9e1c92bb987e1cdea..1fdc7a9ba076ae4271f8b8986c6d55272bd63f55 100644 (file)
@@ -1,9 +1,12 @@
 import { NgModule } from '@angular/core';
 
+import { AlertPanelComponent } from '../components/alert-panel/alert-panel.component';
+import { LoadingPanelComponent } from '../components/loading-panel/loading-panel.component';
 import { AutofocusDirective } from './autofocus.directive';
 import { Copy2ClipboardButtonDirective } from './copy2clipboard-button.directive';
 import { DimlessBinaryPerSecondDirective } from './dimless-binary-per-second.directive';
 import { DimlessBinaryDirective } from './dimless-binary.directive';
+import { FormLoadingDirective } from './form-loading.directive';
 import { IopsDirective } from './iops.directive';
 import { MillisecondsDirective } from './milliseconds.directive';
 import { PasswordButtonDirective } from './password-button.directive';
@@ -19,7 +22,8 @@ import { TrimDirective } from './trim.directive';
     PasswordButtonDirective,
     TrimDirective,
     MillisecondsDirective,
-    IopsDirective
+    IopsDirective,
+    FormLoadingDirective
   ],
   exports: [
     AutofocusDirective,
@@ -29,8 +33,10 @@ import { TrimDirective } from './trim.directive';
     PasswordButtonDirective,
     TrimDirective,
     MillisecondsDirective,
-    IopsDirective
+    IopsDirective,
+    FormLoadingDirective
   ],
-  providers: []
+  providers: [],
+  entryComponents: [LoadingPanelComponent, AlertPanelComponent]
 })
 export class DirectivesModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.spec.ts
new file mode 100644 (file)
index 0000000..e396ecb
--- /dev/null
@@ -0,0 +1,85 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+
+import { AlertModule } from 'ngx-bootstrap/alert';
+
+import { configureTestBed, i18nProviders } from '../../../testing/unit-test-helper';
+import { CdForm } from '../forms/cd-form';
+import { SharedModule } from '../shared.module';
+import { FormLoadingDirective } from './form-loading.directive';
+
+@Component({ selector: 'cd-test-cmp', template: '<span *cdFormLoading="loading">foo</span>' })
+class TestComponent extends CdForm {
+  constructor() {
+    super();
+  }
+}
+
+describe('FormLoadingDirective', () => {
+  let component: TestComponent;
+  let fixture: ComponentFixture<any>;
+
+  const expectShown = (elem: number, error: number, loading: number) => {
+    expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(elem);
+    expect(fixture.debugElement.queryAll(By.css('cd-alert-panel')).length).toEqual(error);
+    expect(fixture.debugElement.queryAll(By.css('cd-loading-panel')).length).toEqual(loading);
+  };
+
+  configureTestBed({
+    declarations: [TestComponent],
+    imports: [AlertModule.forRoot(), SharedModule],
+    providers: [i18nProviders]
+  });
+
+  afterEach(() => {
+    fixture = null;
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(TestComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create an instance', () => {
+    const directive = new FormLoadingDirective(null, null, null, null);
+    expect(directive).toBeTruthy();
+  });
+
+  it('should show loading component by default', () => {
+    expectShown(0, 0, 1);
+
+    const alert = fixture.debugElement.nativeElement.querySelector('cd-loading-panel alert');
+    expect(alert.textContent).toBe('Loading form data...');
+  });
+
+  it('should show error component when calling loadingError()', () => {
+    component.loadingError();
+    fixture.detectChanges();
+
+    expectShown(0, 1, 0);
+
+    const alert = fixture.debugElement.nativeElement.querySelector(
+      'cd-alert-panel .alert-panel-text'
+    );
+    expect(alert.textContent).toBe('Form data could not be loaded.');
+  });
+
+  it('should show original component when calling loadingReady()', () => {
+    component.loadingReady();
+    fixture.detectChanges();
+
+    expectShown(1, 0, 0);
+
+    const alert = fixture.debugElement.nativeElement.querySelector('span');
+    expect(alert.textContent).toBe('foo');
+  });
+
+  it('should show nothing when calling loadingNone()', () => {
+    component.loadingNone();
+    fixture.detectChanges();
+
+    expectShown(0, 0, 0);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.ts
new file mode 100644 (file)
index 0000000..9b06394
--- /dev/null
@@ -0,0 +1,54 @@
+import {
+  ComponentFactoryResolver,
+  Directive,
+  Input,
+  TemplateRef,
+  ViewContainerRef
+} from '@angular/core';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+
+import { AlertPanelComponent } from '../components/alert-panel/alert-panel.component';
+import { LoadingPanelComponent } from '../components/loading-panel/loading-panel.component';
+import { LoadingStatus } from '../forms/cd-form';
+
+@Directive({
+  selector: '[cdFormLoading]'
+})
+export class FormLoadingDirective {
+  constructor(
+    private templateRef: TemplateRef<any>,
+    private viewContainer: ViewContainerRef,
+    private componentFactoryResolver: ComponentFactoryResolver,
+    private i18n: I18n
+  ) {}
+
+  @Input('cdFormLoading') set cdFormLoading(condition: LoadingStatus) {
+    let factory: any;
+    let content: any;
+
+    this.viewContainer.clear();
+
+    switch (condition) {
+      case LoadingStatus.Loading:
+        factory = this.componentFactoryResolver.resolveComponentFactory(LoadingPanelComponent);
+        content = this.resolveNgContent(this.i18n(`Loading form data...`));
+        this.viewContainer.createComponent(factory, null, null, content);
+        break;
+      case LoadingStatus.Ready:
+        this.viewContainer.createEmbeddedView(this.templateRef);
+        break;
+      case LoadingStatus.Error:
+        factory = this.componentFactoryResolver.resolveComponentFactory(AlertPanelComponent);
+        content = this.resolveNgContent(this.i18n(`Form data could not be loaded.`));
+        const componentRef = this.viewContainer.createComponent(factory, null, null, content);
+        (<AlertPanelComponent>componentRef.instance).type = 'error';
+        break;
+    }
+  }
+
+  resolveNgContent(content: string) {
+    const element = document.createTextNode(content);
+    return [[element]];
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.spec.ts
new file mode 100644 (file)
index 0000000..445c31f
--- /dev/null
@@ -0,0 +1,32 @@
+import { CdForm, LoadingStatus } from './cd-form';
+
+describe('CdForm', () => {
+  let form: CdForm;
+
+  beforeEach(() => {
+    form = new CdForm();
+  });
+
+  describe('loading', () => {
+    it('should start in loading state', () => {
+      expect(form.loading).toBe(LoadingStatus.Loading);
+    });
+
+    it('should change to ready when calling loadingReady', () => {
+      form.loadingReady();
+      expect(form.loading).toBe(LoadingStatus.Ready);
+    });
+
+    it('should change to error state calling loadingError', () => {
+      form.loadingError();
+      expect(form.loading).toBe(LoadingStatus.Error);
+    });
+
+    it('should change to loading state calling loadingStart', () => {
+      form.loadingError();
+      expect(form.loading).toBe(LoadingStatus.Error);
+      form.loadingStart();
+      expect(form.loading).toBe(LoadingStatus.Loading);
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.ts
new file mode 100644 (file)
index 0000000..6fcb40e
--- /dev/null
@@ -0,0 +1,26 @@
+export enum LoadingStatus {
+  Loading,
+  Ready,
+  Error,
+  None
+}
+
+export class CdForm {
+  loading = LoadingStatus.Loading;
+
+  loadingStart() {
+    this.loading = LoadingStatus.Loading;
+  }
+
+  loadingReady() {
+    this.loading = LoadingStatus.Ready;
+  }
+
+  loadingError() {
+    this.loading = LoadingStatus.Error;
+  }
+
+  loadingNone() {
+    this.loading = LoadingStatus.None;
+  }
+}