From: Volker Theile Date: Thu, 2 Apr 2020 10:25:28 +0000 (+0200) Subject: mgr/dashboard: move monitoring tabs to a single page X-Git-Tag: v14.2.10~149^2~2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=bf59e1a4af0de6d8d276b3eb7e8f2ef71e772399;p=ceph.git mgr/dashboard: move monitoring tabs to a single page with a tab for 'active alerts', 'all alerts' and 'silences'. Due to ambiguity with existing names, `AlertListComponent` has been renamed to `ActiveAlertListComponent`. Introduces `MonitoringListComponent` as first page for monitoring concerns, using path `/monitoring`. Keeps the activated tab open, independent of the way that's used to go back to the previous page, be it the cancel button or submit button or the link on the breadcrumb. Also keeps the active tab open even when the page is reloaded. Fixes: https://tracker.ceph.com/issues/42877 Signed-off-by: Patrick Seidensal (cherry picked from commit 855f214b29c8ed935c8f4ba0b8a8396692f946a1) Conflicts: src/pybind/mgr/dashboard/frontend/e2e/cluster/alerts.e2e-spec.ts src/pybind/mgr/dashboard/frontend/e2e/cluster/alerts.po.ts src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.spec.ts src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.html --- diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/alerts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/alerts.e2e-spec.ts deleted file mode 100644 index 2f438f8dd20f..000000000000 --- a/src/pybind/mgr/dashboard/frontend/e2e/cluster/alerts.e2e-spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Helper } from '../helper.po'; -import { AlertsPage } from './alerts.po'; - -describe('Alerts page', () => { - let page: AlertsPage; - - beforeAll(() => { - page = new AlertsPage(); - }); - - afterEach(() => { - Helper.checkConsole(); - }); - - describe('breadcrumb test', () => { - beforeAll(() => { - page.navigateTo(); - }); - - it('should open and show breadcrumb', () => { - Helper.waitTextToBePresent(Helper.getBreadcrumb(), 'Alerts'); - }); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/alerts.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/alerts.po.ts deleted file mode 100644 index 076b15b69043..000000000000 --- a/src/pybind/mgr/dashboard/frontend/e2e/cluster/alerts.po.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { browser } from 'protractor'; - -export class AlertsPage { - navigateTo() { - return browser.get('/#/alerts'); - } -} 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 73dbc4088f50..4553ce8e58dd 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 @@ -13,9 +13,8 @@ import { MgrModuleFormComponent } from './ceph/cluster/mgr-modules/mgr-module-fo import { MgrModuleListComponent } from './ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component'; import { MonitorComponent } from './ceph/cluster/monitor/monitor.component'; import { OsdListComponent } from './ceph/cluster/osd/osd-list/osd-list.component'; -import { AlertListComponent } from './ceph/cluster/prometheus/alert-list/alert-list.component'; +import { MonitoringListComponent } from './ceph/cluster/prometheus/monitoring-list/monitoring-list.component'; import { SilenceFormComponent } from './ceph/cluster/prometheus/silence-form/silence-form.component'; -import { SilenceListComponent } from './ceph/cluster/prometheus/silence-list/silence-list.component'; import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component'; import { Nfs501Component } from './ceph/nfs/nfs-501/nfs-501.component'; import { NfsFormComponent } from './ceph/nfs/nfs-form/nfs-form.component'; @@ -110,34 +109,31 @@ const routes: Routes = [ data: { breadcrumbs: 'Cluster/Logs' } }, { - path: 'alerts', - component: AlertListComponent, + path: 'monitoring', canActivate: [AuthGuardService], - data: { breadcrumbs: 'Cluster/Alerts' } - }, - { - path: 'silence', - canActivate: [AuthGuardService], - data: { breadcrumbs: 'Cluster/Silences' }, + data: { breadcrumbs: 'Cluster/Monitoring' }, children: [ - { path: '', component: SilenceListComponent }, { - path: URLVerbs.CREATE, + path: '', + component: MonitoringListComponent + }, + { + path: 'silence/' + URLVerbs.CREATE, component: SilenceFormComponent, - data: { breadcrumbs: ActionLabels.CREATE } + data: { breadcrumbs: `${ActionLabels.CREATE} Silence` } }, { - path: `${URLVerbs.CREATE}/:id`, + path: `silence/${URLVerbs.CREATE}/:id`, component: SilenceFormComponent, data: { breadcrumbs: ActionLabels.CREATE } }, { - path: `${URLVerbs.EDIT}/:id`, + path: `silence/${URLVerbs.EDIT}/:id`, component: SilenceFormComponent, data: { breadcrumbs: ActionLabels.EDIT } }, { - path: `${URLVerbs.RECREATE}/:id`, + path: `silence/${URLVerbs.RECREATE}/:id`, component: SilenceFormComponent, data: { breadcrumbs: ActionLabels.RECREATE } } 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 ab0d71f02ee0..11330561573f 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 @@ -32,8 +32,8 @@ import { OsdPgScrubModalComponent } from './osd/osd-pg-scrub-modal/osd-pg-scrub- import { OsdRecvSpeedModalComponent } from './osd/osd-recv-speed-modal/osd-recv-speed-modal.component'; import { OsdReweightModalComponent } from './osd/osd-reweight-modal/osd-reweight-modal.component'; import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.component'; -import { AlertListComponent } from './prometheus/alert-list/alert-list.component'; -import { PrometheusTabsComponent } from './prometheus/prometheus-tabs/prometheus-tabs.component'; +import { ActiveAlertListComponent } from './prometheus/active-alert-list/active-alert-list.component'; +import { MonitoringListComponent } from './prometheus/monitoring-list/monitoring-list.component'; import { RulesListComponent } from './prometheus/rules-list/rules-list.component'; import { SilenceFormComponent } from './prometheus/silence-form/silence-form.component'; import { SilenceListComponent } from './prometheus/silence-list/silence-list.component'; @@ -86,13 +86,13 @@ import { SilenceMatcherModalComponent } from './prometheus/silence-matcher-modal LogsComponent, OsdRecvSpeedModalComponent, OsdPgScrubModalComponent, - AlertListComponent, + ActiveAlertListComponent, OsdRecvSpeedModalComponent, SilenceFormComponent, SilenceListComponent, - PrometheusTabsComponent, SilenceMatcherModalComponent, - RulesListComponent + RulesListComponent, + MonitoringListComponent ] }) export class ClusterModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html new file mode 100644 index 000000000000..a5b11ee72f71 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + Source + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.spec.ts new file mode 100644 index 000000000000..85eb89f6f3f2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.spec.ts @@ -0,0 +1,132 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { TabsModule } from 'ngx-bootstrap/tabs'; +import { ToastrModule } from 'ngx-toastr'; + +import { + configureTestBed, + i18nProviders, + PermissionHelper +} from '../../../../../testing/unit-test-helper'; +import { CoreModule } from '../../../../core/core.module'; +import { TableActionsComponent } from '../../../../shared/datatable/table-actions/table-actions.component'; +import { SharedModule } from '../../../../shared/shared.module'; +import { CephModule } from '../../../ceph.module'; +import { DashboardModule } from '../../../dashboard/dashboard.module'; +import { ClusterModule } from '../../cluster.module'; +import { ActiveAlertListComponent } from './active-alert-list.component'; + +describe('ActiveAlertListComponent', () => { + let component: ActiveAlertListComponent; + let fixture: ComponentFixture; + + configureTestBed({ + imports: [ + HttpClientTestingModule, + TabsModule.forRoot(), + RouterTestingModule, + ToastrModule.forRoot(), + SharedModule, + ClusterModule, + DashboardModule, + CephModule, + CoreModule + ], + declarations: [], + providers: [i18nProviders] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ActiveAlertListComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + describe('show action buttons and drop down actions depending on permissions', () => { + let tableActions: TableActionsComponent; + let scenario: { fn; empty; single }; + let permissionHelper: PermissionHelper; + let combinations: number[][]; + + const getTableActionComponent = (): TableActionsComponent => { + fixture.detectChanges(); + return fixture.debugElement.query(By.directive(TableActionsComponent)).componentInstance; + }; + + beforeEach(() => { + permissionHelper = new PermissionHelper(component.permission, () => + getTableActionComponent() + ); + scenario = { + fn: () => tableActions.getCurrentButton().name, + single: 'Create Silence', + empty: 'Create Silence' + }; + tableActions = permissionHelper.setPermissionsAndGetActions(1, 1, 1); + }); + + const permissionSwitch = (combination) => { + tableActions = permissionHelper.setPermissionsAndGetActions( + combination[0], + combination[1], + combination[2] + ); + tableActions.tableActions = component.tableActions; + tableActions.ngOnInit(); + }; + + const testCombinations = (test: Function) => { + combinations.forEach((combination) => { + permissionSwitch(combination); + test(); + }); + }; + + describe('with every permission combination that includes create', () => { + beforeEach(() => { + combinations = [[1, 1, 1], [1, 1, 0], [1, 0, 1], [1, 0, 0]]; + }); + + it(`always shows 'Create silence' as main action`, () => { + testCombinations(() => permissionHelper.testScenarios(scenario)); + }); + + it('shows all actions', () => { + testCombinations(() => { + expect(tableActions.tableActions.length).toBe(1); + expect(tableActions.tableActions).toEqual(component.tableActions); + }); + }); + }); + + describe('with every permission combination that does not include create', () => { + beforeEach(() => { + combinations = [[0, 1, 1], [0, 1, 0], [0, 0, 1], [0, 0, 0]]; + }); + + it(`won't show any action`, () => { + testCombinations(() => { + permissionHelper.testScenarios({ + fn: () => tableActions.getCurrentButton(), + single: undefined, + empty: undefined + }); + }); + }); + + it('shows no actions', () => { + testCombinations(() => { + expect(tableActions.tableActions.length).toBe(0); + expect(tableActions.tableActions).toEqual([]); + }); + }); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts new file mode 100644 index 000000000000..a61dae429d60 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts @@ -0,0 +1,97 @@ +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { I18n } from '@ngx-translate/i18n-polyfill'; +import { CellTemplate } from '../../../../shared/enum/cell-template.enum'; +import { Icons } from '../../../../shared/enum/icons.enum'; +import { CdTableAction } from '../../../../shared/models/cd-table-action'; +import { CdTableColumn } from '../../../../shared/models/cd-table-column'; +import { CdTableSelection } from '../../../../shared/models/cd-table-selection'; +import { Permission } from '../../../../shared/models/permissions'; +import { CdDatePipe } from '../../../../shared/pipes/cd-date.pipe'; +import { AuthStorageService } from '../../../../shared/services/auth-storage.service'; +import { PrometheusAlertService } from '../../../../shared/services/prometheus-alert.service'; +import { URLBuilderService } from '../../../../shared/services/url-builder.service'; + +const BASE_URL = 'silence'; // as only silence actions can be used + +@Component({ + selector: 'cd-active-alert-list', + providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }], + templateUrl: './active-alert-list.component.html', + styleUrls: ['./active-alert-list.component.scss'] +}) +export class ActiveAlertListComponent implements OnInit { + @ViewChild('externalLinkTpl') + externalLinkTpl: TemplateRef; + columns: CdTableColumn[]; + tableActions: CdTableAction[]; + permission: Permission; + selection = new CdTableSelection(); + icons = Icons; + customCss = { + 'label label-danger': 'active', + 'label label-warning': 'unprocessed', + 'label label-info': 'suppressed' + }; + + constructor( + // NotificationsComponent will refresh all alerts every 5s (No need to do it here as well) + private authStorageService: AuthStorageService, + public prometheusAlertService: PrometheusAlertService, + private urlBuilder: URLBuilderService, + private i18n: I18n, + private cdDatePipe: CdDatePipe + ) { + this.permission = this.authStorageService.getPermissions().prometheus; + this.tableActions = [ + { + permission: 'create', + canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection, + disable: (selection: CdTableSelection) => + !selection.hasSingleSelection || selection.first().cdExecuting, + icon: Icons.add, + routerLink: () => + '/monitoring' + this.urlBuilder.getCreateFrom(this.selection.first().fingerprint), + name: this.i18n('Create Silence') + } + ]; + } + + ngOnInit() { + this.columns = [ + { + name: this.i18n('Name'), + prop: 'labels.alertname', + flexGrow: 2 + }, + { + name: this.i18n('Job'), + prop: 'labels.job', + flexGrow: 2 + }, + { + name: this.i18n('Severity'), + prop: 'labels.severity' + }, + { + name: this.i18n('State'), + prop: 'status.state', + cellTransformation: CellTemplate.classAdding + }, + { + name: this.i18n('Started'), + prop: 'startsAt', + pipe: this.cdDatePipe + }, + { + name: this.i18n('URL'), + prop: 'generatorURL', + sortable: false, + cellTemplate: this.externalLinkTpl + } + ]; + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.html deleted file mode 100644 index 90b1bf951c30..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.html +++ /dev/null @@ -1,40 +0,0 @@ - - -

