From: Aashish Sharma Date: Wed, 17 Jan 2024 11:44:44 +0000 (+0530) Subject: mgr/dashboard: add multi-cluster management using context sitcher in X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=b6ca4dd2824e993e5a3d3aa457b39d504a0b67a4;p=ceph.git mgr/dashboard: add multi-cluster management using context sitcher in dashboard Allow the user to add a cluster using a form for multi-cluster management and add a context switcher at the top of the navigation bar to allow the user to switch between the clusters that are connected. Signed-off-by: Aashish Sharma --- diff --git a/src/pybind/mgr/dashboard/controllers/auth.py b/src/pybind/mgr/dashboard/controllers/auth.py index 196f027b293ee..e8bb4bbef8e15 100644 --- a/src/pybind/mgr/dashboard/controllers/auth.py +++ b/src/pybind/mgr/dashboard/controllers/auth.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- import http.cookies +import json import logging import sys +import cherrypy + from .. import mgr from ..exceptions import InvalidCredentialsError, UserDoesNotExist from ..services.auth import AuthManager, JwtManager @@ -34,17 +37,66 @@ class Auth(RESTController, ControllerAuthMixin): """ Provide authenticates and returns JWT token. """ - + # pylint: disable=R0912 def create(self, username, password): user_data = AuthManager.authenticate(username, password) user_perms, pwd_expiration_date, pwd_update_required = None, None, None max_attempt = Settings.ACCOUNT_LOCKOUT_ATTEMPTS + origin = cherrypy.request.headers.get('Origin', None) + try: + fsid = mgr.get('config')['fsid'] + except KeyError: + fsid = '' if max_attempt == 0 or mgr.ACCESS_CTRL_DB.get_attempt(username) < max_attempt: if user_data: user_perms = user_data.get('permissions') pwd_expiration_date = user_data.get('pwdExpirationDate', None) pwd_update_required = user_data.get('pwdUpdateRequired', False) + if isinstance(Settings.MULTICLUSTER_CONFIG, str): + try: + item_to_dict = json.loads(Settings.MULTICLUSTER_CONFIG) + except json.JSONDecodeError: + item_to_dict = {} + multicluster_config = item_to_dict.copy() + else: + multicluster_config = Settings.MULTICLUSTER_CONFIG.copy() + try: + if fsid in multicluster_config['config']: + existing_entries = multicluster_config['config'][fsid] + if not any(entry['user'] == username for entry in existing_entries): + existing_entries.append({ + "name": fsid, + "url": origin, + "cluster_alias": "local-cluster", + "user": username + }) + else: + multicluster_config['config'][fsid] = [{ + "name": fsid, + "url": origin, + "cluster_alias": "local-cluster", + "user": username + }] + + except KeyError: + multicluster_config = { + 'current_url': origin, + 'current_user': username, + 'hub_url': origin, + 'config': { + fsid: [ + { + "name": fsid, + "url": origin, + "cluster_alias": "local-cluster", + "user": username + } + ] + } + } + Settings.MULTICLUSTER_CONFIG = multicluster_config + if user_perms is not None: url_prefix = 'https' if mgr.get_localized_module_option('ssl') else 'http' diff --git a/src/pybind/mgr/dashboard/controllers/multi_cluster.py b/src/pybind/mgr/dashboard/controllers/multi_cluster.py index cfb99d201d1b4..d7acec22bebbf 100644 --- a/src/pybind/mgr/dashboard/controllers/multi_cluster.py +++ b/src/pybind/mgr/dashboard/controllers/multi_cluster.py @@ -9,13 +9,14 @@ from ..security import Scope from ..settings import Settings from ..tools import configure_cors from . import APIDoc, APIRouter, CreatePermission, Endpoint, EndpointDoc, \ - RESTController, UIRouter, UpdatePermission + ReadPermission, RESTController, UIRouter, UpdatePermission @APIRouter('/multi-cluster', Scope.CONFIG_OPT) @APIDoc('Multi-cluster Management API', 'Multi-cluster') class MultiCluster(RESTController): - def _proxy(self, method, base_url, path, params=None, payload=None, verify=False, token=None): + def _proxy(self, method, base_url, path, params=None, payload=None, verify=False, + token=None): try: if token: headers = { @@ -46,33 +47,13 @@ class MultiCluster(RESTController): @Endpoint('POST') @CreatePermission @EndpointDoc("Authenticate to a remote cluster") - def auth(self, url: str, name: str, username=None, password=None, token=None, hub_url=None): - multicluster_config = {} + def auth(self, url: str, cluster_alias: str, username=None, + password=None, token=None, hub_url=None): - if isinstance(Settings.MULTICLUSTER_CONFIG, str): - try: - item_to_dict = json.loads(Settings.MULTICLUSTER_CONFIG) - except json.JSONDecodeError: - item_to_dict = {} - multicluster_config = item_to_dict.copy() - else: - multicluster_config = Settings.MULTICLUSTER_CONFIG.copy() - - if 'hub_url' not in multicluster_config: - multicluster_config['hub_url'] = hub_url - - if 'config' not in multicluster_config: - multicluster_config['config'] = [] - - if token: - multicluster_config['config'].append({ - 'name': name, - 'url': url, - 'token': token - }) + multi_cluster_config = self.load_multi_cluster_config() - Settings.MULTICLUSTER_CONFIG = multicluster_config - return + if not url.endswith('/'): + url = url + '/' if username and password: payload = { @@ -87,17 +68,109 @@ class MultiCluster(RESTController): component='dashboard') token = content['token'] - # Set CORS endpoint on remote cluster + + if token: self._proxy('PUT', url, 'ui-api/multi-cluster/set_cors_endpoint', - payload={'url': multicluster_config['hub_url']}, token=token) + payload={'url': hub_url}, token=token) + fsid = self._proxy('GET', url, 'api/health/get_cluster_fsid', token=token) + content = self._proxy('POST', url, 'api/auth/check', payload={'token': token}, + token=token) + if 'username' in content: + username = content['username'] + + if 'config' not in multi_cluster_config: + multi_cluster_config['config'] = {} + + if fsid in multi_cluster_config['config']: + existing_entries = multi_cluster_config['config'][fsid] + if not any(entry['user'] == username for entry in existing_entries): + existing_entries.append({ + "name": fsid, + "url": url, + "cluster_alias": cluster_alias, + "user": username, + "token": token, + }) + else: + multi_cluster_config['current_user'] = username + multi_cluster_config['config'][fsid] = [{ + "name": fsid, + "url": url, + "cluster_alias": cluster_alias, + "user": username, + "token": token, + }] + + Settings.MULTICLUSTER_CONFIG = multi_cluster_config + + def load_multi_cluster_config(self): + if isinstance(Settings.MULTICLUSTER_CONFIG, str): + try: + itemw_to_dict = json.loads(Settings.MULTICLUSTER_CONFIG) + except json.JSONDecodeError: + itemw_to_dict = {} + multi_cluster_config = itemw_to_dict.copy() + else: + multi_cluster_config = Settings.MULTICLUSTER_CONFIG.copy() + + return multi_cluster_config + + @Endpoint('PUT') + @UpdatePermission + def set_config(self, config: object): + multicluster_config = self.load_multi_cluster_config() + multicluster_config.update({'current_url': config['url']}) + multicluster_config.update({'current_user': config['user']}) + Settings.MULTICLUSTER_CONFIG = multicluster_config + return Settings.MULTICLUSTER_CONFIG + + @Endpoint('POST') + @CreatePermission + # pylint: disable=R0911 + def verify_connection(self, url: str, username=None, password=None, token=None): + if not url.endswith('/'): + url = url + '/' - multicluster_config['config'].append({ - 'name': name, - 'url': url, - 'token': token - }) + if token: + try: + payload = { + 'token': token + } + content = self._proxy('POST', url, 'api/auth/check', payload=payload) + if 'permissions' not in content: + return content['detail'] + user_content = self._proxy('GET', url, f'api/user/{username}', + token=content['token']) + if 'status' in user_content and user_content['status'] == '403 Forbidden': + return 'User is not an administrator' + except Exception as e: # pylint: disable=broad-except + if '[Errno 111] Connection refused' in str(e): + return 'Connection refused' + return 'Connection failed' - Settings.MULTICLUSTER_CONFIG = multicluster_config + if username and password: + try: + payload = { + 'username': username, + 'password': password + } + content = self._proxy('POST', url, 'api/auth', payload=payload) + if 'token' not in content: + return content['detail'] + user_content = self._proxy('GET', url, f'api/user/{username}', + token=content['token']) + if 'status' in user_content and user_content['status'] == '403 Forbidden': + return 'User is not an administrator' + except Exception as e: # pylint: disable=broad-except + if '[Errno 111] Connection refused' in str(e): + return 'Connection refused' + return 'Connection failed' + return 'Connection successful' + + @Endpoint() + @ReadPermission + def get_config(self): + return Settings.MULTICLUSTER_CONFIG @UIRouter('/multi-cluster', Scope.CONFIG_OPT) 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 2ba634fa25d0f..48224c844d47b 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 @@ -48,6 +48,7 @@ import { NoSsoGuardService } from './shared/services/no-sso-guard.service'; import { UpgradeComponent } from './ceph/cluster/upgrade/upgrade.component'; import { CephfsVolumeFormComponent } from './ceph/cephfs/cephfs-form/cephfs-form.component'; import { UpgradeProgressComponent } from './ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component'; +import { MultiClusterComponent } from './ceph/cluster/multi-cluster/multi-cluster.component'; @Injectable() export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver { @@ -184,6 +185,10 @@ const routes: Routes = [ } ] }, + { + path: 'multi-cluster', + component: MultiClusterComponent + }, { path: 'inventory', canActivate: [ModuleStatusGuardService], 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 74657ec4010f0..b1eb9275a462c 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 @@ -61,6 +61,8 @@ import { TelemetryComponent } from './telemetry/telemetry.component'; import { UpgradeComponent } from './upgrade/upgrade.component'; import { UpgradeStartModalComponent } from './upgrade/upgrade-form/upgrade-start-modal.component'; import { UpgradeProgressComponent } from './upgrade/upgrade-progress/upgrade-progress.component'; +import { MultiClusterComponent } from './multi-cluster/multi-cluster.component'; +import { MultiClusterFormComponent } from './multi-cluster/multi-cluster-form/multi-cluster-form.component'; @NgModule({ imports: [ @@ -124,7 +126,9 @@ import { UpgradeProgressComponent } from './upgrade/upgrade-progress/upgrade-pro CreateClusterReviewComponent, UpgradeComponent, UpgradeStartModalComponent, - UpgradeProgressComponent + UpgradeProgressComponent, + MultiClusterComponent, + MultiClusterFormComponent ], providers: [NgbActiveModal] }) 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 new file mode 100644 index 0000000000000..cc9ed7453fc4d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html @@ -0,0 +1,160 @@ + + Connect Cluster + + +
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d 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 new file mode 100644 index 0000000000000..71521de56f111 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MultiClusterFormComponent } from './multi-cluster-form.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +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 { RouterTestingModule } from '@angular/router/testing'; +import { SharedModule } from '~/app/shared/shared.module'; + +describe('MultiClusterFormComponent', () => { + let component: MultiClusterFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + SharedModule, + ReactiveFormsModule, + RouterTestingModule, + HttpClientTestingModule, + ToastrModule.forRoot() + ], + declarations: [MultiClusterFormComponent], + providers: [NgbActiveModal, NotificationService, CdDatePipe, DatePipe] + }).compileComponents(); + + fixture = TestBed.createComponent(MultiClusterFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.ts new file mode 100644 index 0000000000000..473a49dab7f79 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.ts @@ -0,0 +1,152 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import _ from 'lodash'; +import { Subscription } from 'rxjs'; +import { MultiClusterService } from '~/app/shared/api/multi-cluster.service'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { NotificationType } from '~/app/shared/enum/notification-type.enum'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { CdValidators } from '~/app/shared/forms/cd-validators'; +import { NotificationService } from '~/app/shared/services/notification.service'; + +@Component({ + selector: 'cd-multi-cluster-form', + templateUrl: './multi-cluster-form.component.html', + styleUrls: ['./multi-cluster-form.component.scss'] +}) +export class MultiClusterFormComponent implements OnInit, OnDestroy { + readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,5}\/?$/; + readonly ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i; + readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i; + remoteClusterForm: CdFormGroup; + showToken = false; + connectionVerified: boolean; + connectionMessage = ''; + private subs = new Subscription(); + showCrossOriginError = false; + crossOriginCmd: string; + + constructor( + public activeModal: NgbActiveModal, + public actionLabels: ActionLabelsI18n, + public notificationService: NotificationService, + private multiClusterService: MultiClusterService + ) { + this.createForm(); + } + ngOnInit(): void {} + + createForm() { + this.remoteClusterForm = new CdFormGroup({ + showToken: new FormControl(false), + username: new FormControl('', [ + CdValidators.requiredIf({ + showToken: false + }) + ]), + password: new FormControl('', [ + CdValidators.requiredIf({ + showToken: false + }) + ]), + remoteClusterUrl: new FormControl(null, { + validators: [ + CdValidators.custom('endpoint', (value: string) => { + if (_.isEmpty(value)) { + return false; + } else { + return ( + !this.endpoints.test(value) && + !this.ipv4Rgx.test(value) && + !this.ipv6Rgx.test(value) + ); + } + }), + Validators.required + ] + }), + apiToken: new FormControl('', [ + CdValidators.requiredIf({ + showToken: true + }) + ]), + clusterAlias: new FormControl('', { + validators: [Validators.required] + }) + }); + } + + ngOnDestroy() { + this.subs.unsubscribe(); + } + + onSubmit() { + const url = this.remoteClusterForm.getValue('remoteClusterUrl'); + const clusterAlias = this.remoteClusterForm.getValue('clusterAlias'); + const username = this.remoteClusterForm.getValue('username'); + const password = this.remoteClusterForm.getValue('password'); + const token = this.remoteClusterForm.getValue('apiToken'); + + this.subs.add( + this.multiClusterService + .addCluster(url, clusterAlias, username, password, token, window.location.origin) + .subscribe({ + error: () => { + this.remoteClusterForm.setErrors({ cdSubmitButton: true }); + }, + complete: () => { + this.notificationService.show( + NotificationType.success, + $localize`Cluster added successfully` + ); + this.activeModal.close(); + } + }) + ); + } + + verifyConnection() { + const url = this.remoteClusterForm.getValue('remoteClusterUrl'); + const username = this.remoteClusterForm.getValue('username'); + const password = this.remoteClusterForm.getValue('password'); + const token = this.remoteClusterForm.getValue('apiToken'); + + this.subs.add( + this.multiClusterService + .verifyConnection(url, username, password, token) + .subscribe((resp: string) => { + switch (resp) { + case 'Connection successful': + this.connectionVerified = true; + this.connectionMessage = 'Connection Verified Successfully'; + this.notificationService.show( + NotificationType.success, + $localize`Connection Verified Successfully` + ); + break; + + case 'Connection refused': + this.connectionVerified = false; + this.showCrossOriginError = true; + this.connectionMessage = resp; + this.crossOriginCmd = `ceph config set mgr mgr/dashboard/cross_origin_url ${window.location.origin} `; + this.notificationService.show( + NotificationType.error, + $localize`Connection to the cluster failed` + ); + break; + + default: + this.connectionVerified = false; + this.connectionMessage = resp; + this.notificationService.show( + NotificationType.error, + $localize`Connection to the cluster failed` + ); + break; + } + }) + ); + } +} 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 new file mode 100644 index 0000000000000..5009909ea3fac --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.html @@ -0,0 +1,61 @@ + +
+
+
+ + +
+

