]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Cluster Creation Add Services Section
authorAashish Sharma <aashishsharma@localhost.localdomain>
Tue, 7 Sep 2021 06:30:45 +0000 (12:00 +0530)
committerNizamudeen A <nia@redhat.com>
Wed, 13 Oct 2021 10:31:21 +0000 (16:01 +0530)
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 <nia@redhat.com>
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
38 files changed:
src/pybind/mgr/dashboard/controllers/host.py
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/create-cluster.po.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-create-osds.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-review.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-cluster-check.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-create-cluster-create-services.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/06-cluster-check.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-modal/osd-devices-selection-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/inventory-device-type.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/models/orchestrator.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/wizard-steps.service.ts
src/pybind/mgr/dashboard/services/orchestrator.py

index 6ba1a5e0f1cbbfbbe77e685d35f7e4f2fc2c131e..c246de8f9f12c60c7249c71a0752502e46462a62 100644 (file)
@@ -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()
index 4ae03f4aaae293f6e937b438059560ca2b52ac44..a528c0fc12e0535ed1c509865743f65320f9da5f 100644 (file)
@@ -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);
+  }
 }
index 7a7e00d6648ace7d6896d2ceb57202a37fe5187e..5f3d39dcedc50a03cf4e4618944578cf3a841a83 100644 (file)
@@ -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
index e28a8fbd57c2663cdb4d7d9458a01aadd081533a..0a943d4b0553fc085fcf1f675bb6e14064150c60 100644 (file)
@@ -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 {
index 92c0739c5ede40656402b8395104ba5a87242382..ee272bcf943b23387857a9c210277c7a3c941a40 100644 (file)
@@ -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 (file)
index 624f457..0000000
+++ /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 (file)
index 116cbd7..0000000
+++ /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 (file)
index 0000000..0a95474
--- /dev/null
@@ -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 (file)
index 0000000..9097034
--- /dev/null
@@ -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 (file)
index 0000000..a5b5478
--- /dev/null
@@ -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);
+    });
+  });
+});
index 7b099fd98acf21fd73daa9f7c285f4b8a12407b1..4e15088a2c9c95ac52c0657dd943fcbe62894a50 100644 (file)
@@ -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');
index bdd2cb89782346d0f00aa9397397cbef20366c6b..e1eb456ee8aa0e20f5c940729306b9f865acdc50 100644 (file)
@@ -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'
           }
         ]
       },
index f95dfdb910f8185020bfd451b710b26abb35137c..e6f31dc9e74f8d089fd4366c6d1bb5f14970c878 100644 (file)
@@ -12,9 +12,8 @@
         <tr>
           <td i18n
               class="bold">Storage Capacity</td>
-          <td><span i18n
-                    *ngIf="filteredDevices && capacity">Number of devices: {{ filteredDevices.length }}. Raw capacity:
-            {{ capacity | dimlessBinary }}.</span></td>
+          <td><span i18n>Number of devices: {{ totalDevices }}. Raw capacity:
+            {{ totalCapacity | dimlessBinary }}.</span></td>
         </tr>
       </table>
     </fieldset>
@@ -22,9 +21,9 @@
 
   <div class="col-lg-8">
     <legend i18n
-            class="cd-header">Hosts by Label</legend>
-    <cd-table [data]="hostsByLabel['data']"
-              [columns]="hostsByLabel['columns']"
+            class="cd-header">Hosts by Services</legend>
+    <cd-table [data]="hostsByService['data']"
+              [columns]="hostsByService['columns']"
               [toolHeader]="false">
     </cd-table>
 
index e823932c09732b7f190d9d44900d65e6a3336182..8122cb682f0badf3b41889719ed5be924951ad6c 100644 (file)
@@ -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<CreateClusterReviewComponent>;
-  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);
   });
 });
