From d10ad900562b68ede186f26ee9c6a9a27d56872d Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Wed, 10 Dec 2025 16:11:50 +0530 Subject: [PATCH] mgr/dashboard: fix multi-cluster context switcher The multi-cluster context switcher stopped working because of a regression caused by this PR https://github.com/ceph/ceph/pull/66034. This PR tends to fix this issue Signed-off-by: Aashish Sharma --- .../multi-cluster/multi-cluster.e2e-spec.ts | 2 +- .../e2e/multi-cluster/multi-cluster.po.ts | 70 +++++--- .../frontend/src/app/app-routing.module.ts | 5 - .../src/app/ceph/cluster/cluster.module.ts | 3 +- .../multi-cluster-form.component.html | 168 ++++++++++-------- .../multi-cluster-form.component.spec.ts | 11 +- .../multi-cluster-list.component.html | 1 - .../multi-cluster-list.component.ts | 24 +-- .../multi-cluster.component.html | 58 +++--- .../multi-cluster/multi-cluster.component.ts | 22 +-- .../navigation/navigation.component.ts | 2 +- .../src/app/shared/enum/icons.enum.ts | 1 + 12 files changed, 182 insertions(+), 185 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/multi-cluster/multi-cluster.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/multi-cluster/multi-cluster.e2e-spec.ts index c37dac2fe6a0..ac10a7239fc0 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/multi-cluster/multi-cluster.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/multi-cluster/multi-cluster.e2e-spec.ts @@ -26,7 +26,7 @@ describe('Muti-cluster management page', () => { }); it('should edit the second cluster', () => { - multiCluster.edit(alias, editedAlias); + multiCluster.edit(alias, editedAlias, password); multiCluster.existTableCell(editedAlias); }); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/multi-cluster/multi-cluster.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/multi-cluster/multi-cluster.po.ts index 13856eefcbcd..e830365cb5ae 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/multi-cluster/multi-cluster.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/multi-cluster/multi-cluster.po.ts @@ -2,7 +2,8 @@ import { PageHelper } from '../page-helper.po'; const pages = { index: { url: '#/multi-cluster/overview', id: 'cd-multi-cluster' }, - 'manage-clusters': { url: '#/multi-cluster/manage-clusters', id: 'cd-multi-cluster-list' } + 'manage-clusters': { url: '#/multi-cluster/manage-clusters', id: 'cd-multi-cluster-list' }, + connect: { url: '#/multi-cluster/manage-clusters/connect', id: 'cd-multi-cluster-form' } }; const WAIT_TIMER = 1000; @@ -18,18 +19,24 @@ export class MultiClusterPageHelper extends PageHelper { auth(url: string, alias: string, username: string, password: string) { cy.contains('button', 'Connect').click(); cy.get('cd-multi-cluster-form').should('exist'); - cy.get('cd-modal').within(() => { - cy.get('input[name=remoteClusterUrl]').type(url); - cy.get('input[name=clusterAlias]').type(alias); - cy.get('input[name=username]').type(username); - cy.get('input[name=password]').type(password); - cy.get('cd-submit-button').click(); - }); + this.navigateTo('connect'); + cy.get('input[name=remoteClusterUrl]').type(url); + cy.get('input[name=clusterAlias]').type(alias); + cy.get('input[name=username]').type(username); + cy.get('#password').type(password); + cy.get('[data-testid=submitBtn]').click(); cy.wait(WAIT_TIMER); } disconnect(alias: string) { - this.clickRowActionButton(alias, 'disconnect'); + this.searchTable(alias); + cy.wait(3000); + cy.get(`[cdstablerow] [cdstabledata]:nth-child(${this.columnIndex.alias})`) + .filter((_i, el) => el.innerText.trim() === alias) + .parent('[cdstablerow]') + .find('[cdstabledata] [data-testid="table-action-btn"]') + .click({ force: true }); + cy.get(`button.disconnect`).click({ force: true }); cy.get('cds-modal').within(() => { cy.get('#confirmation_input').click({ force: true }); cy.get('cd-submit-button').click(); @@ -38,35 +45,48 @@ export class MultiClusterPageHelper extends PageHelper { } reconnect(alias: string, password: string) { - this.clickRowActionButton(alias, 'reconnect'); - cy.get('cd-modal').within(() => { - cy.get('input[name=password]').type(password); - cy.get('cd-submit-button').click(); - }); + this.searchTable(alias); + cy.wait(3000); + cy.get(`[cdstablerow] [cdstabledata]:nth-child(${this.columnIndex.alias})`) + .filter((_i, el) => el.innerText.trim() === alias) + .parent('[cdstablerow]') + .find('[cdstabledata] [data-testid="table-action-btn"]') + .click({ force: true }); + cy.get(`button.reconnect`).click({ force: true }); + cy.get('#password').type(password); + cy.get('[data-testid=submitBtn]').click(); cy.wait(WAIT_TIMER); } - edit(alias: string, newAlias: string) { - this.clickRowActionButton(alias, 'edit'); - cy.get('cd-modal').within(() => { - cy.get('input[name=clusterAlias]').clear().type(newAlias); - cy.get('cd-submit-button').click(); - }); + edit(alias: string, newAlias: string, password: string) { + this.searchTable(alias); + cy.wait(3000); + cy.get(`[cdstablerow] [cdstabledata]:nth-child(${this.columnIndex.alias})`) + .filter((_i, el) => el.innerText.trim() === alias) + .parent('[cdstablerow]') + .find('[cdstabledata] [data-testid="table-action-btn"]') + .click({ force: true }); + cy.get(`button.edit`).click({ force: true }); + cy.get('input[name=clusterAlias]').clear().type(newAlias); + cy.get('#password').type(password); + cy.get('[data-testid=submitBtn]').click(); cy.wait(WAIT_TIMER); } checkConnectionStatus(alias: string, expectedStatus = 'CONNECTED', shouldReload = true) { - let aliasIndex = this.columnIndex.alias; - let statusIndex = this.columnIndex.connection; + const aliasIndex = this.columnIndex.alias; + const statusIndex = this.columnIndex.connection; if (shouldReload) { cy.reload(true, { log: true, timeout: 5 * 1000 }); } - - this.getTableCell(aliasIndex, alias) + this.searchTable(alias); + cy.wait(3000); + cy.get(`[cdstablerow] [cdstabledata]:nth-child(${aliasIndex})`) + .filter((_i, el) => el.innerText.trim() === alias) .parent() .find(`[cdstabledata]:nth-child(${statusIndex}) cds-tag`) .should(($ele) => { - const status = $ele.toArray().map((v) => v.innerText); + const status = $ele.toArray().map((v) => v.innerText.trim()); expect(status).to.include(expectedStatus); }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 0de4ea6404ac..bf0268e1a59f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -49,7 +49,6 @@ import { CephfsVolumeFormComponent } from './ceph/cephfs/cephfs-form/cephfs-form import { UpgradeProgressComponent } from './ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component'; import { MultiClusterComponent } from './ceph/cluster/multi-cluster/multi-cluster.component'; import { MultiClusterListComponent } from './ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component'; -import { MultiClusterDetailsComponent } from './ceph/cluster/multi-cluster/multi-cluster-details/multi-cluster-details.component'; import { SmbClusterFormComponent } from './ceph/smb/smb-cluster-form/smb-cluster-form.component'; import { SmbShareFormComponent } from './ceph/smb/smb-share-form/smb-share-form.component'; import { SmbJoinAuthFormComponent } from './ceph/smb/smb-join-auth-form/smb-join-auth-form.component'; @@ -228,10 +227,6 @@ const routes: Routes = [ path: `${URLVerbs.RECONNECT}/:name`, component: MultiClusterFormComponent, data: { breadcrumbs: ActionLabels.RECONNECT } - }, - { - path: 'performance-details', - component: MultiClusterDetailsComponent } ] } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index 5fe7ac88d81f..b2ffca0a8781 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -24,6 +24,7 @@ import { import Analytics from '@carbon/icons/es/analytics/16'; import CloseFilled from '@carbon/icons/es/close--filled/16'; import ProgressBarRoundIcon from '@carbon/icons/es/progress-bar--round/32'; +import Connect from '@carbon/icons/es/connect/32'; import { NgbActiveModal, NgbDatepickerModule, @@ -176,6 +177,6 @@ import { TextLabelListComponent } from '~/app/shared/components/text-label-list/ }) export class ClusterModule { constructor(private iconService: IconService) { - this.iconService.registerAll([Analytics, CloseFilled, ProgressBarRoundIcon]); + this.iconService.registerAll([Analytics, CloseFilled, ProgressBarRoundIcon, Connect]); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html index ba249eacc9b0..a6b2b1bdad4b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html @@ -1,12 +1,17 @@ -
- -
- -
{{ action | titlecase }} Cluster
+ +
+
+
+

