From: Aashish Sharma Date: Tue, 7 Sep 2021 06:30:45 +0000 (+0530) Subject: mgr/dashboard: Cluster Creation Add Services Section X-Git-Tag: v16.2.7~52^2~18 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=da46bad0397a82f615ebdc56b8274cc7ba8095dc;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 (cherry picked from commit b914f59ff123b9e006d7d11dc786e967c0271dbf) --- diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py index 0c5d85f5458f..e0963570bf35 100644 --- a/src/pybind/mgr/dashboard/controllers/host.py +++ b/src/pybind/mgr/dashboard/controllers/host.py @@ -284,7 +284,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('', @@ -302,9 +302,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 4ae03f4aaae2..a528c0fc12e0 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 7a7e00d6648a..5f3d39dcedc5 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 e28a8fbd57c2..0a943d4b0553 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 92c0739c5ede..ee272bcf943b 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 624f457458ff..000000000000 --- 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 116cbd789c8c..000000000000 --- 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 000000000000..0a95474c91d3 --- /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 000000000000..9097034e1a88 --- /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 000000000000..a5b54786b873 --- /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 7b099fd98acf..4e15088a2c9c 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 8bb642849b34..222ca8531baf 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 @@ -120,7 +120,8 @@ const routes: Routes = [ }, { path: 'services', - canActivateChild: [ModuleStatusGuardService], + component: ServicesComponent, + canActivate: [ModuleStatusGuardService], data: { moduleStatusGuardConfig: { apiPath: 'orchestrator', @@ -132,11 +133,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 f95dfdb910f8..e6f31dc9e74f 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 e823932c0973..8122cb682f0b 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 3abbcb122865..90e25d1162dc 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 d9e8ec43d9cc..9ae80f4c0218 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 8f9d7328e34e..3e0b7f7bdcce 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 6d78a2110b46..7d973119dfea 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 59bab46d72d2..cb0ebca6bcd6 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 049aceba0994..288f62e7a3d2 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 c70f755f799e..6bd8dbbdc252 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 bfbf3dcf143b..8b1f59eac371 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 0d9250000616..dea6746cf951 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 bbeb8b0f0cee..cff0cbc0563f 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 1909803dc338..edfe9d6a7c3e 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 675e20fcf59a..59a17362f6f8 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 }}