From 9de2da19a7ffab373886f74c8c1e39abe7aaaa07 Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Sun, 4 Jul 2021 18:46:45 +0530 Subject: [PATCH 1/2] mgr/dashboard: Routable Modal for Add Host Form - Used routed modal for host Additon form - Renamed the Create to Add in Host Form Fixes: https://tracker.ceph.com/issues/51517 Signed-off-by: Nizamudeen A --- .../cypress/integration/cluster/hosts.po.ts | 17 +- .../orchestrator/01-hosts.e2e-spec.ts | 4 +- .../workflow/01-hosts.e2e-spec.ts | 4 +- .../frontend/src/app/app-routing.module.ts | 6 +- .../hosts/host-form/host-form.component.html | 159 +++++++++--------- .../hosts/host-form/host-form.component.ts | 2 +- .../ceph/cluster/hosts/hosts.component.html | 1 + .../cluster/hosts/hosts.component.spec.ts | 18 +- .../app/ceph/cluster/hosts/hosts.component.ts | 13 +- .../components/modal/modal.component.html | 30 ++-- .../components/modal/modal.component.spec.ts | 15 +- .../components/modal/modal.component.ts | 9 +- .../src/styles/ceph-custom/_forms.scss | 11 ++ 13 files changed, 164 insertions(+), 125 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts index 6752fe9e7870c..7a7e00d6648ac 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts @@ -2,7 +2,7 @@ import { PageHelper } from '../page-helper.po'; const pages = { index: { url: '#/hosts', id: 'cd-hosts' }, - create: { url: '#/hosts/create', id: 'cd-host-form' } + add: { url: '#/hosts/(modal:add)', id: 'cd-host-form' } }; export class HostsPageHelper extends PageHelper { @@ -49,21 +49,20 @@ export class HostsPageHelper extends PageHelper { }); } - @PageHelper.restrictTo(pages.create.url) + @PageHelper.restrictTo(pages.add.url) add(hostname: string, exist?: boolean, maintenance?: boolean) { - cy.get(`${this.pages.create.id}`).within(() => { + cy.get(`${this.pages.add.id}`).within(() => { cy.get('#hostname').type(hostname); if (maintenance) { cy.get('label[for=maintenance]').click(); } + if (exist) { + cy.get('#hostname').should('have.class', 'ng-invalid'); + } cy.get('cd-submit-button').click(); }); - if (exist) { - cy.get('#hostname').should('have.class', 'ng-invalid'); - } else { - // back to host list - cy.get(`${this.pages.index.id}`); - } + // back to host list + cy.get(`${this.pages.index.id}`); } @PageHelper.restrictTo(pages.index.url) diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/01-hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/01-hosts.e2e-spec.ts index cf85642a1b1d2..6c79a74662dff 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/01-hosts.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/01-hosts.e2e-spec.ts @@ -17,7 +17,7 @@ describe('Hosts page', () => { it('should not add an exsiting host', function () { const hostname = Cypress._.sample(this.hosts).name; - hosts.navigateTo('create'); + hosts.navigateTo('add'); hosts.add(hostname, true); }); @@ -26,7 +26,7 @@ describe('Hosts page', () => { hosts.delete(host); // add it back - hosts.navigateTo('create'); + hosts.navigateTo('add'); hosts.add(host); hosts.checkExist(host, true); }); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/01-hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/01-hosts.e2e-spec.ts index b1c8ad0bbc015..60d442b61a3f1 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/01-hosts.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/01-hosts.e2e-spec.ts @@ -4,7 +4,7 @@ describe('Hosts page', () => { const hosts = new HostsPageHelper(); const hostnames = ['ceph-node-00.cephlab.com', 'ceph-node-01.cephlab.com']; const addHost = (hostname: string, exist?: boolean, maintenance?: boolean) => { - hosts.navigateTo('create'); + hosts.navigateTo('add'); hosts.add(hostname, exist, maintenance); hosts.checkExist(hostname, true); }; @@ -37,7 +37,7 @@ describe('Hosts page', () => { }); it('should not add an existing host', function () { - hosts.navigateTo('create'); + hosts.navigateTo('add'); hosts.add(hostnames[0], true); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 0c470449a7916..b28f7213bc403 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -105,13 +105,13 @@ const routes: Routes = [ }, { path: 'hosts', + component: HostsComponent, data: { breadcrumbs: 'Cluster/Hosts' }, children: [ - { path: '', component: HostsComponent }, { - path: URLVerbs.CREATE, + path: URLVerbs.ADD, component: HostFormComponent, - data: { breadcrumbs: ActionLabels.CREATE } + outlet: 'modal' } ] }, 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 2296d7ddae847..b690227e16971 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,91 +1,94 @@ -
-
-
-
{{ action | titlecase }} {{ resource | upperFirst }}
+ + {{ action | titlecase }} {{ resource | upperFirst }} -
+ - -
- -
- - This field is required. - The chosen hostname is already in use. +
+ + + - -
- -
- - The value is not a valid IP address. + +
+ +
+ + The value is not a valid IP address. +
-
- -
- -
- - + +
+ +
+ + +
-
- -
-
-
- - + +
+
+
+ + +
-
- + +
- -
+ + 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 b90312ff855f2..474e2f1df3a37 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 @@ -39,7 +39,7 @@ export class HostFormComponent extends CdForm implements OnInit { ) { super(); this.resource = $localize`host`; - this.action = this.actionLabels.CREATE; + this.action = this.actionLabels.ADD; this.createForm(); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html index f31adf9e5c0e7..c90135c40a207 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html @@ -66,3 +66,4 @@ Are you sure you want to continue? + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts index c92188ed05411..38f6a81bbb950 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts @@ -182,7 +182,7 @@ describe('HostsComponent', () => { const tests = [ { expectResults: { - Create: { disabled: false, disableDesc: '' }, + Add: { disabled: false, disableDesc: '' }, Edit: { disabled: true, disableDesc: '' }, Delete: { disabled: true, disableDesc: '' } } @@ -190,7 +190,7 @@ describe('HostsComponent', () => { { selectRow: fakeHosts[0], // non-orchestrator host expectResults: { - Create: { disabled: false, disableDesc: '' }, + Add: { disabled: false, disableDesc: '' }, Edit: { disabled: true, disableDesc: component.messages.nonOrchHost }, Delete: { disabled: true, disableDesc: component.messages.nonOrchHost } } @@ -198,7 +198,7 @@ describe('HostsComponent', () => { { selectRow: fakeHosts[1], // orchestrator host expectResults: { - Create: { disabled: false, disableDesc: '' }, + Add: { disabled: false, disableDesc: '' }, Edit: { disabled: false, disableDesc: '' }, Delete: { disabled: false, disableDesc: '' } } @@ -222,7 +222,7 @@ describe('HostsComponent', () => { const tests = [ { expectResults: { - Create: resultNoOrchestrator, + Add: resultNoOrchestrator, Edit: { disabled: true, disableDesc: '' }, Delete: { disabled: true, disableDesc: '' } } @@ -230,7 +230,7 @@ describe('HostsComponent', () => { { selectRow: fakeHosts[0], // non-orchestrator host expectResults: { - Create: resultNoOrchestrator, + Add: resultNoOrchestrator, Edit: { disabled: true, disableDesc: component.messages.nonOrchHost }, Delete: { disabled: true, disableDesc: component.messages.nonOrchHost } } @@ -238,7 +238,7 @@ describe('HostsComponent', () => { { selectRow: fakeHosts[1], // orchestrator host expectResults: { - Create: resultNoOrchestrator, + Add: resultNoOrchestrator, Edit: resultNoOrchestrator, Delete: resultNoOrchestrator } @@ -255,7 +255,7 @@ describe('HostsComponent', () => { const tests = [ { expectResults: { - Create: resultMissingFeatures, + Add: resultMissingFeatures, Edit: { disabled: true, disableDesc: '' }, Delete: { disabled: true, disableDesc: '' } } @@ -263,7 +263,7 @@ describe('HostsComponent', () => { { selectRow: fakeHosts[0], // non-orchestrator host expectResults: { - Create: resultMissingFeatures, + Add: resultMissingFeatures, Edit: { disabled: true, disableDesc: component.messages.nonOrchHost }, Delete: { disabled: true, disableDesc: component.messages.nonOrchHost } } @@ -271,7 +271,7 @@ describe('HostsComponent', () => { { selectRow: fakeHosts[1], // orchestrator host expectResults: { - Create: resultMissingFeatures, + Add: resultMissingFeatures, Edit: resultMissingFeatures, Delete: resultMissingFeatures } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts index e4f042699eb39..8a501d18575a7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts @@ -11,7 +11,7 @@ import { ConfirmationModalComponent } from '~/app/shared/components/confirmation import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component'; import { SelectMessages } from '~/app/shared/components/select/select-messages.model'; -import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; import { TableComponent } from '~/app/shared/datatable/table/table.component'; import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; import { Icons } from '~/app/shared/enum/icons.enum'; @@ -67,7 +67,7 @@ export class HostsComponent extends ListWithDetails implements OnInit { orchStatus: OrchestratorStatus; actionOrchFeatures = { - create: [OrchestratorFeature.HOST_CREATE], + add: [OrchestratorFeature.HOST_CREATE], edit: [OrchestratorFeature.HOST_LABEL_ADD, OrchestratorFeature.HOST_LABEL_REMOVE], delete: [OrchestratorFeature.HOST_DELETE], maintenance: [ @@ -80,7 +80,6 @@ export class HostsComponent extends ListWithDetails implements OnInit { private authStorageService: AuthStorageService, private hostService: HostService, private cephShortVersionPipe: CephShortVersionPipe, - private urlBuilder: URLBuilderService, private actionLabels: ActionLabelsI18n, private modalService: ModalService, private taskWrapper: TaskWrapperService, @@ -92,11 +91,11 @@ export class HostsComponent extends ListWithDetails implements OnInit { this.permissions = this.authStorageService.getPermissions(); this.tableActions = [ { - name: this.actionLabels.CREATE, + name: this.actionLabels.ADD, permission: 'create', icon: Icons.add, - click: () => this.router.navigate([this.urlBuilder.getCreate()]), - disable: (selection: CdTableSelection) => this.getDisable('create', selection) + click: () => this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.ADD] } }]), + disable: (selection: CdTableSelection) => this.getDisable('add', selection) }, { name: this.actionLabels.EDIT, @@ -287,7 +286,7 @@ export class HostsComponent extends ListWithDetails implements OnInit { } getDisable( - action: 'create' | 'edit' | 'delete' | 'maintenance', + action: 'add' | 'edit' | 'delete' | 'maintenance', selection: CdTableSelection ): boolean | string { if (action === 'delete' || action === 'edit' || action === 'maintenance') { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.html index 5cbd4f58c52fa..657e0d6053f89 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.html @@ -1,13 +1,19 @@ - +
+
+ +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.spec.ts index d3ee1ca2abd19..cf08bef10090d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.spec.ts @@ -1,4 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; @@ -8,14 +10,18 @@ import { ModalComponent } from './modal.component'; describe('ModalComponent', () => { let component: ModalComponent; let fixture: ComponentFixture; + let routerNavigateSpy: jasmine.Spy; configureTestBed({ - declarations: [ModalComponent] + declarations: [ModalComponent], + imports: [RouterTestingModule] }); beforeEach(() => { fixture = TestBed.createComponent(ModalComponent); component = fixture.componentInstance; + routerNavigateSpy = spyOn(TestBed.inject(Router), 'navigate'); + routerNavigateSpy.and.returnValue(true); fixture.detectChanges(); }); @@ -38,4 +44,11 @@ describe('ModalComponent', () => { component.close(); expect(component.modalRef.close).toHaveBeenCalled(); }); + + it('should hide the routed modal', () => { + component.pageURL = 'hosts'; + component.close(); + expect(routerNavigateSpy).toHaveBeenCalledTimes(1); + expect(routerNavigateSpy).toHaveBeenCalledWith(['hosts', { outlets: { modal: null } }]); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.ts index 730da6d62527b..25e06e62af188 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/modal/modal.component.ts @@ -1,4 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Router } from '@angular/router'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; @@ -10,6 +11,8 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; export class ModalComponent { @Input() modalRef: NgbActiveModal; + @Input() + pageURL: string; /** * Should be a function that is triggered when the modal is hidden. @@ -17,8 +20,12 @@ export class ModalComponent { @Output() hide = new EventEmitter(); + constructor(private router: Router) {} + close() { - this.modalRef?.close(); + this.pageURL + ? this.router.navigate([this.pageURL, { outlets: { modal: null } }]) + : this.modalRef?.close(); this.hide.emit(); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_forms.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_forms.scss index cca9bd5d5d9d8..3c6ddbf80c998 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_forms.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_forms.scss @@ -53,6 +53,17 @@ } cd-modal { + .modal { + /* stylelint-disable */ + background-color: rgba(0, 0, 0, 0.4); + /* stylelint-enable */ + display: block; + } + + .modal-dialog { + max-width: 70vh; + } + .cd-col-form-label { @extend .col-lg-4; } From 03075c53b2605c83645611709de29fc393920dad Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Mon, 10 May 2021 11:37:51 +0530 Subject: [PATCH 2/2] mgr/dashboard: Cluster Creation Add Host Section Add host section of the cluster creation workflow. Fixes: https://tracker.ceph.com/issues/50565 Signed-off-by: Nizamudeen A --- .../frontend/src/app/app-routing.module.ts | 7 ++ .../src/app/ceph/cluster/cluster.module.ts | 1 - .../create-cluster.component.html | 42 +++++++- .../create-cluster.component.scss | 21 ++++ .../create-cluster.component.spec.ts | 69 ++++++++++++- .../create-cluster.component.ts | 98 ++++++++++++++++--- .../hosts/host-form/host-form.component.html | 2 +- .../host-form/host-form.component.spec.ts | 6 ++ .../hosts/host-form/host-form.component.ts | 9 +- .../ceph/cluster/hosts/hosts.component.html | 7 +- .../cluster/hosts/hosts.component.spec.ts | 1 + .../app/ceph/cluster/hosts/hosts.component.ts | 32 ++++-- .../osd-flags-indiv-modal.component.spec.ts | 4 +- .../shared/components/components.module.ts | 7 +- .../components/wizard/wizard.component.html | 21 ++++ .../components/wizard/wizard.component.scss | 20 ++++ .../wizard/wizard.component.spec.ts | 25 +++++ .../components/wizard/wizard.component.ts | 41 ++++++++ .../src/app/shared/models/wizard-steps.ts | 4 + .../services/wizard-steps.service.spec.ts | 16 +++ .../shared/services/wizard-steps.service.ts | 58 +++++++++++ 21 files changed, 451 insertions(+), 40 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/models/wizard-steps.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index b28f7213bc403..4ef4178e28500 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -94,6 +94,13 @@ const routes: Routes = [ path: 'create-cluster', component: CreateClusterComponent, canActivate: [ModuleStatusGuardService], + children: [ + { + path: URLVerbs.ADD, + component: HostFormComponent, + outlet: 'modal' + } + ], data: { moduleStatusGuardConfig: { apiPath: 'orchestrator', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index a2c1e6d2f89ec..0c02772509360 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -106,7 +106,6 @@ import { TelemetryComponent } from './telemetry/telemetry.component'; OsdCreationPreviewModalComponent, RulesListComponent, ActiveAlertListComponent, - HostFormComponent, ServiceDetailsComponent, ServiceDaemonListComponent, TelemetryComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html index 661c13fc931c9..04c8d6158d80c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html @@ -1,4 +1,5 @@ -
+
@@ -14,12 +15,10 @@
@@ -27,3 +26,40 @@
+ +
+
Create Cluster
+
+ +
+ +
+

Add Hosts

+
+ +
+
+

Review

+
+

To be implemented

+
+
+
+
+ +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss index e69de29bb2d1d..bcbfa374927dd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss @@ -0,0 +1,21 @@ +@use './src/styles/vendor/variables' as vv; + +.container-fluid { + align-items: flex-start; + display: flex; + width: 100%; +} + +.card-body { + max-width: 90%; +} + +.vertical-line { + border-left: 1px solid vv.$gray-400; +} + +cd-hosts { + ::ng-deep .nav { + display: none; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts index 7e061b2e25c9a..a6e67167be4ef 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts @@ -5,7 +5,11 @@ import { RouterTestingModule } from '@angular/router/testing'; import { ToastrModule } from 'ngx-toastr'; +import { CephModule } from '~/app/ceph/ceph.module'; +import { CoreModule } from '~/app/core/core.module'; import { ClusterService } from '~/app/shared/api/cluster.service'; +import { HostService } from '~/app/shared/api/host.service'; +import { WizardStepsService } from '~/app/shared/services/wizard-steps.service'; import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed } from '~/testing/unit-test-helper'; import { CreateClusterComponent } from './create-cluster.component'; @@ -14,16 +18,26 @@ describe('CreateClusterComponent', () => { let component: CreateClusterComponent; let fixture: ComponentFixture; let clusterService: ClusterService; + let wizardStepService: WizardStepsService; + let hostService: HostService; configureTestBed({ - declarations: [CreateClusterComponent], - imports: [HttpClientTestingModule, RouterTestingModule, ToastrModule.forRoot(), SharedModule] + imports: [ + HttpClientTestingModule, + RouterTestingModule, + ToastrModule.forRoot(), + SharedModule, + CoreModule, + CephModule + ] }); beforeEach(() => { fixture = TestBed.createComponent(CreateClusterComponent); component = fixture.componentInstance; clusterService = TestBed.inject(ClusterService); + wizardStepService = TestBed.inject(WizardStepsService); + hostService = TestBed.inject(HostService); fixture.detectChanges(); }); @@ -42,4 +56,55 @@ describe('CreateClusterComponent', () => { component.skipClusterCreation(); expect(clusterServiceSpy).toHaveBeenCalledTimes(1); }); + + it('should show the wizard when cluster creation is started', () => { + component.createCluster(); + fixture.detectChanges(); + const nativeEl = fixture.debugElement.nativeElement; + expect(nativeEl.querySelector('cd-wizard')).not.toBe(null); + }); + + it('should have title Add Hosts', () => { + component.createCluster(); + fixture.detectChanges(); + const heading = fixture.debugElement.query(By.css('.title')).nativeElement; + expect(heading.innerHTML).toBe('Add Hosts'); + }); + + it('should show the host list when cluster creation as first step', () => { + component.createCluster(); + fixture.detectChanges(); + const nativeEl = fixture.debugElement.nativeElement; + expect(nativeEl.querySelector('cd-hosts')).not.toBe(null); + }); + + it('should move to next step and show the second page', () => { + const wizardStepServiceSpy = spyOn(wizardStepService, 'moveToNextStep').and.callThrough(); + const hostServiceSpy = spyOn(hostService, 'list').and.callThrough(); + component.createCluster(); + fixture.detectChanges(); + component.onNextStep(); + fixture.detectChanges(); + const heading = fixture.debugElement.query(By.css('.title')).nativeElement; + expect(wizardStepServiceSpy).toHaveBeenCalledTimes(1); + expect(hostServiceSpy).toBeCalledTimes(1); + expect(heading.innerHTML).toBe('Review'); + }); + + it('should show the button labels correctly', () => { + component.createCluster(); + fixture.detectChanges(); + let submitBtnLabel = component.showSubmitButtonLabel(); + expect(submitBtnLabel).toEqual('Next'); + let cancelBtnLabel = component.showCancelButtonLabel(); + expect(cancelBtnLabel).toEqual('Cancel'); + + // Last page of the wizard + component.onNextStep(); + fixture.detectChanges(); + submitBtnLabel = component.showSubmitButtonLabel(); + expect(submitBtnLabel).toEqual('Create Cluster'); + cancelBtnLabel = component.showCancelButtonLabel(); + expect(cancelBtnLabel).toEqual('Back'); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts index 239a4f13ca7f0..bdd854767aef6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts @@ -1,36 +1,52 @@ -import { Component } from '@angular/core'; +import { Component, OnDestroy } from '@angular/core'; +import { Router } from '@angular/router'; + +import { forkJoin, Subscription } from 'rxjs'; +import { finalize } from 'rxjs/operators'; import { ClusterService } from '~/app/shared/api/cluster.service'; -import { AppConstants } from '~/app/shared/constants/app.constants'; +import { HostService } from '~/app/shared/api/host.service'; +import { ActionLabelsI18n, AppConstants } from '~/app/shared/constants/app.constants'; import { NotificationType } from '~/app/shared/enum/notification-type.enum'; -import { Permission } from '~/app/shared/models/permissions'; +import { Permissions } from '~/app/shared/models/permissions'; +import { WizardStepModel } from '~/app/shared/models/wizard-steps'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { NotificationService } from '~/app/shared/services/notification.service'; +import { WizardStepsService } from '~/app/shared/services/wizard-steps.service'; @Component({ selector: 'cd-create-cluster', templateUrl: './create-cluster.component.html', styleUrls: ['./create-cluster.component.scss'] }) -export class CreateClusterComponent { - permission: Permission; - orchStatus = false; - featureAvailable = false; +export class CreateClusterComponent implements OnDestroy { + currentStep: WizardStepModel; + currentStepSub: Subscription; + permissions: Permissions; projectConstants: typeof AppConstants = AppConstants; + hosts: Array = []; + stepTitles = ['Add Hosts', 'Review']; + startClusterCreation = false; + observables: any = []; constructor( private authStorageService: AuthStorageService, - private clusterService: ClusterService, - private notificationService: NotificationService + private stepsService: WizardStepsService, + private router: Router, + private hostService: HostService, + private notificationService: NotificationService, + private actionLabels: ActionLabelsI18n, + private clusterService: ClusterService ) { - this.permission = this.authStorageService.getPermissions().configOpt; + this.permissions = this.authStorageService.getPermissions(); + this.currentStepSub = this.stepsService.getCurrentStep().subscribe((step: WizardStepModel) => { + this.currentStep = step; + }); + this.currentStep.stepIndex = 1; } createCluster() { - this.notificationService.show( - NotificationType.error, - $localize`Cluster creation feature not implemented` - ); + this.startClusterCreation = true; } skipClusterCreation() { @@ -39,6 +55,60 @@ export class CreateClusterComponent { NotificationType.info, $localize`Cluster creation skipped by user` ); + this.router.navigate(['/dashboard']); }); } + + onSubmit() { + forkJoin(this.observables) + .pipe( + finalize(() => + this.clusterService.updateStatus('POST_INSTALLED').subscribe(() => { + this.notificationService.show( + NotificationType.success, + $localize`Cluster creation was successful` + ); + this.router.navigate(['/dashboard']); + }) + ) + ) + .subscribe({ + error: (error) => error.preventDefault() + }); + } + + onNextStep() { + if (!this.stepsService.isLastStep()) { + this.hostService.list().subscribe((hosts) => { + hosts.forEach((host) => { + if (host['status'] === 'maintenance') { + this.observables.push(this.hostService.update(host['hostname'], false, [], true)); + } + }); + }); + this.stepsService.moveToNextStep(); + } else { + this.onSubmit(); + } + } + + onPreviousStep() { + if (!this.stepsService.isFirstStep()) { + this.stepsService.moveToPreviousStep(); + } else { + this.router.navigate(['/dashboard']); + } + } + + showSubmitButtonLabel() { + return !this.stepsService.isLastStep() ? this.actionLabels.NEXT : $localize`Create Cluster`; + } + + showCancelButtonLabel() { + return !this.stepsService.isFirstStep() ? this.actionLabels.BACK : this.actionLabels.CANCEL; + } + + ngOnDestroy(): void { + this.currentStepSub.unsubscribe(); + } } 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 b690227e16971..a35f25c47024a 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,4 +1,4 @@ - + {{ action | titlecase }} {{ resource | upperFirst }} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts index dbb834ea8c82c..95e6179ca0718 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts @@ -32,6 +32,7 @@ describe('HostFormComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(HostFormComponent); component = fixture.componentInstance; + component.ngOnInit(); formHelper = new FormHelper(component.hostForm); fixture.detectChanges(); }); @@ -40,6 +41,11 @@ describe('HostFormComponent', () => { expect(component).toBeTruthy(); }); + it('should open the form in a modal', () => { + const nativeEl = fixture.debugElement.nativeElement; + expect(nativeEl.querySelector('cd-modal')).not.toBe(null); + }); + it('should validate the network address is valid', fakeAsync(() => { formHelper.setValue('addr', '115.42.150.37', true); tick(); 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 474e2f1df3a37..acb1b1731ee37 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 @@ -24,6 +24,7 @@ export class HostFormComponent extends CdForm implements OnInit { addr: string; status: string; allLabels: any; + pageURL: string; messages = new SelectMessages({ empty: $localize`There are no labels.`, @@ -40,10 +41,11 @@ export class HostFormComponent extends CdForm implements OnInit { super(); this.resource = $localize`host`; this.action = this.actionLabels.ADD; - this.createForm(); } ngOnInit() { + this.pageURL = this.router.url.includes('hosts') ? 'hosts' : 'create-cluster'; + this.createForm(); this.hostService.list().subscribe((resp: any[]) => { this.hostnames = resp.map((host) => { return host['hostname']; @@ -53,6 +55,7 @@ export class HostFormComponent extends CdForm implements OnInit { } private createForm() { + const disableMaintenance = this.pageURL !== 'hosts'; this.hostForm = new CdFormGroup({ hostname: new FormControl('', { validators: [ @@ -66,7 +69,7 @@ export class HostFormComponent extends CdForm implements OnInit { validators: [CdValidators.ip()] }), labels: new FormControl([]), - maintenance: new FormControl(false) + maintenance: new FormControl({ value: disableMaintenance, disabled: disableMaintenance }) }); } @@ -87,7 +90,7 @@ export class HostFormComponent extends CdForm implements OnInit { this.hostForm.setErrors({ cdSubmitButton: true }); }, complete: () => { - this.router.navigate(['/hosts']); + this.router.navigate([this.pageURL, { outlets: { modal: null } }]); } }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html index c90135c40a207..59bab46d72d23 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html @@ -5,12 +5,13 @@ Hosts List -
@@ -29,7 +30,7 @@
  • + *ngIf="permissions.grafana.read && !clusterCreation"> Overall Performance diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts index 38f6a81bbb950..beaf6f0d7ed53 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts @@ -72,6 +72,7 @@ describe('HostsComponent', () => { showForceMaintenanceModal = new MockShowForceMaintenanceModal(); fixture = TestBed.createComponent(HostsComponent); component = fixture.componentInstance; + component.clusterCreation = false; hostListSpy = spyOn(TestBed.inject(HostService), 'list'); orchService = TestBed.inject(OrchestratorService); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts index 8a501d18575a7..25a413f2ef0db 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { Router } from '@angular/router'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; @@ -46,6 +46,8 @@ export class HostsComponent extends ListWithDetails implements OnInit { public servicesTpl: TemplateRef; @ViewChild('maintenanceConfirmTpl', { static: true }) maintenanceConfirmTpl: TemplateRef; + @Input() + clusterCreation = false; permissions: Permissions; columns: Array = []; @@ -58,6 +60,7 @@ export class HostsComponent extends ListWithDetails implements OnInit { isExecuting = false; errorMessage: string; enableButton: boolean; + pageURL: string; icons = Icons; @@ -90,13 +93,6 @@ export class HostsComponent extends ListWithDetails implements OnInit { super(); this.permissions = this.authStorageService.getPermissions(); this.tableActions = [ - { - name: this.actionLabels.ADD, - permission: 'create', - icon: Icons.add, - click: () => this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.ADD] } }]), - disable: (selection: CdTableSelection) => this.getDisable('add', selection) - }, { name: this.actionLabels.EDIT, permission: 'update', @@ -117,7 +113,10 @@ export class HostsComponent extends ListWithDetails implements OnInit { icon: Icons.enter, click: () => this.hostMaintenance(), disable: (selection: CdTableSelection) => - this.getDisable('maintenance', selection) || this.isExecuting || this.enableButton + this.getDisable('maintenance', selection) || + this.isExecuting || + this.enableButton || + this.clusterCreation }, { name: this.actionLabels.EXIT_MAINTENANCE, @@ -125,12 +124,23 @@ export class HostsComponent extends ListWithDetails implements OnInit { icon: Icons.exit, click: () => this.hostMaintenance(), disable: (selection: CdTableSelection) => - this.getDisable('maintenance', selection) || this.isExecuting || !this.enableButton + this.getDisable('maintenance', selection) || + this.isExecuting || + !this.enableButton || + this.clusterCreation } ]; } ngOnInit() { + this.clusterCreation ? (this.pageURL = 'create-cluster') : (this.pageURL = BASE_URL); + this.tableActions.unshift({ + name: this.actionLabels.ADD, + permission: 'create', + icon: Icons.add, + click: () => this.router.navigate([this.pageURL, { outlets: { modal: [URLVerbs.ADD] } }]), + disable: (selection: CdTableSelection) => this.getDisable('add', selection) + }); this.columns = [ { name: $localize`Hostname`, @@ -140,6 +150,7 @@ export class HostsComponent extends ListWithDetails implements OnInit { { name: $localize`Services`, prop: 'services', + isHidden: this.clusterCreation, flexGrow: 3, cellTemplate: this.servicesTpl }, @@ -166,6 +177,7 @@ export class HostsComponent extends ListWithDetails implements OnInit { { name: $localize`Version`, prop: 'ceph_version', + isHidden: this.clusterCreation, flexGrow: 1, pipe: this.cephShortVersionPipe } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.spec.ts index e85223c80e39c..93c9e9adcbbf0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.spec.ts @@ -1,6 +1,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; import { NgbActiveModal, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { ToastrModule } from 'ngx-toastr'; @@ -26,7 +27,8 @@ describe('OsdFlagsIndivModalComponent', () => { ReactiveFormsModule, SharedModule, ToastrModule.forRoot(), - NgbTooltipModule + NgbTooltipModule, + RouterTestingModule ], declarations: [OsdFlagsIndivModalComponent], providers: [NgbActiveModal] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts index bccbc645bab1b..1b72c55949145 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts @@ -44,6 +44,7 @@ import { SparklineComponent } from './sparkline/sparkline.component'; import { SubmitButtonComponent } from './submit-button/submit-button.component'; import { TelemetryNotificationComponent } from './telemetry-notification/telemetry-notification.component'; import { UsageBarComponent } from './usage-bar/usage-bar.component'; +import { WizardComponent } from './wizard/wizard.component'; @NgModule({ imports: [ @@ -91,7 +92,8 @@ import { UsageBarComponent } from './usage-bar/usage-bar.component'; DocComponent, Copy2ClipboardButtonComponent, DownloadButtonComponent, - FormButtonPanelComponent + FormButtonPanelComponent, + WizardComponent ], providers: [], exports: [ @@ -117,7 +119,8 @@ import { UsageBarComponent } from './usage-bar/usage-bar.component'; DocComponent, Copy2ClipboardButtonComponent, DownloadButtonComponent, - FormButtonPanelComponent + FormButtonPanelComponent, + WizardComponent ] }) export class ComponentsModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.html new file mode 100644 index 0000000000000..a81d9f978bb68 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.html @@ -0,0 +1,21 @@ + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.scss new file mode 100644 index 0000000000000..76821c5969ac3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.scss @@ -0,0 +1,20 @@ +@use './src/styles/vendor/variables' as vv; + +.wizard-step-container { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; +} + +span.circle-step { + background: vv.$gray-500; + border-radius: 0.8em; + color: vv.$white; + display: inline-block; + font-weight: bold; + line-height: 1.6em; + margin-right: 5px; + text-align: center; + width: 1.6em; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.spec.ts new file mode 100644 index 0000000000000..b42578fb71193 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SharedModule } from '~/app/shared/shared.module'; +import { configureTestBed } from '~/testing/unit-test-helper'; +import { WizardComponent } from './wizard.component'; + +describe('WizardComponent', () => { + let component: WizardComponent; + let fixture: ComponentFixture; + + configureTestBed({ + imports: [SharedModule] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(WizardComponent); + component = fixture.componentInstance; + component.stepsTitle = ['Add Hosts', 'Review']; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.ts new file mode 100644 index 0000000000000..6aa6500499cb1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.ts @@ -0,0 +1,41 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; + +import * as _ from 'lodash'; +import { Observable, Subscription } from 'rxjs'; + +import { WizardStepModel } from '~/app/shared/models/wizard-steps'; +import { WizardStepsService } from '~/app/shared/services/wizard-steps.service'; + +@Component({ + selector: 'cd-wizard', + templateUrl: './wizard.component.html', + styleUrls: ['./wizard.component.scss'] +}) +export class WizardComponent implements OnInit, OnDestroy { + @Input() + stepsTitle: string[]; + + steps: Observable; + currentSteps: Observable; + currentStep: WizardStepModel; + currentStepSub: Subscription; + + constructor(private stepsService: WizardStepsService) {} + + ngOnInit(): void { + this.stepsService.setTotalSteps(this.stepsTitle.length); + this.steps = this.stepsService.getSteps(); + this.currentSteps = this.stepsService.getCurrentStep(); + this.currentStepSub = this.stepsService.getCurrentStep().subscribe((step: WizardStepModel) => { + this.currentStep = step; + }); + } + + onStepClick(step: WizardStepModel) { + this.stepsService.setCurrentStep(step); + } + + ngOnDestroy(): void { + this.currentStepSub.unsubscribe(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/wizard-steps.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/wizard-steps.ts new file mode 100644 index 0000000000000..177feb486d1a1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/wizard-steps.ts @@ -0,0 +1,4 @@ +export interface WizardStepModel { + stepIndex: number; + isComplete: boolean; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.spec.ts new file mode 100644 index 0000000000000..47c2149756703 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { WizardStepsService } from './wizard-steps.service'; + +describe('WizardStepsService', () => { + let service: WizardStepsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(WizardStepsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.ts new file mode 100644 index 0000000000000..e0fb2be944de0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@angular/core'; + +import { BehaviorSubject, Observable } from 'rxjs'; + +import { WizardStepModel } from '~/app/shared/models/wizard-steps'; + +const initialStep = [{ stepIndex: 1, isComplete: false }]; + +@Injectable({ + providedIn: 'root' +}) +export class WizardStepsService { + steps$: BehaviorSubject; + currentStep$: BehaviorSubject = new BehaviorSubject(null); + + constructor() { + this.steps$ = new BehaviorSubject(initialStep); + this.currentStep$.next(this.steps$.value[0]); + } + + setTotalSteps(step: number) { + const steps: WizardStepModel[] = []; + for (let i = 1; i <= step; i++) { + steps.push({ stepIndex: i, isComplete: false }); + } + this.steps$ = new BehaviorSubject(steps); + } + + setCurrentStep(step: WizardStepModel): void { + this.currentStep$.next(step); + } + + getCurrentStep(): Observable { + return this.currentStep$.asObservable(); + } + + getSteps(): Observable { + return this.steps$.asObservable(); + } + + moveToNextStep(): void { + const index = this.currentStep$.value.stepIndex; + this.currentStep$.next(this.steps$.value[index]); + } + + moveToPreviousStep(): void { + const index = this.currentStep$.value.stepIndex - 1; + this.currentStep$.next(this.steps$.value[index - 1]); + } + + isLastStep(): boolean { + return this.currentStep$.value.stepIndex === this.steps$.value.length; + } + + isFirstStep(): boolean { + return this.currentStep$.value?.stepIndex - 1 === 0; + } +}