summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/frontend/cypress
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/cypress')
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/dashboard.e2e-spec.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/navigation.e2e-spec.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.e2e-spec.ts92
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.po.ts110
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.e2e-spec.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.po.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.e2e-spec.ts117
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.po.ts61
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts77
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts75
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.e2e-spec.ts36
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.po.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.e2e-spec.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.po.ts186
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/inventory.po.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.e2e-spec.ts61
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.po.ts77
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.e2e-spec.ts77
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.po.ts57
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.e2e-spec.ts61
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.po.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.e2e-spec.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.po.ts84
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/services.po.ts200
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.e2e-spec.ts46
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.po.ts59
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/common/forms-helper.feature.po.ts77
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/common/global.feature.po.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/common/grafana.feature.po.ts87
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/common/table-helper.feature.po.ts135
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/common/urls.po.ts48
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/filesystems.e2e-spec.feature30
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolume-groups.e2e-spec.feature51
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolumes.e2e-spec.feature51
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/01-hosts.e2e-spec.ts61
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/03-inventory.e2e-spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/04-osds.e2e-spec.ts49
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/05-services.e2e-spec.ts35
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/grafana/grafana.feature60
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/01-create-cluster-welcome.feature26
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/02-create-cluster-add-host.feature76
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts46
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts66
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts82
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/07-osds.e2e-spec.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/08-hosts.e2e-spec.ts48
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/09-services.e2e-spec.ts132
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/10-nfs-exports.e2e-spec.ts82
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/nfs/nfs-export.po.ts52
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts309
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.e2e-spec.ts53
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.po.ts70
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.e2e-spec.ts66
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.po.ts213
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.e2e-spec.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.po.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts45
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.po.ts139
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.e2e-spec.ts14
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.po.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.e2e-spec.ts49
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.po.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.e2e-spec.ts141
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.po.ts31
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.e2e-spec.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.po.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.e2e-spec.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.po.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.e2e-spec.ts23
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts78
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.e2e-spec.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.po.ts45
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.e2e-spec.ts36
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.po.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.e2e-spec.ts36
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.po.ts39
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/dashboard.vrt-spec.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/login.vrt-spec.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/fixtures/block-rbd-status.json1
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/fixtures/nfs-ganesha-status.json4
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/inventory.json390
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/services.json523
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/fixtures/rgw-status.json1
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/plugins/index.js26
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/support/commands.ts130
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/support/e2e.ts19
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/support/eyes-index.d.ts1
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/tsconfig.json17
93 files changed, 6041 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/dashboard.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/dashboard.e2e-spec.ts
new file mode 100644
index 000000000..39a2dbf14
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/dashboard.e2e-spec.ts
@@ -0,0 +1,26 @@
+import { DashboardPageHelper } from '../ui/dashboard.po';
+
+describe('Dashboard Main Page', { retries: 0 }, () => {
+ const dashboard = new DashboardPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ dashboard.navigateTo();
+ });
+
+ describe('Dashboard accessibility', () => {
+ it('should have no accessibility violations', () => {
+ cy.injectAxe();
+ cy.checkAccessibility(
+ {
+ exclude: [['.cd-navbar-main']]
+ },
+ {
+ rules: {
+ 'page-has-heading-one': { enabled: false }
+ }
+ }
+ );
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/navigation.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/navigation.e2e-spec.ts
new file mode 100644
index 000000000..3a0a1a7dc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/a11y/navigation.e2e-spec.ts
@@ -0,0 +1,20 @@
+import { NavigationPageHelper } from '../ui/navigation.po';
+
+describe('Navigation accessibility', { retries: 0 }, () => {
+ const shared = new NavigationPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ shared.navigateTo();
+ });
+
+ it('top-nav should have no accessibility violations', () => {
+ cy.injectAxe();
+ cy.checkAccessibility('.cd-navbar-top');
+ });
+
+ it('sidebar should have no accessibility violations', () => {
+ cy.injectAxe();
+ cy.checkAccessibility('nav[id=sidebar]');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.e2e-spec.ts
new file mode 100644
index 000000000..962c135d5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.e2e-spec.ts
@@ -0,0 +1,92 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { ImagesPageHelper } from './images.po';
+
+describe('Images page', () => {
+ const pools = new PoolPageHelper();
+ const images = new ImagesPageHelper();
+
+ const poolName = 'e2e_images_pool';
+
+ before(() => {
+ cy.login();
+ // Need pool for image testing
+ pools.navigateTo('create');
+ pools.create(poolName, 8, 'rbd');
+ pools.existTableCell(poolName);
+ });
+
+ after(() => {
+ // Deletes images test pool
+ pools.navigateTo();
+ pools.delete(poolName);
+ pools.navigateTo();
+ pools.existTableCell(poolName, false);
+ });
+
+ beforeEach(() => {
+ cy.login();
+ images.navigateTo();
+ });
+
+ it('should open and show breadcrumb', () => {
+ images.expectBreadcrumbText('Images');
+ });
+
+ it('should show four tabs', () => {
+ images.getTabsCount().should('eq', 4);
+ });
+
+ it('should show text for all tabs', () => {
+ images.getTabText(0).should('eq', 'Images');
+ images.getTabText(1).should('eq', 'Namespaces');
+ images.getTabText(2).should('eq', 'Trash');
+ images.getTabText(3).should('eq', 'Overall Performance');
+ });
+
+ describe('create, edit & delete image test', () => {
+ const imageName = 'e2e_images#image';
+ const newImageName = 'e2e_images#image_new';
+
+ it('should create image', () => {
+ images.createImage(imageName, poolName, '1');
+ images.getFirstTableCell(imageName).should('exist');
+ });
+
+ it('should edit image', () => {
+ images.editImage(imageName, poolName, newImageName, '2');
+ images.getFirstTableCell(newImageName).should('exist');
+ });
+
+ it('should delete image', () => {
+ images.delete(newImageName);
+ });
+ });
+
+ describe('move to trash, restore and purge image tests', () => {
+ const imageName = 'e2e_trash#image';
+ const newImageName = 'e2e_newtrash#image';
+
+ before(() => {
+ cy.login();
+ // Need image for trash testing
+ images.createImage(imageName, poolName, '1');
+ images.getFirstTableCell(imageName).should('exist');
+ });
+
+ it('should move the image to the trash', () => {
+ images.moveToTrash(imageName);
+ images.getFirstTableCell(imageName).should('exist');
+ });
+
+ it('should restore image to images table', () => {
+ images.restoreImage(imageName, newImageName);
+ images.getFirstTableCell(newImageName).should('exist');
+ });
+
+ it('should purge trash in images trash tab', () => {
+ images.getFirstTableCell(newImageName).should('exist');
+ images.moveToTrash(newImageName);
+ images.purgeTrash(newImageName, poolName);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.po.ts
new file mode 100644
index 000000000..bf6cbc052
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/images.po.ts
@@ -0,0 +1,110 @@
+import { PageHelper } from '../page-helper.po';
+
+export class ImagesPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/block/rbd', id: 'cd-rbd-list' },
+ create: { url: '#/block/rbd/create', id: 'cd-rbd-form' }
+ };
+
+ // Creates a block image and fills in the name, pool, and size fields.
+ // Then checks if the image is present in the Images table.
+ createImage(name: string, pool: string, size: string) {
+ this.navigateTo('create');
+
+ cy.get('#name').type(name); // Enter in image name
+
+ // Select image pool
+ cy.contains('Loading...').should('not.exist');
+ this.selectOption('pool', pool);
+ cy.get('#pool').should('have.class', 'ng-valid'); // check if selected
+
+ // Enter in the size of the image
+ cy.get('#size').type(size);
+
+ // Click the create button and wait for image to be made
+ cy.get('[data-cy=submitBtn]').click();
+ this.getFirstTableCell(name).should('exist');
+ }
+
+ editImage(name: string, pool: string, newName: string, newSize: string) {
+ this.navigateEdit(name);
+
+ // Wait until data is loaded
+ cy.get('#pool').should('contain.value', pool);
+
+ cy.get('#name').clear().type(newName);
+ cy.get('#size').clear().type(newSize); // click the size box and send new size
+
+ cy.get('[data-cy=submitBtn]').click();
+
+ this.getExpandCollapseElement(newName).click();
+ cy.get('.table.table-striped.table-bordered').contains('td', newSize);
+ }
+
+ // Selects RBD image and moves it to the trash,
+ // checks that it is present in the trash table
+ moveToTrash(name: string) {
+ // wait for image to be created
+ cy.get('.datatable-body').first().should('not.contain.text', '(Creating...)');
+
+ this.getFirstTableCell(name).click();
+
+ // click on the drop down and selects the move to trash option
+ cy.get('.table-actions button.dropdown-toggle').first().click();
+ cy.get('button.move-to-trash').click();
+
+ cy.get('[data-cy=submitBtn]').should('be.visible').click();
+
+ // Clicks trash tab
+ cy.contains('.nav-link', 'Trash').click();
+ this.getFirstTableCell(name).should('exist');
+ }
+
+ // Checks trash tab table for image and then restores it to the RBD Images table
+ // (could change name if new name is given)
+ restoreImage(name: string, newName?: string) {
+ // clicks on trash tab
+ cy.contains('.nav-link', 'Trash').click();
+
+ // wait for table to load
+ this.getFirstTableCell(name).click();
+ cy.contains('button', 'Restore').click();
+
+ // wait for pop-up to be visible (checks for title of pop-up)
+ cy.get('cd-modal #name').should('be.visible');
+
+ // If a new name for the image is passed, it changes the name of the image
+ if (newName !== undefined) {
+ // click name box and send new name
+ cy.get('cd-modal #name').clear().type(newName);
+ }
+
+ cy.get('[data-cy=submitBtn]').click();
+
+ // clicks images tab
+ cy.contains('.nav-link', 'Images').click();
+
+ this.getFirstTableCell(newName).should('exist');
+ }
+
+ // Enters trash tab and purges trash, thus emptying the trash table.
+ // Checks if Image is still in the table.
+ purgeTrash(name: string, pool?: string) {
+ // clicks trash tab
+ cy.contains('.nav-link', 'Trash').click();
+ cy.contains('button', 'Purge Trash').click();
+
+ // Check for visibility of modal container
+ cy.get('.modal-header').should('be.visible');
+
+ // If purgeing a specific pool, selects that pool if given
+ if (pool !== undefined) {
+ this.selectOption('poolName', pool);
+ cy.get('#poolName').should('have.class', 'ng-valid'); // check if pool is selected
+ }
+ cy.get('[data-cy=submitBtn]').click();
+ // Wait for image to delete and check it is not present
+
+ this.getFirstTableCell(name).should('not.exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.e2e-spec.ts
new file mode 100644
index 000000000..2788c4f9b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.e2e-spec.ts
@@ -0,0 +1,24 @@
+import { IscsiPageHelper } from './iscsi.po';
+
+describe('Iscsi Page', () => {
+ const iscsi = new IscsiPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ iscsi.navigateTo();
+ });
+
+ it('should open and show breadcrumb', () => {
+ iscsi.expectBreadcrumbText('Overview');
+ });
+
+ it('should check that tables are displayed and legends are correct', () => {
+ // Check tables are displayed
+ iscsi.getDataTables().its(0).should('be.visible');
+ iscsi.getDataTables().its(1).should('be.visible');
+
+ // Check that legends are correct
+ iscsi.getLegends().its(0).should('contain.text', 'Gateways');
+ iscsi.getLegends().its(1).should('contain.text', 'Images');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.po.ts
new file mode 100644
index 000000000..08efa6408
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/iscsi.po.ts
@@ -0,0 +1,7 @@
+import { PageHelper } from '../page-helper.po';
+
+export class IscsiPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/block/iscsi/overview', id: 'cd-iscsi' }
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.e2e-spec.ts
new file mode 100644
index 000000000..fb7db2712
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.e2e-spec.ts
@@ -0,0 +1,117 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { MirroringPageHelper } from './mirroring.po';
+
+describe('Mirroring page', () => {
+ const pools = new PoolPageHelper();
+ const mirroring = new MirroringPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ mirroring.navigateTo();
+ });
+
+ it('should open and show breadcrumb', () => {
+ mirroring.expectBreadcrumbText('Mirroring');
+ });
+
+ it('should show three tabs', () => {
+ mirroring.getTabsCount().should('eq', 3);
+ });
+
+ it('should show text for all tabs', () => {
+ mirroring.getTabText(0).should('eq', 'Issues (0)');
+ mirroring.getTabText(1).should('eq', 'Syncing (0)');
+ mirroring.getTabText(2).should('eq', 'Ready (0)');
+ });
+
+ describe('rbd mirroring bootstrap', () => {
+ const poolName = 'rbd-mirror';
+
+ beforeEach(() => {
+ // login to the second ceph cluster
+ cy.ceph2Login();
+ cy.login();
+ pools.navigateTo('create');
+ pools.create(poolName, 8, 'rbd');
+ pools.navigateTo();
+ pools.existTableCell(poolName, true);
+ mirroring.navigateTo();
+ });
+
+ it('should generate and import the bootstrap token between clusters', () => {
+ const url: string = Cypress.env('CEPH2_URL');
+ mirroring.navigateTo();
+ mirroring.generateToken(poolName);
+ cy.get('@token').then((bootstrapToken) => {
+ // pass the token to the origin as an arg
+ const args = { name: poolName, bootstrapToken: String(bootstrapToken) };
+ // can't use any imports or functions inside the origin
+ // so writing the code to copy the token inside the origin manually
+ // rather than using a function call
+ // @ts-ignore
+ cy.origin(url, { args }, ({ name, bootstrapToken }) => {
+ // Create an rbd pool in the second cluster
+
+ // Login to the second cluster
+ // Somehow its not working with the cypress login function
+ cy.visit('#/pool/create').wait(100);
+
+ cy.get('[name=username]').type('admin');
+ cy.get('#password').type('admin');
+ cy.get('[type=submit]').click();
+ cy.get('input[name=name]').clear().type(name);
+ cy.get(`select[name=poolType]`).select('replicated');
+ cy.get(`select[name=poolType] option:checked`).contains('replicated');
+ cy.get('.float-start.me-2.select-menu-edit').click();
+ cy.get('.popover-body').should('be.visible');
+ // Choose rbd as the application label
+ cy.get('.select-menu-item-content').contains('rbd').click();
+ cy.get('cd-submit-button').click();
+ cy.get('cd-pool-list').should('exist');
+
+ cy.visit('#/block/mirroring').wait(1000);
+ cy.get('.table-actions button.dropdown-toggle').first().click();
+ cy.get('[aria-label="Import Bootstrap Token"]').click();
+ cy.get('cd-bootstrap-import-modal').within(() => {
+ cy.get(`label[for=${name}]`).click();
+ cy.get('textarea[id=token]').wait(100).type(bootstrapToken);
+ cy.get('button[type=submit]').click();
+ });
+ });
+ });
+
+ // login again since origin removes all the cookies
+ // sessions, localStorage items etc..
+ cy.login();
+ mirroring.navigateTo();
+ mirroring.checkPoolHealthStatus(poolName, 'OK');
+ });
+ });
+
+ describe('checks that edit mode functionality shows in the pools table', () => {
+ const poolName = 'mirroring_test';
+
+ beforeEach(() => {
+ pools.navigateTo('create'); // Need pool for mirroring testing
+ pools.create(poolName, 8, 'rbd');
+ pools.navigateTo();
+ pools.existTableCell(poolName, true);
+ });
+
+ it('tests editing mode for pools', () => {
+ mirroring.navigateTo();
+
+ mirroring.editMirror(poolName, 'Pool');
+ mirroring.getFirstTableCell('pool').should('be.visible');
+ mirroring.editMirror(poolName, 'Image');
+ mirroring.getFirstTableCell('image').should('be.visible');
+ mirroring.editMirror(poolName, 'Disabled');
+ mirroring.getFirstTableCell('disabled').should('be.visible');
+ });
+
+ afterEach(() => {
+ pools.navigateTo();
+ pools.delete(poolName);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.po.ts
new file mode 100644
index 000000000..c4adca8b7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/block/mirroring.po.ts
@@ -0,0 +1,61 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/block/mirroring', id: 'cd-mirroring' }
+};
+
+export class MirroringPageHelper extends PageHelper {
+ pages = pages;
+
+ poolsColumnIndex = {
+ name: 1,
+ health: 6
+ };
+
+ /**
+ * Goes to the mirroring page and edits a pool in the Pool table. Clicks on the
+ * pool and chooses an option (either pool, image, or disabled)
+ */
+ @PageHelper.restrictTo(pages.index.url)
+ editMirror(name: string, option: string) {
+ // Clicks the pool in the table
+ this.getFirstTableCell(name).click();
+
+ // Clicks the Edit Mode button
+ cy.contains('button', 'Edit Mode').click();
+
+ // Clicks the drop down in the edit pop-up, then clicks the Update button
+ cy.get('.modal-content').should('be.visible');
+ this.selectOption('mirrorMode', option);
+
+ // Clicks update button and checks if the mode has been changed
+ cy.contains('button', 'Update').click();
+ cy.contains('.modal-dialog', 'Edit pool mirror mode').should('not.exist');
+ const val = option.toLowerCase(); // used since entries in table are lower case
+ this.getFirstTableCell(val).should('be.visible');
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ generateToken(poolName: string) {
+ cy.get('[aria-label="Create Bootstrap Token"]').first().click();
+ cy.get('cd-bootstrap-create-modal').within(() => {
+ cy.get(`label[for=${poolName}]`).click();
+ cy.get('button[type=submit]').click();
+ cy.get('textarea[id=token]').wait(200).invoke('val').as('token');
+ cy.get('[aria-label="Back"]').click();
+ });
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ checkPoolHealthStatus(poolName: string, status: string) {
+ cy.get('cd-mirroring-pools').within(() => {
+ this.getTableCell(this.poolsColumnIndex.name, poolName)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.poolsColumnIndex.health}) .badge`)
+ .should(($ele) => {
+ const newLabels = $ele.toArray().map((v) => v.innerText);
+ expect(newLabels).to.include(status);
+ });
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts
new file mode 100644
index 000000000..983140a44
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts
@@ -0,0 +1,77 @@
+import { ConfigurationPageHelper } from './configuration.po';
+
+describe('Configuration page', () => {
+ const configuration = new ConfigurationPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ configuration.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ configuration.expectBreadcrumbText('Configuration');
+ });
+ });
+
+ describe('fields check', () => {
+ beforeEach(() => {
+ configuration.getExpandCollapseElement().click();
+ });
+
+ it('should check that details table opens (w/o tab header)', () => {
+ configuration.getStatusTables().should('be.visible');
+ configuration.getTabs().should('not.exist');
+ });
+ });
+
+ describe('edit configuration test', () => {
+ const configName = 'client_cache_size';
+
+ beforeEach(() => {
+ configuration.clearTableSearchInput();
+ configuration.getTableCount('found').as('configFound');
+ });
+
+ after(() => {
+ configuration.configClear(configName);
+ });
+
+ it('should click and edit a configuration and results should appear in the table', () => {
+ configuration.edit(
+ configName,
+ ['global', '1'],
+ ['mon', '2'],
+ ['mgr', '3'],
+ ['osd', '4'],
+ ['mds', '5'],
+ ['client', '6']
+ );
+ });
+
+ it('should verify modified filter is applied properly', () => {
+ configuration.filterTable('Modified', 'no');
+ configuration.getTableCount('found').as('unmodifiedConfigs');
+
+ // Modified filter value to yes
+ configuration.filterTable('Modified', 'yes');
+ configuration.getTableCount('found').as('modifiedConfigs');
+
+ cy.get('@configFound').then((configFound) => {
+ cy.get('@unmodifiedConfigs').then((unmodifiedConfigs) => {
+ const modifiedConfigs = Number(configFound) - Number(unmodifiedConfigs);
+ configuration.getTableCount('found').should('eq', modifiedConfigs);
+ });
+ });
+
+ // Modified filter value to no
+ configuration.filterTable('Modified', 'no');
+ cy.get('@configFound').then((configFound) => {
+ cy.get('@modifiedConfigs').then((modifiedConfigs) => {
+ const unmodifiedConfigs = Number(configFound) - Number(modifiedConfigs);
+ configuration.getTableCount('found').should('eq', unmodifiedConfigs);
+ });
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts
new file mode 100644
index 000000000..0133dc31f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts
@@ -0,0 +1,75 @@
+import { PageHelper } from '../page-helper.po';
+
+export class ConfigurationPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/configuration', id: 'cd-configuration' }
+ };
+
+ /**
+ * Clears out all the values in a config to reset before and after testing
+ * Does not work for configs with checkbox only, possible future PR
+ */
+ configClear(name: string) {
+ const valList = ['global', 'mon', 'mgr', 'osd', 'mds', 'client']; // Editable values
+
+ this.navigateEdit(name);
+ // Waits for the data to load
+ cy.contains('.card-header', `Edit ${name}`);
+
+ for (const i of valList) {
+ cy.get(`#${i}`).clear();
+ }
+ // Clicks save button and checks that values are not present for the selected config
+ cy.get('[data-cy=submitBtn]').click();
+
+ // Enter config setting name into filter box
+ this.searchTable(name);
+
+ // Expand row
+ this.getExpandCollapseElement(name).click();
+
+ // Checks for visibility of details tab
+ this.getStatusTables().should('be.visible');
+
+ for (const i of valList) {
+ // Waits until values are not present in the details table
+ this.getStatusTables().should('not.contain.text', i + ':');
+ }
+ }
+
+ /**
+ * Clicks the designated config, then inputs the values passed into the edit function.
+ * Then checks if the edit is reflected in the config table.
+ * Takes in name of config and a list of tuples of values the user wants edited,
+ * each tuple having the desired value along with the number tehey want for that value.
+ * Ex: [global, '2'] is the global value with an input of 2
+ */
+ edit(name: string, ...values: [string, string][]) {
+ this.navigateEdit(name);
+
+ // Waits for data to load
+ cy.contains('.card-header', `Edit ${name}`);
+
+ values.forEach((valtuple) => {
+ // Finds desired value based off given list
+ cy.get(`#${valtuple[0]}`).type(valtuple[1]); // of values and inserts the given number for the value
+ });
+
+ // Clicks save button then waits until the desired config is visible, clicks it,
+ // then checks that each desired value appears with the desired number
+ cy.get('[data-cy=submitBtn]').click();
+
+ // Enter config setting name into filter box
+ this.searchTable(name);
+
+ // Checks for visibility of config in table
+ this.getExpandCollapseElement(name).should('be.visible').click();
+
+ // Clicks config
+ values.forEach((value) => {
+ // iterates through list of values and
+ // checks if the value appears in details with the correct number attatched
+ cy.contains('.table.table-striped.table-bordered', `${value[0]}\: ${value[1]}`);
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts
new file mode 100644
index 000000000..300eddbcc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts
@@ -0,0 +1,56 @@
+import { PageHelper } from '../page-helper.po';
+import { NotificationSidebarPageHelper } from '../ui/notification.po';
+import { HostsPageHelper } from './hosts.po';
+import { ServicesPageHelper } from './services.po';
+
+const pages = {
+ index: { url: '#/expand-cluster', id: 'cd-create-cluster' }
+};
+export class CreateClusterWizardHelper extends PageHelper {
+ pages = pages;
+
+ createCluster() {
+ cy.get('cd-create-cluster').should('contain.text', 'Please expand your cluster first');
+ cy.get('[name=expand-cluster]').click();
+ cy.get('cd-wizard').should('exist');
+ }
+
+ doSkip() {
+ cy.get('[name=skip-cluster-creation]').click();
+ cy.contains('cd-modal button', 'Continue').click();
+
+ cy.get('cd-dashboard').should('exist');
+ const notification = new NotificationSidebarPageHelper();
+ notification.open();
+ notification.getNotifications().should('contain', 'Cluster expansion skipped by user');
+ }
+}
+
+export class CreateClusterHostPageHelper extends HostsPageHelper {
+ pages = {
+ index: { url: '#/expand-cluster', id: 'cd-wizard' },
+ add: { url: '', id: 'cd-host-form' }
+ };
+
+ columnIndex = {
+ hostname: 1,
+ labels: 2,
+ status: 3,
+ services: 0
+ };
+}
+
+export class CreateClusterServicePageHelper extends ServicesPageHelper {
+ pages = {
+ index: { url: '#/expand-cluster', id: 'cd-wizard' },
+ create: { url: '', id: 'cd-service-form' }
+ };
+
+ columnIndex = {
+ service_name: 1,
+ placement: 2,
+ running: 0,
+ size: 0,
+ last_refresh: 0
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.e2e-spec.ts
new file mode 100644
index 000000000..23497bbd5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.e2e-spec.ts
@@ -0,0 +1,36 @@
+import { CrushMapPageHelper } from './crush-map.po';
+
+describe('CRUSH map page', () => {
+ const crushmap = new CrushMapPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ crushmap.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ crushmap.expectBreadcrumbText('CRUSH map');
+ });
+ });
+
+ describe('fields check', () => {
+ it('should check that title & table appears', () => {
+ // Check that title (CRUSH map viewer) appears
+ crushmap.getPageTitle().should('equal', 'CRUSH map viewer');
+
+ // Check that title appears once OSD is clicked
+ crushmap.getCrushNode(0).click();
+
+ crushmap
+ .getLegends()
+ .invoke('text')
+ .then((legend) => {
+ crushmap.getCrushNode(0).should('have.text', legend);
+ });
+
+ // Check that table appears once OSD is clicked
+ crushmap.getDataTables().should('be.visible');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.po.ts
new file mode 100644
index 000000000..a5d2d591c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/crush-map.po.ts
@@ -0,0 +1,13 @@
+import { PageHelper } from '../page-helper.po';
+
+export class CrushMapPageHelper extends PageHelper {
+ pages = { index: { url: '#/crush-map', id: 'cd-crushmap' } };
+
+ getPageTitle() {
+ return cy.get('cd-crushmap .card-header').text();
+ }
+
+ getCrushNode(idx: number) {
+ return cy.get('.node-name.type-osd').eq(idx);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.e2e-spec.ts
new file mode 100644
index 000000000..26a2a8c0b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.e2e-spec.ts
@@ -0,0 +1,34 @@
+import { HostsPageHelper } from './hosts.po';
+
+describe('Hosts page', () => {
+ const hosts = new HostsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ hosts.navigateTo();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ it('should open and show breadcrumb', () => {
+ hosts.expectBreadcrumbText('Hosts');
+ });
+
+ it('should show two tabs', () => {
+ hosts.getTabsCount().should('eq', 2);
+ });
+
+ it('should show hosts list tab at first', () => {
+ hosts.getTabText(0).should('eq', 'Hosts List');
+ });
+
+ it('should show overall performance as a second tab', () => {
+ hosts.getTabText(1).should('eq', 'Overall Performance');
+ });
+ });
+
+ describe('services link test', () => {
+ it('should check at least one host is present', () => {
+ hosts.check_for_host();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.po.ts
new file mode 100644
index 000000000..f8f21ac22
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/hosts.po.ts
@@ -0,0 +1,186 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/hosts', id: 'cd-hosts' },
+ add: { url: '#/hosts/(modal:add)', id: 'cd-host-form' }
+};
+
+export class HostsPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ hostname: 2,
+ services: 3,
+ labels: 4,
+ status: 5
+ };
+
+ check_for_host() {
+ this.getTableCount('total').should('not.be.eq', 0);
+ }
+
+ add(hostname: string, exist?: boolean, maintenance?: boolean, labels: string[] = []) {
+ cy.get(`${this.pages.add.id}`).within(() => {
+ cy.get('#hostname').type(hostname);
+ if (maintenance) {
+ cy.get('label[for=maintenance]').click();
+ }
+ if (exist) {
+ cy.get('#hostname').should('have.class', 'ng-invalid');
+ }
+ });
+
+ if (labels.length) {
+ this.selectPredefinedLabels(labels);
+ }
+
+ cy.get('cd-submit-button').click();
+ // back to host list
+ cy.get(`${this.pages.index.id}`);
+ }
+
+ selectPredefinedLabels(labels: string[]) {
+ cy.get('a[data-testid=select-menu-edit]').click();
+ for (const label of labels) {
+ cy.get('.popover-body div.select-menu-item-content').contains(label).click();
+ }
+ }
+
+ checkExist(hostname: string, exist: boolean) {
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.hostname}) span`)
+ .should(($elements) => {
+ const hosts = $elements.toArray().map((v) => v.innerText);
+ if (exist) {
+ expect(hosts).to.include(hostname);
+ } else {
+ expect(hosts).to.not.include(hostname);
+ }
+ });
+ }
+
+ remove(hostname: string) {
+ super.delete(hostname, this.columnIndex.hostname, 'hosts');
+ }
+
+ // Add or remove labels on a host, then verify labels in the table
+ editLabels(hostname: string, labels: string[], add: boolean) {
+ this.getTableCell(this.columnIndex.hostname, hostname, true).click();
+ this.clickActionButton('edit');
+
+ // add or remove label badges
+ if (add) {
+ cy.get('cd-modal').find('.select-menu-edit').click();
+ for (const label of labels) {
+ cy.contains('cd-modal .badge', new RegExp(`^${label}$`)).should('not.exist');
+ cy.get('.popover-body input').type(`${label}{enter}`);
+ }
+ } else {
+ for (const label of labels) {
+ cy.contains('cd-modal .badge', new RegExp(`^${label}$`))
+ .find('.badge-remove')
+ .click();
+ }
+ }
+ cy.get('cd-modal cd-submit-button').click();
+ this.checkLabelExists(hostname, labels, add);
+ }
+
+ checkLabelExists(hostname: string, labels: string[], add: boolean) {
+ // Verify labels are added or removed from Labels column
+ // First find row with hostname, then find labels in the row
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .click()
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.labels}) .badge`)
+ .should(($ele) => {
+ const newLabels = $ele.toArray().map((v) => v.innerText);
+ for (const label of labels) {
+ if (add) {
+ expect(newLabels).to.include(label);
+ } else {
+ expect(newLabels).to.not.include(label);
+ }
+ }
+ });
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ maintenance(hostname: string, exit = false, force = false) {
+ this.clearTableSearchInput();
+ if (force) {
+ this.getTableCell(this.columnIndex.hostname, hostname, true).click();
+ this.clickActionButton('enter-maintenance');
+
+ cy.get('cd-modal').within(() => {
+ cy.contains('button', 'Continue').click();
+ });
+
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.include('maintenance');
+ });
+ }
+ if (exit) {
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .click()
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status})`)
+ .then(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ if (status[0].includes('maintenance')) {
+ this.clickActionButton('exit-maintenance');
+ }
+ });
+
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status})`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.not.include('maintenance');
+ });
+ } else {
+ this.getTableCell(this.columnIndex.hostname, hostname, true).click();
+ this.clickActionButton('enter-maintenance');
+
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.include('maintenance');
+ });
+ }
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ drain(hostname: string) {
+ this.getTableCell(this.columnIndex.hostname, hostname, true).click();
+ this.clickActionButton('start-drain');
+ cy.wait(1000);
+ this.checkLabelExists(hostname, ['_no_schedule'], true);
+
+ this.clickTab('cd-host-details', hostname, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ cy.wait(20000);
+ this.expectTableCount('total', 0);
+ });
+ }
+
+ checkServiceInstancesExist(hostname: string, instances: string[]) {
+ this.getTableCell(this.columnIndex.hostname, hostname, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.services}) .badge`)
+ .should(($ele) => {
+ const serviceInstances = $ele.toArray().map((v) => v.innerText);
+ for (const instance of instances) {
+ expect(serviceInstances).to.include(instance);
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/inventory.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/inventory.po.ts
new file mode 100644
index 000000000..5a9abdc03
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/inventory.po.ts
@@ -0,0 +1,22 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/inventory', id: 'cd-inventory' }
+};
+
+export class InventoryPageHelper extends PageHelper {
+ pages = pages;
+
+ identify() {
+ // Nothing we can do, just verify the form is there
+ this.getFirstTableCell().click();
+ cy.contains('cd-table-actions button', 'Identify').click();
+ cy.get('cd-modal').within(() => {
+ cy.get('#duration').select('15 minutes');
+ cy.get('#duration').select('10 minutes');
+ cy.get('cd-back-button').click();
+ });
+ cy.get('cd-modal').should('not.exist');
+ cy.get(`${this.pages.index.id}`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.e2e-spec.ts
new file mode 100644
index 000000000..606f6a3cd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.e2e-spec.ts
@@ -0,0 +1,61 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { LogsPageHelper } from './logs.po';
+
+describe('Logs page', () => {
+ const logs = new LogsPageHelper();
+ const pools = new PoolPageHelper();
+
+ const poolname = 'e2e_logs_test_pool';
+ const today = new Date();
+ let hour = today.getHours();
+ if (hour > 12) {
+ hour = hour - 12;
+ }
+ const minute = today.getMinutes();
+
+ beforeEach(() => {
+ cy.login();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ beforeEach(() => {
+ logs.navigateTo();
+ });
+
+ it('should open and show breadcrumb', () => {
+ logs.expectBreadcrumbText('Logs');
+ });
+
+ it('should show three tabs', () => {
+ logs.getTabsCount().should('eq', 3);
+ });
+
+ it('should show cluster logs tab at first', () => {
+ logs.getTabText(0).should('eq', 'Cluster Logs');
+ });
+
+ it('should show audit logs as a second tab', () => {
+ logs.getTabText(1).should('eq', 'Audit Logs');
+ });
+
+ it('should show daemon logs as a third tab', () => {
+ logs.getTabText(2).should('eq', 'Daemon Logs');
+ });
+ });
+
+ describe('audit logs respond to pool creation and deletion test', () => {
+ it('should create pool and check audit logs reacted', () => {
+ pools.navigateTo('create');
+ pools.create(poolname, 8);
+ pools.navigateTo();
+ pools.existTableCell(poolname, true);
+ logs.checkAuditForPoolFunction(poolname, 'create', hour, minute);
+ });
+
+ it('should delete pool and check audit logs reacted', () => {
+ pools.navigateTo();
+ pools.delete(poolname);
+ logs.checkAuditForPoolFunction(poolname, 'delete', hour, minute);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.po.ts
new file mode 100644
index 000000000..5c34eee5c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/logs.po.ts
@@ -0,0 +1,77 @@
+import { PageHelper } from '../page-helper.po';
+
+export class LogsPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/logs', id: 'cd-logs' }
+ };
+
+ checkAuditForPoolFunction(poolname: string, poolfunction: string, hour: number, minute: number) {
+ this.navigateTo();
+
+ // sometimes the modal from deleting pool is still present at this point.
+ // This wait makes sure it isn't
+ cy.contains('.modal-dialog', 'Delete Pool').should('not.exist');
+
+ // go to audit logs tab
+ cy.contains('.nav-link', 'Audit Logs').click();
+
+ // Enter an earliest time so that no old messages with the same pool name show up
+ cy.get('.ngb-tp-input')
+ .its(0)
+ .then((input) => {
+ cy.wrap(input).clear();
+
+ if (hour < 10) cy.wrap(input).type(`${hour}`);
+ });
+
+ cy.get('.ngb-tp-input')
+ .its(1)
+ .then((input) => {
+ cy.wrap(input).clear();
+
+ if (minute < 10) cy.wrap(input).type(`${minute}`);
+ });
+
+ // Enter the pool name into the filter box
+ cy.get('input.form-control.ng-valid').first().clear().type(poolname);
+
+ cy.get('.tab-pane.active')
+ .get('.card-body')
+ .get('.message')
+ .should('contain.text', poolname)
+ .and('contain.text', `pool ${poolfunction}`);
+ }
+
+ checkAuditForConfigChange(configname: string, setting: string, hour: number, minute: number) {
+ this.navigateTo();
+
+ // go to audit logs tab
+ cy.contains('.nav-link', 'Audit Logs').click();
+
+ // Enter an earliest time so that no old messages with the same config name show up
+ cy.get('.ngb-tp-input')
+ .its(0)
+ .then((input) => {
+ cy.wrap(input).clear();
+
+ if (hour < 10) cy.wrap(input).type(`${hour}`);
+ });
+
+ cy.get('.ngb-tp-input')
+ .its(1)
+ .then((input) => {
+ cy.wrap(input).clear();
+
+ if (minute < 10) cy.wrap(input).type(`${minute}`);
+ });
+
+ // Enter the config name into the filter box
+ cy.get('input.form-control.ng-valid').first().clear().type(configname);
+
+ cy.get('.tab-pane.active')
+ .get('.card-body')
+ .get('.message')
+ .should('contain.text', configname)
+ .and('contain.text', setting);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.e2e-spec.ts
new file mode 100644
index 000000000..3be481059
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.e2e-spec.ts
@@ -0,0 +1,77 @@
+import { Input, ManagerModulesPageHelper } from './mgr-modules.po';
+
+describe('Manager modules page', () => {
+ const mgrmodules = new ManagerModulesPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ mgrmodules.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ mgrmodules.expectBreadcrumbText('Manager Modules');
+ });
+ });
+
+ describe('verifies editing functionality for manager modules', () => {
+ it('should test editing on balancer module', () => {
+ const balancerArr: Input[] = [
+ {
+ id: 'crush_compat_max_iterations',
+ newValue: '123',
+ oldValue: '25'
+ }
+ ];
+ mgrmodules.editMgrModule('balancer', balancerArr);
+ });
+
+ it('should test editing on dashboard module', () => {
+ const dashboardArr: Input[] = [
+ {
+ id: 'GRAFANA_API_PASSWORD',
+ newValue: 'rafa',
+ oldValue: ''
+ }
+ ];
+ mgrmodules.editMgrModule('dashboard', dashboardArr);
+ });
+
+ it('should test editing on devicehealth module', () => {
+ const devHealthArray: Input[] = [
+ {
+ id: 'mark_out_threshold',
+ newValue: '1987',
+ oldValue: '2419200'
+ },
+ {
+ id: 'pool_name',
+ newValue: 'sox',
+ oldValue: '.mgr'
+ },
+ {
+ id: 'retention_period',
+ newValue: '1999',
+ oldValue: '15552000'
+ },
+ {
+ id: 'scrape_frequency',
+ newValue: '2020',
+ oldValue: '86400'
+ },
+ {
+ id: 'sleep_interval',
+ newValue: '456',
+ oldValue: '600'
+ },
+ {
+ id: 'warn_threshold',
+ newValue: '567',
+ oldValue: '7257600'
+ }
+ ];
+
+ mgrmodules.editMgrModule('devicehealth', devHealthArray);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.po.ts
new file mode 100644
index 000000000..04d2eee46
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/mgr-modules.po.ts
@@ -0,0 +1,57 @@
+import { PageHelper } from '../page-helper.po';
+
+export class Input {
+ id: string;
+ oldValue: string;
+ newValue: string;
+}
+
+export class ManagerModulesPageHelper extends PageHelper {
+ pages = { index: { url: '#/mgr-modules', id: 'cd-mgr-module-list' } };
+
+ /**
+ * Selects the Manager Module and then fills in the desired fields.
+ */
+ editMgrModule(name: string, inputs: Input[]) {
+ this.navigateEdit(name);
+
+ for (const input of inputs) {
+ // Clears fields and adds edits
+ cy.get(`#${input.id}`).clear().type(input.newValue);
+ }
+
+ cy.contains('button', 'Update').click();
+ // Checks if edits appear
+ this.getExpandCollapseElement(name).should('be.visible').click();
+
+ for (const input of inputs) {
+ cy.get('.datatable-body').last().contains(input.newValue);
+ }
+
+ // Clear mgr module of all edits made to it
+ this.navigateEdit(name);
+
+ // Clears the editable fields
+ for (const input of inputs) {
+ if (input.oldValue) {
+ const id = `#${input.id}`;
+ cy.get(id).clear();
+ if (input.oldValue) {
+ cy.get(id).type(input.oldValue);
+ }
+ }
+ }
+
+ // Checks that clearing represents in details tab of module
+ cy.contains('button', 'Update').click();
+ this.getExpandCollapseElement(name).should('be.visible').click();
+ for (const input of inputs) {
+ if (input.oldValue) {
+ cy.get('.datatable-body')
+ .eq(1)
+ .should('contain', input.id)
+ .and('not.contain', input.newValue);
+ }
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.e2e-spec.ts
new file mode 100644
index 000000000..8324ff8b5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.e2e-spec.ts
@@ -0,0 +1,61 @@
+import { MonitorsPageHelper } from './monitors.po';
+
+describe('Monitors page', () => {
+ const monitors = new MonitorsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ monitors.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ monitors.expectBreadcrumbText('Monitors');
+ });
+ });
+
+ describe('fields check', () => {
+ it('should check status table is present', () => {
+ // check for table header 'Status'
+ monitors.getLegends().its(0).should('have.text', 'Status');
+
+ // check for fields in table
+ monitors
+ .getStatusTables()
+ .should('contain.text', 'Cluster ID')
+ .and('contain.text', 'monmap modified')
+ .and('contain.text', 'monmap epoch')
+ .and('contain.text', 'quorum con')
+ .and('contain.text', 'quorum mon')
+ .and('contain.text', 'required con')
+ .and('contain.text', 'required mon');
+ });
+
+ it('should check In Quorum and Not In Quorum tables are present', () => {
+ // check for there to be two tables
+ monitors.getDataTables().should('have.length', 2);
+
+ // check for table header 'In Quorum'
+ monitors.getLegends().its(1).should('have.text', 'In Quorum');
+
+ // check for table header 'Not In Quorum'
+ monitors.getLegends().its(2).should('have.text', 'Not In Quorum');
+
+ // verify correct columns on In Quorum table
+ monitors.getDataTableHeaders(0).contains('Name');
+
+ monitors.getDataTableHeaders(0).contains('Rank');
+
+ monitors.getDataTableHeaders(0).contains('Public Address');
+
+ monitors.getDataTableHeaders(0).contains('Open Sessions');
+
+ // verify correct columns on Not In Quorum table
+ monitors.getDataTableHeaders(1).contains('Name');
+
+ monitors.getDataTableHeaders(1).contains('Rank');
+
+ monitors.getDataTableHeaders(1).contains('Public Address');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.po.ts
new file mode 100644
index 000000000..4113b9928
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/monitors.po.ts
@@ -0,0 +1,7 @@
+import { PageHelper } from '../page-helper.po';
+
+export class MonitorsPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/monitor', id: 'cd-monitor' }
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.e2e-spec.ts
new file mode 100644
index 000000000..f134295e4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.e2e-spec.ts
@@ -0,0 +1,56 @@
+import { OSDsPageHelper } from './osds.po';
+
+describe('OSDs page', () => {
+ const osds = new OSDsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ osds.navigateTo();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ it('should open and show breadcrumb', () => {
+ osds.expectBreadcrumbText('OSDs');
+ });
+
+ it('should show two tabs', () => {
+ osds.getTabsCount().should('eq', 2);
+ osds.getTabText(0).should('eq', 'OSDs List');
+ osds.getTabText(1).should('eq', 'Overall Performance');
+ });
+ });
+
+ describe('check existence of fields on OSD page', () => {
+ it('should check that number of rows and count in footer match', () => {
+ osds.getTableCount('total').then((text) => {
+ osds.getTableRows().its('length').should('equal', text);
+ });
+ });
+
+ it('should verify that buttons exist', () => {
+ cy.contains('button', 'Create');
+ cy.contains('button', 'Cluster-wide configuration');
+ });
+
+ describe('by selecting one row in OSDs List', () => {
+ beforeEach(() => {
+ osds.getExpandCollapseElement().click();
+ });
+
+ it('should show the correct text for the tab labels', () => {
+ cy.get('#tabset-osd-details > a').then(($tabs) => {
+ const tabHeadings = $tabs.map((_i, e) => e.textContent).get();
+
+ expect(tabHeadings).to.eql([
+ 'Devices',
+ 'Attributes (OSD map)',
+ 'Metadata',
+ 'Device health',
+ 'Performance counter',
+ 'Performance Details'
+ ]);
+ });
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.po.ts
new file mode 100644
index 000000000..cd812f474
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/osds.po.ts
@@ -0,0 +1,84 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/osd', id: 'cd-osd-list' },
+ create: { url: '#/osd/create', id: 'cd-osd-form' }
+};
+
+export class OSDsPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ id: 3,
+ status: 5
+ };
+
+ create(deviceType: 'hdd' | 'ssd', hostname?: string, expandCluster = false) {
+ cy.get('[aria-label="toggle advanced mode"]').click();
+ // Click Primary devices Add button
+ cy.get('cd-osd-devices-selection-groups[name="Primary"]').as('primaryGroups');
+ cy.get('@primaryGroups').find('button').click();
+
+ // Select all devices with `deviceType`
+ cy.get('cd-osd-devices-selection-modal').within(() => {
+ cy.get('.modal-footer .tc_submitButton').as('addButton').should('be.disabled');
+ this.filterTable('Type', deviceType);
+ if (hostname) {
+ this.filterTable('Hostname', hostname);
+ }
+
+ if (expandCluster) {
+ this.getTableCount('total').should('be.gte', 1);
+ }
+ cy.get('@addButton').click();
+ });
+
+ if (!expandCluster) {
+ cy.get('@primaryGroups').within(() => {
+ this.getTableCount('total').as('newOSDCount');
+ });
+
+ cy.get(`${pages.create.id} .card-footer .tc_submitButton`).click();
+ cy.get(`cd-osd-creation-preview-modal .modal-footer .tc_submitButton`).click();
+ }
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ checkStatus(id: number, status: string[]) {
+ this.searchTable(`id:${id}`);
+ this.expectTableCount('found', 1);
+ cy.get(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`).should(($ele) => {
+ const allStatus = $ele.toArray().map((v) => v.innerText);
+ for (const s of status) {
+ expect(allStatus).to.include(s);
+ }
+ });
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ ensureNoOsd(id: number) {
+ this.searchTable(`id:${id}`);
+ this.expectTableCount('found', 0);
+ this.clearTableSearchInput();
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ deleteByIDs(osdIds: number[], replace?: boolean) {
+ this.getTableRows().each(($el) => {
+ const rowOSD = Number(
+ $el.find('datatable-body-cell .datatable-body-cell-label').get(this.columnIndex.id - 1)
+ .textContent
+ );
+ if (osdIds.includes(rowOSD)) {
+ cy.wrap($el).click();
+ }
+ });
+ this.clickActionButton('delete');
+ if (replace) {
+ cy.get('cd-modal label[for="preserve"]').click();
+ }
+ cy.get('cd-modal label[for="confirmation"]').click();
+ cy.contains('cd-modal button', 'Delete').click();
+ cy.get('cd-modal').should('not.exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/services.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/services.po.ts
new file mode 100644
index 000000000..c464a3f6c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/services.po.ts
@@ -0,0 +1,200 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/services', id: 'cd-services' },
+ create: { url: '#/services/(modal:create)', id: 'cd-service-form' }
+};
+
+export class ServicesPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ service_name: 2,
+ placement: 3,
+ running: 4,
+ size: 5,
+ last_refresh: 6
+ };
+
+ serviceDetailColumnIndex = {
+ daemonName: 2,
+ status: 4
+ };
+
+ check_for_service() {
+ this.getTableCount('total').should('not.be.eq', 0);
+ }
+
+ private selectServiceType(serviceType: string) {
+ return this.selectOption('service_type', serviceType);
+ }
+
+ clickServiceTab(serviceName: string, tabName: string) {
+ this.getExpandCollapseElement(serviceName).click();
+ cy.get('cd-service-details').within(() => {
+ this.getTab(tabName).click();
+ });
+ }
+
+ addService(
+ serviceType: string,
+ exist?: boolean,
+ count = 1,
+ snmpVersion?: string,
+ snmpPrivProtocol?: boolean,
+ unmanaged = false
+ ) {
+ cy.get(`${this.pages.create.id}`).within(() => {
+ this.selectServiceType(serviceType);
+ switch (serviceType) {
+ case 'rgw':
+ cy.get('#service_id').type('foo');
+ unmanaged ? cy.get('label[for=unmanaged]').click() : cy.get('#count').type(String(count));
+ break;
+
+ case 'ingress':
+ if (unmanaged) {
+ cy.get('label[for=unmanaged]').click();
+ }
+ this.selectOption('backend_service', 'rgw.foo');
+ cy.get('#service_id').should('have.value', 'rgw.foo');
+ cy.get('#virtual_ip').type('192.168.100.1/24');
+ cy.get('#frontend_port').type('8081');
+ cy.get('#monitor_port').type('8082');
+ break;
+
+ case 'nfs':
+ cy.get('#service_id').type('testnfs');
+ unmanaged ? cy.get('label[for=unmanaged]').click() : cy.get('#count').type(String(count));
+ break;
+
+ case 'snmp-gateway':
+ this.selectOption('snmp_version', snmpVersion);
+ cy.get('#snmp_destination').type('192.168.0.1:8443');
+ if (snmpVersion === 'V2c') {
+ cy.get('#snmp_community').type('public');
+ } else {
+ cy.get('#engine_id').type('800C53F00000');
+ this.selectOption('auth_protocol', 'SHA');
+ if (snmpPrivProtocol) {
+ this.selectOption('privacy_protocol', 'DES');
+ cy.get('#snmp_v3_priv_password').type('testencrypt');
+ }
+
+ // Credentials
+ cy.get('#snmp_v3_auth_username').type('test');
+ cy.get('#snmp_v3_auth_password').type('testpass');
+ }
+ break;
+
+ default:
+ cy.get('#service_id').type('test');
+ unmanaged ? cy.get('label[for=unmanaged]').click() : cy.get('#count').type(String(count));
+ break;
+ }
+ if (serviceType === 'snmp-gateway') {
+ cy.get('cd-submit-button').dblclick();
+ } else {
+ cy.get('cd-submit-button').click();
+ }
+ });
+ if (exist) {
+ cy.get('#service_id').should('have.class', 'ng-invalid');
+ } else {
+ // back to service list
+ cy.get(`${this.pages.index.id}`);
+ }
+ }
+
+ editService(name: string, daemonCount: string) {
+ this.navigateEdit(name, true, false);
+ cy.get(`${this.pages.create.id}`).within(() => {
+ cy.get('#service_type').should('be.disabled');
+ cy.get('#service_id').should('be.disabled');
+ cy.get('#count').clear().type(daemonCount);
+ cy.get('cd-submit-button').click();
+ });
+ }
+
+ checkServiceStatus(daemon: string, expectedStatus = 'running') {
+ let daemonNameIndex = this.serviceDetailColumnIndex.daemonName;
+ let statusIndex = this.serviceDetailColumnIndex.status;
+
+ // since hostname row is hidden from the hosts details table,
+ // we'll need to manually override the indexes when this check is being
+ // done for the daemons in host details page. So we'll get the url and
+ // verify if the current page is not the services index page
+ cy.url().then((url) => {
+ if (!url.includes(pages.index.url)) {
+ daemonNameIndex = 1;
+ statusIndex = 3;
+ }
+
+ cy.get('cd-service-daemon-list').within(() => {
+ this.getTableCell(daemonNameIndex, daemon, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${statusIndex}) .badge`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.include(expectedStatus);
+ });
+ });
+ });
+ }
+
+ expectPlacementCount(serviceName: string, expectedCount: string) {
+ this.getTableCell(this.columnIndex.service_name, serviceName)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.placement})`)
+ .should(($ele) => {
+ const running = $ele.text().split(';');
+ expect(running).to.include(`count:${expectedCount}`);
+ });
+ }
+
+ checkExist(serviceName: string, exist: boolean) {
+ this.getTableCell(this.columnIndex.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);
+ }
+ });
+ }
+
+ isUnmanaged(serviceName: string, unmanaged: boolean) {
+ this.getTableCell(this.columnIndex.service_name, serviceName)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.placement})`)
+ .should(($ele) => {
+ const placement = $ele.text().split(';');
+ unmanaged
+ ? expect(placement).to.include('unmanaged')
+ : expect(placement).to.not.include('unmanaged');
+ });
+ }
+
+ deleteService(serviceName: string) {
+ const getRow = this.getTableCell.bind(this, this.columnIndex.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');
+ this.checkExist(serviceName, false);
+ }
+
+ daemonAction(daemon: string, action: string) {
+ cy.get('cd-service-daemon-list').within(() => {
+ this.getTableRow(daemon).click();
+ this.clickActionButton(action);
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.e2e-spec.ts
new file mode 100644
index 000000000..0d50d0a22
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.e2e-spec.ts
@@ -0,0 +1,46 @@
+import { UsersPageHelper } from './users.po';
+
+describe('Cluster Ceph Users', () => {
+ const users = new UsersPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ users.navigateTo();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ it('should open and show breadcrumb', () => {
+ users.expectBreadcrumbText('Ceph Users');
+ });
+ });
+
+ describe('Cluster users table', () => {
+ const entityName = 'client.test';
+ const entity = 'mgr';
+ const caps = 'allow r';
+ it('should verify the table is not empty', () => {
+ users.checkForUsers();
+ });
+
+ it('should verify the keys are hidden', () => {
+ users.verifyKeysAreHidden();
+ });
+
+ it('should create a new user', () => {
+ users.navigateTo('create');
+ users.create(entityName, entity, caps);
+ users.existTableCell(entityName, true);
+ });
+
+ it('should edit a user', () => {
+ const newCaps = 'allow *';
+ users.edit(entityName, 'allow *');
+ users.existTableCell(entityName, true);
+ users.checkCaps(entityName, [`${entity}: ${newCaps}`]);
+ });
+
+ it('should delete a user', () => {
+ users.delete(entityName);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.po.ts
new file mode 100644
index 000000000..a5b32b723
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/users.po.ts
@@ -0,0 +1,59 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/ceph-users', id: 'cd-crud-table' },
+ create: { url: '#/cluster/user/create', id: 'cd-crud-form' }
+};
+
+export class UsersPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ entity: 2,
+ capabilities: 3,
+ key: 4
+ };
+
+ checkForUsers() {
+ this.getTableCount('total').should('not.be.eq', 0);
+ }
+
+ verifyKeysAreHidden() {
+ this.getTableCell(this.columnIndex.entity, 'osd.0')
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.key}) span`)
+ .should(($ele) => {
+ const serviceInstances = $ele.toArray().map((v) => v.innerText);
+ expect(serviceInstances).not.contains(/^[a-z0-9]+$/i);
+ });
+ }
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(entityName: string, entityType: string, caps: string) {
+ cy.get('#formly_2_string_user_entity_0').type(entityName);
+ cy.get('#formly_5_string_entity_0').type(entityType);
+ cy.get('#formly_5_string_cap_1').type(caps);
+ cy.get("[aria-label='Create User']").should('exist').click();
+ cy.get('cd-crud-table').should('exist');
+ }
+
+ edit(name: string, newCaps: string) {
+ this.navigateEdit(name);
+ cy.get('#formly_5_string_cap_1').clear().type(newCaps);
+ cy.get("[aria-label='Edit User']").should('exist').click();
+ cy.get('cd-crud-table').should('exist');
+ }
+
+ checkCaps(entityName: string, capabilities: string[]) {
+ this.getTableCell(this.columnIndex.entity, entityName)
+ .click()
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.capabilities}) .badge`)
+ .should(($ele) => {
+ const newCaps = $ele.toArray().map((v) => v.innerText);
+ for (const cap of capabilities) {
+ expect(newCaps).to.include(cap);
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts
new file mode 100644
index 000000000..d18c34855
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts
@@ -0,0 +1,12 @@
+import { Given, Then } from 'cypress-cucumber-preprocessor/steps';
+
+Given('I am on the {string} section', (page: string) => {
+ cy.get('cd-wizard').within(() => {
+ cy.get('.nav-link').should('contain.text', page).first().click();
+ cy.get('.nav-link.active').should('contain.text', page);
+ });
+});
+
+Then('I should see a message {string}', () => {
+ cy.get('cd-create-cluster').should('contain.text', 'Please expand your cluster first');
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/forms-helper.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/forms-helper.feature.po.ts
new file mode 100644
index 000000000..2c14af863
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/forms-helper.feature.po.ts
@@ -0,0 +1,77 @@
+import { And, Then } from 'cypress-cucumber-preprocessor/steps';
+
+/**
+ * Fills in the given field using the value provided
+ * @param field ID of the field that needs to be filled out.
+ * @param value Value that should be filled in the field.
+ */
+And('enter {string} {string}', (field: string, value: string) => {
+ cy.get('.cd-col-form').within(() => {
+ cy.get(`input[id=${field}]`).clear().type(value);
+ });
+});
+
+/**
+ * Fills in the given field using the value provided
+ * @param field ID of the field that needs to be filled out.
+ * @param value Value that should be filled in the field.
+ */
+And('enter {string} {string} in the modal', (field: string, value: string) => {
+ cy.get('cd-modal').within(() => {
+ cy.get(`input[id=${field}]`).clear().type(value);
+ });
+});
+
+And('select options {string}', (labels: string) => {
+ if (labels) {
+ cy.get('a[data-testid=select-menu-edit]').click();
+ for (const label of labels.split(', ')) {
+ cy.get('.popover-body div.select-menu-item-content').contains(label).click();
+ }
+ }
+});
+
+And('{string} option {string}', (action: string, labels: string) => {
+ if (labels) {
+ if (action === 'add') {
+ cy.get('cd-modal').find('.select-menu-edit').click();
+ for (const label of labels.split(', ')) {
+ cy.get('.popover-body input').type(`${label}{enter}`);
+ }
+ } else {
+ for (const label of labels.split(', ')) {
+ cy.contains('cd-modal .badge', new RegExp(`^${label}$`))
+ .find('.badge-remove')
+ .click();
+ }
+ }
+ }
+});
+
+And('I click on submit button', () => {
+ cy.get('[data-cy=submitBtn]').click();
+});
+
+/**
+ * Some modals have an additional confirmation to be provided
+ * by ticking the 'Are you sure?' box.
+ */
+Then('I check the tick box in modal', () => {
+ cy.get('cd-modal input#confirmation').click();
+});
+
+And('I confirm to {string}', (action: string) => {
+ cy.contains('cd-modal button', action).click();
+ cy.get('cd-modal').should('not.exist');
+});
+
+Then('I should see an error in {string} field', (field: string) => {
+ cy.get('cd-modal').within(() => {
+ cy.get(`input[id=${field}]`).should('have.class', 'ng-invalid');
+ });
+});
+
+And('select {string} {string}', (selectionName: string, option: string) => {
+ cy.get(`select[name=${selectionName}]`).select(option);
+ cy.get(`select[name=${selectionName}] option:checked`).contains(option);
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/global.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/global.feature.po.ts
new file mode 100644
index 000000000..c6132ae3d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/global.feature.po.ts
@@ -0,0 +1,40 @@
+import { And, Given, Then, When } from 'cypress-cucumber-preprocessor/steps';
+
+import { UrlsCollection } from './urls.po';
+
+const urlsCollection = new UrlsCollection();
+
+Given('I am logged in', () => {
+ cy.login();
+});
+
+Given('I am on the {string} page', (page: string) => {
+ cy.visit(urlsCollection.pages[page].url);
+ cy.get(urlsCollection.pages[page].id).should('exist');
+});
+
+Then('I should be on the {string} page', (page: string) => {
+ cy.get(urlsCollection.pages[page].id).should('exist');
+});
+
+And('I should see a button to {string}', (button: string) => {
+ cy.get(`[aria-label="${button}"]`).should('be.visible');
+});
+
+When('I click on {string} button', (button: string) => {
+ cy.get(`[aria-label="${button}"]`).first().click();
+});
+
+Then('I should see the modal', () => {
+ cy.get('cd-modal').should('exist');
+});
+
+Then('I should not see the modal', () => {
+ cy.get('cd-modal').should('not.exist');
+});
+
+And('I go to the {string} tab', (names: string) => {
+ for (const name of names.split(', ')) {
+ cy.contains('.nav.nav-tabs a', name).click();
+ }
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/grafana.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/grafana.feature.po.ts
new file mode 100644
index 000000000..edd0e9b56
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/grafana.feature.po.ts
@@ -0,0 +1,87 @@
+import { Then, When } from 'cypress-cucumber-preprocessor/steps';
+import 'cypress-iframe';
+
+function getIframe() {
+ cy.frameLoaded('#iframe');
+ return cy.iframe();
+}
+
+Then('I should see the grafana panel {string}', (panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`).should('be.visible');
+ });
+ }
+ });
+});
+
+When('I view the grafana panel {string}', (panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`).within(() => {
+ cy.get('h2').click();
+ });
+ cy.get('[aria-label="Panel header item View"]').click();
+ });
+ }
+ });
+});
+
+Then('I should not see {string} in the panel {string}', (value: string, panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`)
+ .should('be.visible')
+ .within(() => {
+ cy.get('span').first().should('not.have.text', value);
+ });
+ });
+ }
+ });
+});
+
+Then(
+ 'I should see the legends {string} in the graph {string}',
+ (legends: string, panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`)
+ .should('be.visible')
+ .within(() => {
+ for (const legend of legends.split(', ')) {
+ cy.get(`button`).contains(legend);
+ }
+ });
+ });
+ }
+ });
+ }
+);
+
+Then('I should not see No Data in the graph {string}', (panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`)
+ .should('be.visible')
+ .within(() => {
+ cy.get('div.datapoints-warning').should('not.exist');
+ });
+ });
+ }
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/table-helper.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/table-helper.feature.po.ts
new file mode 100644
index 000000000..82a2c7c35
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/table-helper.feature.po.ts
@@ -0,0 +1,135 @@
+import { And, Then, When } from 'cypress-cucumber-preprocessor/steps';
+
+// When you are clicking on an action in the table actions dropdown button
+When('I click on {string} button from the table actions', (button: string) => {
+ cy.get('.table-actions button.dropdown-toggle').first().click();
+ cy.get(`[aria-label="${button}"]`).first().click();
+});
+
+// When you are clicking on an action inside the expanded table row
+When('I click on {string} button from the expanded row', (button: string) => {
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('.table-actions button.dropdown-toggle').first().click();
+ cy.get(`[aria-label="${button}"]`).first().click();
+ });
+});
+
+When('I click on {string} button from the table actions in the expanded row', (button: string) => {
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('.table-actions button.dropdown-toggle').first().click();
+ cy.get(`[aria-label="${button}"]`).first().click();
+ });
+});
+
+When('I expand the row {string}', (row: string) => {
+ cy.contains('.datatable-body-row', row).first().find('.tc_expand-collapse').click();
+});
+
+/**
+ * Selects any row on the datatable if it matches the given name
+ */
+When('I select a row {string}', (row: string) => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).click();
+});
+
+When('I select a row {string} in the expanded row', (row: string) => {
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).click();
+ });
+});
+
+Then('I should see a row with {string}', (row: string) => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
+ 'exist'
+ );
+});
+
+Then('I should not see a row with {string}', (row: string) => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
+ 'not.exist'
+ );
+});
+
+Then('I should not see a row with {string} in the expanded row', (row: string) => {
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
+ 'not.exist'
+ );
+ });
+});
+
+Then('I should see rows with following entries', (entries) => {
+ entries.hashes().forEach((entry: any) => {
+ cy.get('cd-table .search input').first().clear().type(entry.hostname);
+ cy.contains(
+ `datatable-body-row datatable-body-cell .datatable-body-cell-label`,
+ entry.hostname
+ ).should('exist');
+ });
+});
+
+And('I should see row {string} have {string}', (row: string, options: string) => {
+ if (options) {
+ cy.get('cd-table .search input').first().clear().type(row);
+ for (const option of options.split(',')) {
+ cy.contains(
+ `datatable-body-row datatable-body-cell .datatable-body-cell-label .badge`,
+ option
+ ).should('exist');
+ }
+ }
+});
+
+And('I should see row {string} of the expanded row to have a usage bar', (row: string) => {
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
+ 'exist'
+ );
+ cy.get('.datatable-body-row .datatable-body-cell .datatable-body-cell-label .progress').should(
+ 'exist'
+ );
+ });
+});
+
+And('I should see row {string} does not have {string}', (row: string, options: string) => {
+ if (options) {
+ cy.get('cd-table .search input').first().clear().type(row);
+ for (const option of options.split(',')) {
+ cy.contains(
+ `datatable-body-row datatable-body-cell .datatable-body-cell-label .badge`,
+ option
+ ).should('not.exist');
+ }
+ }
+});
+
+Then('I should see a row with {string} in the expanded row', (row: string) => {
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
+ 'exist'
+ );
+ });
+});
+
+And('I should see row {string} have {string} on this tab', (row: string, options: string) => {
+ if (options) {
+ cy.get('cd-table').should('exist');
+ cy.get('datatable-scroller, .empty-row');
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ for (const option of options.split(',')) {
+ cy.contains(
+ `datatable-body-row datatable-body-cell .datatable-body-cell-label span`,
+ option
+ ).should('exist');
+ }
+ });
+ }
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/urls.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/urls.po.ts
new file mode 100644
index 000000000..6f7316f98
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/urls.po.ts
@@ -0,0 +1,48 @@
+import { PageHelper } from '../page-helper.po';
+
+export class UrlsCollection extends PageHelper {
+ pages = {
+ // Cluster expansion
+ welcome: { url: '#/expand-cluster', id: 'cd-create-cluster' },
+
+ // Landing page
+ dashboard: { url: '#/dashboard', id: 'cd-dashboard' },
+
+ // Hosts
+ hosts: { url: '#/hosts', id: 'cd-hosts' },
+ 'add hosts': { url: '#/hosts/(modal:add)', id: 'cd-host-form' },
+
+ // Services
+ services: { url: '#/services', id: 'cd-services' },
+ 'create services': { url: '#/services/(modal:create)', id: 'cd-service-form' },
+
+ // Physical Disks
+ 'physical disks': { url: '#/inventory', id: 'cd-inventory' },
+
+ // Monitors
+ monitors: { url: '#/monitor', id: 'cd-monitor' },
+
+ // OSDs
+ osds: { url: '#/osd', id: 'cd-osd-list' },
+ 'create osds': { url: '#/osd/create', id: 'cd-osd-form' },
+
+ // Configuration
+ configuration: { url: '#/configuration', id: 'cd-configuration' },
+
+ // Crush Map
+ 'crush map': { url: '#/crush-map', id: 'cd-crushmap' },
+
+ // Mgr modules
+ 'mgr-modules': { url: '#/mgr-modules', id: 'cd-mgr-module-list' },
+
+ // Logs
+ logs: { url: '#/logs', id: 'cd-logs' },
+
+ // RGW Daemons
+ 'rgw daemons': { url: '#/rgw/daemon', id: 'cd-rgw-daemon-list' },
+
+ // CephFS
+ cephfs: { url: '#/cephfs', id: 'cd-cephfs-list' },
+ 'create cephfs': { url: '#/cephfs/create', id: 'cd-cephfs-form' }
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/filesystems.e2e-spec.feature b/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/filesystems.e2e-spec.feature
new file mode 100644
index 000000000..2c08fb56e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/filesystems.e2e-spec.feature
@@ -0,0 +1,30 @@
+Feature: CephFS Management
+
+ Goal: To test out the CephFS management features
+
+ Background: Login
+ Given I am logged in
+
+ Scenario: Create a CephFS Volume
+ Given I am on the "cephfs" page
+ And I click on "Create" button
+ And enter "name" "test_cephfs"
+ And I click on "Create File System" button
+ Then I should see a row with "test_cephfs"
+
+ Scenario: Edit CephFS Volume
+ Given I am on the "cephfs" page
+ And I select a row "test_cephfs"
+ And I click on "Edit" button
+ And enter "name" "test_cephfs_edit"
+ And I click on "Edit File System" button
+ Then I should see a row with "test_cephfs_edit"
+
+ Scenario: Remove CephFS Volume
+ Given I am on the "cephfs" page
+ And I select a row "test_cephfs_edit"
+ And I click on "Remove" button from the table actions
+ Then I should see the modal
+ And I check the tick box in modal
+ And I click on "Remove File System" button
+ Then I should not see a row with "test_cephfs_edit"
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolume-groups.e2e-spec.feature b/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolume-groups.e2e-spec.feature
new file mode 100644
index 000000000..66e3f726a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolume-groups.e2e-spec.feature
@@ -0,0 +1,51 @@
+Feature: CephFS Subvolume Group management
+
+ Goal: To test out the CephFS subvolume group management features
+
+ Background: Login
+ Given I am logged in
+
+ Scenario: Create a CephFS Volume
+ Given I am on the "cephfs" page
+ And I click on "Create" button
+ And enter "name" "test_cephfs"
+ And I click on "Create File System" button
+ Then I should see a row with "test_cephfs"
+
+ Scenario: Create a CephFS Subvolume Group
+ Given I am on the "cephfs" page
+ When I expand the row "test_cephfs"
+ And I go to the "Subvolume groups" tab
+ And I click on "Create" button from the expanded row
+ And enter "subvolumegroupName" "test_subvolume_group" in the modal
+ And I click on "Create Subvolume group" button
+ Then I should see a row with "test_subvolume_group" in the expanded row
+
+ Scenario: Edit a CephFS Subvolume
+ Given I am on the "cephfs" page
+ When I expand the row "test_cephfs"
+ And I go to the "Subvolume groups" tab
+ When I select a row "test_subvolume_group" in the expanded row
+ And I click on "Edit" button from the table actions in the expanded row
+ And enter "size" "1" in the modal
+ And I click on "Edit Subvolume group" button
+ Then I should see row "test_subvolume_group" of the expanded row to have a usage bar
+
+ Scenario: Remove a CephFS Subvolume
+ Given I am on the "cephfs" page
+ When I expand the row "test_cephfs"
+ And I go to the "Subvolume groups" tab
+ When I select a row "test_subvolume_group" in the expanded row
+ And I click on "Remove" button from the table actions in the expanded row
+ And I check the tick box in modal
+ And I click on "Remove subvolume group" button
+ Then I should not see a row with "test_subvolume_group" in the expanded row
+
+ Scenario: Remove CephFS Volume
+ Given I am on the "cephfs" page
+ And I select a row "test_cephfs"
+ And I click on "Remove" button from the table actions
+ Then I should see the modal
+ And I check the tick box in modal
+ And I click on "Remove File System" button
+ Then I should not see a row with "test_cephfs_edit"
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolumes.e2e-spec.feature b/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolumes.e2e-spec.feature
new file mode 100644
index 000000000..ae968d4e9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolumes.e2e-spec.feature
@@ -0,0 +1,51 @@
+Feature: CephFS Subvolume management
+
+ Goal: To test out the CephFS subvolume management features
+
+ Background: Login
+ Given I am logged in
+
+ Scenario: Create a CephFS Volume
+ Given I am on the "cephfs" page
+ And I click on "Create" button
+ And enter "name" "test_cephfs"
+ And I click on "Create File System" button
+ Then I should see a row with "test_cephfs"
+
+ Scenario: Create a CephFS Subvolume
+ Given I am on the "cephfs" page
+ When I expand the row "test_cephfs"
+ And I go to the "Subvolumes" tab
+ And I click on "Create" button from the expanded row
+ And enter "subvolumeName" "test_subvolume" in the modal
+ And I click on "Create Subvolume" button
+ Then I should see a row with "test_subvolume" in the expanded row
+
+ Scenario: Edit a CephFS Subvolume
+ Given I am on the "cephfs" page
+ When I expand the row "test_cephfs"
+ And I go to the "Subvolumes" tab
+ When I select a row "test_subvolume" in the expanded row
+ And I click on "Edit" button from the table actions in the expanded row
+ And enter "size" "1" in the modal
+ And I click on "Edit Subvolume" button
+ Then I should see row "test_subvolume" of the expanded row to have a usage bar
+
+ Scenario: Remove a CephFS Subvolume
+ Given I am on the "cephfs" page
+ When I expand the row "test_cephfs"
+ And I go to the "Subvolumes" tab
+ When I select a row "test_subvolume" in the expanded row
+ And I click on "Remove" button from the table actions in the expanded row
+ And I check the tick box in modal
+ And I click on "Remove Subvolume" button
+ Then I should not see a row with "test_subvolume" in the expanded row
+
+ Scenario: Remove CephFS Volume
+ Given I am on the "cephfs" page
+ And I select a row "test_cephfs"
+ And I click on "Remove" button from the table actions
+ Then I should see the modal
+ And I check the tick box in modal
+ And I click on "Remove File System" button
+ Then I should not see a row with "test_cephfs_edit"
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/01-hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/01-hosts.e2e-spec.ts
new file mode 100644
index 000000000..0afe0d74b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/01-hosts.e2e-spec.ts
@@ -0,0 +1,61 @@
+import { HostsPageHelper } from '../cluster/hosts.po';
+
+describe('Hosts page', () => {
+ const hosts = new HostsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ hosts.navigateTo();
+ });
+
+ describe('when Orchestrator is available', () => {
+ beforeEach(function () {
+ cy.fixture('orchestrator/inventory.json').as('hosts');
+ cy.fixture('orchestrator/services.json').as('services');
+ });
+
+ it('should not add an exsiting host', function () {
+ const hostname = Cypress._.sample(this.hosts).name;
+ hosts.navigateTo('add');
+ hosts.add(hostname, true);
+ });
+
+ it('should drain and remove a host and then add it back', function () {
+ const hostname = Cypress._.last(this.hosts)['name'];
+
+ // should drain the host first before deleting
+ hosts.drain(hostname);
+ hosts.remove(hostname);
+
+ // add it back
+ hosts.navigateTo('add');
+ hosts.add(hostname);
+ hosts.checkExist(hostname, true);
+ });
+
+ it('should display inventory', function () {
+ for (const host of this.hosts) {
+ hosts.clickTab('cd-host-details', host.name, 'Physical Disks');
+ cy.get('cd-host-details').within(() => {
+ hosts.expectTableCount('total', host.devices.length);
+ });
+ }
+ });
+
+ it('should display daemons', function () {
+ for (const host of this.hosts) {
+ hosts.clickTab('cd-host-details', host.name, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ hosts.getTableCount('total').should('be.gte', 0);
+ });
+ }
+ });
+
+ it('should edit host labels', function () {
+ const hostname = Cypress._.sample(this.hosts).name;
+ const labels = ['foo', 'bar'];
+ hosts.editLabels(hostname, labels, true);
+ hosts.editLabels(hostname, labels, false);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/03-inventory.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/03-inventory.e2e-spec.ts
new file mode 100644
index 000000000..fe845e1cf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/03-inventory.e2e-spec.ts
@@ -0,0 +1,25 @@
+import { InventoryPageHelper } from '../cluster/inventory.po';
+
+describe('Physical Disks page', () => {
+ const inventory = new InventoryPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ inventory.navigateTo();
+ });
+
+ it('should have correct devices', () => {
+ cy.fixture('orchestrator/inventory.json').then((hosts) => {
+ const totalDiskCount = Cypress._.sumBy(hosts, 'devices.length');
+ inventory.expectTableCount('total', totalDiskCount);
+ for (const host of hosts) {
+ inventory.filterTable('Hostname', host['name']);
+ inventory.getTableCount('found').should('be.eq', host.devices.length);
+ }
+ });
+ });
+
+ it('should identify device', () => {
+ inventory.identify();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/04-osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/04-osds.e2e-spec.ts
new file mode 100644
index 000000000..e80398d5a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/04-osds.e2e-spec.ts
@@ -0,0 +1,49 @@
+import { OSDsPageHelper } from '../cluster/osds.po';
+import { DashboardPageHelper } from '../ui/dashboard.po';
+
+describe('OSDs page', () => {
+ const osds = new OSDsPageHelper();
+ const dashboard = new DashboardPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ osds.navigateTo();
+ });
+
+ describe('when Orchestrator is available', () => {
+ it('should create and delete OSDs', () => {
+ osds.getTableCount('total').as('initOSDCount');
+ osds.navigateTo('create');
+ osds.create('hdd');
+
+ cy.get('@newOSDCount').then((newCount) => {
+ cy.get('@initOSDCount').then((oldCount) => {
+ const expectedCount = Number(oldCount) + Number(newCount);
+
+ // check total rows
+ osds.expectTableCount('total', expectedCount);
+
+ // landing page is easier to check OSD status
+ dashboard.navigateTo();
+ dashboard.infoCardBody('OSDs').should('contain.text', `${expectedCount} total`);
+ dashboard.infoCardBody('OSDs').should('contain.text', `${expectedCount} up`);
+ dashboard.infoCardBody('OSDs').should('contain.text', `${expectedCount} in`);
+
+ cy.wait(30000);
+ expect(Number(newCount)).to.be.gte(2);
+ // Delete the first OSD we created
+ osds.navigateTo();
+ const deleteOsdId = Number(oldCount);
+ osds.deleteByIDs([deleteOsdId], false);
+ osds.ensureNoOsd(deleteOsdId);
+
+ cy.wait(30000);
+ // Replace the second OSD we created
+ const replaceID = Number(oldCount) + 1;
+ osds.deleteByIDs([replaceID], true);
+ osds.checkStatus(replaceID, ['destroyed']);
+ });
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/05-services.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/05-services.e2e-spec.ts
new file mode 100644
index 000000000..75b46be0f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/05-services.e2e-spec.ts
@@ -0,0 +1,35 @@
+import { ServicesPageHelper } from '../cluster/services.po';
+
+describe('Services page', () => {
+ const services = new ServicesPageHelper();
+ const serviceName = 'rgw.foo';
+
+ beforeEach(() => {
+ cy.login();
+ services.navigateTo();
+ });
+
+ describe('when Orchestrator is available', () => {
+ it('should create an rgw service', () => {
+ services.navigateTo('create');
+ services.addService('rgw');
+
+ services.checkExist(serviceName, true);
+ });
+
+ it('should edit a service', () => {
+ const count = '2';
+ services.editService(serviceName, count);
+ services.expectPlacementCount(serviceName, count);
+ });
+
+ it('should create and delete an ingress service', () => {
+ services.navigateTo('create');
+ services.addService('ingress');
+
+ services.checkExist('ingress.rgw.foo', true);
+
+ services.deleteService('ingress.rgw.foo');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/grafana/grafana.feature b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/grafana/grafana.feature
new file mode 100644
index 000000000..fc023712e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/grafana/grafana.feature
@@ -0,0 +1,60 @@
+Feature: Grafana panels
+
+ Go to some of the grafana performance section and check if
+ panels are populated without any issues
+
+ Background: Log in
+ Given I am logged in
+
+ Scenario Outline: Hosts Overall Performance
+ Given I am on the "hosts" page
+ When I go to the "Overall Performance" tab
+ Then I should see the grafana panel "<panel>"
+ When I view the grafana panel "<panel>"
+ Then I should not see "No Data" in the panel "<panel>"
+
+ Examples:
+ | panel |
+ | OSD Hosts |
+ | AVG CPU Busy |
+ | AVG RAM Utilization |
+ | Physical IOPS |
+ | AVG Disk Utilization |
+ | Network Load |
+ | CPU Busy - Top 10 Hosts |
+ | Network Load - Top 10 Hosts |
+
+ Scenario Outline: RGW Daemon Overall Performance
+ Given I am on the "rgw daemons" page
+ When I go to the "Overall Performance" tab
+ Then I should see the grafana panel "<panel>"
+ When I view the grafana panel "<panel>"
+ Then I should not see No Data in the graph "<panel>"
+ And I should see the legends "<legends>" in the graph "<panel>"
+
+ Examples:
+ | panel | legends |
+ | Total Requests/sec by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
+ | GET Latencies by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
+ | Bandwidth by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
+ | PUT Latencies by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
+ | Average GET/PUT Latencies by RGW Instance | GET, PUT |
+ | Bandwidth Consumed by Type | GETs, PUTs |
+
+ Scenario Outline: RGW per Daemon Performance
+ Given I am on the "rgw daemons" page
+ When I expand the row "<name>"
+ And I go to the "Performance Details" tab
+ Then I should see the grafana panel "<panel>"
+ When I view the grafana panel "<panel>"
+ Then I should not see No Data in the graph "<panel>"
+ And I should see the legends "<name>" in the graph "<panel>"
+
+ Examples:
+ | name | panel |
+ | foo.ceph-node-00 | Bandwidth by HTTP Operation |
+ | foo.ceph-node-00 | HTTP Request Breakdown |
+ | foo.ceph-node-01 | Bandwidth by HTTP Operation |
+ | foo.ceph-node-01 | HTTP Request Breakdown |
+ | foo.ceph-node-02 | Bandwidth by HTTP Operation |
+ | foo.ceph-node-02 | HTTP Request Breakdown |
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/01-create-cluster-welcome.feature b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/01-create-cluster-welcome.feature
new file mode 100644
index 000000000..6ba2fc4fc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/01-create-cluster-welcome.feature
@@ -0,0 +1,26 @@
+Feature: Cluster expansion welcome screen
+
+ Go to the welcome screen and decide whether
+ to proceed to wizard or skips to landing page
+
+ Background: Login
+ Given I am logged in
+
+ Scenario: Cluster expansion welcome screen
+ Given I am on the "welcome" page
+ And I should see a button to "Expand Cluster"
+ And I should see a button to "Skip"
+ And I should see a message "Please expand your cluster first"
+
+ Scenario: Go to the Cluster expansion wizard
+ Given I am on the "welcome" page
+ And I should see a button to "Expand Cluster"
+ When I click on "Expand Cluster" button
+ Then I am on the "Add Hosts" section
+
+ Scenario: Skips the process and go to the landing page
+ Given I am on the "welcome" page
+ And I should see a button to "Skip"
+ When I click on "Skip" button
+ And I confirm to "Continue"
+ Then I should be on the "dashboard" page
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/02-create-cluster-add-host.feature b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/02-create-cluster-add-host.feature
new file mode 100644
index 000000000..ddbfd31a3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/02-create-cluster-add-host.feature
@@ -0,0 +1,76 @@
+Feature: Cluster expansion host addition
+
+ Add some hosts and perform some host related actions like editing the labels
+ and removing the hosts from the cluster and verify all of the actions are performed
+ as expected
+
+ Background: Cluster expansion wizard
+ Given I am logged in
+ And I am on the "welcome" page
+ And I click on "Expand Cluster" button
+
+ Scenario Outline: Add hosts
+ Given I am on the "Add Hosts" section
+ When I click on "Add" button
+ And enter "hostname" "<hostname>" in the modal
+ And select options "<labels>"
+ And I click on "Add Host" button
+ Then I should not see the modal
+ And I should see a row with "<hostname>"
+ And I should see row "<hostname>" have "<labels>"
+
+ Examples:
+ | hostname | labels |
+ | ceph-node-01 | mon, mgr |
+ | ceph-node-02 ||
+
+ Scenario Outline: Remove hosts
+ Given I am on the "Add Hosts" section
+ And I should see a row with "<hostname>"
+ When I select a row "<hostname>"
+ And I click on "Remove" button from the table actions
+ Then I should see the modal
+ And I check the tick box in modal
+ And I click on "Remove Host" button
+ Then I should not see the modal
+ And I should not see a row with "<hostname>"
+
+ Examples:
+ | hostname |
+ | ceph-node-01 |
+ | ceph-node-02 |
+
+ Scenario: Add hosts using pattern 'ceph-node-[01-02]'
+ Given I am on the "Add Hosts" section
+ When I click on "Add" button
+ And enter "hostname" "ceph-node-[01-02]" in the modal
+ And I click on "Add Host" button
+ Then I should not see the modal
+ And I should see rows with following entries
+ | hostname |
+ | ceph-node-01 |
+ | ceph-node-02 |
+
+ Scenario: Add exisiting host and verify it failed
+ Given I am on the "Add Hosts" section
+ And I should see a row with "ceph-node-00"
+ When I click on "Add" button
+ And enter "hostname" "ceph-node-00" in the modal
+ Then I should see an error in "hostname" field
+
+ Scenario Outline: Add and remove labels on host
+ Given I am on the "Add Hosts" section
+ When I select a row "<hostname>"
+ And I click on "Edit" button from the table actions
+ And "add" option "<labels>"
+ And I click on "Edit Host" button
+ Then I should see row "<hostname>" have "<labels>"
+ When I select a row "<hostname>"
+ And I click on "Edit" button from the table actions
+ And "remove" option "<labels>"
+ And I click on "Edit Host" button
+ Then I should see row "<hostname>" does not have "<labels>"
+
+ Examples:
+ | hostname | labels |
+ | ceph-node-01 | foo |
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts
new file mode 100644
index 000000000..0118c85c1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts
@@ -0,0 +1,46 @@
+/* tslint:disable*/
+import {
+ CreateClusterServicePageHelper,
+ CreateClusterWizardHelper
+} from '../../cluster/create-cluster.po';
+/* tslint:enable*/
+
+describe('Create cluster create services page', () => {
+ const createCluster = new CreateClusterWizardHelper();
+ const createClusterServicePage = new CreateClusterServicePageHelper();
+
+ const createService = (serviceType: string, serviceName: string, count = 1) => {
+ cy.get('[aria-label=Create]').first().click();
+ createClusterServicePage.addService(serviceType, false, count);
+ createClusterServicePage.checkExist(serviceName, true);
+ };
+
+ beforeEach(() => {
+ cy.login();
+ createCluster.navigateTo();
+ createCluster.createCluster();
+ cy.get('.nav-link').contains('Create Services').click();
+ });
+
+ it('should check if title contains Create Services', () => {
+ cy.get('.title').should('contain.text', 'Create Services');
+ });
+
+ describe('when Orchestrator is available', () => {
+ const serviceName = 'mds.test';
+
+ it('should create an mds service', () => {
+ createService('mds', serviceName);
+ });
+
+ it('should edit a service', () => {
+ const daemonCount = '2';
+ createClusterServicePage.editService(serviceName, daemonCount);
+ createClusterServicePage.expectPlacementCount(serviceName, daemonCount);
+ });
+
+ it('should delete mds service', () => {
+ createClusterServicePage.deleteService('mds.test');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts
new file mode 100644
index 000000000..5583d37fd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts
@@ -0,0 +1,40 @@
+/* tslint:disable*/
+import { CreateClusterWizardHelper } from '../../cluster/create-cluster.po';
+import { OSDsPageHelper } from '../../cluster/osds.po';
+/* tslint:enable*/
+
+const osds = new OSDsPageHelper();
+
+describe('Create cluster create osds page', () => {
+ const createCluster = new CreateClusterWizardHelper();
+
+ beforeEach(() => {
+ cy.login();
+ createCluster.navigateTo();
+ createCluster.createCluster();
+ cy.get('.nav-link').contains('Create OSDs').click();
+ });
+
+ it('should check if title contains Create OSDs', () => {
+ cy.get('.title').should('contain.text', 'Create OSDs');
+ });
+
+ describe('when Orchestrator is available', () => {
+ it('should create OSDs', () => {
+ const hostnames = ['ceph-node-00', 'ceph-node-01'];
+ for (const hostname of hostnames) {
+ osds.create('hdd', hostname, true);
+
+ // Go to the Review section and Expand the cluster
+ // because the drive group spec is only stored
+ // in frontend and will be lost when refreshed
+ cy.get('.nav-link').contains('Review').click();
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('cd-dashboard').should('exist');
+ createCluster.navigateTo();
+ createCluster.createCluster();
+ cy.get('.nav-link').contains('Create OSDs').click();
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts
new file mode 100644
index 000000000..f910b0d85
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts
@@ -0,0 +1,66 @@
+/* tslint:disable*/
+import {
+ CreateClusterHostPageHelper,
+ CreateClusterWizardHelper
+} from '../../cluster/create-cluster.po';
+/* tslint:enable*/
+
+describe('Create Cluster Review page', () => {
+ const createCluster = new CreateClusterWizardHelper();
+ const createClusterHostPage = new CreateClusterHostPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ createCluster.navigateTo();
+ createCluster.createCluster();
+
+ cy.get('.nav-link').contains('Review').click();
+ });
+
+ describe('navigation link test', () => {
+ it('should check if active nav-link is of Review section', () => {
+ cy.get('.nav-link.active').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');
+ createCluster.getStatusTables().should('contain.text', 'CPUs');
+ createCluster.getStatusTables().should('contain.text', 'Memory');
+ });
+
+ it('should check Host Details table is present', () => {
+ // check for there to be two tables
+ createCluster.getDataTables().should('have.length', 1);
+
+ // verify correct columns on Host Details table
+ createCluster.getDataTableHeaders(0).contains('Hostname');
+
+ createCluster.getDataTableHeaders(0).contains('Labels');
+
+ createCluster.getDataTableHeaders(0).contains('CPUs');
+
+ createCluster.getDataTableHeaders(0).contains('Cores');
+
+ createCluster.getDataTableHeaders(0).contains('Total Memory');
+
+ createCluster.getDataTableHeaders(0).contains('Raw Capacity');
+
+ createCluster.getDataTableHeaders(0).contains('HDDs');
+
+ createCluster.getDataTableHeaders(0).contains('Flash');
+
+ createCluster.getDataTableHeaders(0).contains('NICs');
+ });
+
+ it('should check default host name is present', () => {
+ createClusterHostPage.check_for_host();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts
new file mode 100644
index 000000000..722741a6c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts
@@ -0,0 +1,82 @@
+/* tslint:disable*/
+import { CreateClusterWizardHelper } from '../../cluster/create-cluster.po';
+import { HostsPageHelper } from '../../cluster/hosts.po';
+import { ServicesPageHelper } from '../../cluster/services.po';
+/* tslint:enable*/
+
+describe('when cluster creation is completed', () => {
+ const createCluster = new CreateClusterWizardHelper();
+ const services = new ServicesPageHelper();
+ const hosts = new HostsPageHelper();
+
+ const hostnames = ['ceph-node-00', 'ceph-node-01', 'ceph-node-02', 'ceph-node-03'];
+
+ beforeEach(() => {
+ cy.login();
+ });
+
+ it('should redirect to dashboard landing page after cluster creation', () => {
+ createCluster.navigateTo();
+ createCluster.createCluster();
+
+ // Explicitly skip OSD Creation Step so that it prevents from
+ // deploying OSDs to the hosts automatically.
+ cy.get('.nav-link').contains('Create OSDs').click();
+ cy.get('button[aria-label="Skip this step"]').click();
+
+ cy.get('.nav-link').contains('Review').click();
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('cd-dashboard').should('exist');
+ });
+
+ describe('Hosts page', () => {
+ beforeEach(() => {
+ hosts.navigateTo();
+ });
+
+ it('should add one more host', () => {
+ hosts.navigateTo('add');
+ hosts.add(hostnames[3]);
+ hosts.checkExist(hostnames[3], true);
+ });
+
+ it('should check if monitoring stacks are running on the root host', { retries: 2 }, () => {
+ const monitoringStack = ['alertmanager', 'grafana', 'node-exporter', 'prometheus'];
+ hosts.clickTab('cd-host-details', 'ceph-node-00', 'Daemons');
+ for (const daemon of monitoringStack) {
+ cy.get('cd-host-details').within(() => {
+ services.checkServiceStatus(daemon);
+ });
+ }
+ });
+
+ it('should have removed "_no_schedule" label', () => {
+ for (const hostname of hostnames) {
+ hosts.checkLabelExists(hostname, ['_no_schedule'], false);
+ }
+ });
+
+ it('should display inventory', () => {
+ hosts.clickTab('cd-host-details', hostnames[1], 'Physical Disks');
+ cy.get('cd-host-details').within(() => {
+ hosts.getTableCount('total').should('be.gte', 0);
+ });
+ });
+
+ it('should display daemons', () => {
+ hosts.clickTab('cd-host-details', hostnames[1], 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ hosts.getTableCount('total').should('be.gte', 0);
+ });
+ });
+
+ it('should check if mon daemon is running on all hosts', () => {
+ for (const hostname of hostnames) {
+ hosts.clickTab('cd-host-details', hostname, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ services.checkServiceStatus('mon');
+ });
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/07-osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/07-osds.e2e-spec.ts
new file mode 100644
index 000000000..5a16bfe54
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/07-osds.e2e-spec.ts
@@ -0,0 +1,23 @@
+/* tslint:disable*/
+import { OSDsPageHelper } from '../../cluster/osds.po';
+/* tslint:enable*/
+
+describe('OSDs page', () => {
+ const osds = new OSDsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ osds.navigateTo();
+ });
+
+ it('should check if atleast 3 osds are created', { retries: 3 }, () => {
+ // we have created a total of more than 3 osds throughout
+ // the whole tests so ensuring that atleast
+ // 3 osds are listed in the table. Since the OSD
+ // creation can take more time going with
+ // retry of 3
+ for (let id = 0; id < 3; id++) {
+ osds.checkStatus(id, ['in', 'up']);
+ }
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/08-hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/08-hosts.e2e-spec.ts
new file mode 100644
index 000000000..94c61b25c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/08-hosts.e2e-spec.ts
@@ -0,0 +1,48 @@
+/* tslint:disable*/
+import { HostsPageHelper } from '../../cluster/hosts.po';
+import { ServicesPageHelper } from '../../cluster/services.po';
+/* tslint:enable*/
+
+describe('Host Page', () => {
+ const hosts = new HostsPageHelper();
+ const services = new ServicesPageHelper();
+
+ const hostnames = ['ceph-node-00', 'ceph-node-01', 'ceph-node-02', 'ceph-node-03'];
+
+ beforeEach(() => {
+ cy.login();
+ hosts.navigateTo();
+ });
+
+ // rgw is needed for testing the force maintenance
+ it('should create rgw services', () => {
+ services.navigateTo('create');
+ services.addService('rgw', false, 4);
+ services.checkExist('rgw.foo', true);
+ });
+
+ it('should check if rgw daemon is running on all hosts', () => {
+ for (const hostname of hostnames) {
+ hosts.clickTab('cd-host-details', hostname, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ services.checkServiceStatus('rgw');
+ });
+ }
+ });
+
+ it('should force maintenance and exit', () => {
+ hosts.maintenance(hostnames[3], true, true);
+ });
+
+ it('should drain, remove and add the host back', () => {
+ hosts.drain(hostnames[3]);
+ hosts.remove(hostnames[3]);
+ hosts.navigateTo('add');
+ hosts.add(hostnames[3]);
+ hosts.checkExist(hostnames[3], true);
+ });
+
+ it('should show the exact count of daemons', () => {
+ hosts.checkServiceInstancesExist(hostnames[0], ['mgr: 1', 'prometheus: 1']);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/09-services.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/09-services.e2e-spec.ts
new file mode 100644
index 000000000..88b8ab4c9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/09-services.e2e-spec.ts
@@ -0,0 +1,132 @@
+/* tslint:disable*/
+import { ServicesPageHelper } from '../../cluster/services.po';
+/* tslint:enable*/
+
+describe('Services page', () => {
+ const services = new ServicesPageHelper();
+ const mdsDaemonName = 'mds.test';
+ beforeEach(() => {
+ cy.login();
+ services.navigateTo();
+ });
+
+ it('should check if rgw service is created', () => {
+ services.checkExist('rgw.foo', true);
+ });
+
+ it('should create an mds service', () => {
+ services.navigateTo('create');
+ services.addService('mds', false);
+ services.checkExist(mdsDaemonName, true);
+
+ services.clickServiceTab(mdsDaemonName, 'Daemons');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName);
+ });
+ });
+
+ it('should stop a daemon', () => {
+ services.clickServiceTab(mdsDaemonName, 'Daemons');
+ services.checkServiceStatus(mdsDaemonName);
+
+ services.daemonAction('mds', 'stop');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName, 'stopped');
+ });
+ });
+
+ it('should restart a daemon', () => {
+ services.checkExist(mdsDaemonName, true);
+ services.clickServiceTab(mdsDaemonName, 'Daemons');
+ services.daemonAction('mds', 'restart');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName, 'running');
+ });
+ });
+
+ it('should redeploy a daemon', () => {
+ services.checkExist(mdsDaemonName, true);
+ services.clickServiceTab(mdsDaemonName, 'Daemons');
+
+ services.daemonAction('mds', 'stop');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName, 'stopped');
+ });
+ services.daemonAction('mds', 'redeploy');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName, 'running');
+ });
+ });
+
+ it('should start a daemon', () => {
+ services.checkExist(mdsDaemonName, true);
+ services.clickServiceTab(mdsDaemonName, 'Daemons');
+
+ services.daemonAction('mds', 'stop');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName, 'stopped');
+ });
+ services.daemonAction('mds', 'start');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName, 'running');
+ });
+ });
+
+ it('should delete an mds service', () => {
+ services.deleteService(mdsDaemonName);
+ });
+
+ it('should create and delete snmp-gateway service with version V2c', () => {
+ services.navigateTo('create');
+ services.addService('snmp-gateway', false, 1, 'V2c');
+ services.checkExist('snmp-gateway', true);
+
+ services.clickServiceTab('snmp-gateway', 'Daemons');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus('snmp-gateway');
+ });
+
+ services.deleteService('snmp-gateway');
+ });
+
+ it('should create and delete snmp-gateway service with version V3', () => {
+ services.navigateTo('create');
+ services.addService('snmp-gateway', false, 1, 'V3', true);
+ services.checkExist('snmp-gateway', true);
+
+ services.clickServiceTab('snmp-gateway', 'Daemons');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus('snmp-gateway');
+ });
+
+ services.deleteService('snmp-gateway');
+ });
+
+ it('should create and delete snmp-gateway service with version V3 and w/o privacy protocol', () => {
+ services.navigateTo('create');
+ services.addService('snmp-gateway', false, 1, 'V3', false);
+ services.checkExist('snmp-gateway', true);
+
+ services.clickServiceTab('snmp-gateway', 'Daemons');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus('snmp-gateway');
+ });
+
+ services.deleteService('snmp-gateway');
+ });
+
+ it('should create ingress as unmanaged', () => {
+ services.navigateTo('create');
+ services.addService('ingress', false, undefined, undefined, undefined, true);
+ services.checkExist('ingress.rgw.foo', true);
+ services.isUnmanaged('ingress.rgw.foo', true);
+ services.deleteService('ingress.rgw.foo');
+ });
+
+ it('should check if exporter daemons are running', () => {
+ services.clickServiceTab('ceph-exporter', 'Daemons');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus('ceph-exporter', 'running');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/10-nfs-exports.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/10-nfs-exports.e2e-spec.ts
new file mode 100644
index 000000000..6380e5a13
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/10-nfs-exports.e2e-spec.ts
@@ -0,0 +1,82 @@
+/* tslint:disable*/
+import { ServicesPageHelper } from '../../cluster/services.po';
+import { NFSPageHelper } from '../../orchestrator/workflow/nfs/nfs-export.po';
+import { BucketsPageHelper } from '../../rgw/buckets.po';
+/* tslint:enable*/
+
+describe('nfsExport page', () => {
+ const nfsExport = new NFSPageHelper();
+ const services = new ServicesPageHelper();
+ const buckets = new BucketsPageHelper();
+ const bucketName = 'e2e.nfs.bucket';
+ // @TODO: uncomment this when a CephFS volume can be created through Dashboard.
+ // const fsPseudo = '/fsPseudo';
+ const rgwPseudo = '/rgwPseudo';
+ const editPseudo = '/editPseudo';
+ const backends = ['CephFS', 'Object Gateway'];
+ const squash = 'no_root_squash';
+ const client: object = { addresses: '192.168.0.10' };
+
+ beforeEach(() => {
+ cy.login();
+ nfsExport.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ nfsExport.expectBreadcrumbText('NFS');
+ });
+ });
+
+ describe('Create, edit and delete', () => {
+ it('should create an NFS cluster', () => {
+ services.navigateTo('create');
+
+ services.addService('nfs');
+
+ services.checkExist('nfs.testnfs', true);
+ services.clickServiceTab('nfs.testnfs', 'Daemons');
+ services.checkServiceStatus('nfs');
+ });
+
+ it('should create a nfs-export with RGW backend', () => {
+ buckets.navigateTo('create');
+ buckets.create(bucketName, 'dashboard', 'default-placement');
+
+ nfsExport.navigateTo();
+ nfsExport.existTableCell(rgwPseudo, false);
+ nfsExport.navigateTo('create');
+ nfsExport.create(backends[1], squash, client, rgwPseudo, bucketName);
+ nfsExport.existTableCell(rgwPseudo);
+ });
+
+ // @TODO: uncomment this when a CephFS volume can be created through Dashboard.
+ // it('should create a nfs-export with CephFS backend', () => {
+ // nfsExport.navigateTo();
+ // nfsExport.existTableCell(fsPseudo, false);
+ // nfsExport.navigateTo('create');
+ // nfsExport.create(backends[0], squash, client, fsPseudo);
+ // nfsExport.existTableCell(fsPseudo);
+ // });
+
+ it('should show Clients', () => {
+ nfsExport.clickTab('cd-nfs-details', rgwPseudo, 'Clients (1)');
+ cy.get('cd-nfs-details').within(() => {
+ nfsExport.getTableCount('total').should('be.gte', 0);
+ });
+ });
+
+ it('should edit an export', () => {
+ nfsExport.editExport(rgwPseudo, editPseudo);
+
+ nfsExport.existTableCell(editPseudo);
+ });
+
+ it('should delete exports and bucket', () => {
+ nfsExport.delete(editPseudo);
+
+ buckets.navigateTo();
+ buckets.delete(bucketName);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/nfs/nfs-export.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/nfs/nfs-export.po.ts
new file mode 100644
index 000000000..c700ef058
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/nfs/nfs-export.po.ts
@@ -0,0 +1,52 @@
+/* tslint:disable*/
+import { PageHelper } from '../../../page-helper.po';
+/* tslint:enable*/
+
+const pages = {
+ index: { url: '#/nfs', id: 'cd-nfs-list' },
+ create: { url: '#/nfs/create', id: 'cd-nfs-form' }
+};
+
+export class NFSPageHelper extends PageHelper {
+ pages = pages;
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(backend: string, squash: string, client: object, pseudo: string, rgwPath?: string) {
+ this.selectOption('cluster_id', 'testnfs');
+ // select a storage backend
+ this.selectOption('name', backend);
+ if (backend === 'CephFS') {
+ this.selectOption('fs_name', 'myfs');
+
+ cy.get('#security_label').click({ force: true });
+ } else {
+ cy.get('input[data-testid=rgw_path]').type(rgwPath);
+ }
+
+ cy.get('input[name=pseudo]').type(pseudo);
+ this.selectOption('squash', squash);
+
+ // Add clients
+ cy.get('button[name=add_client]').click({ force: true });
+ cy.get('input[name=addresses]').type(client['addresses']);
+
+ // Check if we can remove clients and add it again
+ cy.get('span[name=remove_client]').click({ force: true });
+ cy.get('button[name=add_client]').click({ force: true });
+ cy.get('input[name=addresses]').type(client['addresses']);
+
+ cy.get('cd-submit-button').click();
+ }
+
+ editExport(pseudo: string, editPseudo: string) {
+ this.navigateEdit(pseudo);
+
+ cy.get('input[name=pseudo]').clear().type(editPseudo);
+
+ cy.get('cd-submit-button').click();
+
+ // Click the export and check its details table for updated content
+ this.getExpandCollapseElement(editPseudo).click();
+ cy.get('.active.tab-pane').should('contain.text', editPseudo);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts
new file mode 100644
index 000000000..2a16ff7e1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts
@@ -0,0 +1,309 @@
+interface Page {
+ url: string;
+ id: string;
+}
+
+export abstract class PageHelper {
+ pages: Record<string, Page>;
+
+ /**
+ * Decorator to be used on Helper methods to restrict access to one particular URL. This shall
+ * help developers to prevent and highlight mistakes. It also reduces boilerplate code and by
+ * thus, increases readability.
+ */
+ static restrictTo(page: string): Function {
+ return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
+ const fn: Function = descriptor.value;
+ descriptor.value = function (...args: any) {
+ cy.location('hash').should((url) => {
+ expect(url).to.eq(
+ page,
+ `Method ${target.constructor.name}::${propertyKey} is supposed to be ` +
+ `run on path "${page}", but was run on URL "${url}"`
+ );
+ });
+ fn.apply(this, args);
+ };
+ };
+ }
+
+ /**
+ * Navigates to the given page or to index.
+ * Waits until the page component is loaded
+ */
+ navigateTo(name: string = null) {
+ name = name || 'index';
+ const page = this.pages[name];
+
+ cy.visit(page.url);
+ cy.get(page.id);
+ }
+
+ /**
+ * Navigates back and waits for the hash to change
+ */
+ navigateBack() {
+ cy.location('hash').then((hash) => {
+ cy.go('back');
+ cy.location('hash').should('not.be', hash);
+ });
+ }
+
+ /**
+ * Navigates to the edit page
+ */
+ navigateEdit(name: string, select = true, breadcrumb = true) {
+ if (select) {
+ this.navigateTo();
+ this.getFirstTableCell(name).click();
+ }
+ cy.contains('Creating...').should('not.exist');
+ cy.contains('button', 'Edit').click();
+ if (breadcrumb) {
+ this.expectBreadcrumbText('Edit');
+ }
+ }
+
+ /**
+ * Checks the active breadcrumb value.
+ */
+ expectBreadcrumbText(text: string) {
+ cy.get('.breadcrumb-item.active').should('have.text', text);
+ }
+
+ getTabs() {
+ return cy.get('.nav.nav-tabs a');
+ }
+
+ getTab(tabName: string) {
+ return cy.contains('.nav.nav-tabs a', tabName);
+ }
+
+ getTabText(index: number) {
+ return this.getTabs().its(index).text();
+ }
+
+ getTabsCount(): any {
+ return this.getTabs().its('length');
+ }
+
+ /**
+ * Helper method to navigate/click a tab inside the expanded table row.
+ * @param selector The selector of the expanded table row.
+ * @param name The name of the row which should expand.
+ * @param tabName Name of the tab to be navigated/clicked.
+ */
+ clickTab(selector: string, name: string, tabName: string) {
+ this.getExpandCollapseElement(name).click();
+ cy.get(selector).within(() => {
+ this.getTab(tabName).click();
+ });
+ }
+
+ /**
+ * Helper method to select an option inside a select element.
+ * This method will also expect that the option was set.
+ * @param option The option text (not value) to be selected.
+ */
+ selectOption(selectionName: string, option: string) {
+ cy.get(`select[name=${selectionName}]`).select(option);
+ return this.expectSelectOption(selectionName, option);
+ }
+
+ /**
+ * Helper method to expect a set option inside a select element.
+ * @param option The selected option text (not value) that is to
+ * be expected.
+ */
+ expectSelectOption(selectionName: string, option: string) {
+ return cy.get(`select[name=${selectionName}] option:checked`).contains(option);
+ }
+
+ getLegends() {
+ return cy.get('legend');
+ }
+
+ getToast() {
+ return cy.get('.ngx-toastr');
+ }
+
+ /**
+ * Waits for the table to load its data
+ * Should be used in all methods that access the datatable
+ */
+ private waitDataTableToLoad() {
+ cy.get('cd-table').should('exist');
+ cy.get('datatable-scroller, .empty-row');
+ }
+
+ getDataTables() {
+ this.waitDataTableToLoad();
+
+ return cy.get('cd-table .dataTables_wrapper');
+ }
+
+ private getTableCountSpan(spanType: 'selected' | 'found' | 'total') {
+ return cy.contains('.datatable-footer-inner .page-count span', spanType);
+ }
+
+ // Get 'selected', 'found', or 'total' row count of a table.
+ getTableCount(spanType: 'selected' | 'found' | 'total') {
+ this.waitDataTableToLoad();
+ return this.getTableCountSpan(spanType).then(($elem) => {
+ const text = $elem
+ .filter((_i, e) => e.innerText.includes(spanType))
+ .first()
+ .text();
+
+ return Number(text.match(/(\d+)\s+\w*/)[1]);
+ });
+ }
+
+ // Wait until selected', 'found', or 'total' row count of a table equal to a number.
+ expectTableCount(spanType: 'selected' | 'found' | 'total', count: number) {
+ this.waitDataTableToLoad();
+ this.getTableCountSpan(spanType).should(($elem) => {
+ const text = $elem.first().text();
+ expect(Number(text.match(/(\d+)\s+\w*/)[1])).to.equal(count);
+ });
+ }
+
+ getTableRow(content: string) {
+ this.waitDataTableToLoad();
+
+ this.searchTable(content);
+ return cy.contains('.datatable-body-row', content);
+ }
+
+ getTableRows() {
+ this.waitDataTableToLoad();
+
+ return cy.get('datatable-row-wrapper');
+ }
+
+ /**
+ * Returns the first table cell.
+ * Optionally, you can specify the content of the cell.
+ */
+ getFirstTableCell(content?: string) {
+ this.waitDataTableToLoad();
+
+ if (content) {
+ this.searchTable(content);
+ return cy.contains('.datatable-body-cell-label', content);
+ } else {
+ return cy.get('.datatable-body-cell-label').first();
+ }
+ }
+
+ getTableCell(columnIndex: number, exactContent: string, partialMatch = false) {
+ this.waitDataTableToLoad();
+ this.clearTableSearchInput();
+ this.searchTable(exactContent);
+ if (partialMatch) {
+ return cy.contains(
+ `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
+ exactContent
+ );
+ }
+ return cy.contains(
+ `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
+ new RegExp(`^${exactContent}$`)
+ );
+ }
+
+ existTableCell(name: string, oughtToBePresent = true) {
+ const waitRule = oughtToBePresent ? 'be.visible' : 'not.exist';
+ this.getFirstTableCell(name).should(waitRule);
+ }
+
+ getExpandCollapseElement(content?: string) {
+ this.waitDataTableToLoad();
+
+ if (content) {
+ return cy.contains('.datatable-body-row', content).find('.tc_expand-collapse');
+ } else {
+ return cy.get('.tc_expand-collapse').first();
+ }
+ }
+
+ /**
+ * Gets column headers of table
+ */
+ getDataTableHeaders(index = 0) {
+ this.waitDataTableToLoad();
+
+ return cy.get('.datatable-header').its(index).find('.datatable-header-cell');
+ }
+
+ /**
+ * Grabs striped tables
+ */
+ getStatusTables() {
+ return cy.get('.table.table-striped');
+ }
+
+ filterTable(name: string, option: string) {
+ this.waitDataTableToLoad();
+
+ cy.get('.tc_filter_name > button').click();
+ cy.contains(`.tc_filter_name .dropdown-item`, name).click();
+
+ cy.get('.tc_filter_option > button').click();
+ cy.contains(`.tc_filter_option .dropdown-item`, option).click();
+ }
+
+ setPageSize(size: string) {
+ cy.get('cd-table .dataTables_paginate input').first().clear({ force: true }).type(size);
+ }
+
+ searchTable(text: string) {
+ this.waitDataTableToLoad();
+
+ this.setPageSize('10');
+ cy.get('[aria-label=search]').first().clear({ force: true }).type(text);
+ }
+
+ clearTableSearchInput() {
+ this.waitDataTableToLoad();
+
+ return cy.get('cd-table .search button').first().click();
+ }
+
+ // Click the action button
+ clickActionButton(action: string) {
+ cy.get('.table-actions button.dropdown-toggle').first().click(); // open submenu
+ cy.get(`button.${action}`).click(); // click on "action" menu item
+ }
+
+ /**
+ * This is a generic method to delete table rows.
+ * It will select the first row that contains the provided name and delete it.
+ * After that it will wait until the row is no longer displayed.
+ * @param name The string to search in table cells.
+ * @param columnIndex If provided, search string in columnIndex column.
+ */
+ delete(name: string, columnIndex?: number, section?: string) {
+ // Selects row
+ const getRow = columnIndex
+ ? this.getTableCell.bind(this, columnIndex, name, true)
+ : this.getFirstTableCell.bind(this);
+ getRow(name).click();
+ let action: string;
+ section === 'hosts' ? (action = 'remove') : (action = 'delete');
+
+ // Clicks on table Delete/Remove button
+ this.clickActionButton(action);
+
+ // 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', actionUpperCase).click();
+
+ // Wait for modal to close
+ cy.get('cd-modal').should('not.exist');
+
+ // Waits for item to be removed from table
+ getRow(name).should('not.exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.e2e-spec.ts
new file mode 100644
index 000000000..dd4ab6f3b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.e2e-spec.ts
@@ -0,0 +1,53 @@
+import { PoolPageHelper } from './pools.po';
+
+describe('Pools page', () => {
+ const pools = new PoolPageHelper();
+ const poolName = 'pool_e2e_pool-test';
+
+ beforeEach(() => {
+ cy.login();
+ pools.navigateTo();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ it('should open and show breadcrumb', () => {
+ pools.expectBreadcrumbText('Pools');
+ });
+
+ it('should show two tabs', () => {
+ pools.getTabsCount().should('equal', 2);
+ });
+
+ it('should show pools list tab at first', () => {
+ pools.getTabText(0).should('eq', 'Pools List');
+ });
+
+ it('should show overall performance as a second tab', () => {
+ pools.getTabText(1).should('eq', 'Overall Performance');
+ });
+ });
+
+ describe('Create, update and destroy', () => {
+ it('should create a pool', () => {
+ pools.existTableCell(poolName, false);
+ pools.navigateTo('create');
+ pools.create(poolName, 8, 'rbd');
+ pools.existTableCell(poolName);
+ });
+
+ it('should edit a pools placement group', () => {
+ pools.existTableCell(poolName);
+ pools.edit_pool_pg(poolName, 32);
+ });
+
+ it('should show updated configuration field values', () => {
+ pools.existTableCell(poolName);
+ const bpsLimit = '4 B/s';
+ pools.edit_pool_configuration(poolName, bpsLimit);
+ });
+
+ it('should delete a pool', () => {
+ pools.delete(poolName);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.po.ts
new file mode 100644
index 000000000..7cca96aa8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/pools/pools.po.ts
@@ -0,0 +1,70 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/pool', id: 'cd-pool-list' },
+ create: { url: '#/pool/create', id: 'cd-pool-form' }
+};
+
+export class PoolPageHelper extends PageHelper {
+ pages = pages;
+
+ private isPowerOf2(n: number) {
+ // tslint:disable-next-line: no-bitwise
+ return expect((n & (n - 1)) === 0, `Placement groups ${n} are not a power of 2`).to.be.true;
+ }
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(name: string, placement_groups: number, ...apps: string[]) {
+ cy.get('input[name=name]').clear().type(name);
+
+ this.isPowerOf2(placement_groups);
+
+ this.selectOption('poolType', 'replicated');
+
+ this.expectSelectOption('pgAutoscaleMode', 'on');
+ this.selectOption('pgAutoscaleMode', 'off'); // To show pgNum field
+ cy.get('input[name=pgNum]').clear().type(`${placement_groups}`);
+ this.setApplications(apps);
+ cy.get('cd-submit-button').click();
+ }
+
+ edit_pool_pg(name: string, new_pg: number, wait = true) {
+ this.isPowerOf2(new_pg);
+ this.navigateEdit(name);
+
+ cy.get('input[name=pgNum]').clear().type(`${new_pg}`);
+ cy.get('cd-submit-button').click();
+ const str = `${new_pg} active+clean`;
+ this.getTableRow(name);
+ if (wait) {
+ this.getTableRow(name).contains(str);
+ }
+ }
+
+ edit_pool_configuration(name: string, bpsLimit: string) {
+ this.navigateEdit(name);
+
+ cy.get('.collapsible').click();
+ cy.get('cd-rbd-configuration-form')
+ .get('input[name=rbd_qos_bps_limit]')
+ .clear()
+ .type(`${bpsLimit}`);
+ cy.get('cd-submit-button').click();
+
+ this.navigateEdit(name);
+
+ cy.get('.collapsible').click();
+ cy.get('cd-rbd-configuration-form')
+ .get('input[name=rbd_qos_bps_limit]')
+ .should('have.value', bpsLimit);
+ }
+
+ private setApplications(apps: string[]) {
+ if (!apps || apps.length === 0) {
+ return;
+ }
+ cy.get('.float-start.me-2.select-menu-edit').click();
+ cy.get('.popover-body').should('be.visible');
+ apps.forEach((app) => cy.get('.select-menu-item-content').contains(app).click());
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.e2e-spec.ts
new file mode 100644
index 000000000..99c0732fc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.e2e-spec.ts
@@ -0,0 +1,66 @@
+import { BucketsPageHelper } from './buckets.po';
+
+describe('RGW buckets page', () => {
+ const buckets = new BucketsPageHelper();
+ const bucket_name = 'e2ebucket';
+
+ beforeEach(() => {
+ cy.login();
+ buckets.navigateTo();
+ });
+
+ describe('breadcrumb tests', () => {
+ it('should open and show breadcrumb', () => {
+ buckets.expectBreadcrumbText('Buckets');
+ });
+ });
+
+ describe('create, edit & delete bucket tests', () => {
+ it('should create bucket', () => {
+ buckets.navigateTo('create');
+ buckets.create(bucket_name, BucketsPageHelper.USERS[0], 'default-placement');
+ buckets.getFirstTableCell(bucket_name).should('exist');
+ });
+
+ it('should edit bucket', () => {
+ buckets.edit(bucket_name, BucketsPageHelper.USERS[1]);
+ buckets.getDataTables().should('contain.text', BucketsPageHelper.USERS[1]);
+ });
+
+ it('should delete bucket', () => {
+ buckets.delete(bucket_name);
+ });
+
+ it('should check default encryption is SSE-S3', () => {
+ buckets.navigateTo('create');
+ buckets.checkForDefaultEncryption();
+ });
+
+ it('should create bucket with object locking enabled', () => {
+ buckets.navigateTo('create');
+ buckets.create(bucket_name, BucketsPageHelper.USERS[0], 'default-placement', true);
+ buckets.getFirstTableCell(bucket_name).should('exist');
+ });
+
+ it('should not allow to edit versioning if object locking is enabled', () => {
+ buckets.edit(bucket_name, BucketsPageHelper.USERS[1], true);
+ buckets.getDataTables().should('contain.text', BucketsPageHelper.USERS[1]);
+
+ buckets.delete(bucket_name);
+ });
+ });
+
+ describe('Invalid Input in Create and Edit tests', () => {
+ it('should test invalid inputs in create fields', () => {
+ buckets.testInvalidCreate();
+ });
+
+ it('should test invalid input in edit owner field', () => {
+ buckets.navigateTo('create');
+ buckets.create(bucket_name, BucketsPageHelper.USERS[0], 'default-placement');
+ buckets.testInvalidEdit(bucket_name);
+ buckets.navigateTo();
+ buckets.delete(bucket_name);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.po.ts
new file mode 100644
index 000000000..47b0639bc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/buckets.po.ts
@@ -0,0 +1,213 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/rgw/bucket', id: 'cd-rgw-bucket-list' },
+ create: { url: '#/rgw/bucket/create', id: 'cd-rgw-bucket-form' }
+};
+
+export class BucketsPageHelper extends PageHelper {
+ static readonly USERS = ['dashboard', 'testid'];
+
+ pages = pages;
+
+ columnIndex = {
+ name: 3,
+ owner: 4
+ };
+
+ versioningStateEnabled = 'Enabled';
+ versioningStateSuspended = 'Suspended';
+
+ private selectOwner(owner: string) {
+ return this.selectOption('owner', owner);
+ }
+
+ private selectPlacementTarget(placementTarget: string) {
+ return this.selectOption('placement-target', placementTarget);
+ }
+
+ private selectLockMode(lockMode: string) {
+ return this.selectOption('lock_mode', lockMode);
+ }
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(name: string, owner: string, placementTarget: string, isLocking = false) {
+ // Enter in bucket name
+ cy.get('#bid').type(name);
+
+ // Select bucket owner
+ this.selectOwner(owner);
+ cy.get('#owner').should('have.class', 'ng-valid');
+
+ // Select bucket placement target:
+ this.selectPlacementTarget(placementTarget);
+ cy.get('#placement-target').should('have.class', 'ng-valid');
+
+ if (isLocking) {
+ cy.get('#lock_enabled').click({ force: true });
+ // Select lock mode:
+ this.selectLockMode('Compliance');
+ cy.get('#lock_mode').should('have.class', 'ng-valid');
+ cy.get('#lock_retention_period_days').type('3');
+ }
+
+ // Click the create button and wait for bucket to be made
+ cy.contains('button', 'Create Bucket').click();
+
+ this.getFirstTableCell(name).should('exist');
+ }
+
+ @PageHelper.restrictTo(pages.create.url)
+ checkForDefaultEncryption() {
+ cy.get("cd-helper[aria-label='toggle encryption helper']").click();
+ cy.get("a[aria-label='click here']").click();
+ cy.get('cd-modal').within(() => {
+ cy.get('input[id=s3Enabled]').should('be.checked');
+ });
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ edit(name: string, new_owner: string, isLocking = false) {
+ this.navigateEdit(name);
+
+ cy.get('input[name=placement-target]').should('have.value', 'default-placement');
+ this.selectOwner(new_owner);
+
+ // If object locking is enabled versioning shouldn't be visible
+ if (isLocking) {
+ cy.get('input[id=versioning]').should('be.disabled');
+ cy.contains('button', 'Edit Bucket').click();
+
+ this.getTableCell(this.columnIndex.name, name)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.owner})`)
+ .should(($elements) => {
+ const bucketName = $elements.text();
+ expect(bucketName).to.eq(new_owner);
+ });
+
+ // wait to be back on buckets page with table visible and click
+ this.getExpandCollapseElement(name).click();
+
+ // check its details table for edited owner field
+ cy.get('.table.table-striped.table-bordered').first().as('bucketDataTable');
+
+ // Check versioning enabled:
+ cy.get('@bucketDataTable').find('tr').its(0).find('td').last().as('versioningValueCell');
+
+ return cy.get('@versioningValueCell').should('have.text', this.versioningStateEnabled);
+ }
+ // Enable versioning
+ cy.get('input[id=versioning]').should('not.be.checked');
+ cy.get('label[for=versioning]').click();
+ cy.get('input[id=versioning]').should('be.checked');
+ cy.contains('button', 'Edit Bucket').click();
+
+ // Check if the owner is updated
+ this.getTableCell(this.columnIndex.name, name)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.owner})`)
+ .should(($elements) => {
+ const bucketName = $elements.text();
+ expect(bucketName).to.eq(new_owner);
+ });
+
+ // wait to be back on buckets page with table visible and click
+ this.getExpandCollapseElement(name).click();
+
+ // Check versioning enabled:
+ cy.get('.table.table-striped.table-bordered').first().as('bucketDataTable');
+ cy.get('@bucketDataTable').find('tr').its(0).find('td').last().as('versioningValueCell');
+
+ cy.get('@versioningValueCell').should('have.text', this.versioningStateEnabled);
+
+ // Disable versioning:
+ this.navigateEdit(name);
+
+ cy.get('label[for=versioning]').click();
+ cy.get('input[id=versioning]').should('not.be.checked');
+ cy.contains('button', 'Edit Bucket').click();
+
+ // Check versioning suspended:
+ this.getExpandCollapseElement(name).click();
+
+ return cy.get('@versioningValueCell').should('have.text', this.versioningStateSuspended);
+ }
+
+ testInvalidCreate() {
+ this.navigateTo('create');
+ cy.get('#bid').as('nameInputField'); // Grabs name box field
+
+ // Gives an invalid name (too short), then waits for dashboard to determine validity
+ cy.get('@nameInputField').type('rq');
+
+ cy.contains('button', 'Create Bucket').click(); // To trigger a validation
+
+ // Waiting for website to decide if name is valid or not
+ // Check that name input field was marked invalid in the css
+ cy.get('@nameInputField')
+ .should('not.have.class', 'ng-pending')
+ .and('have.class', 'ng-invalid');
+
+ // Check that error message was printed under name input field
+ cy.get('#bid + .invalid-feedback').should(
+ 'have.text',
+ 'Bucket names must be 3 to 63 characters long.'
+ );
+
+ // Test invalid owner input
+ // select some valid option. The owner drop down error message will not appear unless a valid user was selected at
+ // one point before the invalid placeholder user is selected.
+ this.selectOwner(BucketsPageHelper.USERS[1]);
+
+ // select the first option, which is invalid because it is a placeholder
+ this.selectOwner('-- Select a user --');
+
+ cy.get('@nameInputField').click();
+
+ // Check that owner drop down field was marked invalid in the css
+ cy.get('#owner').should('have.class', 'ng-invalid');
+
+ // Check that error message was printed under owner drop down field
+ cy.get('#owner + .invalid-feedback').should('have.text', 'This field is required.');
+
+ // Check invalid placement target input
+ this.selectOwner(BucketsPageHelper.USERS[1]);
+ // The drop down error message will not appear unless a valid option is previsously selected.
+ this.selectPlacementTarget('default-placement');
+ this.selectPlacementTarget('-- Select a placement target --');
+ cy.get('@nameInputField').click(); // Trigger validation
+ cy.get('#placement-target').should('have.class', 'ng-invalid');
+ cy.get('#placement-target + .invalid-feedback').should('have.text', 'This field is required.');
+
+ // Clicks the Create Bucket button but the page doesn't move.
+ // Done by testing for the breadcrumb
+ cy.contains('button', 'Create Bucket').click(); // Clicks Create Bucket button
+ this.expectBreadcrumbText('Create');
+ // content in fields seems to subsist through tests if not cleared, so it is cleared
+ cy.get('@nameInputField').clear();
+ return cy.contains('button', 'Cancel').click();
+ }
+
+ testInvalidEdit(name: string) {
+ this.navigateEdit(name);
+
+ cy.get('input[id=versioning]').should('exist').and('not.be.checked');
+
+ // Chooses 'Select a user' rather than a valid owner on Edit Bucket page
+ // and checks if it's an invalid input
+
+ // select the first option, which is invalid because it is a placeholder
+ this.selectOwner('-- Select a user --');
+
+ cy.contains('button', 'Edit Bucket').click();
+
+ // Check that owner drop down field was marked invalid in the css
+ cy.get('#owner').should('have.class', 'ng-invalid');
+
+ // Check that error message was printed under owner drop down field
+ cy.get('#owner + .invalid-feedback').should('have.text', 'This field is required.');
+
+ this.expectBreadcrumbText('Edit');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.e2e-spec.ts
new file mode 100644
index 000000000..b71d715f8
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.e2e-spec.ts
@@ -0,0 +1,34 @@
+import { DaemonsPageHelper } from './daemons.po';
+
+describe('RGW daemons page', () => {
+ const daemons = new DaemonsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ daemons.navigateTo();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ it('should open and show breadcrumb', () => {
+ daemons.expectBreadcrumbText('Gateways');
+ });
+
+ it('should show two tabs', () => {
+ daemons.getTabsCount().should('eq', 2);
+ });
+
+ it('should show daemons list tab at first', () => {
+ daemons.getTabText(0).should('eq', 'Gateways List');
+ });
+
+ it('should show overall performance as a second tab', () => {
+ daemons.getTabText(1).should('eq', 'Overall Performance');
+ });
+ });
+
+ describe('details and performance counters table tests', () => {
+ it('should check that details/performance tables are visible when daemon is selected', () => {
+ daemons.checkTables();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.po.ts
new file mode 100644
index 000000000..82a179463
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/daemons.po.ts
@@ -0,0 +1,34 @@
+import { PageHelper } from '../page-helper.po';
+
+export class DaemonsPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/rgw/daemon', id: 'cd-rgw-daemon-list' }
+ };
+
+ getTableCell() {
+ return cy
+ .get('.tab-content')
+ .its(1)
+ .find('cd-table')
+ .should('have.length', 1) // Only 1 table should be renderer
+ .find('datatable-body-cell');
+ }
+
+ checkTables() {
+ // click on a daemon so details table appears
+ cy.get('.datatable-body-cell-label').first().click();
+
+ // check details table is visible
+ // check at least one field is present
+ this.getTableCell().should('be.visible').should('contain.text', 'ceph_version');
+
+ // click on performance counters tab and check table is loaded
+ cy.contains('.nav-link', 'Performance Counters').click();
+
+ // check at least one field is present
+ this.getTableCell().should('be.visible').should('contain.text', 'objecter.op_r');
+
+ // click on performance details tab
+ cy.contains('.nav-link', 'Performance Details').click();
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts
new file mode 100644
index 000000000..597f7d1be
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.e2e-spec.ts
@@ -0,0 +1,19 @@
+import { RolesPageHelper } from './roles.po';
+
+describe('RGW roles page', () => {
+ const roles = new RolesPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ roles.navigateTo();
+ });
+
+ describe('Create, Edit & Delete rgw roles', () => {
+ it('should create rgw roles', () => {
+ roles.navigateTo('create');
+ roles.create('testRole', '/', '{}');
+ roles.navigateTo();
+ roles.checkExist('testRole', true);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts
new file mode 100644
index 000000000..b72ca5df9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/roles.po.ts
@@ -0,0 +1,37 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/rgw/roles', id: 'cd-crud-table' },
+ create: { url: '#/rgw/roles/create', id: 'cd-crud-form' }
+};
+
+export class RolesPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ roleName: 2,
+ path: 3,
+ arn: 4
+ };
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(name: string, path: string, policyDocument: string) {
+ cy.get('#formly_3_string_role_name_0').type(name);
+ cy.get('#formly_3_textarea_role_assume_policy_doc_2').type(policyDocument);
+ cy.get('#formly_3_string_role_path_1').type(path);
+ cy.get("[aria-label='Create Role']").should('exist').click();
+ cy.get('cd-crud-table').should('exist');
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ checkExist(name: string, exist: boolean) {
+ this.getTableCell(this.columnIndex.roleName, name).should(($elements) => {
+ const roleName = $elements.map((_, el) => el.textContent).get();
+ if (exist) {
+ expect(roleName).to.include(name);
+ } else {
+ expect(roleName).to.not.include(name);
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts
new file mode 100644
index 000000000..c107a08dd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.e2e-spec.ts
@@ -0,0 +1,45 @@
+import { UsersPageHelper } from './users.po';
+
+describe('RGW users page', () => {
+ const users = new UsersPageHelper();
+ const tenant = 'e2e_000tenant';
+ const user_id = 'e2e_000user_create_edit_delete';
+ const user_name = tenant + '$' + user_id;
+
+ beforeEach(() => {
+ cy.login();
+ users.navigateTo();
+ });
+
+ describe('breadcrumb tests', () => {
+ it('should open and show breadcrumb', () => {
+ users.expectBreadcrumbText('Users');
+ });
+ });
+
+ describe('create, edit & delete user tests', () => {
+ it('should create user', () => {
+ users.navigateTo('create');
+ users.create(tenant, user_id, 'Some Name', 'original@website.com', '1200');
+ users.getFirstTableCell(user_id).should('exist');
+ });
+
+ it('should edit users full name, email and max buckets', () => {
+ users.edit(user_name, 'Another Identity', 'changed@othersite.com', '1969');
+ });
+
+ it('should delete user', () => {
+ users.delete(user_name);
+ });
+ });
+
+ describe('Invalid input tests', () => {
+ it('should put invalid input into user creation form and check fields are marked invalid', () => {
+ users.invalidCreate();
+ });
+
+ it('should put invalid input into user edit form and check fields are marked invalid', () => {
+ users.invalidEdit();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.po.ts
new file mode 100644
index 000000000..980cced88
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/users.po.ts
@@ -0,0 +1,139 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/rgw/user', id: 'cd-rgw-user-list' },
+ create: { url: '#/rgw/user/create', id: 'cd-rgw-user-form' }
+};
+
+export class UsersPageHelper extends PageHelper {
+ pages = pages;
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(tenant: string, user_id: string, fullname: string, email: string, maxbuckets: string) {
+ // Enter in user_id
+ cy.get('#user_id').type(user_id);
+ // Show Tenanat
+ cy.get('#show_tenant').click({ force: true });
+ // Enter in tenant
+ cy.get('#tenant').type(tenant);
+ // Enter in full name
+ cy.get('#display_name').click().type(fullname);
+
+ // Enter in email
+ cy.get('#email').click().type(email);
+
+ // Enter max buckets
+ this.selectOption('max_buckets_mode', 'Custom');
+ cy.get('#max_buckets').should('exist').should('have.value', '1000');
+ cy.get('#max_buckets').click().clear().type(maxbuckets);
+
+ // Click the create button and wait for user to be made
+ cy.contains('button', 'Create User').click();
+ this.getFirstTableCell(tenant + '$' + user_id).should('exist');
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ edit(name: string, new_fullname: string, new_email: string, new_maxbuckets: string) {
+ this.navigateEdit(name);
+
+ // Change the full name field
+ cy.get('#display_name').click().clear().type(new_fullname);
+
+ // Change the email field
+ cy.get('#email').click().clear().type(new_email);
+
+ // Change the max buckets field
+ this.selectOption('max_buckets_mode', 'Custom');
+ cy.get('#max_buckets').click().clear().type(new_maxbuckets);
+
+ cy.contains('button', 'Edit User').click();
+
+ // Click the user and check its details table for updated content
+ this.getExpandCollapseElement(name).click();
+ cy.get('.datatable-row-detail')
+ .should('contain.text', new_fullname)
+ .and('contain.text', new_email)
+ .and('contain.text', new_maxbuckets);
+ }
+
+ invalidCreate() {
+ const tenant = '000invalid_tenant';
+ const uname = '000invalid_create_user';
+ // creating this user in order to check that you can't give two users the same name
+ this.navigateTo('create');
+ this.create(tenant, uname, 'xxx', 'xxx@xxx', '1');
+
+ this.navigateTo('create');
+
+ // Username
+ cy.get('#user_id')
+ // No username had been entered. Field should be invalid
+ .should('have.class', 'ng-invalid')
+ // Try to give user already taken name. Should make field invalid.
+ .type(uname);
+ cy.get('#show_tenant').click({ force: true });
+ cy.get('#tenant').type(tenant).should('have.class', 'ng-invalid');
+ cy.contains('#tenant + .invalid-feedback', 'The chosen user ID exists in this tenant.');
+
+ // check that username field is marked invalid if username has been cleared off
+ cy.get('#user_id').clear().blur().should('have.class', 'ng-invalid');
+ cy.contains('#user_id + .invalid-feedback', 'This field is required.');
+
+ // Full name
+ cy.get('#display_name')
+ // No display name has been given so field should be invalid
+ .should('have.class', 'ng-invalid')
+ // display name field should also be marked invalid if given input then emptied
+ .type('a')
+ .clear()
+ .blur()
+ .should('have.class', 'ng-invalid');
+ cy.contains('#display_name + .invalid-feedback', 'This field is required.');
+
+ // put invalid email to make field invalid
+ cy.get('#email').type('a').blur().should('have.class', 'ng-invalid');
+ cy.contains('#email + .invalid-feedback', 'This is not a valid email address.');
+
+ // put negative max buckets to make field invalid
+ this.expectSelectOption('max_buckets_mode', 'Custom');
+ cy.get('#max_buckets').clear().type('-5').blur().should('have.class', 'ng-invalid');
+ cy.contains('#max_buckets + .invalid-feedback', 'The entered value must be >= 1.');
+
+ this.navigateTo();
+ this.delete(tenant + '$' + uname);
+ }
+
+ invalidEdit() {
+ const tenant = '000invalid_tenant';
+ const uname = '000invalid_edit_user';
+ // creating this user to edit for the test
+ this.navigateTo('create');
+ this.create(tenant, uname, 'xxx', 'xxx@xxx', '50');
+ const name = tenant + '$' + uname;
+ this.navigateEdit(name);
+
+ // put invalid email to make field invalid
+ cy.get('#email')
+ .clear()
+ .type('a')
+ .blur()
+ .should('not.have.class', 'ng-pending')
+ .should('have.class', 'ng-invalid');
+ cy.contains('#email + .invalid-feedback', 'This is not a valid email address.');
+
+ // empty the display name field making it invalid
+ cy.get('#display_name').clear().blur().should('have.class', 'ng-invalid');
+ cy.contains('#display_name + .invalid-feedback', 'This field is required.');
+
+ // put negative max buckets to make field invalid
+ this.selectOption('max_buckets_mode', 'Disabled');
+ cy.get('#max_buckets').should('not.exist');
+ this.selectOption('max_buckets_mode', 'Custom');
+ cy.get('#max_buckets').should('exist').should('have.value', '50');
+ cy.get('#max_buckets').clear().type('-5').blur().should('have.class', 'ng-invalid');
+ cy.contains('#max_buckets + .invalid-feedback', 'The entered value must be >= 1.');
+
+ this.navigateTo();
+ this.delete(tenant + '$' + uname);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.e2e-spec.ts
new file mode 100644
index 000000000..388e3dd8c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.e2e-spec.ts
@@ -0,0 +1,14 @@
+import { ApiDocsPageHelper } from '../ui/api-docs.po';
+
+describe('Api Docs Page', () => {
+ const apiDocs = new ApiDocsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ apiDocs.navigateTo();
+ });
+
+ it('should show the API Docs description', () => {
+ cy.get('.renderedMarkdown').first().contains('This is the official Ceph REST API');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.po.ts
new file mode 100644
index 000000000..c7a8d222d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/api-docs.po.ts
@@ -0,0 +1,5 @@
+import { PageHelper } from '../page-helper.po';
+
+export class ApiDocsPageHelper extends PageHelper {
+ pages = { index: { url: '#/api-docs', id: 'cd-api-docs' } };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.e2e-spec.ts
new file mode 100644
index 000000000..3815011a1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.e2e-spec.ts
@@ -0,0 +1,49 @@
+import { ManagerModulesPageHelper } from '../cluster/mgr-modules.po';
+import { DashboardV3PageHelper } from './dashboard-v3.po';
+
+describe('Dashboard-v3 Main Page', () => {
+ const dashboard = new DashboardV3PageHelper();
+ const mgrmodules = new ManagerModulesPageHelper();
+
+ before(() => {
+ cy.login();
+ mgrmodules.navigateTo();
+ mgrmodules.navigateEdit('dashboard');
+ cy.get('#FEATURE_TOGGLE_DASHBOARD').check();
+ cy.contains('button', 'Update').click();
+ });
+
+ beforeEach(() => {
+ cy.login();
+ dashboard.navigateTo();
+ });
+
+ describe('Check that all hyperlinks on inventory card lead to the correct page and fields exist', () => {
+ it('should ensure that all linked pages in the inventory card lead to correct page', () => {
+ const expectationMap = {
+ Host: 'Hosts',
+ Monitor: 'Monitors',
+ OSDs: 'OSDs',
+ Pool: 'Pools',
+ 'Object Gateway': 'Gateways'
+ };
+
+ for (const [linkText, breadcrumbText] of Object.entries(expectationMap)) {
+ cy.location('hash').should('eq', '#/dashboard');
+ dashboard.clickInventoryCardLink(linkText);
+ dashboard.expectBreadcrumbText(breadcrumbText);
+ dashboard.navigateBack();
+ }
+ });
+
+ it('should verify that cards exist on dashboard in proper order', () => {
+ // Ensures that cards are all displayed on the dashboard tab while being in the proper
+ // order, checks for card title and position via indexing into a list of all cards.
+ const order = ['Details', 'Inventory', 'Status', 'Capacity', 'Cluster Utilization'];
+
+ for (let i = 0; i < order.length; i++) {
+ dashboard.card(i).should('contain.text', order[i]);
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.po.ts
new file mode 100644
index 000000000..597d2db9b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard-v3.po.ts
@@ -0,0 +1,20 @@
+import { PageHelper } from '../page-helper.po';
+
+export class DashboardV3PageHelper extends PageHelper {
+ pages = { index: { url: '#/dashboard', id: 'cd-dashboard-v3' } };
+
+ cardTitle(index: number) {
+ return cy.get('.card-title').its(index).text();
+ }
+
+ clickInventoryCardLink(link: string) {
+ console.log(link);
+ cy.get(`cd-card[cardTitle="Inventory"]`).contains('a', link).click();
+ }
+
+ card(indexOrTitle: number) {
+ cy.get('cd-card').as('cards');
+
+ return cy.get('@cards').its(indexOrTitle);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.e2e-spec.ts
new file mode 100644
index 000000000..ef719c9fd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.e2e-spec.ts
@@ -0,0 +1,141 @@
+import { IscsiPageHelper } from '../block/iscsi.po';
+import { HostsPageHelper } from '../cluster/hosts.po';
+import { ManagerModulesPageHelper } from '../cluster/mgr-modules.po';
+import { MonitorsPageHelper } from '../cluster/monitors.po';
+import { OSDsPageHelper } from '../cluster/osds.po';
+import { PageHelper } from '../page-helper.po';
+import { PoolPageHelper } from '../pools/pools.po';
+import { DaemonsPageHelper } from '../rgw/daemons.po';
+import { DashboardPageHelper } from './dashboard.po';
+
+describe('Dashboard Main Page', () => {
+ const dashboard = new DashboardPageHelper();
+ const daemons = new DaemonsPageHelper();
+ const hosts = new HostsPageHelper();
+ const osds = new OSDsPageHelper();
+ const pools = new PoolPageHelper();
+ const monitors = new MonitorsPageHelper();
+ const iscsi = new IscsiPageHelper();
+ const mgrmodules = new ManagerModulesPageHelper();
+
+ before(() => {
+ cy.login();
+ mgrmodules.navigateTo();
+ mgrmodules.navigateEdit('dashboard');
+ cy.get('#FEATURE_TOGGLE_DASHBOARD').uncheck();
+ cy.contains('button', 'Update').click();
+ });
+
+ beforeEach(() => {
+ cy.login();
+ dashboard.navigateTo();
+ });
+
+ describe('Check that all hyperlinks on info cards lead to the correct page and fields exist', () => {
+ it('should ensure that all linked info cards lead to correct page', () => {
+ const expectationMap = {
+ Monitors: 'Monitors',
+ OSDs: 'OSDs',
+ Hosts: 'Hosts',
+ 'Object Gateways': 'Gateways',
+ 'iSCSI Gateways': 'Overview',
+ Pools: 'Pools'
+ };
+
+ for (const [linkText, breadcrumbText] of Object.entries(expectationMap)) {
+ cy.location('hash').should('eq', '#/dashboard');
+ dashboard.clickInfoCardLink(linkText);
+ dashboard.expectBreadcrumbText(breadcrumbText);
+ dashboard.navigateBack();
+ }
+ });
+
+ it('should verify that info cards exist on dashboard in proper order', () => {
+ // Ensures that info cards are all displayed on the dashboard tab while being in the proper
+ // order, checks for card title and position via indexing into a list of all info cards.
+ const order = [
+ 'Cluster Status',
+ 'Hosts',
+ 'Monitors',
+ 'OSDs',
+ 'Managers',
+ 'Object Gateways',
+ 'Metadata Servers',
+ 'iSCSI Gateways',
+ 'Raw Capacity',
+ 'Objects',
+ 'PG Status',
+ 'Pools',
+ 'PGs per OSD',
+ 'Client Read/Write',
+ 'Client Throughput',
+ 'Recovery Throughput',
+ 'Scrubbing'
+ ];
+
+ for (let i = 0; i < order.length; i++) {
+ dashboard.infoCard(i).should('contain.text', order[i]);
+ }
+ });
+
+ it('should verify that info card group titles are present and in the right order', () => {
+ cy.location('hash').should('eq', '#/dashboard');
+ dashboard.infoGroupTitle(0).should('eq', 'Status');
+ dashboard.infoGroupTitle(1).should('eq', 'Capacity');
+ dashboard.infoGroupTitle(2).should('eq', 'Performance');
+ });
+ });
+
+ it('Should check that dashboard cards have correct information', () => {
+ interface TestSpec {
+ cardName: string;
+ regexMatcher?: RegExp;
+ pageObject: PageHelper;
+ }
+ const testSpecs: TestSpec[] = [
+ { cardName: 'Object Gateways', regexMatcher: /(\d+)\s+total/, pageObject: daemons },
+ { cardName: 'Monitors', regexMatcher: /(\d+)\s+\(quorum/, pageObject: monitors },
+ { cardName: 'Hosts', regexMatcher: /(\d+)\s+total/, pageObject: hosts },
+ { cardName: 'OSDs', regexMatcher: /(\d+)\s+total/, pageObject: osds },
+ { cardName: 'Pools', pageObject: pools },
+ { cardName: 'iSCSI Gateways', regexMatcher: /(\d+)\s+total/, pageObject: iscsi }
+ ];
+ for (let i = 0; i < testSpecs.length; i++) {
+ const spec = testSpecs[i];
+ dashboard.navigateTo();
+
+ dashboard.infoCardBodyText(spec.cardName).then((infoCardBodyText: string) => {
+ let dashCount = 0;
+
+ if (spec.regexMatcher) {
+ const match = infoCardBodyText.match(new RegExp(spec.regexMatcher));
+ expect(match).to.length.gt(
+ 1,
+ `Regex ${spec.regexMatcher} did not find a match for card with name ` +
+ `${spec.cardName}`
+ );
+ dashCount = Number(match[1]);
+ } else {
+ dashCount = Number(infoCardBodyText);
+ }
+
+ spec.pageObject.navigateTo();
+ spec.pageObject.getTableCount('total').then((tableCount) => {
+ expect(tableCount).to.eq(
+ dashCount,
+ `Text of card "${spec.cardName}" and regex "${spec.regexMatcher}" resulted in ${dashCount} ` +
+ `but did not match table count ${tableCount}`
+ );
+ });
+ });
+ }
+ });
+
+ after(() => {
+ cy.login();
+ mgrmodules.navigateTo();
+ mgrmodules.navigateEdit('dashboard');
+ cy.get('#FEATURE_TOGGLE_DASHBOARD').click();
+ cy.contains('button', 'Update').click();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.po.ts
new file mode 100644
index 000000000..42d63ef44
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/dashboard.po.ts
@@ -0,0 +1,31 @@
+import { PageHelper } from '../page-helper.po';
+
+export class DashboardPageHelper extends PageHelper {
+ pages = { index: { url: '#/dashboard', id: 'cd-dashboard' } };
+
+ infoGroupTitle(index: number) {
+ return cy.get('.info-group-title').its(index).text();
+ }
+
+ clickInfoCardLink(cardName: string) {
+ cy.get(`cd-info-card[cardtitle="${cardName}"]`).contains('a', cardName).click();
+ }
+
+ infoCard(indexOrTitle: number | string) {
+ cy.get('cd-info-card').as('infoCards');
+
+ if (typeof indexOrTitle === 'number') {
+ return cy.get('@infoCards').its(indexOrTitle);
+ } else {
+ return cy.contains('cd-info-card a', indexOrTitle).parent().parent().parent().parent();
+ }
+ }
+
+ infoCardBodyText(infoCard: string) {
+ return this.infoCard(infoCard).find('.card-text').text();
+ }
+
+ infoCardBody(infoCard: string) {
+ return this.infoCard(infoCard).find('.card-text');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.e2e-spec.ts
new file mode 100644
index 000000000..fa20f0be5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.e2e-spec.ts
@@ -0,0 +1,19 @@
+import { LanguagePageHelper } from './language.po';
+
+describe('Shared pages', () => {
+ const language = new LanguagePageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ language.navigateTo();
+ });
+
+ it('should check default language', () => {
+ language.getLanguageBtn().should('contain.text', 'English');
+ });
+
+ it('should check all available languages', () => {
+ language.getLanguageBtn().click();
+ language.getAllLanguages().should('have.length', 1).should('contain.text', 'English');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.po.ts
new file mode 100644
index 000000000..80e21ba1e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/language.po.ts
@@ -0,0 +1,15 @@
+import { PageHelper } from '../page-helper.po';
+
+export class LanguagePageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/dashboard', id: 'cd-dashboard' }
+ };
+
+ getLanguageBtn() {
+ return cy.get('cd-language-selector a').first();
+ }
+
+ getAllLanguages() {
+ return cy.get('cd-language-selector button');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.e2e-spec.ts
new file mode 100644
index 000000000..2b337e634
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.e2e-spec.ts
@@ -0,0 +1,23 @@
+import { LoginPageHelper } from './login.po';
+
+describe('Login page', () => {
+ const login = new LoginPageHelper();
+
+ it('should login and navigate to dashboard page', () => {
+ login.navigateTo();
+ login.doLogin();
+ });
+
+ it('should logout when clicking the button', () => {
+ login.navigateTo();
+ login.doLogin();
+
+ login.doLogout();
+ });
+
+ it('should have no accessibility violations', () => {
+ login.navigateTo();
+ cy.injectAxe();
+ cy.checkA11y();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.po.ts
new file mode 100644
index 000000000..d4d2c6921
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/login.po.ts
@@ -0,0 +1,22 @@
+import { PageHelper } from '../page-helper.po';
+
+export class LoginPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/login', id: 'cd-login' },
+ dashboard: { url: '#/dashboard', id: 'cd-dashboard' }
+ };
+
+ doLogin() {
+ cy.get('[name=username]').type('admin');
+ cy.get('#password').type('admin');
+ cy.get('[type=submit]').click();
+ cy.get('cd-dashboard').should('exist');
+ }
+
+ doLogout() {
+ cy.get('cd-identity a').click();
+ cy.contains('cd-identity span', 'Sign out').click();
+ cy.get('cd-login').should('exist');
+ cy.location('hash').should('eq', '#/login');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.e2e-spec.ts
new file mode 100644
index 000000000..1625dab4f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.e2e-spec.ts
@@ -0,0 +1,23 @@
+import { NavigationPageHelper } from './navigation.po';
+
+describe('Shared pages', () => {
+ const shared = new NavigationPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ shared.navigateTo();
+ });
+
+ it('should display the vertical menu by default', () => {
+ shared.getVerticalMenu().should('not.have.class', 'active');
+ });
+
+ it('should hide the vertical menu', () => {
+ shared.getMenuToggler().click();
+ shared.getVerticalMenu().should('have.class', 'active');
+ });
+
+ it('should navigate to the correct page', () => {
+ shared.checkNavigations(shared.navigations);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts
new file mode 100644
index 000000000..f797bbc26
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts
@@ -0,0 +1,78 @@
+import { PageHelper } from '../page-helper.po';
+
+export class NavigationPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/dashboard', id: 'cd-dashboard' }
+ };
+
+ navigations = [
+ { menu: 'NFS', component: 'cd-error' },
+ {
+ menu: 'Object Gateway',
+ submenus: [
+ { menu: 'Gateways', component: 'cd-rgw-daemon-list' },
+ { menu: 'Users', component: 'cd-rgw-user-list' },
+ { menu: 'Buckets', component: 'cd-rgw-bucket-list' }
+ ]
+ },
+ { menu: 'Dashboard', component: 'cd-dashboard' },
+ {
+ menu: 'Cluster',
+ submenus: [
+ { menu: 'Hosts', component: 'cd-hosts' },
+ { menu: 'Physical Disks', component: 'cd-error' },
+ { menu: 'Monitors', component: 'cd-monitor' },
+ { menu: 'Services', component: 'cd-error' },
+ { menu: 'OSDs', component: 'cd-osd-list' },
+ { menu: 'Configuration', component: 'cd-configuration' },
+ { menu: 'CRUSH map', component: 'cd-crushmap' },
+ { menu: 'Manager Modules', component: 'cd-mgr-module-list' },
+ { menu: 'Ceph Users', component: 'cd-crud-table' },
+ { menu: 'Logs', component: 'cd-logs' },
+ { menu: 'Alerts', component: 'cd-prometheus-tabs' }
+ ]
+ },
+ { menu: 'Pools', component: 'cd-pool-list' },
+ {
+ menu: 'Block',
+ submenus: [
+ { menu: 'Images', component: 'cd-error' },
+ { menu: 'Mirroring', component: 'cd-mirroring' },
+ { menu: 'iSCSI', component: 'cd-iscsi' }
+ ]
+ },
+ { menu: 'File Systems', component: 'cd-cephfs-list' }
+ ];
+
+ getVerticalMenu() {
+ return cy.get('nav[id=sidebar]');
+ }
+
+ getMenuToggler() {
+ return cy.get('[aria-label="toggle sidebar visibility"]');
+ }
+
+ checkNavigations(navs: any) {
+ // The nfs-ganesha, RGW, and block/rbd status requests are mocked to ensure that this method runs in time
+ cy.intercept('/ui-api/nfs-ganesha/status', { fixture: 'nfs-ganesha-status.json' });
+ cy.intercept('/ui-api/rgw/status', { fixture: 'rgw-status.json' });
+ cy.intercept('/ui-api/block/rbd/status', { fixture: 'block-rbd-status.json' });
+
+ navs.forEach((nav: any) => {
+ cy.contains('.simplebar-content li.nav-item a', nav.menu).click();
+ if (nav.submenus) {
+ this.checkNavSubMenu(nav.menu, nav.submenus);
+ } else {
+ cy.get(nav.component).should('exist');
+ }
+ });
+ }
+
+ checkNavSubMenu(menu: any, submenu: any) {
+ submenu.forEach((nav: any) => {
+ cy.contains('.simplebar-content li.nav-item', menu).within(() => {
+ cy.contains(`ul.list-unstyled li a`, nav.menu).click();
+ });
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.e2e-spec.ts
new file mode 100644
index 000000000..0a25d7e86
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.e2e-spec.ts
@@ -0,0 +1,56 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { NotificationSidebarPageHelper } from './notification.po';
+
+describe('Notification page', () => {
+ const notification = new NotificationSidebarPageHelper();
+ const pools = new PoolPageHelper();
+ const poolName = 'e2e_notification_pool';
+
+ before(() => {
+ cy.login();
+ pools.navigateTo('create');
+ pools.create(poolName, 8);
+ pools.edit_pool_pg(poolName, 4, false);
+ });
+
+ after(() => {
+ cy.login();
+ pools.navigateTo();
+ pools.delete(poolName);
+ });
+
+ beforeEach(() => {
+ cy.login();
+ pools.navigateTo();
+ });
+
+ it('should open notification sidebar', () => {
+ notification.getSidebar().should('not.be.visible');
+ notification.open();
+ notification.getSidebar().should('be.visible');
+ });
+
+ it('should display a running task', () => {
+ notification.getToast().should('not.exist');
+
+ // Check that running task is shown.
+ notification.open();
+ notification.getTasks().contains(poolName).should('exist');
+
+ // Delete pool after task is complete (otherwise we get an error).
+ notification.getTasks().should('not.exist');
+ });
+
+ it('should have notifications', () => {
+ notification.open();
+ notification.getNotifications().should('have.length.gt', 0);
+ });
+
+ it('should clear notifications', () => {
+ notification.getToast().should('not.exist');
+ notification.open();
+ notification.getNotifications().should('have.length.gt', 0);
+ notification.getClearNotficationsBtn().should('be.visible');
+ notification.clearNotifications();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.po.ts
new file mode 100644
index 000000000..12c424e35
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/notification.po.ts
@@ -0,0 +1,45 @@
+import { PageHelper } from '../page-helper.po';
+
+export class NotificationSidebarPageHelper extends PageHelper {
+ getNotificatinoIcon() {
+ return cy.get('cd-notifications a');
+ }
+
+ getSidebar() {
+ return cy.get('cd-notifications-sidebar');
+ }
+
+ getTasks() {
+ return this.getSidebar().find('.card.tc_task');
+ }
+
+ getNotifications() {
+ return this.getSidebar().find('.card.tc_notification');
+ }
+
+ getClearNotficationsBtn() {
+ return this.getSidebar().find('button.btn-block');
+ }
+
+ getCloseBtn() {
+ return this.getSidebar().find('button.close');
+ }
+
+ open() {
+ this.getNotificatinoIcon().click();
+ this.getSidebar().should('be.visible');
+ }
+
+ clearNotifications() {
+ // It can happen that although notifications are cleared, by the time we check the notifications
+ // amount, another notification can appear, so we check it more than once (if needed).
+ this.getClearNotficationsBtn().click();
+ this.getNotifications()
+ .should('have.length.gte', 0)
+ .then(($elems) => {
+ if ($elems.length > 0) {
+ this.clearNotifications();
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.e2e-spec.ts
new file mode 100644
index 000000000..7e76f168e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.e2e-spec.ts
@@ -0,0 +1,36 @@
+import { RoleMgmtPageHelper } from './role-mgmt.po';
+
+describe('Role Management page', () => {
+ const roleMgmt = new RoleMgmtPageHelper();
+ const role_name = 'e2e_role_mgmt_role';
+
+ beforeEach(() => {
+ cy.login();
+ roleMgmt.navigateTo();
+ });
+
+ describe('breadcrumb tests', () => {
+ it('should check breadcrumb on roles tab on user management page', () => {
+ roleMgmt.expectBreadcrumbText('Roles');
+ });
+
+ it('should check breadcrumb on role creation page', () => {
+ roleMgmt.navigateTo('create');
+ roleMgmt.expectBreadcrumbText('Create');
+ });
+ });
+
+ describe('role create, edit & delete test', () => {
+ it('should create a role', () => {
+ roleMgmt.create(role_name, 'An interesting description');
+ });
+
+ it('should edit a role', () => {
+ roleMgmt.edit(role_name, 'A far more interesting description');
+ });
+
+ it('should delete a role', () => {
+ roleMgmt.delete(role_name);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.po.ts
new file mode 100644
index 000000000..1cc3630a4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/role-mgmt.po.ts
@@ -0,0 +1,40 @@
+import { PageHelper } from '../page-helper.po';
+
+export class RoleMgmtPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/user-management/roles', id: 'cd-role-list' },
+ create: { url: '#/user-management/roles/create', id: 'cd-role-form' }
+ };
+
+ create(name: string, description: string) {
+ this.navigateTo('create');
+ // Waits for data to load
+ cy.contains('grafana');
+
+ // fill in fields
+ cy.get('#name').type(name);
+ cy.get('#description').type(description);
+
+ // Click the create button and wait for role to be made
+ cy.get('[data-cy=submitBtn]').click();
+ cy.get('.breadcrumb-item.active').should('not.have.text', 'Create');
+
+ this.getFirstTableCell(name).should('exist');
+ }
+
+ edit(name: string, description: string) {
+ this.navigateEdit(name);
+ // Waits for data to load
+ cy.contains('grafana');
+
+ // fill in fields with new values
+ cy.get('#description').clear().type(description);
+
+ // Click the edit button and check new values are present in table
+ cy.get('[data-cy=submitBtn]').click();
+ cy.get('.breadcrumb-item.active').should('not.have.text', 'Edit');
+
+ this.getFirstTableCell(name).should('exist');
+ this.getFirstTableCell(description).should('exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.e2e-spec.ts
new file mode 100644
index 000000000..57818db0a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.e2e-spec.ts
@@ -0,0 +1,36 @@
+import { UserMgmtPageHelper } from './user-mgmt.po';
+
+describe('User Management page', () => {
+ const userMgmt = new UserMgmtPageHelper();
+ const user_name = 'e2e_user_mgmt_user';
+
+ beforeEach(() => {
+ cy.login();
+ userMgmt.navigateTo();
+ });
+
+ describe('breadcrumb tests', () => {
+ it('should check breadcrumb on users tab of user management page', () => {
+ userMgmt.expectBreadcrumbText('Users');
+ });
+
+ it('should check breadcrumb on user creation page', () => {
+ userMgmt.navigateTo('create');
+ userMgmt.expectBreadcrumbText('Create');
+ });
+ });
+
+ describe('user create, edit & delete test', () => {
+ it('should create a user', () => {
+ userMgmt.create(user_name, 'cool_password', 'Jeff', 'realemail@realwebsite.com');
+ });
+
+ it('should edit a user', () => {
+ userMgmt.edit(user_name, 'cool_password_number_2', 'Geoff', 'w@m');
+ });
+
+ it('should delete a user', () => {
+ userMgmt.delete(user_name);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.po.ts
new file mode 100644
index 000000000..fb2b79129
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/user-mgmt.po.ts
@@ -0,0 +1,39 @@
+import { PageHelper } from '../page-helper.po';
+
+export class UserMgmtPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/user-management/users', id: 'cd-user-list' },
+ create: { url: '#/user-management/users/create', id: 'cd-user-form' }
+ };
+
+ create(username: string, password: string, name: string, email: string) {
+ this.navigateTo('create');
+
+ // fill in fields
+ cy.get('#username').type(username);
+ cy.get('#password').type(password);
+ cy.get('#confirmpassword').type(password);
+ cy.get('#name').type(name);
+ cy.get('#email').type(email);
+
+ // Click the create button and wait for user to be made
+ cy.get('[data-cy=submitBtn]').click();
+ this.getFirstTableCell(username).should('exist');
+ }
+
+ edit(username: string, password: string, name: string, email: string) {
+ this.navigateEdit(username);
+
+ // fill in fields with new values
+ cy.get('#password').clear().type(password);
+ cy.get('#confirmpassword').clear().type(password);
+ cy.get('#name').clear().type(name);
+ cy.get('#email').clear().type(email);
+
+ // Click the edit button and check new values are present in table
+ const editButton = cy.get('[data-cy=submitBtn]');
+ editButton.click();
+ this.getFirstTableCell(email).should('exist');
+ this.getFirstTableCell(name).should('exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/dashboard.vrt-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/dashboard.vrt-spec.ts
new file mode 100644
index 000000000..450cff871
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/dashboard.vrt-spec.ts
@@ -0,0 +1,24 @@
+import { LoginPageHelper } from '../ui/login.po';
+
+describe('Dashboard Landing Page', () => {
+ const login = new LoginPageHelper();
+
+ beforeEach(() => {
+ cy.eyesOpen({
+ testName: 'Dashboard Component'
+ });
+ });
+
+ afterEach(() => {
+ cy.eyesClose();
+ });
+
+ it('should take screenshot of dashboard landing page', () => {
+ login.navigateTo();
+ login.doLogin();
+ cy.get('[aria-label="Status card"]').should('be.visible');
+ cy.get('[aria-label="Inventory card"]').should('be.visible');
+ cy.get('[aria-label="Cluster utilization card"]').should('be.visible');
+ cy.eyesCheckWindow({ tag: 'Dashboard landing page' });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/login.vrt-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/login.vrt-spec.ts
new file mode 100644
index 000000000..ea74f1d0f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/visualTests/login.vrt-spec.ts
@@ -0,0 +1,19 @@
+describe('Login Page', () => {
+ beforeEach(() => {
+ cy.visit('#/login');
+ cy.eyesOpen({
+ appName: 'Ceph',
+ testName: 'Login Component Check'
+ });
+ });
+
+ afterEach(() => {
+ cy.eyesClose();
+ });
+
+ it('types login credentials and takes screenshot', () => {
+ cy.get('[name=username]').type('admin');
+ cy.get('#password').type('admin');
+ cy.eyesCheckWindow({ tag: 'Login Screen with credentials typed' });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/fixtures/block-rbd-status.json b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/block-rbd-status.json
new file mode 100644
index 000000000..1d6f30b9a
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/block-rbd-status.json
@@ -0,0 +1 @@
+{ "available": false, "message": "No RBD pools in the cluster. Please create a pool with the \"rbd\" application label." } \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/fixtures/nfs-ganesha-status.json b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/nfs-ganesha-status.json
new file mode 100644
index 000000000..4dbbaaccc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/nfs-ganesha-status.json
@@ -0,0 +1,4 @@
+{
+ "available": false,
+ "message": "Ganesha config location is not configured. Please set the GANESHA_RADOS_POOL_NAMESPACE setting."
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/inventory.json b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/inventory.json
new file mode 100644
index 000000000..21386f2d5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/inventory.json
@@ -0,0 +1,390 @@
+[
+ {
+ "addr": "node1",
+ "devices": [
+ {
+ "available": false,
+ "device_id": "",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vda",
+ "rejected_reasons": ["locked"],
+ "sys_api": {
+ "human_readable_size": "42.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {
+ "vda1": {
+ "holders": [],
+ "human_readable_size": "2.00 MB",
+ "sectors": "4096",
+ "sectorsize": 512,
+ "size": 2097152.0,
+ "start": "2048"
+ },
+ "vda2": {
+ "holders": [],
+ "human_readable_size": "20.00 MB",
+ "sectors": "40960",
+ "sectorsize": 512,
+ "size": 20971520.0,
+ "start": "6144"
+ },
+ "vda3": {
+ "holders": [],
+ "human_readable_size": "41.98 GB",
+ "sectors": "88033247",
+ "sectorsize": 512,
+ "size": 45073022464.0,
+ "start": "47104"
+ }
+ },
+ "path": "/dev/vda",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 45097156608.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": false,
+ "device_id": "641526",
+ "human_readable_type": "hdd",
+ "lvs": [
+ {
+ "block_uuid": "355c2I-e5kg-WWeT-bOsI-0Ez5-sfb7-7TZyE4",
+ "cluster_fsid": "68a32428-e2ab-11ea-9d25-525400ef4c6e",
+ "cluster_name": "ceph",
+ "name": "osd-data-3de18e23-8849-494c-83b0-458d97d32d72",
+ "osd_fsid": "a438ac13-f1bd-412c-9626-e2f063dbbf94",
+ "osd_id": "0",
+ "osdspec_affinity": "dashboard-admin-1597903910143",
+ "type": "block"
+ }
+ ],
+ "path": "/dev/vdb",
+ "rejected_reasons": ["locked", "LVM detected", "Insufficient space (<5GB) on vgs"],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdb",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": false,
+ "device_id": "467047",
+ "human_readable_type": "hdd",
+ "lvs": [
+ {
+ "block_uuid": "iGC2VU-MSTt-ZP05-kKCP-5EtO-F1Y3-DYAAeb",
+ "cluster_fsid": "68a32428-e2ab-11ea-9d25-525400ef4c6e",
+ "cluster_name": "ceph",
+ "name": "osd-data-2031893c-c83b-4ff0-bfa1-de548044f707",
+ "osd_fsid": "6f544fc4-a3ea-40f9-9c48-69b5ee866709",
+ "osd_id": "1",
+ "osdspec_affinity": "dashboard-admin-1597903910143",
+ "type": "block"
+ }
+ ],
+ "path": "/dev/vdc",
+ "rejected_reasons": ["locked", "LVM detected", "Insufficient space (<5GB) on vgs"],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdc",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": false,
+ "device_id": "900807",
+ "human_readable_type": "hdd",
+ "lvs": [
+ {
+ "block_uuid": "nO2VSn-IbXr-pxnx-ieXx-kIxk-B4hB-BM6ADc",
+ "cluster_fsid": "68a32428-e2ab-11ea-9d25-525400ef4c6e",
+ "cluster_name": "ceph",
+ "name": "osd-data-537f7b60-5887-440e-80c7-759c028db12d",
+ "osd_fsid": "adeddd37-5cc9-406a-88e5-2add3f81d089",
+ "osd_id": "2",
+ "osdspec_affinity": "dashboard-admin-1597903910143",
+ "type": "block"
+ }
+ ],
+ "path": "/dev/vdd",
+ "rejected_reasons": ["locked", "LVM detected", "Insufficient space (<5GB) on vgs"],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdd",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": false,
+ "device_id": "757404",
+ "human_readable_type": "hdd",
+ "lvs": [
+ {
+ "block_uuid": "3YSAlw-VMeK-XfUK-rbOB-IKD1-Z9ZI-hUzlDe",
+ "cluster_fsid": "68a32428-e2ab-11ea-9d25-525400ef4c6e",
+ "cluster_name": "ceph",
+ "name": "osd-data-15b39d59-f259-4e93-adc6-bdac7d490d88",
+ "osd_fsid": "840a7138-88e2-4ecb-b88d-6fa2d04d88e7",
+ "osd_id": "3",
+ "osdspec_affinity": "dashboard-admin-1597903910143",
+ "type": "block"
+ }
+ ],
+ "path": "/dev/vde",
+ "rejected_reasons": ["locked", "LVM detected", "Insufficient space (<5GB) on vgs"],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vde",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ }
+ ],
+ "labels": [],
+ "name": "node1"
+ },
+ {
+ "addr": "node2",
+ "devices": [
+ {
+ "available": true,
+ "device_id": "115432",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vdb",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdb",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": true,
+ "device_id": "937699",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vdc",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdc",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": true,
+ "device_id": "854127",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vdd",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vdd",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": true,
+ "device_id": "122615",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vde",
+ "rejected_reasons": [],
+ "sys_api": {
+ "human_readable_size": "8.00 GB",
+ "locked": 0,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {},
+ "path": "/dev/vde",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 8589934592.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ },
+ {
+ "available": false,
+ "device_id": "",
+ "human_readable_type": "hdd",
+ "lvs": [],
+ "path": "/dev/vda",
+ "rejected_reasons": ["locked"],
+ "sys_api": {
+ "human_readable_size": "42.00 GB",
+ "locked": 1,
+ "model": "",
+ "nr_requests": "256",
+ "partitions": {
+ "vda1": {
+ "holders": [],
+ "human_readable_size": "2.00 MB",
+ "sectors": "4096",
+ "sectorsize": 512,
+ "size": 2097152.0,
+ "start": "2048"
+ },
+ "vda2": {
+ "holders": [],
+ "human_readable_size": "20.00 MB",
+ "sectors": "40960",
+ "sectorsize": 512,
+ "size": 20971520.0,
+ "start": "6144"
+ },
+ "vda3": {
+ "holders": [],
+ "human_readable_size": "41.98 GB",
+ "sectors": "88033247",
+ "sectorsize": 512,
+ "size": 45073022464.0,
+ "start": "47104"
+ }
+ },
+ "path": "/dev/vda",
+ "removable": "0",
+ "rev": "",
+ "ro": "0",
+ "rotational": "1",
+ "sas_address": "",
+ "sas_device_handle": "",
+ "scheduler_mode": "mq-deadline",
+ "sectors": 0,
+ "sectorsize": "512",
+ "size": 45097156608.0,
+ "support_discard": "512",
+ "vendor": "0x1af4"
+ }
+ }
+ ],
+ "labels": [],
+ "name": "node2"
+ }
+]
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/services.json b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/services.json
new file mode 100644
index 000000000..433da1fb3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/services.json
@@ -0,0 +1,523 @@
+[
+ {
+ "container_id": "9fa324d32bc8",
+ "container_image_digests": [
+ "docker.io/prom/alertmanager@sha256:7e4e9f7a0954b45736d149c40e9620a6664036bb05f0dce447bef5042b139f5d",
+ "docker.io/prom/alertmanager@sha256:b9323917a2eda265bec69e59a457f001c529facbbc8166df277f4850cdac61a0"
+ ],
+ "container_image_id": "0881eb8f169f5556a292b4e2c01d683172b12830a62a9225a98a8e206bb734f0",
+ "container_image_name": "docker.io/prom/alertmanager:v0.20.0",
+ "created": "2021-04-04T14:20:55.872521Z",
+ "daemon_id": "ceph-node-00",
+ "daemon_type": "alertmanager",
+ "events": [
+ "2021-04-04T14:20:55.970128Z daemon:alertmanager.ceph-node-00 [INFO] \"Deployed alertmanager.ceph-node-00 on host 'ceph-node-00.cephlab.com'\"",
+ "2021-04-04T14:25:37.637716Z daemon:alertmanager.ceph-node-00 [INFO] \"Reconfigured alertmanager.ceph-node-00 on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": true,
+ "last_refresh": "2021-04-04T14:27:38.610198Z",
+ "memory_usage": 10471079,
+ "ports": [
+ 9093,
+ 9094
+ ],
+ "started": "2021-04-04T14:25:36.837872Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "0.20.0"
+ },
+ {
+ "container_id": "44add59a53bc",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:21:00.330646Z",
+ "daemon_id": "ceph-node-00",
+ "daemon_type": "crash",
+ "events": [
+ "2021-04-04T14:21:00.456022Z daemon:crash.ceph-node-00 [INFO] \"Deployed crash.ceph-node-00 on host 'ceph-node-00.cephlab.com'\"",
+ "2021-04-04T14:25:41.234986Z daemon:crash.ceph-node-00 [INFO] \"Reconfigured crash.ceph-node-00 on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:38.610356Z",
+ "memory_usage": 7190085,
+ "ports": [],
+ "started": "2021-04-04T14:20:59.550334Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "4a2180e2e4ae",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:24:23.552501Z",
+ "daemon_id": "ceph-node-01",
+ "daemon_type": "crash",
+ "events": [
+ "2021-04-04T14:24:23.591035Z daemon:crash.ceph-node-01 [INFO] \"Deployed crash.ceph-node-01 on host 'ceph-node-01.cephlab.com'\"",
+ "2021-04-04T14:25:42.677262Z daemon:crash.ceph-node-01 [INFO] \"Reconfigured crash.ceph-node-01 on host 'ceph-node-01.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-01.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:26:25.839645Z",
+ "memory_usage": 7147094,
+ "ports": [],
+ "started": "2021-04-04T14:24:23.188059Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "2eb2f0a13f46",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:24:21.012014Z",
+ "daemon_id": "ceph-node-02",
+ "daemon_type": "crash",
+ "events": [
+ "2021-04-04T14:24:21.047797Z daemon:crash.ceph-node-02 [INFO] \"Deployed crash.ceph-node-02 on host 'ceph-node-02.cephlab.com'\"",
+ "2021-04-04T14:25:43.974052Z daemon:crash.ceph-node-02 [INFO] \"Reconfigured crash.ceph-node-02 on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.470841Z",
+ "memory_usage": 8018460,
+ "ports": [],
+ "started": "2021-04-04T14:24:20.664558Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "10359b995638",
+ "container_image_digests": [
+ "docker.io/ceph/ceph-grafana@sha256:44f6f2bfa52724d4db9a2ce343b299ff70a18dc21f1420548d5643df4ee18a6b"
+ ],
+ "container_image_id": "80728b29ad3f603cb306daeb6b0fb6c4c388e29e7eaac82cd3d3582ffd96b931",
+ "container_image_name": "docker.io/ceph/ceph-grafana:6.7.4",
+ "created": "2021-04-04T14:21:41.602878Z",
+ "daemon_id": "ceph-node-00",
+ "daemon_type": "grafana",
+ "events": [
+ "2021-04-04T14:21:41.651390Z daemon:grafana.ceph-node-00 [INFO] \"Deployed grafana.ceph-node-00 on host 'ceph-node-00.cephlab.com'\"",
+ "2021-04-04T14:25:26.705257Z daemon:grafana.ceph-node-00 [INFO] \"Reconfigured grafana.ceph-node-00 on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": true,
+ "last_refresh": "2021-04-04T14:27:38.609816Z",
+ "memory_usage": 27797749,
+ "ports": [
+ 3000
+ ],
+ "started": "2021-04-04T14:25:26.020123Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "6.7.4"
+ },
+ {
+ "container_id": "04e86dfde3ae",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph:master",
+ "created": "2021-04-04T14:17:17.458301Z",
+ "daemon_id": "ceph-node-00.cephlab.com.qqwcpr",
+ "daemon_type": "mgr",
+ "events": [
+ "2021-04-04T14:25:24.076974Z daemon:mgr.ceph-node-00.cephlab.com.qqwcpr [ERROR] \"\"",
+ "2021-04-04T14:25:39.425312Z daemon:mgr.ceph-node-00.cephlab.com.qqwcpr [INFO] \"Reconfigured mgr.ceph-node-00.cephlab.com.qqwcpr on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "",
+ "is_active": true,
+ "last_refresh": "2021-04-04T14:20:21.353502Z",
+ "memory_usage": 411670937,
+ "ports": [
+ 9283
+ ],
+ "started": "2021-04-04T14:17:16.779682Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "04e86dfde3ae",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph:master",
+ "created": "2021-04-04T14:17:17.458301Z",
+ "daemon_id": "ceph-node-00.cephlab.com.qqwcpr",
+ "daemon_type": "mgr",
+ "events": [
+ "2021-04-04T14:25:24.076974Z daemon:mgr.ceph-node-00.cephlab.com.qqwcpr [ERROR] \"\"",
+ "2021-04-04T14:25:39.425312Z daemon:mgr.ceph-node-00.cephlab.com.qqwcpr [INFO] \"Reconfigured mgr.ceph-node-00.cephlab.com.qqwcpr on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": true,
+ "last_refresh": "2021-04-04T14:27:38.610265Z",
+ "memory_usage": 468608614,
+ "ports": [
+ 9283
+ ],
+ "started": "2021-04-04T14:17:16.779682Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "7bfba45507ab",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:24:25.445135Z",
+ "daemon_id": "ceph-node-02.mywsmi",
+ "daemon_type": "mgr",
+ "events": [
+ "2021-04-04T14:24:25.484361Z daemon:mgr.ceph-node-02.mywsmi [INFO] \"Deployed mgr.ceph-node-02.mywsmi on host 'ceph-node-02.cephlab.com'\"",
+ "2021-04-04T14:25:46.457476Z daemon:mgr.ceph-node-02.mywsmi [INFO] \"Reconfigured mgr.ceph-node-02.mywsmi on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.471837Z",
+ "memory_usage": 384617676,
+ "ports": [
+ 8443,
+ 9283
+ ],
+ "started": "2021-04-04T14:24:25.142998Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "6045be766e88",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph:master",
+ "created": "2021-04-04T14:17:07.904023Z",
+ "daemon_id": "ceph-node-00.cephlab.com",
+ "daemon_type": "mon",
+ "events": [
+ "2021-04-04T14:25:24.076865Z daemon:mon.ceph-node-00.cephlab.com [ERROR] \"\"",
+ "2021-04-04T14:25:28.250425Z daemon:mon.ceph-node-00.cephlab.com [INFO] \"Reconfigured mon.ceph-node-00.cephlab.com on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:20:21.353077Z",
+ "memory_usage": 35871784,
+ "ports": [],
+ "started": "2021-04-04T14:17:13.608122Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "6045be766e88",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph:master",
+ "created": "2021-04-04T14:17:07.904023Z",
+ "daemon_id": "ceph-node-00.cephlab.com",
+ "daemon_type": "mon",
+ "events": [
+ "2021-04-04T14:25:24.076865Z daemon:mon.ceph-node-00.cephlab.com [ERROR] \"\"",
+ "2021-04-04T14:25:28.250425Z daemon:mon.ceph-node-00.cephlab.com [INFO] \"Reconfigured mon.ceph-node-00.cephlab.com on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:38.609967Z",
+ "memory_usage": 74826383,
+ "ports": [],
+ "started": "2021-04-04T14:17:13.608122Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "d2d261f4eb17",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:24:28.269212Z",
+ "daemon_id": "ceph-node-02",
+ "daemon_type": "mon",
+ "events": [
+ "2021-04-04T14:24:28.314782Z daemon:mon.ceph-node-02 [INFO] \"Deployed mon.ceph-node-02 on host 'ceph-node-02.cephlab.com'\"",
+ "2021-04-04T14:25:45.448194Z daemon:mon.ceph-node-02 [INFO] \"Reconfigured mon.ceph-node-02 on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.471665Z",
+ "memory_usage": 65515028,
+ "ports": [],
+ "started": "2021-04-04T14:24:28.147109Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "51c04231de4c",
+ "container_image_digests": [
+ "docker.io/prom/node-exporter@sha256:a2f29256e53cc3e0b64d7a472512600b2e9410347d53cdc85b49f659c17e02ee",
+ "docker.io/prom/node-exporter@sha256:b630fb29d99b3483c73a2a7db5fc01a967392a3d7ad754c8eccf9f4a67e7ee31"
+ ],
+ "container_image_id": "e5a616e4b9cf68dfcad7782b78e118be4310022e874d52da85c55923fb615f87",
+ "container_image_name": "docker.io/prom/node-exporter:v0.18.1",
+ "created": "2021-04-04T14:21:52.336199Z",
+ "daemon_id": "ceph-node-00",
+ "daemon_type": "node-exporter",
+ "events": [
+ "2021-04-04T14:21:52.372374Z daemon:node-exporter.ceph-node-00 [INFO] \"Deployed node-exporter.ceph-node-00 on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:38.610044Z",
+ "memory_usage": 8001683,
+ "ports": [
+ 9100
+ ],
+ "started": "2021-04-04T14:21:52.044759Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "0.18.1"
+ },
+ {
+ "container_id": "ac9e1d055972",
+ "container_image_digests": [
+ "docker.io/prom/node-exporter@sha256:a2f29256e53cc3e0b64d7a472512600b2e9410347d53cdc85b49f659c17e02ee",
+ "docker.io/prom/node-exporter@sha256:b630fb29d99b3483c73a2a7db5fc01a967392a3d7ad754c8eccf9f4a67e7ee31"
+ ],
+ "container_image_id": "e5a616e4b9cf68dfcad7782b78e118be4310022e874d52da85c55923fb615f87",
+ "container_image_name": "docker.io/prom/node-exporter:v0.18.1",
+ "created": "2021-04-04T14:24:39.469923Z",
+ "daemon_id": "ceph-node-01",
+ "daemon_type": "node-exporter",
+ "events": [
+ "2021-04-04T14:24:39.508244Z daemon:node-exporter.ceph-node-01 [INFO] \"Deployed node-exporter.ceph-node-01 on host 'ceph-node-01.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-01.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:26:25.839072Z",
+ "memory_usage": 7052722,
+ "ports": [
+ 9100
+ ],
+ "started": "2021-04-04T14:24:39.156587Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "0.18.1"
+ },
+ {
+ "container_id": "b133dbf9cff8",
+ "container_image_digests": [
+ "docker.io/prom/node-exporter@sha256:a2f29256e53cc3e0b64d7a472512600b2e9410347d53cdc85b49f659c17e02ee",
+ "docker.io/prom/node-exporter@sha256:b630fb29d99b3483c73a2a7db5fc01a967392a3d7ad754c8eccf9f4a67e7ee31"
+ ],
+ "container_image_id": "e5a616e4b9cf68dfcad7782b78e118be4310022e874d52da85c55923fb615f87",
+ "container_image_name": "docker.io/prom/node-exporter:v0.18.1",
+ "created": "2021-04-04T14:24:49.840797Z",
+ "daemon_id": "ceph-node-02",
+ "daemon_type": "node-exporter",
+ "events": [
+ "2021-04-04T14:24:49.901437Z daemon:node-exporter.ceph-node-02 [INFO] \"Deployed node-exporter.ceph-node-02 on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.471349Z",
+ "memory_usage": 7696547,
+ "ports": [
+ 9100
+ ],
+ "started": "2021-04-04T14:24:49.524299Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "0.18.1"
+ },
+ {
+ "container_id": "51d864a583df",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:25:03.086634Z",
+ "daemon_id": "0",
+ "daemon_type": "osd",
+ "events": [
+ "2021-04-04T14:25:03.152770Z daemon:osd.0 [INFO] \"Deployed osd.0 on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:38.610426Z",
+ "memory_usage": 63826821,
+ "osdspec_affinity": "all-available-devices",
+ "ports": [],
+ "started": "2021-04-04T14:25:02.948826Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "7a141557611e",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:25:02.803534Z",
+ "daemon_id": "1",
+ "daemon_type": "osd",
+ "events": [
+ "2021-04-04T14:25:02.905863Z daemon:osd.1 [INFO] \"Deployed osd.1 on host 'ceph-node-01.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-01.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:26:25.839343Z",
+ "memory_usage": 44155535,
+ "osdspec_affinity": "all-available-devices",
+ "ports": [],
+ "started": "2021-04-04T14:25:02.650699Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "bbf4cc5b870a",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:25:03.771174Z",
+ "daemon_id": "2",
+ "daemon_type": "osd",
+ "events": [
+ "2021-04-04T14:25:03.827365Z daemon:osd.2 [INFO] \"Deployed osd.2 on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.471996Z",
+ "memory_usage": 62495129,
+ "osdspec_affinity": "all-available-devices",
+ "ports": [],
+ "started": "2021-04-04T14:25:08.134780Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "e36d84e5608b",
+ "container_image_digests": [
+ "docker.io/prom/prometheus@sha256:5880ec936055fad18ccee798d2a63f64ed85bd28e8e0af17c6923a090b686c3d",
+ "docker.io/prom/prometheus@sha256:b4e6cd0275a26750505e539f8528e891053434ebd3972be02645bed5f02f0795"
+ ],
+ "container_image_id": "de242295e2257c37c8cadfd962369228f8f10b2d48a44259b65fef44ad4f6490",
+ "container_image_name": "docker.io/prom/prometheus:v2.18.1",
+ "created": "2021-04-04T14:22:11.310763Z",
+ "daemon_id": "ceph-node-00",
+ "daemon_type": "prometheus",
+ "events": [
+ "2021-04-04T14:22:11.356043Z daemon:prometheus.ceph-node-00 [INFO] \"Deployed prometheus.ceph-node-00 on host 'ceph-node-00.cephlab.com'\"",
+ "2021-04-04T14:25:33.086106Z daemon:prometheus.ceph-node-00 [INFO] \"Reconfigured prometheus.ceph-node-00 on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": true,
+ "last_refresh": "2021-04-04T14:27:38.610128Z",
+ "memory_usage": 27724349,
+ "ports": [
+ 9095
+ ],
+ "started": "2021-04-04T14:25:32.344156Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "2.18.1"
+ },
+ {
+ "container_id": "5cdeb705c7f6",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:27:26.775628Z",
+ "daemon_id": "foo.ceph-node-00.qknfoh",
+ "daemon_type": "rgw",
+ "events": [
+ "2021-04-04T14:27:26.824821Z daemon:rgw.foo.ceph-node-00.qknfoh [INFO] \"Deployed rgw.foo.ceph-node-00.qknfoh on host 'ceph-node-00.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-00.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:38.610617Z",
+ "memory_usage": 53309603,
+ "ports": [
+ 80
+ ],
+ "started": "2021-04-04T14:27:26.350981Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_id": "18a2179a35c0",
+ "container_image_digests": [
+ "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4"
+ ],
+ "container_image_id": "f72dfde44435bedf5e4c8be05c8194cc57f5f654b9bb146b73e81f1c5358b4c5",
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:27:24.200977Z",
+ "daemon_id": "foo.ceph-node-02.fgzmmm",
+ "daemon_type": "rgw",
+ "events": [
+ "2021-04-04T14:27:24.300473Z daemon:rgw.foo.ceph-node-02.fgzmmm [INFO] \"Deployed rgw.foo.ceph-node-02.fgzmmm on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.471149Z",
+ "memory_usage": 53487861,
+ "ports": [
+ 80
+ ],
+ "started": "2021-04-04T14:27:23.793957Z",
+ "status": 1,
+ "status_desc": "running",
+ "version": "17.0.0-2786-g7fb0569e"
+ },
+ {
+ "container_image_name": "quay.ceph.io/ceph-ci/ceph@sha256:cfd9dc4a437e11894a9a0d930ed1221ccc4e939b943981f6dfbdc611816904d4",
+ "created": "2021-04-04T14:27:30.048136Z",
+ "daemon_id": "foo.ceph-node-02.hqjyla",
+ "daemon_type": "rgw",
+ "events": [
+ "2021-04-04T14:27:30.115692Z daemon:rgw.foo.ceph-node-02.hqjyla [INFO] \"Deployed rgw.foo.ceph-node-02.hqjyla on host 'ceph-node-02.cephlab.com'\""
+ ],
+ "hostname": "ceph-node-02.cephlab.com",
+ "is_active": false,
+ "last_refresh": "2021-04-04T14:27:37.471564Z",
+ "ports": [
+ 80
+ ],
+ "status": -1,
+ "status_desc": "unknown"
+ }
+] \ No newline at end of file
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/fixtures/rgw-status.json b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/rgw-status.json
new file mode 100644
index 000000000..faa8c0418
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/rgw-status.json
@@ -0,0 +1 @@
+{ "available": true, "message": null }
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/plugins/index.js b/src/pybind/mgr/dashboard/frontend/cypress/plugins/index.js
new file mode 100644
index 000000000..b1ba01b66
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/plugins/index.js
@@ -0,0 +1,26 @@
+
+const browserify = require('@cypress/browserify-preprocessor');
+const cucumber = require('cypress-cucumber-preprocessor').default;
+module.exports = (on, _config) => {
+ const options = {
+ ...browserify.defaultOptions,
+ typescript: require.resolve("typescript"),
+ };
+
+ on('file:preprocessor', cucumber(options));
+ on('before:browser:launch', (browser, launchOptions) => {
+ if (browser.name === 'chrome' && browser.isHeadless) {
+ launchOptions.args.push('--disable-gpu');
+ return launchOptions;
+ }
+ });
+
+ on('task', {
+ log({ message, optional }) {
+ optional ? console.log(message, optional) : console.log(message);
+ return null;
+ }
+ });
+};
+
+require('@applitools/eyes-cypress')(module);
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/support/commands.ts b/src/pybind/mgr/dashboard/frontend/cypress/support/commands.ts
new file mode 100644
index 000000000..09a2788eb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/support/commands.ts
@@ -0,0 +1,130 @@
+declare global {
+ namespace Cypress {
+ interface Chainable<Subject> {
+ login(username?: string, password?: string): void;
+ logToConsole(message: string, optional?: any): void;
+ text(): Chainable<string>;
+ ceph2Login(username?: string, password?: string): Chainable<any>;
+ checkAccessibility(subject: any, axeOptions?: any, skip?: boolean): void;
+ }
+ }
+}
+// Disabling tslint rule since cypress-cucumber has
+// issues with absolute import paths.
+// This can be removed when
+// https://github.com/cypress-io/cypress-browserify-preprocessor/issues/53
+// is fixed.
+/* tslint:disable*/
+import { CdHelperClass } from '../../src/app/shared/classes/cd-helper.class';
+import { Permissions } from '../../src/app/shared/models/permissions';
+import { table } from 'table';
+/* tslint:enable*/
+let auth: any;
+
+const fillAuth = () => {
+ window.localStorage.setItem('dashboard_username', auth.username);
+ window.localStorage.setItem('dashboard_permissions', auth.permissions);
+ window.localStorage.setItem('user_pwd_expiration_date', auth.pwdExpirationDate);
+ window.localStorage.setItem('user_pwd_update_required', auth.pwdUpdateRequired);
+ window.localStorage.setItem('sso', auth.sso);
+};
+
+Cypress.Commands.add('login', (username, password) => {
+ cy.session([username, password], () => {
+ requestAuth(username, password).then((resp) => {
+ auth = resp.body;
+ auth.permissions = JSON.stringify(new Permissions(auth.permissions));
+ auth.pwdExpirationDate = String(auth.pwdExpirationDate);
+ auth.pwdUpdateRequired = String(auth.pwdUpdateRequired);
+ auth.sso = String(auth.sso);
+ fillAuth();
+ });
+ });
+});
+
+Cypress.Commands.add('ceph2Login', (username, password) => {
+ const url: string = Cypress.env('CEPH2_URL');
+ cy.session([username, password, url], () => {
+ requestAuth(username, password, url).then((resp) => {
+ auth = resp.body;
+ auth.permissions = JSON.stringify(new Permissions(auth.permissions));
+ auth.pwdExpirationDate = String(auth.pwdExpirationDate);
+ auth.pwdUpdateRequired = String(auth.pwdUpdateRequired);
+ auth.sso = String(auth.sso);
+ const args = {
+ username: auth.username,
+ permissions: auth.permissions,
+ pwdExpirationDate: auth.pwdExpirationDate,
+ pwdUpdateRequired: auth.pwdUpdateRequired,
+ sso: auth.sso
+ };
+ // @ts-ignore
+ cy.origin(
+ url,
+ { args },
+ ({ uname, permissions, pwdExpirationDate, pwdUpdateRequired, sso }: any) => {
+ window.localStorage.setItem('dashboard_username', uname);
+ window.localStorage.setItem('dashboard_permissions', permissions);
+ window.localStorage.setItem('user_pwd_expiration_date', pwdExpirationDate);
+ window.localStorage.setItem('user_pwd_update_required', pwdUpdateRequired);
+ window.localStorage.setItem('sso', sso);
+ }
+ );
+ });
+ });
+});
+
+function requestAuth(username: string, password: string, url = '') {
+ username = username ? username : Cypress.env('LOGIN_USER');
+ password = password ? password : Cypress.env('LOGIN_PWD');
+ return cy.request({
+ method: 'POST',
+ url: !url ? 'api/auth' : `${url}api/auth`,
+ headers: { Accept: CdHelperClass.cdVersionHeader('1', '0') },
+ body: { username: username, password: password }
+ });
+}
+
+// @ts-ignore
+Cypress.Commands.add('text', { prevSubject: true }, ($element: JQuery<HTMLElement>) => {
+ cy.wrap($element).scrollIntoView();
+ return cy
+ .wrap($element)
+ .invoke('text')
+ .then((text: string) => {
+ return text.toString();
+ });
+});
+
+Cypress.Commands.add('logToConsole', (message: string, optional?: any) => {
+ cy.task('log', { message: `(${new Date().toISOString()}) ${message}`, optional });
+});
+
+// Print cypress-axe violations to the terminal
+function a11yErrorLogger(violations: any) {
+ const violationData = violations.flatMap(({ id, impact, description, nodes }: any) => {
+ return nodes.flatMap(({ html }: any) => {
+ return [
+ ['Test', Cypress.currentTest.title],
+ ['Error', id],
+ ['Impact', impact],
+ ['Description', description],
+ ['Element', html],
+ ['', '']
+ ];
+ });
+ });
+
+ cy.task('log', {
+ message: table(violationData, {
+ header: {
+ alignment: 'left',
+ content: Cypress.spec.relative
+ }
+ })
+ });
+}
+
+Cypress.Commands.add('checkAccessibility', (subject: any, axeOptions?: any, skip?: boolean) => {
+ cy.checkA11y(subject, axeOptions, a11yErrorLogger, skip);
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/support/e2e.ts b/src/pybind/mgr/dashboard/frontend/cypress/support/e2e.ts
new file mode 100644
index 000000000..4db2c6a49
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/support/e2e.ts
@@ -0,0 +1,19 @@
+import '@applitools/eyes-cypress/commands';
+import 'cypress-axe';
+
+import './commands';
+
+afterEach(() => {
+ cy.visit('#/403');
+});
+
+Cypress.on('uncaught:exception', (err: Error) => {
+ if (
+ err.message.includes('ResizeObserver loop limit exceeded') ||
+ err.message.includes('api/prometheus/rules') ||
+ err.message.includes('NG0100: ExpressionChangedAfterItHasBeenCheckedError')
+ ) {
+ return false;
+ }
+ return true;
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/support/eyes-index.d.ts b/src/pybind/mgr/dashboard/frontend/cypress/support/eyes-index.d.ts
new file mode 100644
index 000000000..59fc1eca4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/support/eyes-index.d.ts
@@ -0,0 +1 @@
+import '@applitools/eyes-cypress';
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/tsconfig.json b/src/pybind/mgr/dashboard/frontend/cypress/tsconfig.json
new file mode 100644
index 000000000..0d1f6b468
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../tsconfig.json",
+ "exclude": [],
+ "include": [
+ "**/*.ts",
+ "plugins/index.js"
+ ],
+ "compilerOptions": {
+ "sourceMap": false,
+ "types": [
+ "cypress",
+ "cypress-axe",
+ "@applitools/eyes-cypress"
+ ],
+ "target": "es6"
+ }
+}