All Alerts

- - -

Active Alerts

- - - - - - - - - - - - - Source - - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.spec.ts deleted file mode 100644 index a684943f94f3..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { TabsModule } from 'ngx-bootstrap/tabs'; -import { ToastrModule } from 'ngx-toastr'; - -import { - configureTestBed, - i18nProviders, - PermissionHelper -} from '../../../../../testing/unit-test-helper'; -import { CoreModule } from '../../../../core/core.module'; -import { TableActionsComponent } from '../../../../shared/datatable/table-actions/table-actions.component'; -import { SharedModule } from '../../../../shared/shared.module'; -import { CephModule } from '../../../ceph.module'; -import { DashboardModule } from '../../../dashboard/dashboard.module'; -import { ClusterModule } from '../../cluster.module'; -import { AlertListComponent } from './alert-list.component'; - -describe('PrometheusListComponent', () => { - let component: AlertListComponent; - let fixture: ComponentFixture; - - configureTestBed({ - imports: [ - HttpClientTestingModule, - TabsModule.forRoot(), - RouterTestingModule, - ToastrModule.forRoot(), - SharedModule, - ClusterModule, - DashboardModule, - CephModule, - CoreModule - ], - declarations: [], - providers: [i18nProviders] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(AlertListComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - describe('show action buttons and drop down actions depending on permissions', () => { - let tableActions: TableActionsComponent; - let scenario: { fn; empty; single }; - let permissionHelper: PermissionHelper; - let combinations: number[][]; - - const getTableActionComponent = (): TableActionsComponent => { - fixture.detectChanges(); - return fixture.debugElement.query(By.directive(TableActionsComponent)).componentInstance; - }; - - beforeEach(() => { - permissionHelper = new PermissionHelper(component.permission, () => - getTableActionComponent() - ); - scenario = { - fn: () => tableActions.getCurrentButton().name, - single: 'Create silence', - empty: 'Create silence' - }; - tableActions = permissionHelper.setPermissionsAndGetActions(1, 1, 1); - }); - - const permissionSwitch = (combination) => { - tableActions = permissionHelper.setPermissionsAndGetActions( - combination[0], - combination[1], - combination[2] - ); - tableActions.tableActions = component.tableActions; - tableActions.ngOnInit(); - }; - - const testCombinations = (test: Function) => { - combinations.forEach((combination) => { - permissionSwitch(combination); - test(); - }); - }; - - describe('with every permission combination that includes create', () => { - beforeEach(() => { - combinations = [[1, 1, 1], [1, 1, 0], [1, 0, 1], [1, 0, 0]]; - }); - - it(`always shows 'Create silence' as main action`, () => { - testCombinations(() => permissionHelper.testScenarios(scenario)); - }); - - it('shows all actions', () => { - testCombinations(() => { - expect(tableActions.tableActions.length).toBe(1); - expect(tableActions.tableActions).toEqual(component.tableActions); - }); - }); - }); - - describe('with every permission combination that does not include create', () => { - beforeEach(() => { - combinations = [[0, 1, 1], [0, 1, 0], [0, 0, 1], [0, 0, 0]]; - }); - - it(`won't show any action`, () => { - testCombinations(() => { - permissionHelper.testScenarios({ - fn: () => tableActions.getCurrentButton(), - single: undefined, - empty: undefined - }); - }); - }); - - it('shows no actions', () => { - testCombinations(() => { - expect(tableActions.tableActions.length).toBe(0); - expect(tableActions.tableActions).toEqual([]); - }); - }); - }); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.ts deleted file mode 100644 index 927f3cff4d4b..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/alert-list/alert-list.component.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; -import { I18n } from '@ngx-translate/i18n-polyfill'; -import { CellTemplate } from '../../../../shared/enum/cell-template.enum'; -import { Icons } from '../../../../shared/enum/icons.enum'; -import { CdTableAction } from '../../../../shared/models/cd-table-action'; -import { CdTableColumn } from '../../../../shared/models/cd-table-column'; -import { CdTableSelection } from '../../../../shared/models/cd-table-selection'; -import { Permission } from '../../../../shared/models/permissions'; -import { CdDatePipe } from '../../../../shared/pipes/cd-date.pipe'; -import { AuthStorageService } from '../../../../shared/services/auth-storage.service'; -import { PrometheusAlertService } from '../../../../shared/services/prometheus-alert.service'; -import { URLBuilderService } from '../../../../shared/services/url-builder.service'; - -const BASE_URL = 'silence'; // as only silence actions can be used - -@Component({ - selector: 'cd-prometheus-list', - providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }], - templateUrl: './alert-list.component.html', - styleUrls: ['./alert-list.component.scss'] -}) -export class AlertListComponent implements OnInit { - @ViewChild('externalLinkTpl') - externalLinkTpl: TemplateRef; - columns: CdTableColumn[]; - tableActions: CdTableAction[]; - permission: Permission; - selection = new CdTableSelection(); - icons = Icons; - customCss = { - 'label label-danger': 'active', - 'label label-warning': 'unprocessed', - 'label label-info': 'suppressed' - }; - - constructor( - // NotificationsComponent will refresh all alerts every 5s (No need to do it here as well) - private authStorageService: AuthStorageService, - public prometheusAlertService: PrometheusAlertService, - private urlBuilder: URLBuilderService, - private i18n: I18n, - private cdDatePipe: CdDatePipe - ) { - this.permission = this.authStorageService.getPermissions().prometheus; - this.tableActions = [ - { - permission: 'create', - canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection, - disable: (selection: CdTableSelection) => - !selection.hasSingleSelection || selection.first().cdExecuting, - icon: Icons.add, - routerLink: () => this.urlBuilder.getCreateFrom(this.selection.first().fingerprint), - name: this.i18n('Create silence') - } - ]; - } - - ngOnInit() { - this.columns = [ - { - name: this.i18n('Name'), - prop: 'labels.alertname', - flexGrow: 2 - }, - { - name: this.i18n('Job'), - prop: 'labels.job', - flexGrow: 2 - }, - { - name: this.i18n('Severity'), - prop: 'labels.severity' - }, - { - name: this.i18n('State'), - prop: 'status.state', - cellTransformation: CellTemplate.classAdding - }, - { - name: this.i18n('Started'), - prop: 'startsAt', - pipe: this.cdDatePipe - }, - { - name: this.i18n('URL'), - prop: 'generatorURL', - sortable: false, - cellTemplate: this.externalLinkTpl - } - ]; - } - - updateSelection(selection: CdTableSelection) { - this.selection = selection; - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/monitoring-list/monitoring-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/monitoring-list/monitoring-list.component.html new file mode 100644 index 000000000000..55f571d8427a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/monitoring-list/monitoring-list.component.html @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/monitoring-list/monitoring-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/monitoring-list/monitoring-list.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/monitoring-list/monitoring-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/monitoring-list/monitoring-list.component.spec.ts new file mode 100644 index 000000000000..bf0bf6c05672 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/monitoring-list/monitoring-list.component.spec.ts @@ -0,0 +1,46 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +import { ToastrModule } from 'ngx-toastr'; + +import { i18nProviders } from '../../../../../testing/unit-test-helper'; +import { AuthModule } from '../../../../core/auth/auth.module'; +import { CoreModule } from '../../../../core/core.module'; +import { CephfsModule } from '../../../cephfs/cephfs.module'; +import { DashboardModule } from '../../../dashboard/dashboard.module'; +import { NfsModule } from '../../../nfs/nfs.module'; +import { ClusterModule } from '../../cluster.module'; +import { MonitoringListComponent } from './monitoring-list.component'; + +describe('MonitoringListComponent', () => { + let component: MonitoringListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ClusterModule, + DashboardModule, + CephfsModule, + AuthModule, + NfsModule, + CoreModule, + ToastrModule.forRoot(), + HttpClientTestingModule + ], + declarations: [], + providers: [i18nProviders] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitoringListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/monitoring-list/monitoring-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/monitoring-list/monitoring-list.component.ts new file mode 100644 index 000000000000..efc30190faed --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/monitoring-list/monitoring-list.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { TabDirective, TabsetComponent } from 'ngx-bootstrap/tabs'; + +import { PrometheusAlertService } from '../../../../shared/services/prometheus-alert.service'; + +@Component({ + selector: 'cd-monitoring-list', + templateUrl: './monitoring-list.component.html', + styleUrls: ['./monitoring-list.component.scss'] +}) +export class MonitoringListComponent implements OnInit { + @ViewChild('tabs') + tabs: TabsetComponent; + + constructor( + public prometheusAlertService: PrometheusAlertService, + private route: ActivatedRoute, + private router: Router + ) {} + + ngOnInit() { + // Activate tab according to given fragment + if (this.route.snapshot.fragment) { + const tab = this.tabs.tabs.find( + (t) => t.elementRef.nativeElement.id === this.route.snapshot.fragment + ); + if (tab) { + tab.active = true; + } + // Ensure fragment is not removed, so page can always be reloaded with the same tab open. + this.router.navigate([], { fragment: this.route.snapshot.fragment }); + } + } + + setFragment(element: TabDirective) { + this.router.navigate([], { fragment: element.id }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html deleted file mode 100644 index b2f530a7e862..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts deleted file mode 100644 index 65cc58d235dd..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { Router } from '@angular/router'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { TabsModule } from 'ngx-bootstrap/tabs'; - -import { configureTestBed } from '../../../../../testing/unit-test-helper'; -import { PrometheusTabsComponent } from './prometheus-tabs.component'; - -describe('PrometheusTabsComponent', () => { - let component: PrometheusTabsComponent; - let fixture: ComponentFixture; - let router: Router; - - const selectTab = (index) => { - fixture.debugElement.queryAll(By.css('tab'))[index].triggerEventHandler('select', null); - }; - - configureTestBed({ - declarations: [PrometheusTabsComponent], - imports: [RouterTestingModule, HttpClientTestingModule, TabsModule.forRoot()] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(PrometheusTabsComponent); - component = fixture.componentInstance; - router = TestBed.get(Router); - spyOn(router, 'navigate').and.stub(); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should redirect to alert listing', () => { - selectTab(0); - expect(router.navigate).toHaveBeenCalledWith(['/alerts']); - }); - - it('should redirect to silence listing', () => { - selectTab(1); - expect(router.navigate).toHaveBeenCalledWith(['/silence']); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.ts deleted file mode 100644 index 5675eb710023..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; - -@Component({ - selector: 'cd-prometheus-tabs', - templateUrl: './prometheus-tabs.component.html', - styleUrls: ['./prometheus-tabs.component.scss'] -}) -export class PrometheusTabsComponent { - url: string; - - constructor(private router: Router) { - this.url = this.router.url; - } - - navigateTo(url) { - this.router.navigate([url]); - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts index cd338b4355d7..73d5fd8a620c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts @@ -45,7 +45,7 @@ describe('SilenceFormComponent', () => { let ifPrometheusSpy; // Helper let prometheus: PrometheusHelper; - let formH: FormHelper; + let formHelper: FormHelper; let fixtureH: FixtureHelper; let params; // Date mocking related @@ -86,10 +86,10 @@ describe('SilenceFormComponent', () => { const changeAction = (action: string) => { const modes = { - add: '/silence/add', - alertAdd: '/silence/add/someAlert', - recreate: '/silence/recreate/someExpiredId', - edit: '/silence/edit/someNotExpiredId' + add: '/monitoring/silence/add', + alertAdd: '/monitoring/silence/add/someAlert', + recreate: '/monitoring/silence/recreate/someExpiredId', + edit: '/monitoring/silence/edit/someNotExpiredId' }; Object.defineProperty(router, 'url', { value: modes[action] }); callInit(); @@ -138,7 +138,7 @@ describe('SilenceFormComponent', () => { fixtureH = new FixtureHelper(fixture); component = fixture.componentInstance; form = component.form; - formH = new FormHelper(form); + formHelper = new FormHelper(form); fixture.detectChanges(); }); @@ -333,7 +333,7 @@ describe('SilenceFormComponent', () => { it('should raise invalid start date error', fakeAsync(() => { changeStartDate('No valid date'); - formH.expectError('startsAt', 'bsDate'); + formHelper.expectError('startsAt', 'bsDate'); expect(form.getValue('startsAt').toString()).toBe('Invalid Date'); expect(form.getValue('endsAt')).toEqual(new Date('2022-02-22T02:00:00')); })); @@ -341,9 +341,9 @@ describe('SilenceFormComponent', () => { describe('on duration change', () => { it('changes end date if duration is changed', () => { - formH.setValue('duration', '15m'); + formHelper.setValue('duration', '15m'); expect(form.getValue('endsAt')).toEqual(new Date('2022-02-22T00:15')); - formH.setValue('duration', '5d 23h'); + formHelper.setValue('duration', '5d 23h'); expect(form.getValue('endsAt')).toEqual(new Date('2022-02-27T23:00')); }); }); @@ -363,7 +363,7 @@ describe('SilenceFormComponent', () => { it('should raise invalid end date error', fakeAsync(() => { changeEndDate('No valid date'); - formH.expectError('endsAt', 'bsDate'); + formHelper.expectError('endsAt', 'bsDate'); expect(form.getValue('endsAt').toString()).toBe('Invalid Date'); expect(form.getValue('startsAt')).toEqual(baseTime); })); @@ -371,20 +371,20 @@ describe('SilenceFormComponent', () => { }); it('should have a creator field', () => { - formH.expectValid('createdBy'); - formH.expectErrorChange('createdBy', '', 'required'); - formH.expectValidChange('createdBy', 'Mighty FSM'); + formHelper.expectValid('createdBy'); + formHelper.expectErrorChange('createdBy', '', 'required'); + formHelper.expectValidChange('createdBy', 'Mighty FSM'); }); it('should have a comment field', () => { - formH.expectError('comment', 'required'); - formH.expectValidChange('comment', 'A pretty long comment'); + formHelper.expectError('comment', 'required'); + formHelper.expectValidChange('comment', 'A pretty long comment'); }); it('should be a valid form if all inputs are filled and at least one matcher was added', () => { expect(form.valid).toBeFalsy(); - formH.expectValidChange('createdBy', 'Mighty FSM'); - formH.expectValidChange('comment', 'A pretty long comment'); + formHelper.expectValidChange('createdBy', 'Mighty FSM'); + formHelper.expectValidChange('comment', 'A pretty long comment'); addMatcher('job', 'someJob', false); expect(form.valid).toBeTruthy(); }); @@ -525,7 +525,7 @@ describe('SilenceFormComponent', () => { const fillAndSubmit = () => { ['createdBy', 'comment'].forEach((attr) => { - formH.setValue(attr, silence[attr]); + formHelper.setValue(attr, silence[attr]); }); silence.matchers.forEach((matcher) => addMatcher(matcher.name, matcher.value, matcher.isRegex) @@ -574,10 +574,10 @@ describe('SilenceFormComponent', () => { expect(router.navigate).not.toHaveBeenCalled(); }); - it('should route back to "/silence" on success', () => { + it('should route back to previous tab on success', () => { fillAndSubmit(); expect(form.valid).toBeTruthy(); - expect(router.navigate).toHaveBeenCalledWith(['/silence']); + expect(router.navigate).toHaveBeenCalledWith(['/monitoring'], { fragment: 'silences' }); }); it('should create a silence', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts index 8e984e907584..d5ef6b35e6f5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts @@ -96,8 +96,8 @@ export class SilenceFormComponent { } private chooseMode() { - this.edit = this.router.url.startsWith('/silence/edit'); - this.recreate = this.router.url.startsWith('/silence/recreate'); + this.edit = this.router.url.startsWith('/monitoring/silence/edit'); + this.recreate = this.router.url.startsWith('/monitoring/silence/recreate'); if (this.edit) { this.action = this.actionLabels.EDIT; } else if (this.recreate) { @@ -294,7 +294,7 @@ export class SilenceFormComponent { } this.prometheusService.setSilence(this.getSubmitData()).subscribe( (resp) => { - this.router.navigate(['/silence']); + this.router.navigate(['/monitoring'], { fragment: 'silences' }); this.notificationService.show( NotificationType.success, this.getNotificationTile(resp.body['silenceId']), diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.html index f67f127cd03c..fef517d85711 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.html @@ -1,5 +1,3 @@ - - { @@ -38,7 +37,7 @@ describe('SilenceListComponent', () => { RouterTestingModule, HttpClientTestingModule ], - declarations: [SilenceListComponent, PrometheusTabsComponent], + declarations: [SilenceListComponent], providers: [i18nProviders] }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts index 4cab7463a539..c423b001c51d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts @@ -24,7 +24,7 @@ import { AuthStorageService } from '../../../../shared/services/auth-storage.ser import { NotificationService } from '../../../../shared/services/notification.service'; import { URLBuilderService } from '../../../../shared/services/url-builder.service'; -const BASE_URL = 'silence'; +const BASE_URL = 'monitoring/silence'; @Component({ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }], @@ -68,6 +68,7 @@ export class SilenceListComponent implements OnInit { permission: 'create', icon: Icons.add, routerLink: () => this.urlBuilder.getCreate(), + preserveFragment: true, canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection, name: this.actionLabels.CREATE }, @@ -82,6 +83,7 @@ export class SilenceListComponent implements OnInit { !selectionExpired(selection), icon: Icons.copy, routerLink: () => this.urlBuilder.getRecreate(this.selection.first().id), + preserveFragment: true, name: this.actionLabels.RECREATE }, { @@ -95,6 +97,7 @@ export class SilenceListComponent implements OnInit { (selection.first().cdExecuting && !selectionExpired(selection)) || selectionExpired(selection), routerLink: () => this.urlBuilder.getEdit(this.selection.first().id), + preserveFragment: true, name: this.actionLabels.EDIT }, { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.html index 3ca38d9735dc..cca56a32dff7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.html @@ -4,7 +4,8 @@ [ngClass]="{ 'active': last }" class="breadcrumb-item"> {{ crumb.text }} + [routerLink]="crumb.path" + preserveFragment>{{ crumb.text }} {{ crumb.text }} 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 681fa6dc25f3..309214d72afb 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 @@ -99,16 +99,10 @@ routerLink="/logs">Logs
  • Alerts -
  • -
  • - Silences + routerLink="/monitoring">Monitoring
  • 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 3e0cd6f1d13f..b5ecd68fd55f 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 @@ -40,7 +40,9 @@ export class NavigationComponent implements OnInit { this.summaryData = data; }); if (this.permissions.configOpt.read) { - this.prometheusService.ifAlertmanagerConfigured(() => (this.prometheusConfigured = true)); + this.prometheusService.ifAlertmanagerConfigured(() => { + this.prometheusConfigured = true; + }); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.html index e0d6c209f923..65ad67defb4c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-actions/table-actions.component.html @@ -5,7 +5,8 @@ class="btn btn-sm btn-primary" [ngClass]="{'disabled': disableSelectionAction(action)}" (click)="useClickAction(action)" - [routerLink]="useRouterLink(action)"> + [routerLink]="useRouterLink(action)" + [preserveFragment]="action.preserveFragment ? '' : null"> {{ action.name }} @@ -28,7 +29,8 @@ data-toggle="tooltip" title="{{ useDisableDesc(action) }}"> + [routerLink]="useRouterLink(action)" + [preserveFragment]="action.preserveFragment ? '' : null"> {{ action.name }} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-action.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-action.ts index c5c6fab131e1..ac8dcb61a98d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-action.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-action.ts @@ -6,6 +6,8 @@ export class CdTableAction { // or none if it's not needed routerLink?: string | Function; + preserveFragment? = false; + // This is the function that will be triggered on a click event if defined click?: Function;