index 3abbcb122865e8b2bd94b19a22f4ab8444263ee5..90e25d1162dc7905b09bf8f91e8d37b456b7e613 100644 (file)
@@ -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<string> = new Set();
-  filteredDevices: InventoryDevice[] = [];
-  capacity = 0;
+  serviceCount: number;
+  serviceOccurrences = {};
+  hostsCountPerService: object[] = [];
+  uniqueServices: Set<string> = new Set();
+  totalDevices: number;
+  totalCapacity = 0;
+  services: Array<CephServiceSpec> = [];
 
-  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<CephServiceSpec>) => {
+      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;
   }
 }
index d9e8ec43d9cc97a515fda97b375b8f814289ee5b..9ae80f4c0218d7c62a43941ccc16826d454bedd8 100644 (file)
           <h4 class="title"
               i18n>Add Hosts</h4>
           <br>
-          <cd-hosts [clusterCreation]="true"></cd-hosts>
+          <cd-hosts [hiddenColumns]="['services', 'ceph_version']"
+                    [hideTitle]="true"
+                    [hideSubmitBtn]="true"
+                    [hasTableDetails]="false"
+                    [showGeneralActionsOnly]="true"></cd-hosts>
         </div>
         <div *ngSwitchCase="'2'"
              class="ml-5">
               i18n>Create OSDs</h4>
           <br>
           <div class="alignForm">
-            <cd-osd-form [clusterCreation]="true"></cd-osd-form>
+            <cd-osd-form [hideTitle]="true"
+                         [hideSubmitBtn]="true"
+                         (emitDriveGroup)="getDriveGroup($event)"></cd-osd-form>
           </div>
         </div>
         <div *ngSwitchCase="'3'"
              class="ml-5">
+          <h4 class="title"
+              i18n>Create Services</h4>
+          <br>
+          <cd-services [hasDetails]="false"
+                       [hiddenServices]="['mon', 'mgr', 'crash', 'agent']"
+                       [hiddenColumns]="['status.running', 'status.size', 'status.last_refresh']"
+                       [modal]="false"></cd-services>
+        </div>
+        <div *ngSwitchCase="'4'"
+             class="ml-5">
           <cd-create-cluster-review></cd-create-cluster-review>
         </div>
       </ng-container>
index 8f9d7328e34ee821725fbd62e50371b47e217877..3e0b7f7bdcce71ac83f165127ab8044909110501 100644 (file)
@@ -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<CreateClusterComponent>;
   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);
+  });
 });
index 6d78a2110b46179d52f215a39f850223c5701870..7d973119dfea29ec1890f281e58bc8f41132423a 100644 (file)
@@ -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 {
index 59bab46d72d237160f000f46f3cb98bbbe0c130d..cb0ebca6bcd6dba537d64a5b5646597d232cdf89 100644 (file)
@@ -11,7 +11,7 @@
                 columnMode="flex"
                 (fetchData)="getHosts($event)"
                 selectionType="single"
-                [hasDetails]="!clusterCreation"
+                [hasDetails]="hasTableDetails"
                 (setExpandedRow)="setExpandedRow($event)"
                 (updateSelection)="updateSelection($event)">
         <div class="table-actions btn-toolbar">
@@ -30,7 +30,7 @@
     </ng-template>
   </li>
   <li ngbNavItem
-      *ngIf="permissions.grafana.read && !clusterCreation">
+      *ngIf="permissions.grafana.read">
     <a ngbNavLink
        i18n>Overall Performance</a>
     <ng-template ngbNavContent>
index 049aceba0994a77ad2af35e95ea60f63a36682f3..288f62e7a3d29d72ed69d8c6e8e92759f0550729 100644 (file)
@@ -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
           }
         }
       ];
index c70f755f799e84bd2ca0442a462c9e9540d792df..6bd8dbbdc2520a60ee387691f70ea9663657efb2 100644 (file)
@@ -47,8 +47,21 @@ export class HostsComponent extends ListWithDetails implements OnInit {
   public servicesTpl: TemplateRef<any>;
   @ViewChild('maintenanceConfirmTpl', { static: true })
   maintenanceConfirmTpl: TemplateRef<any>;
+
+  @Input()
+  hiddenColumns: string[] = [];
+
+  @Input()
+  hideTitle = false;
+
+  @Input()
+  hideSubmitBtn = false;
+
+  @Input()
+  hasTableDetails = true;
+
   @Input()
-  clusterCreation = false;
+  showGeneralActionsOnly = false;
 
   permissions: Permissions;
   columns: Array<CdTableColumn> = [];
@@ -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)
         })
     });