{{ action | titlecase }} Cluster

+
-
+
Cluster API URL - - + - This field is required. - Please enter a valid URL. - The hub cluster cannot be connected. + This field is required. + + Please enter a valid URL. + + The hub cluster cannot be connected. + - - Enter the Dashboard API URL. CLI: {{ clusterApiUrlCmd }} - + + Enter the Dashboard API URL. CLI: {{ clusterApiUrlCmd }} +
-
+
- - + This field is required. - + The chosen alias name is already in use.
-
+
- - + This field is required. - + A cluster with the chosen user is already connected.
-
+
Password - +
-
- +
+ @@ -141,7 +149,8 @@
-
+
-
+
-
- - - +
+ +
- The SSL certificate in PEM format. - - + This field is required. - + Invalid SSL certificate. @@ -186,6 +198,6 @@ [submitText]="(action | titlecase) + ' ' + 'Cluster'" wrappingClass="text-right"> - - -
+
+
+ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.spec.ts index 71521de56f11..fada4f33f4fe 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.spec.ts @@ -6,10 +6,11 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ToastrModule } from 'ngx-toastr'; import { NotificationService } from '~/app/shared/services/notification.service'; import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe'; -import { DatePipe } from '@angular/common'; -import { ReactiveFormsModule } from '@angular/forms'; +import { CommonModule, DatePipe } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterTestingModule } from '@angular/router/testing'; import { SharedModule } from '~/app/shared/shared.module'; +import { CheckboxModule, GridModule, InputModule, SelectModule } from 'carbon-components-angular'; describe('MultiClusterFormComponent', () => { let component: MultiClusterFormComponent; @@ -19,7 +20,13 @@ describe('MultiClusterFormComponent', () => { await TestBed.configureTestingModule({ imports: [ SharedModule, + CommonModule, + FormsModule, + CheckboxModule, + GridModule, ReactiveFormsModule, + InputModule, + SelectModule, RouterTestingModule, HttpClientTestingModule, ToastrModule.forRoot() diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html index 0aab4463947e..79cd60c970ff 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html @@ -54,7 +54,6 @@
-
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.ts index 7abe686107d8..4abf5cbf34c1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.ts @@ -6,8 +6,6 @@ import { Icons } from '~/app/shared/enum/icons.enum'; import { CdTableAction } from '~/app/shared/models/cd-table-action'; import { CdTableColumn } from '~/app/shared/models/cd-table-column'; import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; -import { ModalService } from '~/app/shared/services/modal.service'; -import { MultiClusterFormComponent } from '../multi-cluster-form/multi-cluster-form.component'; import { TableComponent } from '~/app/shared/datatable/table/table.component'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { Permissions } from '~/app/shared/models/permissions'; @@ -16,7 +14,7 @@ import { NotificationService } from '~/app/shared/services/notification.service' import { NotificationType } from '~/app/shared/enum/notification-type.enum'; import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; import { MultiCluster } from '~/app/shared/models/multi-cluster'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { CookiesService } from '~/app/shared/services/cookie.service'; import { Observable, Subscription } from 'rxjs'; import { SettingsService } from '~/app/shared/api/settings.service'; @@ -62,11 +60,9 @@ export class MultiClusterListComponent extends ListWithDetails implements OnInit public actionLabels: ActionLabelsI18n, private notificationService: NotificationService, private authStorageService: AuthStorageService, - private modalService: ModalService, private cookieService: CookiesService, private settingsService: SettingsService, private cdsModalService: ModalCdsService, - private route: ActivatedRoute, private urlBuilder: URLBuilderService ) { super(); @@ -216,23 +212,6 @@ export class MultiClusterListComponent extends ListWithDetails implements OnInit } } - openRemoteClusterInfoModal(action: string) { - const initialState = { - clustersData: this.data, - action: action, - cluster: this.selection.first() - }; - this.bsModalRef = this.modalService.show(MultiClusterFormComponent, initialState, { - size: 'xl' - }); - this.bsModalRef.componentInstance.submitAction.subscribe(() => { - const currentRoute = this.router.url.split('?')[0]; - this.multiClusterService.refreshMultiCluster(currentRoute); - this.checkClusterConnectionStatus(); - this.multiClusterService.isClusterAdded(true); - }); - } - openDeleteClusterModal() { const cluster = this.selection.first(); this.modalRef = this.cdsModalService.show(DeleteConfirmationModalComponent, { @@ -277,7 +256,6 @@ export class MultiClusterListComponent extends ListWithDetails implements OnInit setExpandedRow(expandedRow: any) { super.setExpandedRow(expandedRow); - this.router.navigate(['performance-details'], { relativeTo: this.route }); } refresh() { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.html index 708b9cbd6325..2daec3654d65 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.html @@ -3,36 +3,36 @@ *ngIf="managedByConfig$ | async as managedByConfig">
- - -
-

Connect Cluster

-

Upgrade your current cluster to a multi-cluster setup effortlessly. - Click on the "Connect Cluster" button to begin the process.

-
-
-

This cluster is already managed by cluster - - - {{ managedByConfig['MANAGED_BY_CLUSTERS'][0]['fsid'] }} - - -

-
-
-
- + +
+

Connect Cluster

+

Upgrade your current cluster to a multi-cluster setup effortlessly. + Click on the "Connect Cluster" button to begin the process.

+
+
+

This cluster is already managed by cluster - + + {{ managedByConfig['MANAGED_BY_CLUSTERS'][0]['fsid'] }} + + +

+
+
+
+ +
-
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts index 370c1ee7f1ad..dcd232d9a7fd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts @@ -3,8 +3,6 @@ import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { Observable, Subscription } from 'rxjs'; import { MultiClusterService } from '~/app/shared/api/multi-cluster.service'; import { Icons } from '~/app/shared/enum/icons.enum'; -import { ModalService } from '~/app/shared/services/modal.service'; -import { MultiClusterFormComponent } from './multi-cluster-form/multi-cluster-form.component'; import { PrometheusService } from '~/app/shared/api/prometheus.service'; import { CdTableColumn } from '~/app/shared/models/cd-table-column'; import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; @@ -51,7 +49,7 @@ export class MultiClusterComponent implements OnInit, OnDestroy { POOL_CAPACITY_UTILIZATION: 0, POOL_IOPS_UTILIZATION: 0, POOL_THROUGHPUT_UTILIZATION: 0, - TOTAL_CAPACITY: 0, + TOTAL_CAPACITY: [], USED_CAPACITY: 0, HOSTS: 0, POOLS: 0, @@ -103,7 +101,6 @@ export class MultiClusterComponent implements OnInit, OnDestroy { constructor( private multiClusterService: MultiClusterService, private settingsService: SettingsService, - private modalService: ModalService, private router: Router, private prometheusService: PrometheusService, private notificationService: NotificationService @@ -213,21 +210,8 @@ export class MultiClusterComponent implements OnInit, OnDestroy { } } - openRemoteClusterInfoModal() { - const initialState = { - action: 'connect' - }; - this.bsModalRef = this.modalService.show(MultiClusterFormComponent, initialState, { - size: 'lg' - }); - this.bsModalRef.componentInstance.submitAction.subscribe(() => { - this.loading = true; - setTimeout(() => { - const currentRoute = this.router.url.split('?')[0]; - this.multiClusterService.refreshMultiCluster(currentRoute); - this.getPrometheusData(this.prometheusService.lastHourDateObject); - }, this.PROMETHEUS_DELAY); - }); + openConnectClusterForm() { + this.router.navigate(['multi-cluster/manage-clusters/connect']); } getPrometheusData(selectedTime: any, selectedQueries?: string) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts index 26506d2bc893..612ef52ddc37 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts @@ -186,7 +186,7 @@ export class NavigationComponent implements OnInit, OnDestroy { const clusterUser = clusterDetails[USER]; if ( - clusterName === this.selectedCluster[USER] && + clusterName === this.selectedCluster['name'] && clusterUser === this.selectedCluster[USER] && clusterDetails['cluster_alias'] !== 'local-cluster' ) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts index 697fc17d1018..50db9d77e826 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts @@ -86,6 +86,7 @@ export enum Icons { idea = 'idea', userAccessLocked = 'user--access-locked', // User access locked chevronDown = 'chevron--down', + connect = 'connect', /* Icons for special effect */ size16 = '16', size20 = '20', -- 2.47.3