it must be explicitly loaded in the configuration file or code (see https://github.com/openssl/openssl/blob/master/README-PROVIDERS.md).
* RGW: Fixed bucket notification events so the 'x_amz_request_id' in NotificationEvent now matches the 'x_amz_request_id' returned by the corresponding S3 operation.
-
+* DASHBOARD: Introduces a new landing page - "Overview". This revamps UX and adds more information in the landing page - overall cluster health, health checks, resilency panel (showing active/clean Pgs status), total and used raw capacity, alerts, capacity breakdown by object, file and block and performance charts - throughput, latency and IOPS. This renames teh landing page from "Dashboard" to "Overview"
* DASHBOARD: Removed the older landing page which was deprecated in Quincy.
Admins can no longer enable the older, deprecated landing page layout by
adjusting FEATURE_TOGGLE_DASHBOARD.
env: {
LOGIN_USER: 'admin',
LOGIN_PWD: 'admin',
- CEPH2_URL: 'https://localhost:11002/'
+ CEPH2_URL: 'https://localhost:4202/'
},
chromeWebSecurity: false,
)
return require('./cypress/plugins/index.js')(on, config);
},
- baseUrl: 'https://localhost:11000/',
+ baseUrl: 'https://localhost:4200/',
excludeSpecPattern: ['*.po.ts', '**/orchestrator/**'],
experimentalSessionAndOrigin: true,
specPattern: 'cypress/e2e/**/*-spec.{js,jsx,ts,tsx,feature}'
-import { OvevriewPagehelper } from '../ui/dashboard-v3.po';
+import { OverviewPagehelper } from '../ui/overview.po';
describe('Overview Page', { retries: 0 }, () => {
- const overview = new OvevriewPagehelper();
+ const overview = new OverviewPagehelper();
beforeEach(() => {
cy.intercept('GET', '**/api/prometheus/data*', {
cy.get('[type=submit]').click();
cy.get('[data-testid="pool-name"]').clear().type(name);
- cy.get('[data-testid="pool-type-select"]').select('replicated');
- cy.get('[data-testid="pool-type-select"] option:checked').contains('replicated');
+
+ cy.get(
+ '[data-testid="pool-type-select"] cds-radio input[type="radio"][value="replicated"]'
+ ).check({ force: true });
+
+ cy.get('cds-combo-box[id="applications"] input.cds--text-input').click({ force: true });
+ cy.get('.cds--list-box__menu.cds--multi-select').should('be.visible');
+ cy.get('.cds--list-box__menu.cds--multi-select .cds--checkbox-label')
+ .contains('.cds--checkbox-label-text', 'rbd', { matchCase: false })
+ .parent()
+ .click({ force: true });
+ cy.get('body').type('{esc}');
+
cy.get('cd-submit-button').click();
// Wait for form submission navigation to complete
cy.url().should('include', '/pool');
import { OSDsPageHelper } from '../cluster/osds.po';
-import { OvevriewPagehelper } from '../ui/dashboard-v3.po';
+import { OverviewPagehelper } from '../ui/overview.po';
describe('OSDs page', () => {
const osds = new OSDsPageHelper();
- const overview = new OvevriewPagehelper();
+ const overview = new OverviewPagehelper();
before(() => {
cy.login();
// landing page is easier to check OSD status
overview.navigateTo();
- overview.cardRow('OSD').should('contain.text', `${expectedCount} OSDs`);
+ overview.clickSystemsTab();
+ cy.get(`[data-test-id="OSD-value"]`).should(
+ 'contain.text',
+ `${expectedCount}/${expectedCount} in/up`
+ );
cy.wait(30000);
expect(Number(newCount)).to.be.gte(2);
+++ /dev/null
-import { OvevriewPagehelper } from './dashboard-v3.po';
-
-describe('Dashboard-v3 Main Page', () => {
- const overview = new OvevriewPagehelper();
-
- before(() => {
- cy.login();
- });
-
- beforeEach(() => {
- cy.login();
- overview.navigateTo();
- });
-
- describe('Check that all hyperlinks on inventory card lead to the correct page and fields exist', () => {
- it('should ensure that all linked pages in the inventory card lead to correct page', () => {
- const expectationMap = {
- Host: 'Hosts',
- Monitor: 'Monitors',
- OSDs: 'OSDs',
- Pool: 'Pools',
- 'Object Gateway': 'Gateways'
- };
-
- for (const [linkText, breadcrumbText] of Object.entries(expectationMap)) {
- cy.location('hash').should('eq', '#/overview');
- overview.clickInventoryCardLink(linkText);
- overview.expectBreadcrumbText(breadcrumbText);
- overview.navigateBack();
- }
- });
-
- it('should verify that cards exist on overview in proper order', () => {
- // Ensures that cards are all displayed on the overview tab while being in the proper
- // order, checks for card title and position via indexing into a list of all cards.
- const order = ['Details', 'Inventory', 'Status', 'Capacity', 'Cluster Utilization'];
-
- for (let i = 0; i < order.length; i++) {
- overview.card(i).should('contain.text', order[i]);
- }
- });
- });
-});
+++ /dev/null
-import { PageHelper } from '../page-helper.po';
-
-export class OvevriewPagehelper extends PageHelper {
- pages = { index: { url: '#/overview', id: 'cd-overview' } };
-
- cardTitle(index: number) {
- return cy.get('.card-title').its(index).text();
- }
-
- clickInventoryCardLink(link: string) {
- console.log(link);
- cy.get(`cd-card[cardTitle="Inventory"]`).contains('a', link).click();
- }
-
- card(indexOrTitle: number) {
- cy.get('cd-card').as('cards');
-
- return cy.get('@cards').its(indexOrTitle);
- }
-
- cardRow(rowName: string) {
- return cy.get(`[data-testid=${rowName}]`);
- }
-}
export class LanguagePageHelper extends PageHelper {
pages = {
- index: { url: '#/overview', id: 'cd-dashboard' }
+ index: { url: '#/overview', id: 'cd-overview' }
};
getLanguageBtn() {
export class LoginPageHelper extends PageHelper {
pages = {
index: { url: '#/login', id: 'cd-login' },
- overview: { url: '#/overview', id: 'cd-dashboard' }
+ overview: { url: '#/overview', id: 'cd-overview' }
};
doLogin() {
cy.get('[name=username]').type('admin');
cy.get('#password').type('admin');
cy.get('[type=submit]').click();
- cy.get('cd-dashboard').should('exist');
+ cy.get('cd-overview').should('exist');
}
doLogout() {
navs.forEach((nav: any) => {
cy.get('cds-sidenav-item').each(($link) => {
if ($link.text().trim() === nav.menu.trim()) {
- cy.wrap($link).click();
+ cy.wrap($link).click({ force: true });
}
});
if (nav.submenus) {
export class NotificationSidebarPageHelper extends PageHelper {
getNotificationIcon() {
- return cy.get('cd-notifications a');
+ return cy.get(`[data-testid='header-notification-icon']`);
}
getPanel() {
}
open() {
- this.getNotificationIcon().click();
+ this.getNotificationIcon().click({ force: true });
this.getPanel().should('exist');
this.getSidebar().should('exist');
}
--- /dev/null
+import { PageHelper } from '../page-helper.po';
+
+export class OverviewPagehelper extends PageHelper {
+ pages = { index: { url: '#/overview', id: 'cd-overview' } };
+
+ cardTitle(index: number) {
+ return cy.get('.card-title').its(index).text();
+ }
+
+ clickInventoryCardLink(link: string) {
+ console.log(link);
+ cy.get(`cd-card[cardTitle="Inventory"]`).contains('a', link).click();
+ }
+
+ card(indexOrTitle: number) {
+ cy.get('cd-card').as('cards');
+
+ return cy.get('@cards').its(indexOrTitle);
+ }
+
+ clickSystemsTab() {
+ cy.get(`[data-test-id="systems-tab"]`).click();
+ }
+
+ cardRow(rowName: string) {
+ return cy.get(`[data-testid=${rowName}]`);
+ }
+}
import { PageHelper } from '../page-helper.po';
const pages = {
- cephfsMirroring: { url: '#/cephfs/mirroring', id: 'cd-cephfs-mirroring-list' }
+ cephfsMirroring: { url: '#/cephfs/mirroring', id: 'cd-cephfs-mirroring-error' }
};
export class PageHeaderPageHelper extends PageHelper {
let auth: any;
const fillAuth = () => {
- window.localStorage.setItem(LocalStorage.DASHBOARD_USRENAME, auth.username);
+ window.localStorage.setItem(LocalStorage.DASHBOARD_USERNAME, auth.username);
window.localStorage.setItem('dashboard_permissions', auth.permissions);
window.localStorage.setItem('user_pwd_expiration_date', auth.pwdExpirationDate);
window.localStorage.setItem('user_pwd_update_required', auth.pwdUpdateRequired);
class="cds-ml-2"
[caret]="true"
(click)="toggleSection('system')"
+ data-test-id="systems-tab"
description=""
i18n-description>
<span
[class.border-subtle-right]="!isLast">
<span class="overview-health-card-icon-and-text">
<cd-icon [type]="vm?.[item.key]?.severity"></cd-icon>
- <span class="cds--type-body-compact-01">
+ <span
+ class="cds--type-body-compact-01"
+ [data-test-id]="item.label">
{{ item.label }}
</span>
</span>
- <p class="cds--type-label-01 cds-mt-3 cds-mb-0 overview-health-card-secondary-text">
+ <p
+ class="cds--type-label-01 cds-mt-3 cds-mb-0 overview-health-card-secondary-text"
+ [data-test-id]="item.label+'-value'">
{{ vm?.[item.key]?.value }}
</p>
</div>
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import {
+ ComponentFixture,
+ discardPeriodicTasks,
+ fakeAsync,
+ TestBed,
+ tick
+} from '@angular/core/testing';
import { of, Subject, throwError } from 'rxjs';
import { OverviewComponent } from './overview.component';
};
let mockPrometheusService: {
- isPrometheusUsable: jest.Mock;
+ refreshPrometheusUsable: jest.Mock;
};
beforeEach(async () => {
mockPrometheusService = {
- isPrometheusUsable: jest.fn().mockReturnValue(of(true))
+ refreshPrometheusUsable: jest.fn().mockReturnValue(of(true))
};
mockHealthService = { getHealthSnapshot: jest.fn() };
mockRefreshIntervalService.intervalData$.complete();
});
- it('storageCardVm$ should emit storage view model with mapped fields', (done) => {
+ it('storageCardVm$ should emit storage view model with mapped fields', fakeAsync((done) => {
const mockData: HealthSnapshotMap = {
fsid: 'fsid-storage',
health: { status: 'HEALTH_OK', checks: {} },
done();
});
+ tick(0);
mockRefreshIntervalService.intervalData$.next();
- });
+ discardPeriodicTasks();
+ }));
it('storageCardVm$ should emit safe defaults before storage side streams resolve', (done) => {
const mockData: HealthSnapshotMap = {
} as any;
mockHealthService.getHealthSnapshot.mockReturnValue(of(mockData));
- mockOverviewStorageService.getStorageBreakdown.mockReturnValue(of(null));
+ mockOverviewStorageService.getStorageBreakdown.mockReturnValue(of({ result: [] }));
const sub = component.storageCardVm$.subscribe((vm) => {
expect(vm.totalCapacity).toBe(1000);
ViewEncapsulation
} from '@angular/core';
import { GridModule, LayoutModule, TilesModule } from 'carbon-components-angular';
-import { combineLatest, EMPTY, Observable } from 'rxjs';
-import { catchError, exhaustMap, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
+import { combineLatest, EMPTY, Observable, timer } from 'rxjs';
+import {
+ catchError,
+ distinctUntilChanged,
+ exhaustMap,
+ map,
+ shareReplay,
+ startWith,
+ switchMap
+} from 'rxjs/operators';
import { HealthService } from '~/app/shared/api/health.service';
import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
const SECONDS_PER_HOUR = 3600;
const SECONDS_PER_DAY = 86400;
const TREND_DAYS = 7;
+const PROMETHUES_CONFIG_POLL_INTERVAL = 60000;
@Component({
selector: 'cd-overview',
);
/* EMPTY STATE DATA */
- readonly isPrometheusUsable$ = this.prometheusService
- .isPrometheusUsable()
- .pipe(shareReplay({ bufferSize: 1, refCount: true }));
+ readonly isPromethuesConfigured$ = timer(0, PROMETHUES_CONFIG_POLL_INTERVAL).pipe(
+ switchMap(() => this.prometheusService.refreshPrometheusUsable()),
+ distinctUntilChanged(),
+ shareReplay({ bufferSize: 1, refCount: true })
+ );
readonly hasNoOSDs$ = this.healthData$.pipe(
map((data: HealthSnapshotMap) => (data?.osdmap?.num_osds ?? 0) === 0),
readonly storageEmptyState$ = this.hasNoOSDs$.pipe(startWith(false)).pipe(
map((hasNoOSDs) => {
if (hasNoOSDs) {
- return $localize`You must have storage configured to access this capability.`;
+ return $localize`You can view capacity usage and related metrics here once you add storage.`;
}
return '';
}),
shareReplay({ bufferSize: 1, refCount: true })
);
- readonly prometheusEmptyState$ = this.isPrometheusUsable$.pipe(
- map((isPrometheusUsable) =>
- isPrometheusUsable
+ readonly prometheusEmptyState$ = this.isPromethuesConfigured$.pipe(
+ map((isPromethuesConfigured) =>
+ isPromethuesConfigured
? ''
: $localize`You must have Prometheus configured to access this capability.`
),
shareReplay({ bufferSize: 1, refCount: true })
);
- readonly averageConsumption$ = this.isPrometheusUsable$.pipe(
- switchMap((usable) =>
- usable
+ readonly averageConsumption$ = this.isPromethuesConfigured$.pipe(
+ switchMap((isConfigured) =>
+ isConfigured
? this.refreshIntervalObs(() => this.overviewStorageService.getAverageConsumption())
: EMPTY
),
shareReplay({ bufferSize: 1, refCount: true })
);
- readonly timeUntilFull$ = this.isPrometheusUsable$.pipe(
- switchMap((usable) =>
- usable ? this.refreshIntervalObs(() => this.overviewStorageService.getTimeUntilFull()) : EMPTY
+ readonly timeUntilFull$ = this.isPromethuesConfigured$.pipe(
+ switchMap((isConfigured) =>
+ isConfigured
+ ? this.refreshIntervalObs(() => this.overviewStorageService.getTimeUntilFull())
+ : EMPTY
),
shareReplay({ bufferSize: 1, refCount: true })
);
- readonly breakdownRawData$ = this.isPrometheusUsable$.pipe(
- switchMap((usable) =>
- usable
+ readonly breakdownRawData$ = this.isPromethuesConfigured$.pipe(
+ switchMap((isConfigured) =>
+ isConfigured
? this.refreshIntervalObs(() => this.overviewStorageService.getStorageBreakdown())
: EMPTY
),
shareReplay({ bufferSize: 1, refCount: true })
);
- readonly capacityThresholds$ = this.isPrometheusUsable$.pipe(
- switchMap((usable) =>
- usable
+ readonly capacityThresholds$ = this.isPromethuesConfigured$.pipe(
+ switchMap((isConfigured) =>
+ isConfigured
? this.refreshIntervalObs(() => this.overviewStorageService.getRawCapacityThresholds())
: EMPTY
),
// getTrendData() is already a polling stream through getRangeQueriesData()
// hence no refresh needed.
- readonly trendData$ = this.isPrometheusUsable$.pipe(
- switchMap((usable) =>
- usable
+ readonly trendData$ = this.isPromethuesConfigured$.pipe(
+ switchMap((isConfigured) =>
+ isConfigured
? this.overviewStorageService.getTrendData(
Math.floor(Date.now() / 1000) - TREND_DAYS * SECONDS_PER_DAY,
Math.floor(Date.now() / 1000),
setVariables() {
const NOT_AVAILABLE = $localize`Not available`;
const project = {} as any;
- project.user = localStorage.getItem(LocalStorage.DASHBOARD_USRENAME);
+ project.user = localStorage.getItem(LocalStorage.DASHBOARD_USERNAME);
project.role = USER;
if (this.userPermission.read) {
this.userService.get(project.user).subscribe((data: any) => {
<cd-language-selector class="d-flex"></cd-language-selector>
</cds-header-navigation>
<div class="cds--btn cds--btn--icon-only cds--header__action"
+ data-testid="header-notification-icon"
(click)="onNotificationSelected($event)">
<cd-notifications></cd-notifications>
</div>
expect(req.request.body).toEqual(fakeCredentials);
req.flush(fakeResponse);
tick();
- expect(localStorage.getItem(LocalStorage.DASHBOARD_USRENAME)).toBe('foo');
+ expect(localStorage.getItem(LocalStorage.DASHBOARD_USERNAME)).toBe('foo');
}));
it('should logout and remove the user', () => {
const req = httpTesting.expectOne('api/auth/logout');
expect(req.request.method).toBe('POST');
req.flush({ redirect_url: '#/login' });
- expect(localStorage.getItem(LocalStorage.DASHBOARD_USRENAME)).toBe(null);
+ expect(localStorage.getItem(LocalStorage.DASHBOARD_USERNAME)).toBe(null);
expect(router.navigate).toBeCalledTimes(1);
});
});
}
isPrometheusUsable(): Observable<boolean> {
- return this.isPrometheusModuleEnabled().pipe(
- switchMap((enabled) =>
- enabled ? this.isSettingConfigured(this.settingsKey.prometheus) : of(false)
- ),
+ return this.isSettingConfigured(this.settingsKey.prometheus).pipe(
+ map((isConfigured) => isConfigured),
catchError(() => of(false))
);
}
+ refreshPrometheusUsable(): Observable<boolean> {
+ delete this.settings[this.settingsKey.prometheus];
+ return this.isPrometheusUsable();
+ }
+
isAlertmanagerUsable(): Observable<boolean> {
return this.isPrometheusModuleEnabled().pipe(
switchMap((enabled) =>
});
describe('getStorageBreakdown', () => {
- it('should call getPrometheusQueryData with storage breakdown query', () => {
+ it('should call getGaugeQueryData with storage breakdown query', () => {
const promSpy = jest
- .spyOn(service['prom'], 'getPrometheusQueryData')
+ .spyOn(service['prom'], 'getGaugeQueryData')
.mockReturnValue(of({}) as any);
service.getStorageBreakdown().subscribe();
- expect(promSpy).toHaveBeenCalledWith({
- params:
- 'sum by (application) (ceph_pool_bytes_used * on(pool_id) group_left(instance, name, application) ceph_pool_metadata{application=~"(.*Block.*)|(.*Filesystem.*)|(.*Object.*)|(..*)"})'
- });
+ expect(promSpy).toHaveBeenCalledWith(
+ 'sum by (application) (ceph_pool_bytes_used * on(pool_id) group_left(instance, name, application) ceph_pool_metadata{application=~"(.*Block.*)|(.*Filesystem.*)|(.*Object.*)|(..*)"})'
+ );
});
});
}
getStorageBreakdown(): Observable<PromqlGuageMetric> {
- return this.prom.getPrometheusQueryData({ params: this.RAW_USED_BY_STORAGE_TYPE_QUERY });
+ return this.prom.getGaugeQueryData(this.RAW_USED_BY_STORAGE_TYPE_QUERY);
}
getThresholdStatus(total, used, nearfull, full): CapacityThreshold {
@use '@carbon/colors';
-empty-state {
+.empty-state {
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: var(--cds-spacing-05);
- height: 350px;
+ margin-top: 283px;
+ padding: var(--cds-spacing-05) var(--cds-spacing-05) var(--cds-spacing-07) var(--cds-spacing-05);
+ width: 264px;
+ margin-left: var(--cds-spacing-05);
- p {
- font-size: 12px !important;
+ img {
+ width: 80px !important;
+ height: 80px !important;
}
- img {
- width: 100px !important;
- height: 100px !important;
+ span {
+ color: var(--cds-text-secondary);
}
}
export enum LocalStorage {
- DASHBOARD_USRENAME = 'dashboard_username'
+ DASHBOARD_USERNAME = 'dashboard_username'
}
it('should store username', () => {
service.set(username, '');
- expect(localStorage.getItem(LocalStorage.DASHBOARD_USRENAME)).toBe(username);
+ expect(localStorage.getItem(LocalStorage.DASHBOARD_USERNAME)).toBe(username);
});
it('should remove username', () => {
service.set(username, '');
service.remove();
- expect(localStorage.getItem(LocalStorage.DASHBOARD_USRENAME)).toBe(null);
+ expect(localStorage.getItem(LocalStorage.DASHBOARD_USERNAME)).toBe(null);
});
it('should be loggedIn', () => {
pwdExpirationDate: number = null,
pwdUpdateRequired: boolean = false
) {
- localStorage.setItem(LocalStorage.DASHBOARD_USRENAME, username);
+ localStorage.setItem(LocalStorage.DASHBOARD_USERNAME, username);
localStorage.setItem('dashboard_permissions', JSON.stringify(new Permissions(permissions)));
localStorage.setItem('user_pwd_expiration_date', String(pwdExpirationDate));
localStorage.setItem('user_pwd_update_required', String(pwdUpdateRequired));
}
remove() {
- localStorage.removeItem(LocalStorage.DASHBOARD_USRENAME);
+ localStorage.removeItem(LocalStorage.DASHBOARD_USERNAME);
localStorage.removeItem('user_pwd_expiration_data');
localStorage.removeItem('user_pwd_update_required');
}
isLoggedIn() {
- return localStorage.getItem(LocalStorage.DASHBOARD_USRENAME) !== null;
+ return localStorage.getItem(LocalStorage.DASHBOARD_USERNAME) !== null;
}
getUsername() {
- return localStorage.getItem(LocalStorage.DASHBOARD_USRENAME);
+ return localStorage.getItem(LocalStorage.DASHBOARD_USERNAME);
}
getPermissions(): Permissions {