index bfbf3dcf143b426318cc5535e9d56b763b5ffe5e..8b1f59eac371c922d991abbe2cac964936dff1f9 100644 (file)
@@ -19,7 +19,7 @@
               (click)="showSelectionModal()"
               data-toggle="tooltip"
               [title]="addButtonTooltip"
-              [disabled]="availDevices.length === 0 || !canSelect">
+              [disabled]="availDevices.length === 0 || !canSelect || expansionCanSelect">
         <i [ngClass]="[icons.add]"></i>
         <ng-container i18n>Add</ng-container>
       </button>
index 0d9250000616cfc69c9c50946170b8bcbc7517d4..dea6746cf95125511231bcc1f3722b3f078f08eb 100644 (file)
@@ -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();
     });
 
index bbeb8b0f0cee3891db3ec9504f60bb19a191a1cc..cff0cbc0563fc8a39f146933a256042bd47fb204 100644 (file)
@@ -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]
index 1909803dc3380fb3aeca5361ccd977b281c2a809..edfe9d6a7c3e208243e3a1dc63ca764da9da3bdd 100644 (file)
@@ -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;
     }
   }
 
index 675e20fcf59a62f8b8e4b1f3903b816058b85c7c..59a17362f6f834eb55da7c9f3999e8e90202dbbc 100644 (file)
@@ -9,7 +9,7 @@
     <div class="card">
       <div i18n="form title|Example: Create Pool@@formTitle"
            class="card-header"
-           *ngIf="!clusterCreation">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+           *ngIf="!hideTitle">{{ action | titlecase }} {{ resource | upperFirst }}</div>
       <div class="card-body">
         <fieldset>
           <cd-osd-devices-selection-groups #dataDeviceSelectionGroups
         </fieldset>
       </div>
       <div class="card-footer"
-           *ngIf="!clusterCreation">
+           *ngIf="!hideSubmitBtn">
         <cd-form-button-panel #previewButtonPanel
                               (submitActionEvent)="submit()"
                               [form]="form"
index 411e0f16c88740beb7076633d479f3eaebd83c6b..9f229b66f8c24e7466968d2c52d6141320b53af0 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
 import { FormControl, Validators } from '@angular/forms';
 import { Router } from '@angular/router';
 