Connect Cluster

+

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

+
+
+
+ +
+
+
+
+
+
+ + + + + +
+
+ +
+ + + + +
+
+
+
+ +
+
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.scss new file mode 100644 index 0000000000000..2931ef94fba2e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.scss @@ -0,0 +1,7 @@ +@use '../../../../styles/vendor/variables' as vv; + +.fa-wrench { + color: vv.$info; + font-size: 6em; + margin-top: 200px; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.spec.ts new file mode 100644 index 0000000000000..8db81cd790fc6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { MultiClusterComponent } from './multi-cluster.component'; + +describe('MultiClusterComponent', () => { + let component: MultiClusterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + declarations: [MultiClusterComponent], + providers: [NgbActiveModal] + }).compileComponents(); + + fixture = TestBed.createComponent(MultiClusterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000000000..2630c839a4245 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts @@ -0,0 +1,57 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { 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'; + +@Component({ + selector: 'cd-multi-cluster', + templateUrl: './multi-cluster.component.html', + styleUrls: ['./multi-cluster.component.scss'] +}) +export class MultiClusterComponent implements OnInit { + @ViewChild('nameTpl', { static: true }) + nameTpl: any; + + private subs = new Subscription(); + dashboardClustersMap: Map = new Map(); + icons = Icons; + loading = true; + bsModalRef: NgbModalRef; + + constructor( + private multiClusterService: MultiClusterService, + private modalService: ModalService + ) {} + + ngOnInit(): void { + this.subs.add( + this.multiClusterService.subscribe((resp: any) => { + const clustersConfig = resp['config']; + if (clustersConfig) { + Object.keys(clustersConfig).forEach((clusterKey: string) => { + const clusterDetailsList = clustersConfig[clusterKey]; + + clusterDetailsList.forEach((clusterDetails: any) => { + const clusterUrl = clusterDetails['url']; + const clusterName = clusterDetails['name']; + this.dashboardClustersMap.set(clusterUrl, clusterName); + }); + }); + + if (this.dashboardClustersMap.size >= 1) { + this.loading = false; + } + } + }) + ); + } + + openRemoteClusterInfoModal() { + this.bsModalRef = this.modalService.show(MultiClusterFormComponent, { + size: 'xl' + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts index a98548f94c766..57039c0f6d0c4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts @@ -64,6 +64,7 @@ export class LoginComponent implements OnInit { } login() { + localStorage.setItem('cluster_api_url', window.location.origin); this.authService.login(this.model).subscribe(() => { const urlPath = this.postInstalled ? '/' : '/expand-cluster'; let url = _.get(this.route.snapshot.queryParams, 'returnUrl', urlPath); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html index fe3bfc6acf9ea..1c1846dae15ea 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html @@ -1,7 +1,7 @@
+ [ngClass]="{'dashboard': (router.url == '/dashboard' || router.url == '/dashboard_3' || router.url == '/multi-cluster'), 'rgw-dashboard': (router.url == '/rgw/overview')}"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts index afc7a83bb277e..1d7c4bb751cb6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts @@ -2,6 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Subscription } from 'rxjs'; +import { MultiClusterService } from '~/app/shared/api/multi-cluster.service'; import { FaviconService } from '~/app/shared/services/favicon.service'; import { SummaryService } from '~/app/shared/services/summary.service'; @@ -20,10 +21,12 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy { public router: Router, private summaryService: SummaryService, private taskManagerService: TaskManagerService, + private multiClusterService: MultiClusterService, private faviconService: FaviconService ) {} ngOnInit() { + this.subs.add(this.multiClusterService.startPolling()); this.subs.add(this.summaryService.startPolling()); this.subs.add(this.taskManagerService.init(this.summaryService)); this.faviconService.init(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html index abd2e1ae6506e..6af3799b4ef84 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -30,6 +30,29 @@