From cf58d7e04d0365cd61255d77abe12fd780f5470c Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Tue, 7 May 2024 09:17:24 +0530 Subject: [PATCH] mgr/dashboard: add a wizard to setup rgw multisite replication Fixes: https://tracker.ceph.com/issues/66227 Signed-off-by: Aashish Sharma --- .../rgw-multisite-details.component.html | 214 +++++++-------- .../rgw-multisite-details.component.spec.ts | 7 +- .../rgw-multisite-details.component.ts | 124 +++++---- .../rgw-multisite-wizard.component.html | 253 +++++++++++++++++ .../rgw-multisite-wizard.component.scss | 11 + .../rgw-multisite-wizard.component.spec.ts | 29 ++ .../rgw-multisite-wizard.component.ts | 257 ++++++++++++++++++ .../frontend/src/app/ceph/rgw/rgw.module.ts | 10 +- .../app/shared/api/rgw-multisite.service.ts | 24 ++ .../back-button/back-button.component.html | 1 + .../back-button/back-button.component.ts | 11 +- .../components/wizard/wizard.component.html | 2 +- .../src/app/shared/constants/app.constants.ts | 3 + 13 files changed, 774 insertions(+), 172 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html index 921a2dfe3eb7d..0acf38b345348 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html @@ -8,136 +8,123 @@ i18n>Configuration
- In order to access the import/export feature, the rgw module must be enabled - + In order to access the import/export feature, the rgw module must be enabled + (click)="enableRgwModule()">Enable - Please restart all Ceph Object Gateway instances in all zones to ensure consistent - multisite configuration updates. + Please restart all Ceph Object Gateway instances in all zones to ensure consistent multisite configuration updates. Cluster->Services + routerLink="/services"> + Cluster->Services - - - - + + - + + + + + + + - +
Topology Viewer
-
-
-
- - - - - - Topology Viewer +
+
+
+ + + + + + - - - {{ node.data.name }} + [ngClass]="icons.danger"> - + + {{ node.data.name }} + + default - - master - - secondary-zone - -
-
- -
-
- -
+ + master + + secondary-zone + +
+
+
- - -
- + + +
+
@@ -154,3 +141,4 @@
+ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts index ef833a0324ce8..bf36bee1d82e1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts @@ -8,7 +8,7 @@ import { SharedModule } from '~/app/shared/shared.module'; import { RgwMultisiteDetailsComponent } from './rgw-multisite-details.component'; import { RouterTestingModule } from '@angular/router/testing'; import { configureTestBed } from '~/testing/unit-test-helper'; -import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbNavModule, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; describe('RgwMultisiteDetailsComponent', () => { let component: RgwMultisiteDetailsComponent; @@ -24,7 +24,8 @@ describe('RgwMultisiteDetailsComponent', () => { ToastrModule.forRoot(), RouterTestingModule, NgbNavModule - ] + ], + providers: [NgbActiveModal] }); beforeEach(() => { @@ -40,6 +41,6 @@ describe('RgwMultisiteDetailsComponent', () => { it('should display right title', () => { const span = debugElement.nativeElement.querySelector('.card-header'); - expect(span.textContent).toBe('Topology Viewer'); + expect(span.textContent.trim()).toBe('Topology Viewer'); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts index 4b65b7e37bd45..1c7ab210d6b5d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts @@ -6,7 +6,7 @@ import { TreeNode, TREE_ACTIONS } from '@circlon/angular-tree-component'; -import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import _ from 'lodash'; import { forkJoin, Subscription, timer as observableTimer } from 'rxjs'; @@ -37,6 +37,9 @@ import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service'; import { MgrModuleService } from '~/app/shared/api/mgr-module.service'; import { BlockUI, NgBlockUI } from 'ng-block-ui'; import { Router } from '@angular/router'; +import { RgwMultisiteWizardComponent } from '../rgw-multisite-wizard/rgw-multisite-wizard.component'; + +const BASE_URL = 'rgw/multisite'; @Component({ selector: 'cd-rgw-multisite-details', @@ -65,6 +68,7 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { migrateTableAction: CdTableAction[]; importAction: CdTableAction[]; exportAction: CdTableAction[]; + multisiteReplicationActions: CdTableAction[]; loadingIndicator = true; nodes: object[] = []; treeOptions: ITreeOptions = { @@ -92,7 +96,7 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { defaultZoneId = ''; multisiteInfo: object[] = []; defaultsInfo: string[] = []; - showMigrateAction: boolean = false; + showMigrateAndReplicationActions = false; editTitle: string = 'Edit'; deleteTitle: string = 'Delete'; disableExport = true; @@ -102,6 +106,7 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { activeId: string; constructor( + public activeModal: NgbActiveModal, private modalService: ModalService, private timerService: TimerService, private authStorageService: AuthStorageService, @@ -147,6 +152,12 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { } } + openMultisiteSetupWizard() { + this.bsModalRef = this.modalService.show(RgwMultisiteWizardComponent, { + size: 'lg' + }); + } + openMigrateModal() { const initialState = { multisiteInfo: this.multisiteInfo @@ -206,49 +217,62 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { } ngOnInit() { - const createRealmAction: CdTableAction = { - permission: 'create', - icon: Icons.add, - name: this.actionLabels.CREATE + ' Realm', - click: () => this.openModal('realm') - }; - const createZonegroupAction: CdTableAction = { - permission: 'create', - icon: Icons.add, - name: this.actionLabels.CREATE + ' Zone Group', - click: () => this.openModal('zonegroup'), - disable: () => this.getDisable() - }; - const createZoneAction: CdTableAction = { - permission: 'create', - icon: Icons.add, - name: this.actionLabels.CREATE + ' Zone', - click: () => this.openModal('zone') - }; - const migrateMultsiteAction: CdTableAction = { - permission: 'read', - icon: Icons.exchange, - name: this.actionLabels.MIGRATE, - click: () => this.openMigrateModal() - }; - const importMultsiteAction: CdTableAction = { - permission: 'read', - icon: Icons.download, - name: this.actionLabels.IMPORT, - click: () => this.openImportModal(), - disable: () => this.getDisableImport() - }; - const exportMultsiteAction: CdTableAction = { - permission: 'read', - icon: Icons.upload, - name: this.actionLabels.EXPORT, - click: () => this.openExportModal(), - disable: () => this.getDisableExport() - }; - this.createTableActions = [createRealmAction, createZonegroupAction, createZoneAction]; - this.migrateTableAction = [migrateMultsiteAction]; - this.importAction = [importMultsiteAction]; - this.exportAction = [exportMultsiteAction]; + this.createTableActions = [ + { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE + ' Realm', + click: () => this.openModal('realm') + }, + { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE + ' Zone Group', + click: () => this.openModal('zonegroup'), + disable: () => this.getDisable() + }, + { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE + ' Zone', + click: () => this.openModal('zone') + } + ]; + this.migrateTableAction = [ + { + permission: 'create', + icon: Icons.wrench, + name: this.actionLabels.MIGRATE, + click: () => this.openMigrateModal() + } + ]; + this.importAction = [ + { + permission: 'create', + icon: Icons.download, + name: this.actionLabels.IMPORT, + click: () => this.openImportModal(), + disable: () => this.getDisableImport() + } + ]; + this.exportAction = [ + { + permission: 'create', + icon: Icons.upload, + name: this.actionLabels.EXPORT, + click: () => this.openExportModal(), + disable: () => this.getDisableExport() + } + ]; + this.multisiteReplicationActions = [ + { + permission: 'create', + icon: Icons.wrench, + name: this.actionLabels.SETUP_MULTISITE_REPLICATION, + click: () => + this.router.navigate([BASE_URL, { outlets: { modal: 'setup-multisite-replication' } }]) + } + ]; const observables = [ this.rgwRealmService.getAllRealmsInfo(), @@ -390,7 +414,7 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { } this.realmIds = []; this.zoneIds = []; - this.getDisableMigrate(); + this.evaluateMigrateAndReplicationActions(); this.rgwDaemonService.list().subscribe((data: any) => { const realmName = data.map((item: { [x: string]: any }) => item['realm_name']); if ( @@ -457,7 +481,7 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { } } - getDisableMigrate() { + evaluateMigrateAndReplicationActions() { if ( this.realms.length === 0 && this.zonegroups.length === 1 && @@ -465,11 +489,11 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { this.zones.length === 1 && this.zones[0].name === 'default' ) { - this.showMigrateAction = true; + this.showMigrateAndReplicationActions = true; } else { - this.showMigrateAction = false; + this.showMigrateAndReplicationActions = false; } - return this.showMigrateAction; + return this.showMigrateAndReplicationActions; } isDeleteDisabled(node: TreeNode): boolean { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html new file mode 100644 index 0000000000000..5c00a54a933c0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html @@ -0,0 +1,253 @@ +
+ + Set up Multi-site Replication + +
+
+ +
+
+ + + Please note that this process can take some time. During this period, do not click the back button or close the wizard. Thank you for your patience. + +
+

Create Realm & Zonegroup

+
+ +
+
+

Create Zone

+
+ +
+ + + Enter a unique name for the Zone. A Zone represents a distinct data center or geographical location within a Zonegroup. + + This field is required. + The chosen zone name is already in use. +
+
+
+ +
+ + + + Select the endpoints for the Zone. Endpoints are the URLs or IP addresses from which the rgw gateways in that zone can be accessed. You can select multiple endpoints in case you have multiple rgw gateways in a zone + +
+
+
+ +
+ + + Specify the username for the system user. + + + This user will be created automatically as part of the process, and it will have the necessary permissions to manage and synchronize resources across zones. + + This field is required. + The username already exists. +
+
+
+
+
+

Select Cluster

+
+ +
+ + + Choose the cluster where you want to apply this multisite configuration. The selected cluster will integrate the defined Realm, Zonegroup, and Zones, enabling data synchronization and management across the multisite setup. + + + Before submitting this form, please verify that the selected cluster has an active RGW (Rados Gateway) service running. + +
+
+
+ +

Export Token

+
+
+ +
+ + + Name of the realm that will be involved in replication. + +
+
+
+ +
+ + + + + This field displays the token needed to import the multisite configuration into a secondary cluster. Copy this token securely and use it on the secondary cluster to replicate the current multisite setup. Ensure that the token is handled securely to prevent unauthorized access. + +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.scss new file mode 100644 index 0000000000000..6f91a2864a87e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.scss @@ -0,0 +1,11 @@ +.container-fluid { + align-items: flex-start; + display: flex; + padding-left: 0; + width: 100%; +} + +::ng-deep .custom-modal-content .modal-content { + right: 40vh; + width: 140vh; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.spec.ts new file mode 100644 index 0000000000000..11799a430f094 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RgwMultisiteWizardComponent } from './rgw-multisite-wizard.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { SharedModule } from '~/app/shared/shared.module'; +import { ReactiveFormsModule } from '@angular/forms'; +import { ToastrModule } from 'ngx-toastr'; + +describe('RgwMultisiteWizardComponent', () => { + let component: RgwMultisiteWizardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RgwMultisiteWizardComponent], + imports: [HttpClientTestingModule, SharedModule, ReactiveFormsModule, ToastrModule.forRoot()], + providers: [NgbActiveModal] + }).compileComponents(); + + fixture = TestBed.createComponent(RgwMultisiteWizardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts new file mode 100644 index 0000000000000..f84419dd7a838 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts @@ -0,0 +1,257 @@ +import { Component, OnInit } from '@angular/core'; +import { UntypedFormControl, Validators } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { Subscription, forkJoin } from 'rxjs'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { WizardStepModel } from '~/app/shared/models/wizard-steps'; +import { WizardStepsService } from '~/app/shared/services/wizard-steps.service'; +import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service'; +import { RgwDaemon } from '../models/rgw-daemon'; +import { MultiClusterService } from '~/app/shared/api/multi-cluster.service'; +import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { SelectOption } from '~/app/shared/components/select/select-option.model'; +import _ from 'lodash'; +import { SelectMessages } from '~/app/shared/components/select/select-messages.model'; +import { NotificationType } from '~/app/shared/enum/notification-type.enum'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { Router } from '@angular/router'; +import { map, switchMap } from 'rxjs/operators'; + +@Component({ + selector: 'cd-rgw-multisite-wizard', + templateUrl: './rgw-multisite-wizard.component.html', + styleUrls: ['./rgw-multisite-wizard.component.scss'] +}) +export class RgwMultisiteWizardComponent implements OnInit { + multisiteSetupForm: CdFormGroup; + currentStep: WizardStepModel; + currentStepSub: Subscription; + permissions: Permissions; + stepTitles = ['Create Realm & Zonegroup', 'Create Zone', 'Select Cluster']; + stepsToSkip: { [steps: string]: boolean } = {}; + daemons: RgwDaemon[] = []; + selectedCluster = ''; + clusterDetailsArray: any; + isMultiClusterConfigured = false; + exportTokenForm: CdFormGroup; + realms: any; + loading = false; + pageURL: string; + icons = Icons; + rgwEndpoints: { value: any[]; options: any[]; messages: any }; + + constructor( + private wizardStepsService: WizardStepsService, + public activeModal: NgbActiveModal, + public actionLabels: ActionLabelsI18n, + private rgwDaemonService: RgwDaemonService, + private multiClusterService: MultiClusterService, + private rgwMultisiteService: RgwMultisiteService, + public notificationService: NotificationService, + private router: Router + ) { + this.pageURL = 'rgw/multisite'; + this.currentStepSub = this.wizardStepsService + .getCurrentStep() + .subscribe((step: WizardStepModel) => { + this.currentStep = step; + }); + this.currentStep.stepIndex = 1; + this.createForm(); + this.rgwEndpoints = { + value: [], + options: [], + messages: new SelectMessages({ + empty: $localize`There are no endpoints.`, + filter: $localize`Select endpoints` + }) + }; + } + + ngOnInit(): void { + this.rgwDaemonService + .list() + .pipe( + switchMap((daemons) => { + this.daemons = daemons; + const daemonStatsObservables = daemons.map((daemon) => + this.rgwDaemonService.get(daemon.id).pipe( + map((daemonStats) => ({ + hostname: daemon.server_hostname, + port: daemon.port, + frontendConfig: daemonStats['rgw_metadata']['frontend_config#0'] + })) + ) + ); + return forkJoin(daemonStatsObservables); + }) + ) + .subscribe((daemonStatsArray) => { + this.rgwEndpoints.value = daemonStatsArray.map((daemonStats) => { + const protocol = daemonStats.frontendConfig.includes('ssl_port') ? 'https' : 'http'; + return `${protocol}://${daemonStats.hostname}:${daemonStats.port}`; + }); + const options: SelectOption[] = this.rgwEndpoints.value.map( + (endpoint: string) => new SelectOption(false, endpoint, '') + ); + this.rgwEndpoints.options = [...options]; + }); + + this.multiClusterService.getCluster().subscribe((clusters) => { + this.clusterDetailsArray = Object.values(clusters['config']) + .flat() + .filter((cluster) => cluster['cluster_alias'] !== 'local-cluster'); + this.isMultiClusterConfigured = this.clusterDetailsArray.length > 0; + if (!this.isMultiClusterConfigured) { + this.stepTitles = ['Create Realm & Zonegroup', 'Create Zone', 'Export Multi-site token']; + } else { + this.selectedCluster = this.clusterDetailsArray[0]['name']; + } + }); + } + + createForm() { + this.multisiteSetupForm = new CdFormGroup({ + realmName: new UntypedFormControl('default_realm', { + validators: [Validators.required] + }), + zonegroupName: new UntypedFormControl('default_zonegroup', { + validators: [Validators.required] + }), + zonegroup_endpoints: new UntypedFormControl(null, [Validators.required]), + zoneName: new UntypedFormControl('default_zone', { + validators: [Validators.required] + }), + zone_endpoints: new UntypedFormControl(null, { + validators: [Validators.required] + }), + username: new UntypedFormControl('default_system_user', { + validators: [Validators.required] + }), + cluster: new UntypedFormControl(null, { + validators: [Validators.required] + }) + }); + + if (!this.isMultiClusterConfigured) { + this.exportTokenForm = new CdFormGroup({}); + } + } + + showSubmitButtonLabel() { + if (this.isMultiClusterConfigured) { + return !this.wizardStepsService.isLastStep() + ? this.actionLabels.NEXT + : $localize`Configure Multi-site`; + } else { + return !this.wizardStepsService.isLastStep() ? this.actionLabels.NEXT : $localize`Close`; + } + } + + showCancelButtonLabel() { + return !this.wizardStepsService.isFirstStep() + ? this.actionLabels.BACK + : this.actionLabels.CANCEL; + } + + onNextStep() { + if (!this.wizardStepsService.isLastStep()) { + this.wizardStepsService.getCurrentStep().subscribe((step: WizardStepModel) => { + this.currentStep = step; + }); + if (this.currentStep.stepIndex === 2 && !this.isMultiClusterConfigured) { + this.onSubmit(); + } else { + this.wizardStepsService.moveToNextStep(); + } + } else { + this.onSubmit(); + } + } + + onSubmit() { + this.loading = true; + const values = this.multisiteSetupForm.value; + const realmName = values['realmName']; + const zonegroupName = values['zonegroupName']; + const zonegroupEndpoints = this.rgwEndpoints.value.join(','); + const zoneName = values['zoneName']; + const zoneEndpoints = this.rgwEndpoints.value.join(','); + const username = values['username']; + if (!this.isMultiClusterConfigured) { + if (this.wizardStepsService.isLastStep()) { + this.activeModal.close(); + this.refreshMultisitePage(); + } else { + this.rgwMultisiteService + .setUpMultisiteReplication( + realmName, + zonegroupName, + zonegroupEndpoints, + zoneName, + zoneEndpoints, + username + ) + .subscribe((data: object[]) => { + this.loading = false; + this.realms = data; + this.wizardStepsService.moveToNextStep(); + this.showSuccessNotification(); + }); + } + } else { + const cluster = values['cluster']; + this.rgwMultisiteService + .setUpMultisiteReplication( + realmName, + zonegroupName, + zonegroupEndpoints, + zoneName, + zoneEndpoints, + username, + cluster + ) + .subscribe( + () => { + this.showSuccessNotification(); + this.activeModal.close(); + this.refreshMultisitePage(); + }, + () => { + this.multisiteSetupForm.setErrors({ cdSubmitButton: true }); + } + ); + } + } + + showSuccessNotification() { + this.notificationService.show( + NotificationType.success, + $localize`Multi-site setup completed successfully.` + ); + } + + refreshMultisitePage() { + const currentRoute = this.router.url.split('?')[0]; + const navigateTo = currentRoute.includes('multisite') ? '/pool' : '/'; + this.router.navigateByUrl(navigateTo, { skipLocationChange: true }).then(() => { + this.router.navigate([currentRoute]); + }); + } + + onPreviousStep() { + if (!this.wizardStepsService.isFirstStep()) { + this.wizardStepsService.moveToPreviousStep(); + } else { + this.activeModal.close(); + } + } + + onSkip() { + const stepTitle = this.stepTitles[this.currentStep.stepIndex - 1]; + this.stepsToSkip[stepTitle] = true; + this.onNextStep(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index dde6cff4866be..9767230ba1b40 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -58,6 +58,7 @@ import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy/rgw import { RgwMultisiteSyncPolicyFormComponent } from './rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component'; import { RgwConfigurationPageComponent } from './rgw-configuration-page/rgw-configuration-page.component'; import { RgwConfigDetailsComponent } from './rgw-config-details/rgw-config-details.component'; +import { RgwMultisiteWizardComponent } from './rgw-multisite-wizard/rgw-multisite-wizard.component'; @NgModule({ imports: [ @@ -120,7 +121,8 @@ import { RgwConfigDetailsComponent } from './rgw-config-details/rgw-config-detai RgwMultisiteSyncPolicyComponent, RgwMultisiteSyncPolicyFormComponent, RgwConfigDetailsComponent, - RgwConfigurationPageComponent + RgwConfigurationPageComponent, + RgwMultisiteWizardComponent ], providers: [TitleCasePipe] }) @@ -211,6 +213,7 @@ const routes: Routes = [ }, { path: 'multisite', + component: RgwMultisiteDetailsComponent, data: { breadcrumbs: 'Multi-site' }, children: [ { path: '', component: RgwMultisiteDetailsComponent }, @@ -228,6 +231,11 @@ const routes: Routes = [ path: `sync-policy/${URLVerbs.EDIT}/:groupName/:bucketName`, component: RgwMultisiteSyncPolicyFormComponent, data: { breadcrumbs: `${ActionLabels.EDIT} Sync Policy` } + }, + { + path: 'setup-multisite-replication', + component: RgwMultisiteWizardComponent, + outlet: 'modal' } ] }, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts index cc03042815e7b..e77e6afab1f92 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts @@ -71,4 +71,28 @@ export class RgwMultisiteService { } return this.http.delete(`${this.url}/sync-policy-group/${group_id}`, { params }); } + + setUpMultisiteReplication( + realmName: string, + zonegroupName: string, + zonegroupEndpoints: string, + zoneName: string, + zoneEndpoints: string, + username: string, + cluster?: string + ) { + let params = new HttpParams() + .set('realm_name', realmName) + .set('zonegroup_name', zonegroupName) + .set('zonegroup_endpoints', zonegroupEndpoints) + .set('zone_name', zoneName) + .set('zone_endpoints', zoneEndpoints) + .set('username', username); + + if (cluster) { + params = params.set('cluster_fsid', cluster); + } + + return this.http.post(`${this.uiUrl}/multisite-replications`, null, { params: params }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.html index 2d8a787c0d146..077232bd9a972 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.html @@ -1,6 +1,7 @@ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.ts index 64563ea2c3bbe..c84c19a818876 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/back-button/back-button.component.ts @@ -11,6 +11,7 @@ import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; export class BackButtonComponent implements OnInit { @Output() backAction = new EventEmitter(); @Input() name?: string; + @Input() disabled = false; constructor(private location: Location, private actionLabels: ActionLabelsI18n) {} @@ -19,10 +20,12 @@ export class BackButtonComponent implements OnInit { } back() { - if (this.backAction.observers.length === 0) { - this.location.back(); - } else { - this.backAction.emit(); + if (!this.disabled) { + if (this.backAction.observers.length === 0) { + this.location.back(); + } else { + this.backAction.emit(); + } } } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.html index 25aa3e1df855e..7ffd1b320592f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.html @@ -3,7 +3,7 @@