From: Tiago Melo Date: Fri, 24 Apr 2020 11:20:51 +0000 (+0000) Subject: mgr/dashboard: Forms now wait for all data to load until displayed X-Git-Tag: wip-pdonnell-testing-20200918.022351~1298^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=8afe26213d74e49c9735cb69a4a06cbd1b705da2;p=ceph-ci.git mgr/dashboard: Forms now wait for all data to load until displayed 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 --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.html index 217ec33a32d..dc432a095fa 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.html @@ -1,9 +1,9 @@ -
+
+ novalidate>
{{ action | titlecase }} {{ resource | upperFirst }}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts index 5c4d362bae0..a5d2acf60d3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts @@ -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(); }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html index 3b4315df1b1..75f86642df3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html @@ -1,4 +1,5 @@ -
+
{ fixture = TestBed.createComponent(RbdFormComponent); component = fixture.componentInstance; activatedRoute = TestBed.get(ActivatedRoute); + + component.loadingReady(); }); it('should create', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts index 7820ad7d8ee..112e2de3005 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts @@ -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) => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html index eff51522301..ab23e58c59f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html @@ -1,4 +1,5 @@ -
+
{ this.setResponse(resp); + this.loadingReady(); }); }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html index c224c713180..0fcf0cd1a5f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html @@ -1,9 +1,6 @@ -Loading... - -
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts index bfa2987107a..1949338594a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts @@ -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(); }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html index f246824f084..aef2f59679f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html @@ -1,11 +1,5 @@ -Loading configuration... -The configuration could not be loaded. -
+ *cdFormLoading="loading"> { 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(); } ); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html index b3f02dcf160..998d5d9856b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html @@ -1,6 +1,7 @@ +
+ *cdFormLoading="loading"> = []; - 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(); } ); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.html index 8d02a031a24..6aba5b50430 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.html @@ -1,11 +1,5 @@ -Loading configuration... -The configuration could not be loaded. -
+ *cdFormLoading="loading">
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.ts index b258d2c1869..c1d1640b53d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.ts @@ -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(); } ); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html index ad2cab02246..f351ea9f059 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html @@ -1,4 +1,5 @@ -
+
Loading... - -
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts index c4ee052ae1f..d172c7c4563 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts @@ -201,6 +201,8 @@ describe('PoolFormComponent', () => { navigationSpy = spyOn(router, 'navigate').and.stub(); setUpPoolComponent(); + + component.loadingReady(); }); it('should create', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts index 47519c0a0a3..5197e5e7023 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts @@ -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(); }) ); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html index df2d3ac369d..07aa631bf35 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html @@ -1,8 +1,5 @@ -Loading bucket data... -
+ *cdFormLoading="loading"> { 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, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts index 6ee152f5781..3c9cba3d49b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts @@ -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 = (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(); }); }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html index a34d3f94990..92888df49f4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html @@ -1,12 +1,5 @@ -Loading user data... -The user data could not be loaded. -
+ *cdFormLoading="loading"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts index 77321d6578b..19cdf6ad779 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts @@ -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[] = []; 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(); } ); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html index e7a6ee4a7e5..00260011115 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html @@ -1,4 +1,5 @@ -
+
; @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) => { 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(); }); }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html index e0a27c9a0c2..abdcaffea9f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html @@ -1,11 +1,8 @@ -Loading... - -
+
; @@ -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(); }); }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html index a1b4c9a04b5..f216ac6d6b8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html @@ -38,12 +38,3 @@ - -
- -
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts index 5798d24df11..10e0801e8ea 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts @@ -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() diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts index 65a02888339..82db2629053 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts @@ -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; configureTestBed({ - declarations: [MockComponent, CriticalConfirmationModalComponent], + declarations: [ + MockComponent, + CriticalConfirmationModalComponent, + LoadingPanelComponent, + AlertPanelComponent + ], schemas: [NO_ERRORS_SCHEMA], imports: [ModalModule.forRoot(), ReactiveFormsModule, MockModule, DirectivesModule], providers: [BsModalRef] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.html index a07b9f91f31..785f08606b7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/loading-panel/loading-panel.component.html @@ -1,7 +1,8 @@ + aria-hidden="true" + class="mr-2"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts index 1ed763052b2..1fdc7a9ba07 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts @@ -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 index 00000000000..e396ecb4cb6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.spec.ts @@ -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: 'foo' }) +class TestComponent extends CdForm { + constructor() { + super(); + } +} + +describe('FormLoadingDirective', () => { + let component: TestComponent; + let fixture: ComponentFixture; + + 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 index 00000000000..9b063948520 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/form-loading.directive.ts @@ -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, + 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); + (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 index 00000000000..445c31faf1d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.spec.ts @@ -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 index 00000000000..6fcb40e7df2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-form.ts @@ -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; + } +}