@@ -41,6 +41,14 @@ export class OsdFormComponent extends CdForm implements OnInit {
   @ViewChild('previewButtonPanel')
   previewButtonPanel: FormButtonPanelComponent;
 
+  @Input()
+  hideTitle = false;
+
+  @Input()
+  hideSubmitBtn = false;
+
+  @Output() emitDriveGroup: EventEmitter<DriveGroup> = new EventEmitter();
+
   icons = Icons;
 
   form: CdFormGroup;
@@ -62,8 +70,6 @@ export class OsdFormComponent extends CdForm implements OnInit {
   featureList: OsdFeature[] = [];
 
   hasOrchestrator = true;
-  @Input()
-  clusterCreation = false;
 
   constructor(
     public actionLabels: ActionLabelsI18n,
@@ -186,9 +192,8 @@ export class OsdFormComponent extends CdForm implements OnInit {
       this.enableFeatures();
     }
     this.driveGroup.setDeviceSelection(event.type, event.filters);
-    if (this.clusterCreation) {
-      this.wizardStepService.sharedData = this.driveGroup;
-    }
+
+    this.emitDriveGroup.emit(this.driveGroup);
   }
 
   onDevicesCleared(event: DevicesSelectionClearEvent) {
index 3bda00511039175a49b4a8a76cfb1cdc7d0873cb..e8e340a7dc8dc108e0c6645d9156c371b8f12cb5 100644 (file)
@@ -1,12 +1,13 @@
-<div class="cd-col-form">
-  <form #frm="ngForm"
-        [formGroup]="serviceForm"
-        novalidate>
-    <div class="card">
-      <div i18n="form title"
-           class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+<cd-modal [pageURL]="pageURL"
+          [modalRef]="activeModal">
+  <span class="modal-title"
+        i18n>{{ action | titlecase }} {{ resource | upperFirst }}</span>
+  <ng-container class="modal-content">
+    <form #frm="ngForm"
+          [formGroup]="serviceForm"
+          novalidate>
+      <div class="modal-body">
 
-      <div class="card-body">
         <!-- Service type -->
         <div class="form-group row">
           <label class="cd-col-form-label required"
         </ng-container>
       </div>
 
-      <div class="card-footer">
+      <div class="modal-footer">
         <div class="text-right">
           <cd-form-button-panel (submitActionEvent)="onSubmit()"
                                 [form]="serviceForm"
                                 [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
         </div>
       </div>
-    </div>
-  </form>
-</div>
+    </form>
+  </ng-container>
+</cd-modal>
index 11c0663efd246fa85685ac7482d8b06a62735dd8..78863435ea328e06f92a2dd0d6ac0ae60b1a2204 100644 (file)
@@ -4,7 +4,7 @@ import { ReactiveFormsModule } from '@angular/forms';
 import { By } from '@angular/platform-browser';
 import { RouterTestingModule } from '@angular/router/testing';
 
-import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
 import { ToastrModule } from 'ngx-toastr';
 
@@ -23,6 +23,7 @@ describe('ServiceFormComponent', () => {
 
   configureTestBed({
     declarations: [ServiceFormComponent],
+    providers: [NgbActiveModal],
     imports: [
       HttpClientTestingModule,
       NgbTypeaheadModule,
@@ -36,6 +37,7 @@ describe('ServiceFormComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(ServiceFormComponent);
     component = fixture.componentInstance;
+    component.ngOnInit();
     form = component.serviceForm;
     formHelper = new FormHelper(form);
     fixture.detectChanges();
index cb80a2571c0ae73a2a474b555d0f2bdf54aa339c..941eabfb67c623bba65ee0d47da97c90fc28b328 100644 (file)
@@ -1,8 +1,8 @@
-import { Component, OnInit, ViewChild } from '@angular/core';
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
 import { AbstractControl, Validators } from '@angular/forms';
 import { Router } from '@angular/router';
 
-import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
+import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
 import { merge, Observable, Subject } from 'rxjs';
 import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
@@ -31,6 +31,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
   @ViewChild(NgbTypeahead, { static: false })
   typeahead: NgbTypeahead;
 
+  @Input() public hiddenServices: string[] = [];
+
   serviceForm: CdFormGroup;
   action: string;
   resource: string;
@@ -41,6 +43,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
   labelFocus = new Subject<string>();
   pools: Array<object>;
   services: Array<CephServiceSpec> = [];
+  pageURL: string;
 
   constructor(
     public actionLabels: ActionLabelsI18n,
@@ -49,7 +52,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
     private hostService: HostService,
     private poolService: PoolService,
     private router: Router,
-    private taskWrapperService: TaskWrapperService
+    private taskWrapperService: TaskWrapperService,
+    public activeModal: NgbActiveModal
   ) {
     super();
     this.resource = $localize`service`;
@@ -215,12 +219,17 @@ export class ServiceFormComponent extends CdForm implements OnInit {
   }
 
   ngOnInit(): void {
+    if (this.router.url.includes('services')) {
+      this.pageURL = 'services';
+    }
     this.action = this.actionLabels.CREATE;
     this.cephServiceService.getKnownTypes().subscribe((resp: Array<string>) => {
       // Remove service types:
       // osd       - This is deployed a different way.
       // container - This should only be used in the CLI.
-      this.serviceTypes = _.difference(resp, ['container', 'osd']).sort();
+      this.hiddenServices.push('osd', 'container');
+
+      this.serviceTypes = _.difference(resp, this.hiddenServices).sort();
     });
     this.hostService.list().subscribe((resp: object[]) => {
       const options: SelectOption[] = [];
@@ -243,10 +252,6 @@ export class ServiceFormComponent extends CdForm implements OnInit {
     });
   }
 
-  goToListView() {
-    this.router.navigate(['/services']);
-  }
-
   searchLabels = (text$: Observable<string>) => {
     return merge(
       text$.pipe(debounceTime(200), distinctUntilChanged()),
@@ -385,8 +390,10 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         error() {
           self.serviceForm.setErrors({ cdSubmitButton: true });
         },
-        complete() {
-          self.goToListView();
+        complete: () => {
+          this.pageURL === 'services'
+            ? this.router.navigate([this.pageURL, { outlets: { modal: null } }])
+            : this.activeModal.close();
         }
       });
   }
index f6f66ee51485f337197a1fffa136b93db2fb7946..36ab431fc96b32b73b5bbfb0688a3781a9096e14 100644 (file)
@@ -8,7 +8,7 @@
             selectionType="single"
             [autoReload]="5000"
             (fetchData)="getServices($event)"
-            [hasDetails]="true"
+            [hasDetails]="hasDetails"
             (setExpandedRow)="setExpandedRow($event)"
             (updateSelection)="updateSelection($event)">
     <cd-table-actions class="table-actions"
@@ -22,3 +22,4 @@
     </cd-service-details>
   </cd-table>
 </ng-container>
+<router-outlet name="modal"></router-outlet>
index 7cba81ee49eab486c95e88cbbdb70609058c48db..c2346c2747990aaa9938e18bdea790478c84dc15 100644 (file)
@@ -1,5 +1,7 @@
 import { Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
+import { Router } from '@angular/router';
 
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 import { delay } from 'rxjs/operators';
 
 import { CephServiceService } from '~/app/shared/api/ceph-service.service';
@@ -24,6 +26,7 @@ import { ModalService } from '~/app/shared/services/modal.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
 import { PlacementPipe } from './placement.pipe';
+import { ServiceFormComponent } from './service-form/service-form.component';
 
 const BASE_URL = 'services';
 
@@ -42,9 +45,16 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
   // Do not display these columns
   @Input() hiddenColumns: string[] = [];
 
+  @Input() hiddenServices: string[] = [];
+
+  @Input() hasDetails = true;
+
+  @Input() modal = true;
+
   permissions: Permissions;
   tableActions: CdTableAction[];
   showDocPanel = false;
+  bsModalRef: NgbModalRef;
 
   orchStatus: OrchestratorStatus;
   actionOrchFeatures = {
@@ -65,7 +75,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
     private cephServiceService: CephServiceService,
     private relativeDatePipe: RelativeDatePipe,
     private taskWrapperService: TaskWrapperService,
-    private urlBuilder: URLBuilderService
+    private router: Router
   ) {
     super();
     this.permissions = this.authStorageService.getPermissions();
@@ -73,7 +83,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
       {
         permission: 'create',
         icon: Icons.add,
-        routerLink: () => this.urlBuilder.getCreate(),
+        click: () => this.openModal(),
         name: this.actionLabels.CREATE,
         canBePrimary: (selection: CdTableSelection) => !selection.hasSelection,
         disable: (selection: CdTableSelection) => this.getDisable('create', selection)
@@ -88,6 +98,15 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
     ];
   }
 
+  openModal() {
+    if (this.modal) {
+      this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.CREATE] } }]);
+    } else {
+      this.bsModalRef = this.modalService.show(ServiceFormComponent);
+      this.bsModalRef.componentInstance.hiddenServices = this.hiddenServices;
+    }
+  }
+
   ngOnInit() {
     const columns = [
       {
@@ -156,6 +175,9 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
     this.cephServiceService.list().subscribe(
       (services: CephServiceSpec[]) => {
         this.services = services;
+        this.services = this.services.filter((col: any) => {
+          return !this.hiddenServices.includes(col.service_name);
+        });
         this.isLoadingServices = false;
       },
       () => {
index 9bff8083936e1fd2d6f07d89a05fa6e9a939a16f..e33f2c3fc0fa1df8cac30fcc2dd4e36693485678 100644 (file)
@@ -5,6 +5,7 @@ import _ from 'lodash';
 import { map } from 'rxjs/operators';
 
 import { CdDevice } from '../models/devices';
+import { InventoryDeviceType } from '../models/inventory-device-type.model';
 import { SmartDataResponseV1 } from '../models/smart';
 import { DeviceService } from '../services/device.service';
 
@@ -13,6 +14,7 @@ import { DeviceService } from '../services/device.service';
 })
 export class OsdService {
   private path = 'api/osd';
+  osdDevices: InventoryDeviceType[] = [];
 
   osdRecvSpeedModalPriorities = {
     KNOWN_PRIORITIES: [
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/inventory-device-type.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/inventory-device-type.model.ts
new file mode 100644 (file)
index 0000000..2155c2d
--- /dev/null
@@ -0,0 +1,9 @@
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+
+export interface InventoryDeviceType {
+  type: string;
+  capacity: number;
+  devices: InventoryDevice[];
+  canSelect: boolean;
+  totalDevices: number;
+}
index 8c740f4e01756fd1b02c887ffb85d3c902086880..cf4ba76a9cf0960beef7ef9486606e5d697d3a9a 100644 (file)
@@ -1,7 +1,7 @@
 export enum OrchestratorFeature {
   HOST_LIST = 'get_hosts',
-  HOST_CREATE = 'add_host',
-  HOST_DELETE = 'remove_host',
+  HOST_ADD = 'add_host',
+  HOST_REMOVE = 'remove_host',
   HOST_LABEL_ADD = 'add_host_label',
   HOST_LABEL_REMOVE = 'remove_host_label',
   HOST_MAINTENANCE_ENTER = 'enter_host_maintenance',
index 44eb9bd30978a829b425535c70a630608be1c724..8705f29991665442308c31fb6378cdbaa6bd43a8 100644 (file)
@@ -115,7 +115,7 @@ export class TaskMessageService {
   messages = {
     // Host tasks
     'host/add': this.newTaskMessage(this.commonOperations.add, (metadata) => this.host(metadata)),
-    'host/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+    'host/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
       this.host(metadata)
     ),
     'host/identify_device': this.newTaskMessage(
index 1c8114033c15e840bbebae811f7ba78246d7b73c..e0fb2be944de02195147569f7e4ffa6478dcd4d7 100644 (file)
@@ -2,8 +2,6 @@ import { Injectable } from '@angular/core';
 
 import { BehaviorSubject, Observable } from 'rxjs';
 
-import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
-import { DriveGroup } from '~/app/ceph/cluster/osd/osd-form/drive-group.model';
 import { WizardStepModel } from '~/app/shared/models/wizard-steps';
 
 const initialStep = [{ stepIndex: 1, isComplete: false }];
@@ -14,9 +12,6 @@ const initialStep = [{ stepIndex: 1, isComplete: false }];
 export class WizardStepsService {
   steps$: BehaviorSubject<WizardStepModel[]>;
   currentStep$: BehaviorSubject<WizardStepModel> = new BehaviorSubject<WizardStepModel>(null);
-  sharedData = new DriveGroup();
-  osdDevices: InventoryDevice[] = [];
-  osdCapacity = 0;
 
   constructor() {
     this.steps$ = new BehaviorSubject<WizardStepModel[]>(initialStep);
index 88a6cb3a8608530ca44d8ab47c53bcf16e14dfe8..671ebdb751a39269a6ced199ff7b6a49503fd0a5 100644 (file)
@@ -192,8 +192,8 @@ class OrchClient(object):
 
 class OrchFeature(object):
     HOST_LIST = 'get_hosts'
-    HOST_CREATE = 'add_host'
-    HOST_DELETE = 'remove_host'
+    HOST_ADD = 'add_host'
+    HOST_REMOVE = 'remove_host'
     HOST_LABEL_ADD = 'add_host_label'
     HOST_LABEL_REMOVE = 'remove_host_label'
     HOST_MAINTENANCE_ENTER = 'enter_host_maintenance'