From: Aashish Sharma Date: Tue, 7 Sep 2021 06:30:45 +0000 (+0530) Subject: mgr/dashboard: Cluster Creation Add Services Section X-Git-Tag: v17.1.0~699^2~2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=b914f59ff123b9e006d7d11dc786e967c0271dbf;p=ceph.git mgr/dashboard: Cluster Creation Add Services Section Add Services section in cluster creation wizard Create Cluster OSD Section Followups 1. The device preview disappearing when going to next step and coming back to the previous step 2. Even when clearing the device preview, the Storage Capacity count and the drive group spec doesn't get cleared. 3. Expanding the cluster without selecting any devices gives a 400 error. 4. Renamed "Delete Host" to "Remove Host" 5. Generalizing most of the sub component code Fixes: https://tracker.ceph.com/issues/52499 Fixes: https://tracker.ceph.com/issues/51991 Signed-off-by: Nizamudeen A Signed-off-by: Aashish Sharma --- diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py index 6ba1a5e0f1cbb..c246de8f9f12c 100644 --- a/src/pybind/mgr/dashboard/controllers/host.py +++ b/src/pybind/mgr/dashboard/controllers/host.py @@ -283,7 +283,7 @@ class Host(RESTController): from_orchestrator = 'orchestrator' in _sources return get_hosts(from_ceph, from_orchestrator) - @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_CREATE]) + @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_ADD]) @handle_orchestrator_error('host') @host_task('add', {'hostname': '{hostname}'}) @EndpointDoc('', @@ -301,9 +301,9 @@ class Host(RESTController): status: Optional[str] = None): # pragma: no cover - requires realtime env add_host(hostname, addr, labels, status) - @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_DELETE]) + @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_REMOVE]) @handle_orchestrator_error('host') - @host_task('delete', {'hostname': '{hostname}'}) + @host_task('remove', {'hostname': '{hostname}'}) @allow_empty_body def delete(self, hostname): # pragma: no cover - requires realtime env orch_client = OrchClient.instance() diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/create-cluster.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/create-cluster.po.ts index 4ae03f4aaae29..a528c0fc12e05 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/create-cluster.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/create-cluster.po.ts @@ -13,6 +13,11 @@ export class CreateClusterWizardHelper extends PageHelper { status: 3 }; + serviceColumnIndex = { + service_name: 1, + placement: 2 + }; + createCluster() { cy.get('cd-create-cluster').should('contain.text', 'Please expand your cluster first'); cy.get('[name=expand-cluster]').click(); @@ -41,7 +46,6 @@ export class CreateClusterWizardHelper extends PageHelper { add(hostname: string, exist?: boolean, maintenance?: boolean) { cy.get('.btn.btn-accent').first().click({ force: true }); - cy.get('cd-modal').should('exist'); cy.get('cd-modal').within(() => { cy.get('#hostname').type(hostname); @@ -70,7 +74,7 @@ export class CreateClusterWizardHelper extends PageHelper { } delete(hostname: string) { - super.delete(hostname, this.columnIndex.hostname); + super.delete(hostname, this.columnIndex.hostname, 'hosts'); } // Add or remove labels on a host, then verify labels in the table @@ -127,4 +131,59 @@ export class CreateClusterWizardHelper extends PageHelper { cy.get('@addButton').click(); }); } + + private selectServiceType(serviceType: string) { + return this.selectOption('service_type', serviceType); + } + + addService(serviceType: string) { + cy.get('.btn.btn-accent').first().click({ force: true }); + cy.get('cd-modal').should('exist'); + cy.get('cd-modal').within(() => { + this.selectServiceType(serviceType); + if (serviceType === 'rgw') { + cy.get('#service_id').type('rgw'); + cy.get('#count').type('1'); + } else if (serviceType === 'ingress') { + this.selectOption('backend_service', 'rgw.rgw'); + cy.get('#service_id').should('have.value', 'rgw.rgw'); + cy.get('#virtual_ip').type('192.168.20.1/24'); + cy.get('#frontend_port').type('8081'); + cy.get('#monitor_port').type('8082'); + } + + cy.get('cd-submit-button').click(); + }); + } + + checkServiceExist(serviceName: string, exist: boolean) { + this.getTableCell(this.serviceColumnIndex.service_name, serviceName).should(($elements) => { + const services = $elements.map((_, el) => el.textContent).get(); + if (exist) { + expect(services).to.include(serviceName); + } else { + expect(services).to.not.include(serviceName); + } + }); + } + + deleteService(serviceName: string, wait: number) { + const getRow = this.getTableCell.bind(this, this.serviceColumnIndex.service_name); + getRow(serviceName).click(); + + // Clicks on table Delete button + this.clickActionButton('delete'); + + // Confirms deletion + cy.get('cd-modal .custom-control-label').click(); + cy.contains('cd-modal button', 'Delete').click(); + + // Wait for modal to close + cy.get('cd-modal').should('not.exist'); + + // wait for delete operation to complete: tearing down the service daemons + cy.wait(wait); + + this.checkServiceExist(serviceName, false); + } } 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 7a7e00d6648ac..5f3d39dcedc50 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 @@ -79,7 +79,7 @@ export class HostsPageHelper extends PageHelper { @PageHelper.restrictTo(pages.index.url) delete(hostname: string) { - super.delete(hostname, this.columnIndex.hostname); + super.delete(hostname, this.columnIndex.hostname, 'hosts'); } // Add or remove labels on a host, then verify labels in the table diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts index e28a8fbd57c26..0a943d4b0553f 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts @@ -2,7 +2,7 @@ import { PageHelper } from '../page-helper.po'; const pages = { index: { url: '#/services', id: 'cd-services' }, - create: { url: '#/services/create', id: 'cd-service-form' } + create: { url: '#/services/(modal:create)', id: 'cd-service-form' } }; export class ServicesPageHelper extends PageHelper { diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-create-osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-create-osds.e2e-spec.ts index 92c0739c5ede4..ee272bcf943b2 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-create-osds.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-create-osds.e2e-spec.ts @@ -33,6 +33,7 @@ describe('Create cluster create osds page', () => { cy.get('button[aria-label="Next"]').click(); cy.get('button[aria-label="Next"]').click(); + cy.get('button[aria-label="Next"]').click(); }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-review.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-review.e2e-spec.ts deleted file mode 100644 index 624f457458ff2..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-review.e2e-spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { CreateClusterWizardHelper } from 'cypress/integration/cluster/create-cluster.po'; - -describe('Create Cluster Review page', () => { - const createCluster = new CreateClusterWizardHelper(); - - beforeEach(() => { - cy.login(); - Cypress.Cookies.preserveOnce('token'); - createCluster.navigateTo(); - createCluster.createCluster(); - - cy.get('button[aria-label="Next"]').click(); - cy.get('button[aria-label="Next"]').click(); - }); - - describe('navigation link and title test', () => { - it('should check if nav-link and title contains Review', () => { - cy.get('.nav-link').should('contain.text', 'Review'); - }); - }); - - describe('fields check', () => { - it('should check cluster resources table is present', () => { - // check for table header 'Cluster Resources' - createCluster.getLegends().its(0).should('have.text', 'Cluster Resources'); - - // check for fields in table - createCluster.getStatusTables().should('contain.text', 'Hosts'); - createCluster.getStatusTables().should('contain.text', 'Storage Capacity'); - }); - - it('should check Hosts by Label and Host Details tables are present', () => { - // check for there to be two tables - createCluster.getDataTables().should('have.length', 2); - - // check for table header 'Hosts by Label' - createCluster.getLegends().its(1).should('have.text', 'Hosts by Label'); - - // check for table header 'Host Details' - createCluster.getLegends().its(2).should('have.text', 'Host Details'); - - // verify correct columns on Hosts by Label table - createCluster.getDataTableHeaders(0).contains('Label'); - - createCluster.getDataTableHeaders(0).contains('Number of Hosts'); - - // verify correct columns on Host Details table - createCluster.getDataTableHeaders(1).contains('Host Name'); - - createCluster.getDataTableHeaders(1).contains('Labels'); - }); - - it('should check hosts count and default host name are present', () => { - createCluster.getStatusTables().contains(2); - - createCluster.check_for_host(); - }); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-cluster-check.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-cluster-check.e2e-spec.ts deleted file mode 100644 index 116cbd789c8c0..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-cluster-check.e2e-spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { CreateClusterWizardHelper } from 'cypress/integration/cluster/create-cluster.po'; -import { HostsPageHelper } from 'cypress/integration/cluster/hosts.po'; -import { OSDsPageHelper } from 'cypress/integration/cluster/osds.po'; - -describe('when cluster creation is completed', () => { - const createCluster = new CreateClusterWizardHelper(); - - beforeEach(() => { - cy.login(); - Cypress.Cookies.preserveOnce('token'); - }); - - it('should redirect to dashboard landing page after cluster creation', () => { - createCluster.navigateTo(); - createCluster.createCluster(); - - cy.get('button[aria-label="Next"]').click(); - cy.get('button[aria-label="Next"]').click(); - cy.get('button[aria-label="Next"]').click(); - - cy.get('cd-dashboard').should('exist'); - }); - - describe('Hosts page', () => { - const hosts = new HostsPageHelper(); - const hostnames = ['ceph-node-00.cephlab.com', 'ceph-node-02.cephlab.com']; - - beforeEach(() => { - hosts.navigateTo(); - }); - it('should have removed "_no_schedule" label', () => { - for (let host = 0; host < hostnames.length; host++) { - cy.get('datatable-row-wrapper').should('not.have.text', '_no_schedule'); - } - }); - - it('should display inventory', () => { - hosts.clickHostTab(hostnames[1], 'Physical Disks'); - cy.get('cd-host-details').within(() => { - hosts.getTableCount('total').should('be.gte', 0); - }); - }); - - it('should display daemons', () => { - hosts.clickHostTab(hostnames[1], 'Daemons'); - cy.get('cd-host-details').within(() => { - hosts.getTableCount('total').should('be.gte', 0); - }); - }); - }); - - describe('OSDs page', () => { - const osds = new OSDsPageHelper(); - - beforeEach(() => { - osds.navigateTo(); - }); - - it('should check if osds are created', { retries: 1 }, () => { - osds.expectTableCount('total', 2); - }); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-create-cluster-create-services.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-create-cluster-create-services.e2e-spec.ts new file mode 100644 index 0000000000000..0a95474c91d3f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-create-cluster-create-services.e2e-spec.ts @@ -0,0 +1,36 @@ +import { CreateClusterWizardHelper } from 'cypress/integration/cluster/create-cluster.po'; + +describe('Create cluster create services page', () => { + const createCluster = new CreateClusterWizardHelper(); + + beforeEach(() => { + cy.login(); + Cypress.Cookies.preserveOnce('token'); + createCluster.navigateTo(); + createCluster.createCluster(); + cy.get('button[aria-label="Next"]').click(); + cy.get('button[aria-label="Next"]').click(); + }); + + it('should check if nav-link and title contains Create Services', () => { + cy.get('.nav-link').should('contain.text', 'Create Services'); + + cy.get('.title').should('contain.text', 'Create Services'); + }); + + describe('when Orchestrator is available', () => { + it('should create an rgw service', () => { + createCluster.addService('rgw'); + + createCluster.checkExist('rgw.rgw', true); + }); + + it('should create and delete an ingress service', () => { + createCluster.addService('ingress'); + + createCluster.checkExist('ingress.rgw.rgw', true); + + createCluster.deleteService('ingress.rgw.rgw', 60000); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts new file mode 100644 index 0000000000000..9097034e1a88c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts @@ -0,0 +1,60 @@ +import { CreateClusterWizardHelper } from 'cypress/integration/cluster/create-cluster.po'; + +describe('Create Cluster Review page', () => { + const createCluster = new CreateClusterWizardHelper(); + + beforeEach(() => { + cy.login(); + Cypress.Cookies.preserveOnce('token'); + createCluster.navigateTo(); + createCluster.createCluster(); + + cy.get('button[aria-label="Next"]').click(); + cy.get('button[aria-label="Next"]').click(); + cy.get('button[aria-label="Next"]').click(); + }); + + describe('navigation link and title test', () => { + it('should check if nav-link and title contains Review', () => { + cy.get('.nav-link').should('contain.text', 'Review'); + }); + }); + + describe('fields check', () => { + it('should check cluster resources table is present', () => { + // check for table header 'Cluster Resources' + createCluster.getLegends().its(0).should('have.text', 'Cluster Resources'); + + // check for fields in table + createCluster.getStatusTables().should('contain.text', 'Hosts'); + createCluster.getStatusTables().should('contain.text', 'Storage Capacity'); + }); + + it('should check Hosts by Services and Host Details tables are present', () => { + // check for there to be two tables + createCluster.getDataTables().should('have.length', 2); + + // check for table header 'Hosts by Services' + createCluster.getLegends().its(1).should('have.text', 'Hosts by Services'); + + // check for table header 'Host Details' + createCluster.getLegends().its(2).should('have.text', 'Host Details'); + + // verify correct columns on Hosts by Services table + createCluster.getDataTableHeaders(0).contains('Services'); + + createCluster.getDataTableHeaders(0).contains('Number of Hosts'); + + // verify correct columns on Host Details table + createCluster.getDataTableHeaders(1).contains('Host Name'); + + createCluster.getDataTableHeaders(1).contains('Labels'); + }); + + it('should check hosts count and default host name are present', () => { + createCluster.getStatusTables().contains(2); + + createCluster.check_for_host(); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/06-cluster-check.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/06-cluster-check.e2e-spec.ts new file mode 100644 index 0000000000000..a5b54786b8738 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/06-cluster-check.e2e-spec.ts @@ -0,0 +1,77 @@ +import { CreateClusterWizardHelper } from 'cypress/integration/cluster/create-cluster.po'; +import { HostsPageHelper } from 'cypress/integration/cluster/hosts.po'; +import { OSDsPageHelper } from 'cypress/integration/cluster/osds.po'; +import { ServicesPageHelper } from 'cypress/integration/cluster/services.po'; + +describe('when cluster creation is completed', () => { + const createCluster = new CreateClusterWizardHelper(); + + beforeEach(() => { + cy.login(); + Cypress.Cookies.preserveOnce('token'); + }); + + it('should redirect to dashboard landing page after cluster creation', () => { + createCluster.navigateTo(); + createCluster.createCluster(); + + cy.get('button[aria-label="Next"]').click(); + cy.get('button[aria-label="Next"]').click(); + cy.get('button[aria-label="Next"]').click(); + cy.get('button[aria-label="Next"]').click(); + + cy.get('cd-dashboard').should('exist'); + }); + + describe('Hosts page', () => { + const hosts = new HostsPageHelper(); + const hostnames = ['ceph-node-00.cephlab.com', 'ceph-node-02.cephlab.com']; + + beforeEach(() => { + hosts.navigateTo(); + }); + it('should have removed "_no_schedule" label', () => { + for (let host = 0; host < hostnames.length; host++) { + cy.get('datatable-row-wrapper').should('not.have.text', '_no_schedule'); + } + }); + + it('should display inventory', () => { + hosts.clickHostTab(hostnames[1], 'Physical Disks'); + cy.get('cd-host-details').within(() => { + hosts.getTableCount('total').should('be.gte', 0); + }); + }); + + it('should display daemons', () => { + hosts.clickHostTab(hostnames[1], 'Daemons'); + cy.get('cd-host-details').within(() => { + hosts.getTableCount('total').should('be.gte', 0); + }); + }); + }); + + describe('OSDs page', () => { + const osds = new OSDsPageHelper(); + + beforeEach(() => { + osds.navigateTo(); + }); + + it('should check if osds are created', { retries: 1 }, () => { + osds.expectTableCount('total', 2); + }); + }); + + describe('Services page', () => { + const services = new ServicesPageHelper(); + + beforeEach(() => { + services.navigateTo(); + }); + + it('should check if services are created', () => { + services.checkExist('rgw.rgw', true); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts index 7b099fd98acf2..4e15088a2c9c9 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts @@ -256,19 +256,22 @@ export abstract class PageHelper { * @param name The string to search in table cells. * @param columnIndex If provided, search string in columnIndex column. */ - delete(name: string, columnIndex?: number) { + delete(name: string, columnIndex?: number, section?: string) { // Selects row const getRow = columnIndex ? this.getTableCell.bind(this, columnIndex) : this.getFirstTableCell.bind(this); getRow(name).click(); + let action: string; + section === 'hosts' ? (action = 'remove') : (action = 'delete'); - // Clicks on table Delete button - this.clickActionButton('delete'); + // Clicks on table Delete/Remove button + this.clickActionButton(action); - // Confirms deletion + // Convert action to SentenceCase and Confirms deletion + const actionUpperCase = action.charAt(0).toUpperCase() + action.slice(1); cy.get('cd-modal .custom-control-label').click(); - cy.contains('cd-modal button', 'Delete').click(); + cy.contains('cd-modal button', actionUpperCase).click(); // Wait for modal to close cy.get('cd-modal').should('not.exist'); 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 bdd2cb8978234..e1eb456ee8aa0 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 @@ -122,7 +122,8 @@ const routes: Routes = [ }, { path: 'services', - canActivateChild: [ModuleStatusGuardService], + component: ServicesComponent, + canActivate: [ModuleStatusGuardService], data: { moduleStatusGuardConfig: { apiPath: 'orchestrator', @@ -134,11 +135,10 @@ const routes: Routes = [ breadcrumbs: 'Cluster/Services' }, children: [ - { path: '', component: ServicesComponent }, { path: URLVerbs.CREATE, component: ServiceFormComponent, - data: { breadcrumbs: ActionLabels.CREATE } + outlet: 'modal' } ] }, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.html index f95dfdb910f81..e6f31dc9e74f8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.html @@ -12,9 +12,8 @@ Storage Capacity - Number of devices: {{ filteredDevices.length }}. Raw capacity: - {{ capacity | dimlessBinary }}. + Number of devices: {{ totalDevices }}. Raw capacity: + {{ totalCapacity | dimlessBinary }}. @@ -22,9 +21,9 @@
Hosts by Label - Hosts by Services + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.spec.ts index e823932c09732..8122cb682f0ba 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.spec.ts @@ -6,7 +6,7 @@ import { of } from 'rxjs'; import { CephModule } from '~/app/ceph/ceph.module'; import { CoreModule } from '~/app/core/core.module'; -import { HostService } from '~/app/shared/api/host.service'; +import { CephServiceService } from '~/app/shared/api/ceph-service.service'; import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed } from '~/testing/unit-test-helper'; import { CreateClusterReviewComponent } from './create-cluster-review.component'; @@ -14,8 +14,8 @@ import { CreateClusterReviewComponent } from './create-cluster-review.component' describe('CreateClusterReviewComponent', () => { let component: CreateClusterReviewComponent; let fixture: ComponentFixture; - let hostService: HostService; - let hostListSpy: jasmine.Spy; + let cephServiceService: CephServiceService; + let serviceListSpy: jasmine.Spy; configureTestBed({ imports: [HttpClientTestingModule, SharedModule, CoreModule, CephModule] @@ -24,8 +24,8 @@ describe('CreateClusterReviewComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(CreateClusterReviewComponent); component = fixture.componentInstance; - hostService = TestBed.inject(HostService); - hostListSpy = spyOn(hostService, 'list'); + cephServiceService = TestBed.inject(CephServiceService); + serviceListSpy = spyOn(cephServiceService, 'list'); }); it('should create', () => { @@ -37,25 +37,18 @@ describe('CreateClusterReviewComponent', () => { const payload = [ { hostname: hostnames[0], - ceph_version: 'ceph version Development', - labels: ['foo', 'bar'] + service_type: ['mgr', 'mon'] }, { hostname: hostnames[1], - ceph_version: 'ceph version Development', - labels: ['foo1', 'bar1'] + service_type: ['mgr', 'alertmanager'] } ]; - hostListSpy.and.callFake(() => of(payload)); + serviceListSpy.and.callFake(() => of(payload)); fixture.detectChanges(); - expect(hostListSpy).toHaveBeenCalled(); + expect(serviceListSpy).toHaveBeenCalled(); - expect(component.hostsCount).toBe(2); - expect(component.uniqueLabels.size).toBe(4); - const labels = ['foo', 'bar', 'foo1', 'bar1']; - - labels.forEach((label) => { - expect(component.labelOccurrences[label]).toBe(1); - }); + expect(component.serviceCount).toBe(2); + expect(component.uniqueServices.size).toBe(2); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts index 3abbcb122865e..90e25d1162dc7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts @@ -2,10 +2,12 @@ import { Component, OnInit } from '@angular/core'; import _ from 'lodash'; +import { CephServiceService } from '~/app/shared/api/ceph-service.service'; import { HostService } from '~/app/shared/api/host.service'; +import { OsdService } from '~/app/shared/api/osd.service'; import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; +import { CephServiceSpec } from '~/app/shared/models/service.interface'; import { WizardStepsService } from '~/app/shared/services/wizard-steps.service'; -import { InventoryDevice } from '../inventory/inventory-devices/inventory-device.model'; @Component({ selector: 'cd-create-cluster-review', @@ -15,17 +17,30 @@ import { InventoryDevice } from '../inventory/inventory-devices/inventory-device export class CreateClusterReviewComponent implements OnInit { hosts: object[] = []; hostsDetails: object; - hostsByLabel: object; + hostsByService: object; hostsCount: number; - labelOccurrences = {}; - hostsCountPerLabel: object[] = []; - uniqueLabels: Set = new Set(); - filteredDevices: InventoryDevice[] = []; - capacity = 0; + serviceCount: number; + serviceOccurrences = {}; + hostsCountPerService: object[] = []; + uniqueServices: Set = new Set(); + totalDevices: number; + totalCapacity = 0; + services: Array = []; - constructor(private hostService: HostService, public wizardStepService: WizardStepsService) {} + constructor( + public wizardStepsService: WizardStepsService, + public cephServiceService: CephServiceService, + public hostService: HostService, + private osdService: OsdService + ) {} ngOnInit() { + let dataDevices = 0; + let dataDeviceCapacity = 0; + let walDevices = 0; + let walDeviceCapacity = 0; + let dbDevices = 0; + let dbDeviceCapacity = 0; this.hostsDetails = { columns: [ { @@ -45,11 +60,11 @@ export class CreateClusterReviewComponent implements OnInit { ] }; - this.hostsByLabel = { + this.hostsByService = { columns: [ { - prop: 'label', - name: $localize`Labels`, + prop: 'service_type', + name: $localize`Services`, flexGrow: 1, cellTransformation: CellTemplate.badge, customTemplateConfig: { @@ -58,36 +73,55 @@ export class CreateClusterReviewComponent implements OnInit { }, { name: $localize`Number of Hosts`, - prop: 'hosts_per_label', + prop: 'hosts_per_service', flexGrow: 1 } ] }; - this.hostService.list().subscribe((resp: object[]) => { - this.hosts = resp; - this.hostsCount = this.hosts.length; + this.cephServiceService.list().subscribe((resp: Array) => { + this.services = resp; + this.serviceCount = this.services.length; - _.forEach(this.hosts, (hostKey) => { - const labels = hostKey['labels']; - _.forEach(labels, (label) => { - this.labelOccurrences[label] = (this.labelOccurrences[label] || 0) + 1; - this.uniqueLabels.add(label); - }); + _.forEach(this.services, (serviceKey) => { + this.serviceOccurrences[serviceKey['service_type']] = + (this.serviceOccurrences[serviceKey['service_type']] || 0) + 1; + this.uniqueServices.add(serviceKey['service_type']); }); - this.uniqueLabels.forEach((label) => { - this.hostsCountPerLabel.push({ - label: label, - hosts_per_label: this.labelOccurrences[label] + this.uniqueServices.forEach((serviceType) => { + this.hostsCountPerService.push({ + service_type: serviceType, + hosts_per_service: this.serviceOccurrences[serviceType] }); }); - this.hostsByLabel['data'] = [...this.hostsCountPerLabel]; + this.hostsByService['data'] = [...this.hostsCountPerService]; + }); + + this.hostService.list().subscribe((resp: object[]) => { + this.hosts = resp; + this.hostsCount = this.hosts.length; this.hostsDetails['data'] = [...this.hosts]; }); - this.filteredDevices = this.wizardStepService.osdDevices; - this.capacity = this.wizardStepService.osdCapacity; + if (this.osdService.osdDevices['data']) { + dataDevices = this.osdService.osdDevices['data']?.length; + dataDeviceCapacity = this.osdService.osdDevices['data']['capacity']; + } + + if (this.osdService.osdDevices['wal']) { + walDevices = this.osdService.osdDevices['wal']?.length; + walDeviceCapacity = this.osdService.osdDevices['wal']['capacity']; + } + + if (this.osdService.osdDevices['db']) { + dbDevices = this.osdService.osdDevices['db']?.length; + dbDeviceCapacity = this.osdService.osdDevices['db']['capacity']; + } + + this.totalDevices = dataDevices + walDevices + dbDevices; + this.osdService.osdDevices['totalDevices'] = this.totalDevices; + this.totalCapacity = dataDeviceCapacity + walDeviceCapacity + dbDeviceCapacity; } } 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 d9e8ec43d9cc9..9ae80f4c0218d 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 @@ -40,7 +40,11 @@

Add Hosts


- +
@@ -48,11 +52,23 @@ i18n>Create OSDs
- +
+

Create Services

+
+ +
+
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 8f9d7328e34ee..3e0b7f7bdcce7 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 @@ -8,6 +8,7 @@ import { ToastrModule } from 'ngx-toastr'; import { CephModule } from '~/app/ceph/ceph.module'; import { CoreModule } from '~/app/core/core.module'; import { HostService } from '~/app/shared/api/host.service'; +import { OsdService } from '~/app/shared/api/osd.service'; import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component'; import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component'; import { AppConstants } from '~/app/shared/constants/app.constants'; @@ -22,6 +23,7 @@ describe('CreateClusterComponent', () => { let fixture: ComponentFixture; let wizardStepService: WizardStepsService; let hostService: HostService; + let osdService: OsdService; let modalServiceShowSpy: jasmine.Spy; const projectConstants: typeof AppConstants = AppConstants; @@ -44,6 +46,7 @@ describe('CreateClusterComponent', () => { component = fixture.componentInstance; wizardStepService = TestBed.inject(WizardStepsService); hostService = TestBed.inject(HostService); + osdService = TestBed.inject(OsdService); modalServiceShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.returnValue({ // mock the close function, it might be called if there are async tests. close: jest.fn() @@ -89,13 +92,11 @@ describe('CreateClusterComponent', () => { 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(); expect(wizardStepServiceSpy).toHaveBeenCalledTimes(1); - expect(hostServiceSpy).toBeCalledTimes(1); }); it('should show the button labels correctly', () => { @@ -113,6 +114,13 @@ describe('CreateClusterComponent', () => { cancelBtnLabel = component.showCancelButtonLabel(); expect(cancelBtnLabel).toEqual('Back'); + component.onNextStep(); + fixture.detectChanges(); + submitBtnLabel = component.showSubmitButtonLabel(); + expect(submitBtnLabel).toEqual('Next'); + cancelBtnLabel = component.showCancelButtonLabel(); + expect(cancelBtnLabel).toEqual('Back'); + // Last page of the wizard component.onNextStep(); fixture.detectChanges(); @@ -121,4 +129,25 @@ describe('CreateClusterComponent', () => { cancelBtnLabel = component.showCancelButtonLabel(); expect(cancelBtnLabel).toEqual('Back'); }); + + it('should ensure osd creation did not happen when no devices are selected', () => { + const osdServiceSpy = spyOn(osdService, 'create').and.callThrough(); + component.onSubmit(); + fixture.detectChanges(); + expect(osdServiceSpy).toBeCalledTimes(0); + }); + + it('should ensure osd creation did happen when devices are selected', () => { + const osdServiceSpy = spyOn(osdService, 'create').and.callThrough(); + osdService.osdDevices['totalDevices'] = 1; + component.onSubmit(); + fixture.detectChanges(); + expect(osdServiceSpy).toBeCalledTimes(1); + }); + + it('should ensure host list call happened', () => { + const hostServiceSpy = spyOn(hostService, 'list').and.callThrough(); + component.onSubmit(); + expect(hostServiceSpy).toHaveBeenCalledTimes(1); + }); }); 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 6d78a2110b461..7d973119dfea2 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 @@ -34,7 +34,7 @@ export class CreateClusterComponent implements OnDestroy { currentStepSub: Subscription; permissions: Permissions; projectConstants: typeof AppConstants = AppConstants; - stepTitles = ['Add Hosts', 'Create OSDs', 'Review']; + stepTitles = ['Add Hosts', 'Create OSDs', 'Create Services', 'Review']; startClusterCreation = false; observables: any = []; modalRef: NgbModalRef; @@ -46,7 +46,7 @@ export class CreateClusterComponent implements OnDestroy { constructor( private authStorageService: AuthStorageService, - private stepsService: WizardStepsService, + private wizardStepsService: WizardStepsService, private router: Router, private hostService: HostService, private notificationService: NotificationService, @@ -54,13 +54,14 @@ export class CreateClusterComponent implements OnDestroy { private clusterService: ClusterService, private modalService: ModalService, private taskWrapper: TaskWrapperService, - private osdService: OsdService, - private wizardStepService: WizardStepsService + private osdService: OsdService ) { this.permissions = this.authStorageService.getPermissions(); - this.currentStepSub = this.stepsService.getCurrentStep().subscribe((step: WizardStepModel) => { - this.currentStep = step; - }); + this.currentStepSub = this.wizardStepsService + .getCurrentStep() + .subscribe((step: WizardStepModel) => { + this.currentStep = step; + }); this.currentStep.stepIndex = 1; } @@ -93,77 +94,87 @@ export class CreateClusterComponent implements OnDestroy { } onSubmit() { - forkJoin(this.observables) - .pipe( - finalize(() => - this.clusterService.updateStatus('POST_INSTALLED').subscribe(() => { - this.notificationService.show( - NotificationType.success, - $localize`Cluster expansion was successful` - ); - this.router.navigate(['/dashboard']); - }) - ) - ) - .subscribe({ - error: (error) => error.preventDefault() - }); - - this.taskWrapper - .wrapTaskAroundCall({ - task: new FinishedTask('osd/' + URLVerbs.CREATE, { - tracking_id: _.join(_.map(this.driveGroups, 'service_id'), ', ') - }), - call: this.osdService.create(this.driveGroups) - }) - .subscribe({ - error: (error) => error.preventDefault(), - complete: () => { - this.submitAction.emit(); + this.hostService.list().subscribe((hosts) => { + hosts.forEach((host) => { + const index = host['labels'].indexOf('_no_schedule', 0); + if (index > -1) { + host['labels'].splice(index, 1); + this.observables.push(this.hostService.update(host['hostname'], true, host['labels'])); } }); - } + forkJoin(this.observables) + .pipe( + finalize(() => + this.clusterService.updateStatus('POST_INSTALLED').subscribe(() => { + this.notificationService.show( + NotificationType.success, + $localize`Cluster expansion was successful` + ); + this.router.navigate(['/dashboard']); + }) + ) + ) + .subscribe({ + error: (error) => error.preventDefault() + }); + }); + if (this.driveGroup) { + const user = this.authStorageService.getUsername(); + this.driveGroup.setName(`dashboard-${user}-${_.now()}`); + this.driveGroups.push(this.driveGroup.spec); + } - onNextStep() { - if (!this.stepsService.isLastStep()) { - this.hostService.list().subscribe((hosts) => { - hosts.forEach((host) => { - const index = host['labels'].indexOf('_no_schedule', 0); - if (index > -1) { - host['labels'].splice(index, 1); - this.observables.push(this.hostService.update(host['hostname'], true, host['labels'])); + if (this.osdService.osdDevices['totalDevices'] > 0) { + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('osd/' + URLVerbs.CREATE, { + tracking_id: _.join(_.map(this.driveGroups, 'service_id'), ', ') + }), + call: this.osdService.create(this.driveGroups) + }) + .subscribe({ + error: (error) => error.preventDefault(), + complete: () => { + this.submitAction.emit(); + this.osdService.osdDevices = []; } }); - }); - this.driveGroup = this.wizardStepService.sharedData; - this.stepsService.getCurrentStep().subscribe((step: WizardStepModel) => { + } + } + + getDriveGroup(driveGroup: DriveGroup) { + this.driveGroup = driveGroup; + } + + onNextStep() { + if (!this.wizardStepsService.isLastStep()) { + this.wizardStepsService.getCurrentStep().subscribe((step: WizardStepModel) => { this.currentStep = step; }); - if (this.currentStep.stepIndex === 2 && this.driveGroup) { - const user = this.authStorageService.getUsername(); - this.driveGroup.setName(`dashboard-${user}-${_.now()}`); - this.driveGroups.push(this.driveGroup.spec); - } - this.stepsService.moveToNextStep(); + this.wizardStepsService.moveToNextStep(); } else { this.onSubmit(); } } onPreviousStep() { - if (!this.stepsService.isFirstStep()) { - this.stepsService.moveToPreviousStep(); + if (!this.wizardStepsService.isFirstStep()) { + this.wizardStepsService.moveToPreviousStep(); } else { this.router.navigate(['/dashboard']); } } showSubmitButtonLabel() { - return !this.stepsService.isLastStep() ? this.actionLabels.NEXT : $localize`Expand Cluster`; + return !this.wizardStepsService.isLastStep() + ? this.actionLabels.NEXT + : $localize`Expand Cluster`; } showCancelButtonLabel() { - return !this.stepsService.isFirstStep() ? this.actionLabels.BACK : this.actionLabels.CANCEL; + return !this.wizardStepsService.isFirstStep() + ? this.actionLabels.BACK + : this.actionLabels.CANCEL; } ngOnDestroy(): void { 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 59bab46d72d23..cb0ebca6bcd6d 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 @@ -11,7 +11,7 @@ columnMode="flex" (fetchData)="getHosts($event)" selectionType="single" - [hasDetails]="!clusterCreation" + [hasDetails]="hasTableDetails" (setExpandedRow)="setExpandedRow($event)" (updateSelection)="updateSelection($event)">
@@ -30,7 +30,7 @@
  • + *ngIf="permissions.grafana.read"> 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 049aceba0994a..288f62e7a3d29 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,7 +72,6 @@ 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); }); @@ -185,7 +184,7 @@ describe('HostsComponent', () => { expectResults: { Add: { disabled: false, disableDesc: '' }, Edit: { disabled: true, disableDesc: '' }, - Delete: { disabled: true, disableDesc: '' } + Remove: { disabled: true, disableDesc: '' } } }, { @@ -193,7 +192,7 @@ describe('HostsComponent', () => { expectResults: { Add: { disabled: false, disableDesc: '' }, Edit: { disabled: true, disableDesc: component.messages.nonOrchHost }, - Delete: { disabled: true, disableDesc: component.messages.nonOrchHost } + Remove: { disabled: true, disableDesc: component.messages.nonOrchHost } } }, { @@ -201,15 +200,15 @@ describe('HostsComponent', () => { expectResults: { Add: { disabled: false, disableDesc: '' }, Edit: { disabled: false, disableDesc: '' }, - Delete: { disabled: false, disableDesc: '' } + Remove: { disabled: false, disableDesc: '' } } } ]; const features = [ - OrchestratorFeature.HOST_CREATE, + OrchestratorFeature.HOST_ADD, OrchestratorFeature.HOST_LABEL_ADD, - OrchestratorFeature.HOST_DELETE, + OrchestratorFeature.HOST_REMOVE, OrchestratorFeature.HOST_LABEL_REMOVE ]; await testTableActions(true, features, tests); @@ -225,7 +224,7 @@ describe('HostsComponent', () => { expectResults: { Add: resultNoOrchestrator, Edit: { disabled: true, disableDesc: '' }, - Delete: { disabled: true, disableDesc: '' } + Remove: { disabled: true, disableDesc: '' } } }, { @@ -233,7 +232,7 @@ describe('HostsComponent', () => { expectResults: { Add: resultNoOrchestrator, Edit: { disabled: true, disableDesc: component.messages.nonOrchHost }, - Delete: { disabled: true, disableDesc: component.messages.nonOrchHost } + Remove: { disabled: true, disableDesc: component.messages.nonOrchHost } } }, { @@ -241,7 +240,7 @@ describe('HostsComponent', () => { expectResults: { Add: resultNoOrchestrator, Edit: resultNoOrchestrator, - Delete: resultNoOrchestrator + Remove: resultNoOrchestrator } } ]; @@ -258,7 +257,7 @@ describe('HostsComponent', () => { expectResults: { Add: resultMissingFeatures, Edit: { disabled: true, disableDesc: '' }, - Delete: { disabled: true, disableDesc: '' } + Remove: { disabled: true, disableDesc: '' } } }, { @@ -266,7 +265,7 @@ describe('HostsComponent', () => { expectResults: { Add: resultMissingFeatures, Edit: { disabled: true, disableDesc: component.messages.nonOrchHost }, - Delete: { disabled: true, disableDesc: component.messages.nonOrchHost } + Remove: { disabled: true, disableDesc: component.messages.nonOrchHost } } }, { @@ -274,7 +273,7 @@ describe('HostsComponent', () => { expectResults: { Add: resultMissingFeatures, Edit: resultMissingFeatures, - Delete: resultMissingFeatures + Remove: 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 c70f755f799e8..6bd8dbbdc2520 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 @@ -47,8 +47,21 @@ export class HostsComponent extends ListWithDetails implements OnInit { public servicesTpl: TemplateRef; @ViewChild('maintenanceConfirmTpl', { static: true }) maintenanceConfirmTpl: TemplateRef; + + @Input() + hiddenColumns: string[] = []; + + @Input() + hideTitle = false; + + @Input() + hideSubmitBtn = false; + + @Input() + hasTableDetails = true; + @Input() - clusterCreation = false; + showGeneralActionsOnly = false; permissions: Permissions; columns: Array = []; @@ -61,7 +74,6 @@ export class HostsComponent extends ListWithDetails implements OnInit { isExecuting = false; errorMessage: string; enableButton: boolean; - pageURL: string; bsModalRef: NgbModalRef; icons = Icons; @@ -72,9 +84,9 @@ export class HostsComponent extends ListWithDetails implements OnInit { orchStatus: OrchestratorStatus; actionOrchFeatures = { - add: [OrchestratorFeature.HOST_CREATE], + add: [OrchestratorFeature.HOST_ADD], edit: [OrchestratorFeature.HOST_LABEL_ADD, OrchestratorFeature.HOST_LABEL_REMOVE], - delete: [OrchestratorFeature.HOST_DELETE], + remove: [OrchestratorFeature.HOST_REMOVE], maintenance: [ OrchestratorFeature.HOST_MAINTENANCE_ENTER, OrchestratorFeature.HOST_MAINTENANCE_EXIT @@ -95,6 +107,16 @@ 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.url.includes('/hosts') + ? this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.ADD] } }]) + : (this.bsModalRef = this.modalService.show(HostFormComponent)), + disable: (selection: CdTableSelection) => this.getDisable('add', selection) + }, { name: this.actionLabels.EDIT, permission: 'update', @@ -103,11 +125,11 @@ export class HostsComponent extends ListWithDetails implements OnInit { disable: (selection: CdTableSelection) => this.getDisable('edit', selection) }, { - name: this.actionLabels.DELETE, + name: this.actionLabels.REMOVE, permission: 'delete', icon: Icons.destroy, click: () => this.deleteAction(), - disable: (selection: CdTableSelection) => this.getDisable('delete', selection) + disable: (selection: CdTableSelection) => this.getDisable('remove', selection) }, { name: this.actionLabels.ENTER_MAINTENANCE, @@ -116,7 +138,7 @@ export class HostsComponent extends ListWithDetails implements OnInit { click: () => this.hostMaintenance(), disable: (selection: CdTableSelection) => this.getDisable('maintenance', selection) || this.isExecuting || this.enableButton, - visible: () => !this.clusterCreation + visible: () => !this.showGeneralActionsOnly }, { name: this.actionLabels.EXIT_MAINTENANCE, @@ -124,23 +146,13 @@ export class HostsComponent extends ListWithDetails implements OnInit { icon: Icons.exit, click: () => this.hostMaintenance(), disable: (selection: CdTableSelection) => - this.getDisable('maintenance', selection) || this.isExecuting || this.enableButton, - visible: () => !this.clusterCreation + this.getDisable('maintenance', selection) || this.isExecuting || !this.enableButton, + visible: () => !this.showGeneralActionsOnly } ]; } ngOnInit() { - this.tableActions.unshift({ - name: this.actionLabels.ADD, - permission: 'create', - icon: Icons.add, - click: () => - this.clusterCreation - ? (this.bsModalRef = this.modalService.show(HostFormComponent)) - : this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.ADD] } }]), - disable: (selection: CdTableSelection) => this.getDisable('add', selection) - }); this.columns = [ { name: $localize`Hostname`, @@ -150,7 +162,6 @@ export class HostsComponent extends ListWithDetails implements OnInit { { name: $localize`Services`, prop: 'services', - isHidden: this.clusterCreation, flexGrow: 3, cellTemplate: this.servicesTpl }, @@ -177,7 +188,6 @@ export class HostsComponent extends ListWithDetails implements OnInit { { name: $localize`Version`, prop: 'ceph_version', - isHidden: this.clusterCreation, flexGrow: 1, pipe: this.cephShortVersionPipe } @@ -186,12 +196,9 @@ export class HostsComponent extends ListWithDetails implements OnInit { this.orchStatus = status; }); - if (this.clusterCreation) { - const hiddenColumns = ['services', 'ceph_version']; - this.columns = this.columns.filter((col: any) => { - return !hiddenColumns.includes(col.prop); - }); - } + this.columns = this.columns.filter((col: any) => { + return !this.hiddenColumns.includes(col.prop); + }); } updateSelection(selection: CdTableSelection) { @@ -305,10 +312,10 @@ export class HostsComponent extends ListWithDetails implements OnInit { } getDisable( - action: 'add' | 'edit' | 'delete' | 'maintenance', + action: 'add' | 'edit' | 'remove' | 'maintenance', selection: CdTableSelection ): boolean | string { - if (action === 'delete' || action === 'edit' || action === 'maintenance') { + if (action === 'remove' || action === 'edit' || action === 'maintenance') { if (!selection?.hasSingleSelection) { return true; } @@ -327,10 +334,10 @@ export class HostsComponent extends ListWithDetails implements OnInit { this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { itemDescription: 'Host', itemNames: [hostname], - actionDescription: 'delete', + actionDescription: 'remove', submitActionObservable: () => this.taskWrapper.wrapTaskAroundCall({ - task: new FinishedTask('host/delete', { hostname: hostname }), + task: new FinishedTask('host/remove', { hostname: hostname }), call: this.hostService.delete(hostname) }) }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.html index bfbf3dcf143b4..8b1f59eac371c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.html @@ -19,7 +19,7 @@ (click)="showSelectionModal()" data-toggle="tooltip" [title]="addButtonTooltip" - [disabled]="availDevices.length === 0 || !canSelect"> + [disabled]="availDevices.length === 0 || !canSelect || expansionCanSelect"> Add diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.spec.ts index 0d9250000616c..dea6746cf9512 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.spec.ts @@ -2,6 +2,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; import { ToastrModule } from 'ngx-toastr'; @@ -34,7 +35,8 @@ describe('OsdDevicesSelectionGroupsComponent', () => { FormsModule, HttpClientTestingModule, SharedModule, - ToastrModule.forRoot() + ToastrModule.forRoot(), + RouterTestingModule ], declarations: [OsdDevicesSelectionGroupsComponent, InventoryDevicesComponent] }); @@ -92,8 +94,10 @@ describe('OsdDevicesSelectionGroupsComponent', () => { describe('with devices selected', () => { beforeEach(() => { + component.isOsdPage = true; component.availDevices = []; component.devices = devices; + component.ngOnInit(); fixture.detectChanges(); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.ts index bbeb8b0f0cee3..cff0cbc0563fc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.ts @@ -1,8 +1,10 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; +import { Router } from '@angular/router'; import _ from 'lodash'; import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model'; +import { OsdService } from '~/app/shared/api/osd.service'; import { Icons } from '~/app/shared/enum/icons.enum'; import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change'; import { ModalService } from '~/app/shared/services/modal.service'; @@ -37,7 +39,9 @@ export class OsdDevicesSelectionGroupsComponent implements OnInit, OnChanges { icons = Icons; devices: InventoryDevice[] = []; capacity = 0; - appliedFilters: any[] = []; + appliedFilters = new Array(); + expansionCanSelect = false; + isOsdPage: boolean; addButtonTooltip: String; tooltips = { @@ -46,9 +50,24 @@ export class OsdDevicesSelectionGroupsComponent implements OnInit, OnChanges { addByFilters: $localize`Add devices by using filters` }; - constructor(private modalService: ModalService) {} + constructor( + private modalService: ModalService, + public osdService: OsdService, + private router: Router + ) { + this.isOsdPage = this.router.url.includes('/osd'); + } ngOnInit() { + if (!this.isOsdPage) { + this.osdService?.osdDevices[this.type] + ? (this.devices = this.osdService.osdDevices[this.type]) + : (this.devices = []); + this.capacity = _.sumBy(this.devices, 'sys_api.size'); + this.osdService?.osdDevices + ? (this.expansionCanSelect = this.osdService?.osdDevices['disableSelect']) + : (this.expansionCanSelect = false); + } this.updateAddButtonTooltip(); } @@ -75,6 +94,12 @@ export class OsdDevicesSelectionGroupsComponent implements OnInit, OnChanges { this.capacity = _.sumBy(this.devices, 'sys_api.size'); this.appliedFilters = result.filters; const event = _.assign({ type: this.type }, result); + if (!this.isOsdPage) { + this.osdService.osdDevices[this.type] = this.devices; + this.osdService.osdDevices['disableSelect'] = + this.canSelect || this.devices.length === this.availDevices.length; + this.osdService.osdDevices[this.type]['capacity'] = this.capacity; + } this.selected.emit(event); }); } @@ -95,6 +120,11 @@ export class OsdDevicesSelectionGroupsComponent implements OnInit, OnChanges { } clearDevices() { + if (!this.isOsdPage) { + this.expansionCanSelect = false; + this.osdService.osdDevices['disableSelect'] = false; + this.osdService.osdDevices = []; + } const event = { type: this.type, clearedDevices: [...this.devices] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.ts index 1909803dc3380..edfe9d6a7c3e2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.ts @@ -82,8 +82,6 @@ export class OsdDevicesSelectionModalComponent implements AfterViewInit { this.filteredDevices = event.data; this.capacity = _.sumBy(this.filteredDevices, 'sys_api.size'); this.event = event; - this.wizardStepService.osdDevices = this.filteredDevices; - this.wizardStepService.osdCapacity = this.capacity; } } 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 675e20fcf59a6..59a17362f6f83 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 @@ -9,7 +9,7 @@
    {{ action | titlecase }} {{ resource | upperFirst }}
    + *ngIf="!hideTitle">{{ action | titlecase }} {{ resource | upperFirst }}