+@use './src/styles/vendor/variables' as vv;
+
+.info-card-content-clickable {
+ border: 1px solid vv.$gray-200;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 1.25em;
+ padding: 7px;
+}
+
+.info-card-content-clickable:hover {
+ background-color: vv.$gray-200;
+ border-color: vv.$gray-400;
+}
+@use './src/styles/vendor/variables' as vv;
+
.details {
font-size: larger;
white-space: normal;
}
}
+
+.info-card-popover-cluster-status {
+ max-height: 20vh;
+ max-width: 23vw;
+
+ .popover-body {
+ font-size: 1rem;
+ max-height: 19vh;
+ max-width: 100%;
+ overflow: auto;
+
+ li {
+ span {
+ font-size: 1.1em;
+ font-weight: bold;
+ }
+
+ span.health-warn-description {
+ color: vv.$health-color-warning-800 !important;
+ }
+ }
+ }
+}
+
+@media (max-width: vv.$screen-lg-max) {
+ .info-card-popover-cluster-status {
+ max-width: 31vw;
+ }
+}
+
+@media (max-width: vv.$screen-md-max) {
+ .info-card-popover-cluster-status {
+ max-width: 46vw;
+ }
+}
+
+@media (max-width: vv.$screen-sm-max) {
+ .info-card-popover-cluster-status {
+ max-width: 83vw;
+ }
+}
import { FeedbackComponent } from '../shared/feedback/feedback.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HealthPieComponent } from './health-pie/health-pie.component';
-import { HealthComponent } from './health/health.component';
-import { InfoCardComponent } from './info-card/info-card.component';
-import { InfoGroupComponent } from './info-group/info-group.component';
-import { MdsDashboardSummaryPipe } from './mds-dashboard-summary.pipe';
-import { MgrDashboardSummaryPipe } from './mgr-dashboard-summary.pipe';
-import { MonSummaryPipe } from './mon-summary.pipe';
-import { osdDashboardSummaryPipe } from './osd-dashboard-summary.pipe';
import { ToggletipModule } from 'carbon-components-angular';
@NgModule({
BaseChartDirective,
ToggletipModule
],
- declarations: [
- HealthComponent,
- DashboardComponent,
- MonSummaryPipe,
- osdDashboardSummaryPipe,
- MgrDashboardSummaryPipe,
- MdsDashboardSummaryPipe,
- HealthPieComponent,
- InfoCardComponent,
- InfoGroupComponent,
- FeedbackComponent
- ],
+ declarations: [DashboardComponent, HealthPieComponent, FeedbackComponent],
providers: [provideCharts(withDefaultRegisterables())]
})
export class DashboardModule {}
<a href="#main"
class="sr-only">skip to content</a>
- <ng-container *ngIf="(enabledFeature$ | async)?.dashboard === false; else dashboardV3"
- class="main-padding">
- <cd-refresh-selector></cd-refresh-selector>
-
- <cd-health id="main"></cd-health>
- </ng-container>
-
- <ng-template #dashboardV3>
+ <ng-container class="main-padding">
<cd-dashboard-v3></cd-dashboard-v3>
- </ng-template>
+ </ng-container>
</main>
-main:has(cd-health) {
- padding-top: 20px;
-}
import { Component } from '@angular/core';
-import { Observable } from 'rxjs';
-import { FeatureTogglesService } from '~/app/shared/services/feature-toggles.service';
@Component({
selector: 'cd-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
-export class DashboardComponent {
- enabledFeature$: Observable<Object>;
-
- constructor(private featureToggles: FeatureTogglesService) {
- this.enabledFeature$ = this.featureToggles.get();
- }
-}
+export class DashboardComponent {}
+++ /dev/null
-<div *ngIf="healthData && enabledFeature$ | async as enabledFeature"
- class="container-fluid">
- <cd-info-group groupTitle="Status"
- i18n-groupTitle
- *ngIf="healthData?.health?.status
- || healthData?.mon_status
- || healthData?.osd_map
- || healthData?.mgr_map
- || healthData?.hosts != null
- || healthData?.rgw != null
- || healthData?.fs_map
- || healthData?.iscsi_daemons != null">
-
- <cd-info-card cardTitle="Cluster Status"
- i18n-cardTitle
- class="cd-status-card"
- contentClass="content-highlight"
- *ngIf="healthData.health?.status">
- <ng-container *ngIf="healthData.health?.checks?.length > 0">
- <cds-toggletip [dropShadow]="true"
- [autoAlign]="true">
- <div cdsToggletipButton>
- <span [ngStyle]="healthData.health.status | healthColor"
- class="info-card-content-clickable">
- {{ healthData.health.status | healthLabel | uppercase }}
- <i *ngIf="healthData.health?.status !== 'HEALTH_OK'"
- class="fa fa-exclamation-triangle"></i>
- </span>
- </div>
- <div cdsToggletipContent>
- <div class="cds--popover-scroll-container">
- <cd-health-checks [healthData]="healthData"></cd-health-checks>
- </div>
- </div>
- </cds-toggletip>
- </ng-container>
- <ng-container *ngIf="!healthData?.health?.checks?.length">
- <div [ngStyle]="healthData?.health.status | healthColor">
- {{ healthData?.health.status | healthLabel | uppercase }}
- </div>
- </ng-container>
- </cd-info-card>
-
- <cd-info-card cardTitle="Hosts"
- i18n-cardTitle
- link="/hosts"
- class="cd-status-card"
- contentClass="content-highlight"
- *ngIf="healthData?.hosts != null">
- {{ healthData?.hosts }} total
- </cd-info-card>
-
- <cd-info-card cardTitle="Monitors"
- i18n-cardTitle
- link="/monitor"
- class="cd-status-card"
- contentClass="content-highlight"
- *ngIf="healthData?.mon_status">
- {{ healthData?.mon_status | monSummary }}
- </cd-info-card>
-
- <cd-info-card cardTitle="OSDs"
- i18n-cardTitle
- link="/osd"
- class="cd-status-card"
- *ngIf="(healthData?.osd_map | osdDashboardSummary) as transformedResult"
- contentClass="content-highlight">
- <span *ngFor="let result of transformedResult"
- [ngClass]="result.class">
- {{ result.content }}
- </span>
- </cd-info-card>
-
- <cd-info-card cardTitle="Managers"
- i18n-cardTitle
- class="cd-status-card"
- contentClass="content-highlight"
- *ngIf="healthData?.mgr_map">
- <span *ngFor="let result of (healthData?.mgr_map | mgrDashboardSummary)"
- [ngClass]="result.class"
- [title]="result.titleText != null ? result.titleText : ''">
- {{ result.content }}
- </span>
- </cd-info-card>
-
- <cd-info-card cardTitle="Object Gateways"
- i18n-cardTitle
- link="/rgw/daemon"
- class="cd-status-card"
- contentClass="content-highlight"
- *ngIf="enabledFeature?.rgw && healthData?.rgw != null">
- {{ healthData?.rgw }} total
- </cd-info-card>
-
- <cd-info-card cardTitle="Metadata Servers"
- i18n-cardTitle
- class="cd-status-card"
- *ngIf="(enabledFeature?.cephfs && healthData?.fs_map | mdsDashboardSummary) as transformedResult"
- [contentClass]="(transformedResult.length > 1 ? 'text-area-size-2' : '') + ' content-highlight'">
- <!-- TODO: check text-area-size-2 -->
- <span *ngFor="let result of transformedResult"
- [ngClass]="result.class"
- [title]="result.titleText !== null ? result.titleText : ''">
- {{ result.content }}
- </span>
- </cd-info-card>
-
- <cd-info-card cardTitle="iSCSI Gateways"
- i18n-cardTitle
- link="/block/iscsi"
- class="cd-status-card"
- contentClass="content-highlight"
- *ngIf="enabledFeature?.iscsi && healthData?.iscsi_daemons != null">
- {{ healthData?.iscsi_daemons.up + healthData?.iscsi_daemons.down }} total
- <span class="card-text-line-break"></span>
- {{ healthData?.iscsi_daemons.up }} up,
- <span [ngClass]="{'card-text-error': healthData?.iscsi_daemons.down > 0}">{{ healthData?.iscsi_daemons.down }}
- down</span>
- </cd-info-card>
- </cd-info-group>
-
- <cd-info-group groupTitle="Capacity"
- i18n-groupTitle
- *ngIf="healthData?.pools
- || healthData?.df
- || healthData?.pg_info">
- <cd-info-card cardTitle="Raw Capacity"
- i18n-cardTitle
- class="cd-capacity-card cd-chart-card"
- contentClass="content-chart"
- *ngIf="healthData?.df">
- <cd-health-pie [data]="healthData"
- [config]="rawCapacityChartConfig"
- [isBytesData]="true"
- (prepareFn)="prepareRawUsage($event[0], $event[1])">
- </cd-health-pie>
- </cd-info-card>
-
- <cd-info-card cardTitle="Objects"
- i18n-cardTitle
- class="cd-capacity-card cd-chart-card"
- contentClass="content-chart"
- *ngIf="healthData?.pg_info?.object_stats?.num_objects != null">
- <cd-health-pie [data]="healthData"
- (prepareFn)="prepareObjects($event[0], $event[1])">
- </cd-health-pie>
- </cd-info-card>
-
- <cd-info-card cardTitle="PG Status"
- i18n-cardTitle
- class="cd-capacity-card cd-chart-card"
- contentClass="content-chart"
- *ngIf="healthData.pg_info">
- <div cdsPopover
- [dropShadow]="true"
- placement="bottom"
- class="pg-status-popover-wrapper">
-
- <div class="popover-trigger">
- <cd-health-pie [data]="healthData"
- [config]="pgStatusChartConfig"
- (prepareFn)="preparePgStatus($event[0], $event[1])">
- </cd-health-pie>
- </div>
- <cds-popover-content>
- <div class="cds--popover-scroll-container">
- <ng-container *ngTemplateOutlet="logsLink"></ng-container>
- <ul>
- <li *ngFor="let pgStatesText of healthData.pg_info.statuses | keyvalue">
- {{ pgStatesText.key }}: {{ pgStatesText.value }}
- </li>
- </ul>
- </div>
- </cds-popover-content>
- </div>
- </cd-info-card>
-
- <cd-info-card cardTitle="Pools"
- i18n-cardTitle
- link="/pool"
- class="cd-capacity-card"
- contentClass="content-highlight"
- *ngIf="healthData?.pools">
- {{ healthData?.pools.length }}
- </cd-info-card>
-
- <cd-info-card cardTitle="PGs per OSD"
- i18n-cardTitle
- class="cd-capacity-card"
- contentClass="content-highlight"
- *ngIf="healthData?.pg_info">
- {{ healthData?.pg_info.pgs_per_osd | dimless }}
- </cd-info-card>
- </cd-info-group>
-
- <cd-info-group groupTitle="Performance"
- i18n-groupTitle
- *ngIf="healthData?.client_perf || healthData?.scrub_status">
- <cd-info-card cardTitle="Client Read/Write"
- i18n-cardTitle
- class="cd-performance-card cd-chart-card"
- contentClass="content-chart"
- *ngIf="healthData?.client_perf">
- <cd-health-pie [data]="healthData"
- [config]="clientStatsConfig"
- (prepareFn)="prepareReadWriteRatio($event[0], $event[1])">
- </cd-health-pie>
- </cd-info-card>
-
- <cd-info-card cardTitle="Client Throughput"
- i18n-cardTitle
- class="cd-performance-card cd-chart-card"
- contentClass="content-chart"
- *ngIf="healthData?.client_perf">
- <cd-health-pie [data]="healthData"
- [config]="clientStatsConfig"
- (prepareFn)="prepareClientThroughput($event[0], $event[1])">
- </cd-health-pie>
- </cd-info-card>
-
- <cd-info-card cardTitle="Recovery Throughput"
- i18n-cardTitle
- class="cd-performance-card"
- contentClass="content-highlight"
- *ngIf="healthData?.client_perf">
- {{ (healthData?.client_perf.recovering_bytes_per_sec | dimlessBinary) + '/s' }}
- </cd-info-card>
-
- <cd-info-card cardTitle="Scrubbing"
- i18n-cardTitle
- class="cd-performance-card"
- contentClass="content-highlight"
- *ngIf="healthData?.scrub_status">
- {{ healthData?.scrub_status }}
- </cd-info-card>
- </cd-info-group>
-
- <ng-template #logsLink>
- <ng-container *ngIf="permissions?.log?.read">
- <p class="logs-link"
- i18n>
- <i [ngClass]="[icons.infoCircle]"></i> See
- <a routerLink="/logs">Logs</a> for more details.
- </p>
- </ng-container>
- </ng-template>
-</div>
+++ /dev/null
-@use './src/styles/vendor/variables' as vv;
-
-cd-info-card {
- padding: 0 0.5vw;
-}
-
-::ng-deep cd-health .pg-status-popover-wrapper {
- position: relative;
-
- .popover {
- max-height: 20vh;
- max-width: unset !important;
- min-width: unset !important;
- position: absolute;
- width: 116%;
-
- .popover-body {
- font-size: 1rem;
- max-height: 19vh;
- max-width: 100%;
- }
- }
-}
-
-.logs-link {
- text-align: center;
-
- a {
- color: vv.$primary;
- }
-}
-
-.card-text-error {
- color: vv.$chart-danger;
- display: inline;
-}
-
-.card-text-line-break::after {
- content: '\A';
- white-space: pre;
-}
-
-.popover-info:hover {
- cursor: pointer;
-}
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { By } from '@angular/platform-browser';
-
-import _ from 'lodash';
-import { of } from 'rxjs';
-
-import { PgCategoryService } from '~/app/ceph/shared/pg-category.service';
-import { HealthService } from '~/app/shared/api/health.service';
-import { CssHelper } from '~/app/shared/classes/css-helper';
-import { Permissions } from '~/app/shared/models/permissions';
-import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
-import { FeatureTogglesService } from '~/app/shared/services/feature-toggles.service';
-import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
-import { SharedModule } from '~/app/shared/shared.module';
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { HealthPieComponent } from '../health-pie/health-pie.component';
-import { MdsDashboardSummaryPipe } from '../mds-dashboard-summary.pipe';
-import { MgrDashboardSummaryPipe } from '../mgr-dashboard-summary.pipe';
-import { MonSummaryPipe } from '../mon-summary.pipe';
-import { osdDashboardSummaryPipe } from '../osd-dashboard-summary.pipe';
-import { HealthComponent } from './health.component';
-
-describe('HealthComponent', () => {
- let component: HealthComponent;
- let fixture: ComponentFixture<HealthComponent>;
- let getHealthSpy: jasmine.Spy;
- const healthPayload: Record<string, any> = {
- health: { status: 'HEALTH_OK' },
- mon_status: { monmap: { mons: [] }, quorum: [] },
- osd_map: { osds: [] },
- mgr_map: { standbys: [] },
- hosts: 0,
- rgw: 0,
- fs_map: { filesystems: [], standbys: [] },
- iscsi_daemons: 0,
- client_perf: {},
- scrub_status: 'Inactive',
- pools: [],
- df: { stats: {} },
- pg_info: { object_stats: { num_objects: 0 } }
- };
- const fakeAuthStorageService = {
- getPermissions: () => {
- return new Permissions({ log: ['read'] });
- }
- };
- let fakeFeatureTogglesService: jasmine.Spy;
-
- configureTestBed({
- imports: [SharedModule, HttpClientTestingModule],
- declarations: [
- HealthComponent,
- HealthPieComponent,
- MonSummaryPipe,
- osdDashboardSummaryPipe,
- MdsDashboardSummaryPipe,
- MgrDashboardSummaryPipe
- ],
- schemas: [NO_ERRORS_SCHEMA],
- providers: [
- { provide: AuthStorageService, useValue: fakeAuthStorageService },
- PgCategoryService,
- RefreshIntervalService,
- CssHelper
- ]
- });
-
- beforeEach(() => {
- fakeFeatureTogglesService = spyOn(TestBed.inject(FeatureTogglesService), 'get').and.returnValue(
- of({
- rbd: true,
- mirroring: true,
- iscsi: true,
- cephfs: true,
- rgw: true
- })
- );
- fixture = TestBed.createComponent(HealthComponent);
- component = fixture.componentInstance;
- getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth');
- getHealthSpy.and.returnValue(of(healthPayload));
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('should render all info groups and all info cards', () => {
- fixture.detectChanges();
-
- const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group');
- expect(infoGroups.length).toBe(3);
-
- const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
- expect(infoCards.length).toBe(17);
- });
-
- describe('features disabled', () => {
- beforeEach(() => {
- fakeFeatureTogglesService.and.returnValue(
- of({
- rbd: false,
- mirroring: false,
- iscsi: false,
- cephfs: false,
- rgw: false
- })
- );
- fixture = TestBed.createComponent(HealthComponent);
- component = fixture.componentInstance;
- });
-
- it('should not render cards related to disabled features', () => {
- fixture.detectChanges();
-
- const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group');
- expect(infoGroups.length).toBe(3);
-
- const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
- expect(infoCards.length).toBe(14);
- });
- });
-
- it('should render all except "Status" group and cards', () => {
- const payload = _.cloneDeep(healthPayload);
- payload.health.status = '';
- payload.mon_status = null;
- payload.osd_map = null;
- payload.mgr_map = null;
- payload.hosts = null;
- payload.rgw = null;
- payload.fs_map = null;
- payload.iscsi_daemons = null;
-
- getHealthSpy.and.returnValue(of(payload));
- fixture.detectChanges();
-
- const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group');
- expect(infoGroups.length).toBe(2);
-
- const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
- expect(infoCards.length).toBe(9);
- });
-
- it('should render all except "Performance" group and cards', () => {
- const payload = _.cloneDeep(healthPayload);
- payload.scrub_status = '';
- payload.client_perf = null;
-
- getHealthSpy.and.returnValue(of(payload));
- fixture.detectChanges();
-
- const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group');
- expect(infoGroups.length).toBe(2);
-
- const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
- expect(infoCards.length).toBe(13);
- });
-
- it('should render all except "Capacity" group and cards', () => {
- const payload = _.cloneDeep(healthPayload);
- payload.pools = null;
- payload.df = null;
- payload.pg_info = null;
-
- getHealthSpy.and.returnValue(of(payload));
- fixture.detectChanges();
-
- const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group');
- expect(infoGroups.length).toBe(2);
-
- const infoCards = fixture.debugElement.nativeElement.querySelectorAll('cd-info-card');
- expect(infoCards.length).toBe(12);
- });
-
- it('should render all groups and 1 card per group', () => {
- const payload: Record<string, any> = { hosts: 0, scrub_status: 'Inactive', pools: [] };
-
- getHealthSpy.and.returnValue(of(payload));
- fixture.detectChanges();
-
- const infoGroups = fixture.debugElement.nativeElement.querySelectorAll('cd-info-group');
- expect(infoGroups.length).toBe(3);
-
- _.each(infoGroups, (infoGroup) => {
- expect(infoGroup.querySelectorAll('cd-info-card').length).toBe(1);
- });
- });
-
- it('should render "Cluster Status" card text that is not clickable', () => {
- fixture.detectChanges();
-
- const clusterStatusCard = fixture.debugElement.query(
- By.css('cd-info-card[cardTitle="Cluster Status"]')
- );
- const clickableContent = clusterStatusCard.query(By.css('.info-card-content-clickable'));
- expect(clickableContent).toBeNull();
- expect(clusterStatusCard.nativeElement.textContent).toEqual(' OK ');
- });
-
- it('should render "Cluster Status" card text that is clickable (popover)', () => {
- const payload = _.cloneDeep(healthPayload);
- payload.health['status'] = 'HEALTH_WARN';
- payload.health['checks'] = [
- { severity: 'HEALTH_WARN', type: 'WRN', summary: { message: 'fake warning' } }
- ];
-
- getHealthSpy.and.returnValue(of(payload));
- fixture.detectChanges();
-
- expect(component.permissions.log.read).toBeTruthy();
-
- const clusterStatusCard = fixture.debugElement.query(
- By.css('cd-info-card[cardTitle="Cluster Status"]')
- );
- const clickableContent = clusterStatusCard.query(By.css('.info-card-content-clickable'));
- expect(clickableContent.nativeElement.textContent).toEqual(' WARNING ');
- });
-
- it('event binding "prepareReadWriteRatio" is called', () => {
- const prepareReadWriteRatio = spyOn(component, 'prepareReadWriteRatio').and.callThrough();
-
- const payload = _.cloneDeep(healthPayload);
- payload.client_perf['read_op_per_sec'] = 1;
- payload.client_perf['write_op_per_sec'] = 3;
- getHealthSpy.and.returnValue(of(payload));
- fixture.detectChanges();
-
- expect(prepareReadWriteRatio).toHaveBeenCalled();
- expect(prepareReadWriteRatio.calls.mostRecent().args[0].dataset[0].data).toEqual([25, 75]);
- });
-
- it('event binding "prepareRawUsage" is called', () => {
- const prepareRawUsage = spyOn(component, 'prepareRawUsage');
-
- fixture.detectChanges();
-
- expect(prepareRawUsage).toHaveBeenCalled();
- });
-
- it('event binding "preparePgStatus" is called', () => {
- const preparePgStatus = spyOn(component, 'preparePgStatus');
-
- fixture.detectChanges();
-
- expect(preparePgStatus).toHaveBeenCalled();
- });
-
- it('event binding "prepareObjects" is called', () => {
- const prepareObjects = spyOn(component, 'prepareObjects');
-
- fixture.detectChanges();
-
- expect(prepareObjects).toHaveBeenCalled();
- });
-
- describe('preparePgStatus', () => {
- const expectedChart = (data: number[], label: string = null) => ({
- labels: [
- `Clean: ${component['dimless'].transform(data[0])}`,
- `Working: ${component['dimless'].transform(data[1])}`,
- `Warning: ${component['dimless'].transform(data[2])}`,
- `Unknown: ${component['dimless'].transform(data[3])}`
- ],
- options: {},
- dataset: [
- {
- data: data.map((i) =>
- component['calcPercentage'](
- i,
- data.reduce((j, k) => j + k)
- )
- ),
- label: label
- }
- ]
- });
-
- it('gets no data', () => {
- const chart = { dataset: [{}], options: {} };
- component.preparePgStatus(chart, {
- pg_info: {}
- });
- expect(chart).toEqual(expectedChart([0, 0, 0, 0], '0\nPGs'));
- });
-
- it('gets data from all categories', () => {
- const chart = { dataset: [{}], options: {} };
- component.preparePgStatus(chart, {
- pg_info: {
- statuses: {
- 'clean+active+scrubbing+nonMappedState': 4,
- 'clean+active+scrubbing': 2,
- 'clean+active': 1,
- 'clean+active+scrubbing+down': 3
- }
- }
- });
- expect(chart).toEqual(expectedChart([1, 2, 3, 4], '10\nPGs'));
- });
- });
-
- describe('isClientReadWriteChartShowable', () => {
- beforeEach(() => {
- component.healthData = healthPayload;
- });
-
- it('returns false', () => {
- component.healthData['client_perf'] = {};
-
- expect(component.isClientReadWriteChartShowable()).toBeFalsy();
- });
-
- it('returns false', () => {
- component.healthData['client_perf'] = { read_op_per_sec: undefined, write_op_per_sec: 0 };
-
- expect(component.isClientReadWriteChartShowable()).toBeFalsy();
- });
-
- it('returns true', () => {
- component.healthData['client_perf'] = { read_op_per_sec: 1, write_op_per_sec: undefined };
-
- expect(component.isClientReadWriteChartShowable()).toBeTruthy();
- });
-
- it('returns true', () => {
- component.healthData['client_perf'] = { read_op_per_sec: 2, write_op_per_sec: 3 };
-
- expect(component.isClientReadWriteChartShowable()).toBeTruthy();
- });
- });
-
- describe('calcPercentage', () => {
- it('returns correct value', () => {
- expect(component['calcPercentage'](1, undefined)).toEqual(0);
- expect(component['calcPercentage'](1, null)).toEqual(0);
- expect(component['calcPercentage'](1, 0)).toEqual(0);
- expect(component['calcPercentage'](undefined, 1)).toEqual(0);
- expect(component['calcPercentage'](null, 1)).toEqual(0);
- expect(component['calcPercentage'](0, 1)).toEqual(0);
- expect(component['calcPercentage'](1, 100000)).toEqual(0.01);
- expect(component['calcPercentage'](2.346, 10)).toEqual(23.46);
- expect(component['calcPercentage'](2.56, 10)).toEqual(25.6);
- });
- });
-});
+++ /dev/null
-import { Component, OnDestroy, OnInit } from '@angular/core';
-
-import _ from 'lodash';
-import { Subscription } from 'rxjs';
-import { take } from 'rxjs/operators';
-
-import { PgCategoryService } from '~/app/ceph/shared/pg-category.service';
-import { HealthService } from '~/app/shared/api/health.service';
-import { OsdService } from '~/app/shared/api/osd.service';
-import { CssHelper } from '~/app/shared/classes/css-helper';
-import { Icons } from '~/app/shared/enum/icons.enum';
-import { OsdSettings } from '~/app/shared/models/osd-settings';
-import { Permissions } from '~/app/shared/models/permissions';
-import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
-import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
-import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
-import {
- FeatureTogglesMap$,
- FeatureTogglesService
-} from '~/app/shared/services/feature-toggles.service';
-import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
-
-@Component({
- selector: 'cd-health',
- templateUrl: './health.component.html',
- styleUrls: ['./health.component.scss']
-})
-export class HealthComponent implements OnInit, OnDestroy {
- healthData: any;
- osdSettings = new OsdSettings();
- interval = new Subscription();
- permissions: Permissions;
- enabledFeature$: FeatureTogglesMap$;
- icons = Icons;
- color: string;
- clientStatsConfig: any = {};
- rawCapacityChartConfig: any = {};
- pgStatusChartConfig = {
- options: {
- events: ['']
- }
- };
-
- constructor(
- private healthService: HealthService,
- private osdService: OsdService,
- private authStorageService: AuthStorageService,
- private pgCategoryService: PgCategoryService,
- private featureToggles: FeatureTogglesService,
- private refreshIntervalService: RefreshIntervalService,
- private dimlessBinary: DimlessBinaryPipe,
- private dimless: DimlessPipe,
- private cssHelper: CssHelper
- ) {
- this.permissions = this.authStorageService.getPermissions();
- this.enabledFeature$ = this.featureToggles.get();
- }
-
- ngOnInit() {
- this.clientStatsConfig = {
- dataset: [
- {
- backgroundColor: [
- this.cssHelper.propertyValue('chart-color-cyan'),
- this.cssHelper.propertyValue('chart-color-purple')
- ]
- }
- ]
- };
-
- this.rawCapacityChartConfig = {
- dataset: [
- {
- backgroundColor: [
- this.cssHelper.propertyValue('chart-color-blue'),
- this.cssHelper.propertyValue('chart-color-gray')
- ]
- }
- ]
- };
- this.interval = this.refreshIntervalService.intervalData$.subscribe(() => {
- this.getHealth();
- });
-
- this.osdService
- .getOsdSettings()
- .pipe(take(1))
- .subscribe((data: any) => {
- this.osdSettings = data;
- });
- }
-
- ngOnDestroy() {
- this.interval.unsubscribe();
- }
-
- getHealth() {
- this.healthService.getMinimalHealth().subscribe((data: any) => {
- this.healthData = data;
- });
- }
-
- prepareReadWriteRatio(chart: Record<string, any>) {
- const ratioLabels = [];
- const ratioData = [];
-
- const total =
- this.healthData.client_perf.write_op_per_sec + this.healthData.client_perf.read_op_per_sec;
-
- ratioLabels.push(
- `${$localize`Reads`}: ${this.dimless.transform(
- this.healthData.client_perf.read_op_per_sec
- )} ${$localize`/s`}`
- );
- ratioData.push(this.calcPercentage(this.healthData.client_perf.read_op_per_sec, total));
- ratioLabels.push(
- `${$localize`Writes`}: ${this.dimless.transform(
- this.healthData.client_perf.write_op_per_sec
- )} ${$localize`/s`}`
- );
- ratioData.push(this.calcPercentage(this.healthData.client_perf.write_op_per_sec, total));
-
- chart.labels = ratioLabels;
- chart.dataset[0].data = ratioData;
- chart.dataset[0].label = `${this.dimless.transform(total)}\n${$localize`IOPS`}`;
- }
-
- prepareClientThroughput(chart: Record<string, any>) {
- const ratioLabels = [];
- const ratioData = [];
-
- const total =
- this.healthData.client_perf.read_bytes_sec + this.healthData.client_perf.write_bytes_sec;
-
- ratioLabels.push(
- `${$localize`Reads`}: ${this.dimlessBinary.transform(
- this.healthData.client_perf.read_bytes_sec
- )}${$localize`/s`}`
- );
- ratioData.push(this.calcPercentage(this.healthData.client_perf.read_bytes_sec, total));
- ratioLabels.push(
- `${$localize`Writes`}: ${this.dimlessBinary.transform(
- this.healthData.client_perf.write_bytes_sec
- )}${$localize`/s`}`
- );
- ratioData.push(this.calcPercentage(this.healthData.client_perf.write_bytes_sec, total));
-
- chart.labels = ratioLabels;
- chart.dataset[0].data = ratioData;
- chart.dataset[0].label = `${this.dimlessBinary
- .transform(total)
- .replace(' ', '\n')}${$localize`/s`}`;
- }
-
- prepareRawUsage(chart: Record<string, any>, data: Record<string, any>) {
- const percentAvailable = this.calcPercentage(
- data.df.stats.total_bytes - data.df.stats.total_used_raw_bytes,
- data.df.stats.total_bytes
- );
- const percentUsed = this.calcPercentage(
- data.df.stats.total_used_raw_bytes,
- data.df.stats.total_bytes
- );
-
- const nearfullRatio = this.osdSettings.nearfull_ratio;
- const fullRatio = this.osdSettings.nearfull_ratio;
-
- if (nearfullRatio >= 0 && percentUsed / 100 >= nearfullRatio) {
- this.color = 'chart-color-red';
- } else if (fullRatio >= 0 && percentUsed / 100 >= fullRatio) {
- this.color = 'chart-color-yellow';
- } else {
- this.color = 'chart-color-blue';
- }
- this.rawCapacityChartConfig.dataset[0].backgroundColor[0] = this.cssHelper.propertyValue(
- this.color
- );
-
- chart.dataset[0].data = [percentUsed, percentAvailable];
-
- chart.labels = [
- `${$localize`Used`}: ${this.dimlessBinary.transform(data.df.stats.total_used_raw_bytes)}`,
- `${$localize`Avail.`}: ${this.dimlessBinary.transform(
- data.df.stats.total_bytes - data.df.stats.total_used_raw_bytes
- )}`
- ];
-
- chart.dataset[0].label = `${percentUsed}%\nof ${this.dimlessBinary.transform(
- data.df.stats.total_bytes
- )}`;
- }
-
- preparePgStatus(chart: Record<string, any>, data: Record<string, any>) {
- const categoryPgAmount: Record<string, number> = {};
- let totalPgs = 0;
-
- _.forEach(data.pg_info.statuses, (pgAmount, pgStatesText) => {
- const categoryType = this.pgCategoryService.getTypeByStates(pgStatesText);
-
- if (_.isUndefined(categoryPgAmount[categoryType])) {
- categoryPgAmount[categoryType] = 0;
- }
- categoryPgAmount[categoryType] += pgAmount;
- totalPgs += pgAmount;
- });
-
- for (const categoryType of this.pgCategoryService.getAllTypes()) {
- if (_.isUndefined(categoryPgAmount[categoryType])) {
- categoryPgAmount[categoryType] = 0;
- }
- }
-
- chart.dataset[0].data = this.pgCategoryService
- .getAllTypes()
- .map((categoryType) => this.calcPercentage(categoryPgAmount[categoryType], totalPgs));
-
- chart.labels = [
- `${$localize`Clean`}: ${this.dimless.transform(categoryPgAmount['clean'])}`,
- `${$localize`Working`}: ${this.dimless.transform(categoryPgAmount['working'])}`,
- `${$localize`Warning`}: ${this.dimless.transform(categoryPgAmount['warning'])}`,
- `${$localize`Unknown`}: ${this.dimless.transform(categoryPgAmount['unknown'])}`
- ];
-
- chart.dataset[0].label = `${totalPgs}\n${$localize`PGs`}`;
- }
-
- prepareObjects(chart: Record<string, any>, data: Record<string, any>) {
- const objectCopies = data.pg_info.object_stats.num_object_copies;
- const healthy =
- objectCopies -
- data.pg_info.object_stats.num_objects_misplaced -
- data.pg_info.object_stats.num_objects_degraded -
- data.pg_info.object_stats.num_objects_unfound;
- const healthyPercentage = this.calcPercentage(healthy, objectCopies);
- const misplacedPercentage = this.calcPercentage(
- data.pg_info.object_stats.num_objects_misplaced,
- objectCopies
- );
- const degradedPercentage = this.calcPercentage(
- data.pg_info.object_stats.num_objects_degraded,
- objectCopies
- );
- const unfoundPercentage = this.calcPercentage(
- data.pg_info.object_stats.num_objects_unfound,
- objectCopies
- );
-
- chart.labels = [
- `${$localize`Healthy`}: ${healthyPercentage}%`,
- `${$localize`Misplaced`}: ${misplacedPercentage}%`,
- `${$localize`Degraded`}: ${degradedPercentage}%`,
- `${$localize`Unfound`}: ${unfoundPercentage}%`
- ];
-
- chart.dataset[0].data = [
- healthyPercentage,
- misplacedPercentage,
- degradedPercentage,
- unfoundPercentage
- ];
-
- chart.dataset[0].label = `${this.dimless.transform(
- data.pg_info.object_stats.num_objects
- )}\n${$localize`objects`}`;
- }
-
- isClientReadWriteChartShowable() {
- const readOps = this.healthData.client_perf.read_op_per_sec || 0;
- const writeOps = this.healthData.client_perf.write_op_per_sec || 0;
-
- return readOps + writeOps > 0;
- }
-
- private calcPercentage(dividend: number, divisor: number) {
- if (!_.isNumber(dividend) || !_.isNumber(divisor) || divisor === 0) {
- return 0;
- }
-
- return Math.ceil((dividend / divisor) * 100 * 100) / 100;
- }
-}
+++ /dev/null
-@use './src/styles/vendor/variables' as vv;
-
-.info-card-popover-cluster-status {
- max-height: 20vh;
- max-width: 23vw;
-
- .popover-body {
- font-size: 1rem;
- max-height: 19vh;
- max-width: 100%;
- overflow: auto;
-
- li {
- span {
- font-size: 1.1em;
- font-weight: bold;
- }
-
- span.health-warn-description {
- color: vv.$health-color-warning-800 !important;
- }
- }
- }
-}
-
-@media (max-width: vv.$screen-lg-max) {
- .info-card-popover-cluster-status {
- max-width: 31vw;
- }
-}
-
-@media (max-width: vv.$screen-md-max) {
- .info-card-popover-cluster-status {
- max-width: 46vw;
- }
-}
-
-@media (max-width: vv.$screen-sm-max) {
- .info-card-popover-cluster-status {
- max-width: 83vw;
- }
-}
-
-.info-card-content-clickable {
- border: 1px solid vv.$gray-200;
- border-radius: 3px;
- cursor: pointer;
- font-size: 1.25em;
- padding: 7px;
-}
-
-.info-card-content-clickable:hover {
- background-color: vv.$gray-200;
- border-color: vv.$gray-400;
-}
+++ /dev/null
-<div class="card shadow-sm"
- [ngClass]="cardClass">
- <div class="card-body d-flex align-items-center justify-content-center">
- <h4 class="card-title m-4">
- <a *ngIf="link; else noLinkTitle"
- [routerLink]="link">{{ cardTitle }}</a>
-
- <ng-template #noLinkTitle>
- {{ cardTitle }}
- </ng-template>
- </h4>
-
- <div class="card-text text-center"
- [ngClass]="contentClass">
- <ng-content></ng-content>
- </div>
- </div>
-</div>
+++ /dev/null
-@use './src/styles/vendor/variables' as vv;
-@use './src/styles/defaults/mixins';
-
-$card-font-min-width: 320px;
-$card-font-max-width: 2048px;
-$card-font-min-size: 12px;
-$card-font-max-size: 21px;
-
-.card {
- @include mixins.fluid-font-size(
- $card-font-min-width,
- $card-font-max-width,
- $card-font-min-size,
- $card-font-max-size
- );
-
- border: 0.5px solid vv.$gray-300;
- border-radius: 3px;
- height: 100%;
-
- .card-body {
- padding-top: 40px !important;
-
- .card-title {
- left: -0.6rem;
- position: absolute;
- top: -0.3rem;
- }
-
- .card-title > a {
- color: vv.$primary;
- }
- }
-}
-
-.no-center {
- left: unset;
- position: unset;
- top: unset;
- transform: unset;
-}
-
-.content-highlight {
- font-weight: bold;
-}
+++ /dev/null
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { InfoCardComponent } from './info-card.component';
-
-describe('InfoCardComponent', () => {
- let component: InfoCardComponent;
- let fixture: ComponentFixture<InfoCardComponent>;
-
- configureTestBed({
- imports: [RouterTestingModule],
- declarations: [InfoCardComponent]
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(InfoCardComponent);
- component = fixture.componentInstance;
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('Setting cardTitle makes title visible', () => {
- const cardTitle = 'Card Title';
- component.cardTitle = cardTitle;
- fixture.detectChanges();
- const titleDiv = fixture.debugElement.nativeElement.querySelector('.card-title');
-
- expect(titleDiv.textContent).toContain(cardTitle);
- });
-
- it('Setting link makes anchor visible', () => {
- const cardTitle = 'Card Title';
- const link = '/dashboard';
- component.cardTitle = cardTitle;
- component.link = link;
- fixture.detectChanges();
- const anchor = fixture.debugElement.nativeElement
- .querySelector('.card-title')
- .querySelector('a');
-
- expect(anchor.textContent).toContain(cardTitle);
- expect(anchor.href).toBe(`http://localhost${link}`);
- });
-
- it('Setting cardClass makes class set', () => {
- const cardClass = 'my-css-card-class';
- component.cardClass = cardClass;
- fixture.detectChanges();
- const card = fixture.debugElement.nativeElement.querySelector(`.card.${cardClass}`);
-
- expect(card).toBeTruthy();
- });
-
- it('Setting contentClass makes class set', () => {
- const contentClass = 'my-css-content-class';
- component.contentClass = contentClass;
- fixture.detectChanges();
- const card = fixture.debugElement.nativeElement.querySelector(`.card-body .${contentClass}`);
-
- expect(card).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, Input } from '@angular/core';
-
-@Component({
- selector: 'cd-info-card',
- templateUrl: './info-card.component.html',
- styleUrls: ['./info-card.component.scss']
-})
-export class InfoCardComponent {
- @Input()
- cardTitle: string;
- @Input()
- link: string;
- @Input()
- cardClass = '';
- @Input()
- contentClass: string;
-}
+++ /dev/null
-<div class="row">
- <div class="info-group-title">
- <span data-testid="group-title">{{ groupTitle }}</span>
- <cd-helper iconClass="fa fa-info-circle fa-2xs">
- <div class="text-center"
- i18n>For an overview of {{ groupTitle|lowercase }} widgets click
- <cd-doc section="dashboard-landing-page-{{ groupTitle|lowercase }}"
- docText="here"
- i18n-docText></cd-doc>
- </div>
- </cd-helper>
- </div>
-</div>
-
-<div class="row">
- <ng-content></ng-content>
-</div>
+++ /dev/null
-@use './src/styles/vendor/variables' as vv;
-
-.info-group-title {
- font-size: 1.75rem;
- margin: 0 0 0.5vw;
-}
-
-.popover-icon {
- color: vv.$primary;
-}
-
-.popover-icon:focus {
- box-shadow: none;
-}
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
-
-import { SharedModule } from '~/app/shared/shared.module';
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { InfoGroupComponent } from './info-group.component';
-
-describe('InfoGroupComponent', () => {
- let component: InfoGroupComponent;
- let fixture: ComponentFixture<InfoGroupComponent>;
-
- configureTestBed({
- imports: [NgbPopoverModule, SharedModule, HttpClientTestingModule],
- declarations: [InfoGroupComponent]
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(InfoGroupComponent);
- component = fixture.componentInstance;
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('Setting groupTitle makes title visible', () => {
- const groupTitle = 'Group Title';
- component.groupTitle = groupTitle;
- fixture.detectChanges();
- const titleDiv = fixture.debugElement.nativeElement.querySelector('.info-group-title');
-
- expect(titleDiv.textContent).toContain(groupTitle);
- });
-});
+++ /dev/null
-import { Component, Input } from '@angular/core';
-
-import { Icons } from '~/app/shared/enum/icons.enum';
-
-@Component({
- selector: 'cd-info-group',
- templateUrl: './info-group.component.html',
- styleUrls: ['./info-group.component.scss']
-})
-export class InfoGroupComponent {
- icons = Icons;
- @Input()
- groupTitle: string;
-}
+++ /dev/null
-import { TestBed } from '@angular/core/testing';
-
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { MdsDashboardSummaryPipe } from './mds-dashboard-summary.pipe';
-
-describe('MdsDashboardSummaryPipe', () => {
- let pipe: MdsDashboardSummaryPipe;
-
- configureTestBed({
- providers: [MdsDashboardSummaryPipe]
- });
-
- beforeEach(() => {
- pipe = TestBed.inject(MdsDashboardSummaryPipe);
- });
-
- it('create an instance', () => {
- expect(pipe).toBeTruthy();
- });
-
- it('transforms with 0 active and 2 standy', () => {
- const payload = {
- standbys: [{ name: 'a' }],
- filesystems: [{ mdsmap: { info: [{ state: 'up:standby-replay' }] } }]
- };
- const expected = [
- { class: 'popover-info', content: '0 active', titleText: '1 standbyReplay' },
- { class: 'card-text-line-break', content: '', titleText: '' },
- { class: 'popover-info', content: '2 standby', titleText: 'standby daemons: a' }
- ];
-
- expect(pipe.transform(payload)).toEqual(expected);
- });
-
- it('transforms with 1 active and 1 standy', () => {
- const payload = {
- standbys: [{ name: 'b' }],
- filesystems: [{ mdsmap: { info: [{ state: 'up:active', name: 'a' }] } }]
- };
- const expected = [
- { class: 'popover-info', content: '1 active', titleText: 'active daemon: a' },
- { class: 'card-text-line-break', content: '', titleText: '' },
- { class: 'popover-info', content: '1 standby', titleText: 'standby daemons: b' }
- ];
- expect(pipe.transform(payload)).toEqual(expected);
- });
-
- it('transforms with 0 filesystems', () => {
- const payload: Record<string, any> = {
- standbys: [0],
- filesystems: []
- };
- const expected = [{ class: 'popover-info', content: 'no filesystems', titleText: '' }];
-
- expect(pipe.transform(payload)).toEqual(expected);
- });
-
- it('transforms without filesystem', () => {
- const payload = { standbys: [{ name: 'a' }] };
- const expected = [
- { class: 'popover-info', content: '1 up', titleText: '' },
- { class: 'card-text-line-break', content: '', titleText: '' },
- { class: 'popover-info', content: 'no filesystems', titleText: 'standby daemons: a' }
- ];
-
- expect(pipe.transform(payload)).toEqual(expected);
- });
-
- it('transforms without value', () => {
- expect(pipe.transform(undefined)).toBe('');
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-import _ from 'lodash';
-
-@Pipe({
- name: 'mdsDashboardSummary'
-})
-export class MdsDashboardSummaryPipe implements PipeTransform {
- transform(value: any): any {
- if (!value) {
- return '';
- }
-
- let contentLine1 = '';
- let contentLine2 = '';
- let standbys = 0;
- let active = 0;
- let standbyReplay = 0;
- _.each(value.standbys, () => {
- standbys += 1;
- });
-
- if (value.standbys && !value.filesystems) {
- contentLine1 = `${standbys} ${$localize`up`}`;
- contentLine2 = $localize`no filesystems`;
- } else if (value.filesystems.length === 0) {
- contentLine1 = $localize`no filesystems`;
- } else {
- _.each(value.filesystems, (fs) => {
- _.each(fs.mdsmap.info, (mds) => {
- if (mds.state === 'up:standby-replay') {
- standbyReplay += 1;
- } else {
- active += 1;
- }
- });
- });
-
- contentLine1 = `${active} ${$localize`active`}`;
- contentLine2 = `${standbys + standbyReplay} ${$localize`standby`}`;
- }
- const standbyHoverText = value.standbys.map((s: any): string => s.name).join(', ');
- const standbyTitleText = !standbyHoverText
- ? ''
- : `${$localize`standby daemons`}: ${standbyHoverText}`;
- const fsLength = value.filesystems ? value.filesystems.length : 0;
- const infoObject = fsLength > 0 ? value.filesystems[0].mdsmap.info : {};
- const activeHoverText = Object.values(infoObject)
- .map((info: any): string => info.name)
- .join(', ');
- let activeTitleText = !activeHoverText ? '' : `${$localize`active daemon`}: ${activeHoverText}`;
- // There is always one standbyreplay to replace active daemon, if active one is down
- if (!active && fsLength > 0) {
- activeTitleText = `${standbyReplay} ${$localize`standbyReplay`}`;
- }
- const mgrSummary = [
- {
- content: contentLine1,
- class: 'popover-info',
- titleText: activeTitleText
- }
- ];
- if (contentLine2) {
- mgrSummary.push({
- content: '',
- class: 'card-text-line-break',
- titleText: ''
- });
- mgrSummary.push({
- content: contentLine2,
- class: 'popover-info',
- titleText: standbyTitleText
- });
- }
-
- return mgrSummary;
- }
-}
+++ /dev/null
-import { TestBed } from '@angular/core/testing';
-
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { MgrDashboardSummaryPipe } from './mgr-dashboard-summary.pipe';
-
-describe('MgrDashboardSummaryPipe', () => {
- let pipe: MgrDashboardSummaryPipe;
-
- configureTestBed({
- providers: [MgrDashboardSummaryPipe]
- });
-
- beforeEach(() => {
- pipe = TestBed.inject(MgrDashboardSummaryPipe);
- });
-
- it('create an instance', () => {
- expect(pipe).toBeTruthy();
- });
-
- it('transforms without value', () => {
- expect(pipe.transform(undefined)).toBe('');
- });
-
- it('transforms with active_name undefined', () => {
- const payload: Record<string, any> = {
- active_name: undefined,
- standbys: []
- };
- const expected = [
- { class: 'popover-info', content: 'n/a active', titleText: '' },
- { class: 'card-text-line-break', content: '', titleText: '' },
- { class: 'popover-info', content: '0 standby', titleText: '' }
- ];
-
- expect(pipe.transform(payload)).toEqual(expected);
- });
-
- it('transforms with 1 active and 2 standbys', () => {
- const payload = {
- active_name: 'x',
- standbys: [{ name: 'y' }, { name: 'z' }]
- };
- const expected = [
- { class: 'popover-info', content: '1 active', titleText: 'active daemon: x' },
- { class: 'card-text-line-break', content: '', titleText: '' },
- { class: 'popover-info', content: '2 standby', titleText: 'standby daemons: y, z' }
- ];
-
- expect(pipe.transform(payload)).toEqual(expected);
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-import _ from 'lodash';
-
-@Pipe({
- name: 'mgrDashboardSummary'
-})
-export class MgrDashboardSummaryPipe implements PipeTransform {
- transform(value: any): any {
- if (!value) {
- return '';
- }
-
- let activeCount = $localize`n/a`;
- const activeTitleText = _.isUndefined(value.active_name)
- ? ''
- : `${$localize`active daemon`}: ${value.active_name}`;
- // There is always one standbyreplay to replace active daemon, if active one is down
- if (activeTitleText.length > 0) {
- activeCount = '1';
- }
- const standbyHoverText = value.standbys.map((s: any): string => s.name).join(', ');
- const standbyTitleText = !standbyHoverText
- ? ''
- : `${$localize`standby daemons`}: ${standbyHoverText}`;
- const standbyCount = value.standbys.length;
- const mgrSummary = [
- {
- content: `${activeCount} ${$localize`active`}`,
- class: 'popover-info',
- titleText: activeTitleText
- }
- ];
-
- mgrSummary.push({
- content: '',
- class: 'card-text-line-break',
- titleText: ''
- });
- mgrSummary.push({
- content: `${standbyCount} ${$localize`standby`}`,
- class: 'popover-info',
- titleText: standbyTitleText
- });
-
- return mgrSummary;
- }
-}
+++ /dev/null
-import { TestBed } from '@angular/core/testing';
-
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { MonSummaryPipe } from './mon-summary.pipe';
-
-describe('MonSummaryPipe', () => {
- let pipe: MonSummaryPipe;
-
- configureTestBed({
- providers: [MonSummaryPipe]
- });
-
- beforeEach(() => {
- pipe = TestBed.inject(MonSummaryPipe);
- });
-
- it('create an instance', () => {
- expect(pipe).toBeTruthy();
- });
-
- it('transforms without value', () => {
- expect(pipe.transform(undefined)).toBe('');
- });
-
- it('transforms with 3 mons in quorum', () => {
- const value = {
- monmap: { mons: [0, 1, 2] },
- quorum: [0, 1, 2]
- };
- expect(pipe.transform(value)).toBe('3 (quorum 0, 1, 2)');
- });
-
- it('transforms with 2/3 mons in quorum', () => {
- const value = {
- monmap: { mons: [0, 1, 2] },
- quorum: [0, 1]
- };
- expect(pipe.transform(value)).toBe('3 (quorum 0, 1)');
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
- name: 'monSummary'
-})
-export class MonSummaryPipe implements PipeTransform {
- transform(value: any): any {
- if (!value) {
- return '';
- }
-
- const result = $localize`${value.monmap.mons.length.toString()} (quorum \
-${value.quorum.join(', ')})`;
-
- return result;
- }
-}
+++ /dev/null
-import { TestBed } from '@angular/core/testing';
-
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { osdDashboardSummaryPipe } from './osd-dashboard-summary.pipe';
-
-describe('osdDashboardSummaryPipe', () => {
- let pipe: osdDashboardSummaryPipe;
-
- configureTestBed({
- providers: [osdDashboardSummaryPipe]
- });
-
- beforeEach(() => {
- pipe = TestBed.inject(osdDashboardSummaryPipe);
- });
-
- it('create an instance', () => {
- expect(pipe).toBeTruthy();
- });
-
- it('transforms without value', () => {
- expect(pipe.transform(undefined)).toBe('');
- });
-
- it('transforms having 3 osd with 3 up, 3 in, 0 down, 0 out', () => {
- const value = {
- osds: [
- { up: 1, in: 1, state: ['up', 'exists'] },
- { up: 1, in: 1, state: ['up', 'exists'] },
- { up: 1, in: 1, state: ['up', 'exists'] }
- ]
- };
- expect(pipe.transform(value)).toEqual([
- {
- content: '3 total',
- class: ''
- },
- {
- content: '',
- class: 'card-text-line-break'
- },
- {
- content: '3 up, 3 in',
- class: ''
- }
- ]);
- });
-
- it('transforms having 3 osd with 2 up, 1 in, 1 down, 2 out', () => {
- const value = {
- osds: [
- { up: 1, in: 1, state: ['up', 'exists'] },
- { up: 1, in: 0, state: ['up', 'exists'] },
- { up: 0, in: 0, state: ['exists'] }
- ]
- };
- expect(pipe.transform(value)).toEqual([
- {
- content: '3 total',
- class: ''
- },
- {
- content: '',
- class: 'card-text-line-break'
- },
- {
- content: '2 up, 1 in',
- class: ''
- },
- {
- content: '',
- class: 'card-text-line-break'
- },
- {
- content: '1 down, 2 out',
- class: 'card-text-error'
- }
- ]);
- });
-
- it('transforms having 3 osd with 2 up, 3 in, 1 full, 1 nearfull, 1 down, 0 out', () => {
- const value = {
- osds: [
- { up: 1, in: 1, state: ['up', 'nearfull'] },
- { up: 1, in: 1, state: ['up', 'exists'] },
- { up: 0, in: 1, state: ['full'] }
- ]
- };
- expect(pipe.transform(value)).toEqual([
- {
- content: '3 total',
- class: ''
- },
- {
- content: '',
- class: 'card-text-line-break'
- },
- {
- content: '2 up, 3 in',
- class: ''
- },
- {
- content: '',
- class: 'card-text-line-break'
- },
- {
- content: '1 down',
- class: 'card-text-error'
- },
- {
- content: '',
- class: 'card-text-line-break'
- },
- {
- content: '1 near full',
- class: 'card-text-error'
- },
- {
- content: '',
- class: 'card-text-line-break'
- },
- {
- content: '1 full',
- class: 'card-text-error'
- }
- ]);
- });
-
- it('transforms having 3 osd with 3 up, 2 in, 0 down, 1 out', () => {
- const value = {
- osds: [
- { up: 1, in: 1, state: ['up', 'exists'] },
- { up: 1, in: 1, state: ['up', 'exists'] },
- { up: 1, in: 0, state: ['up', 'exists'] }
- ]
- };
- expect(pipe.transform(value)).toEqual([
- {
- content: '3 total',
- class: ''
- },
- {
- content: '',
- class: 'card-text-line-break'
- },
- {
- content: '3 up, 2 in',
- class: ''
- },
- {
- content: '',
- class: 'card-text-line-break'
- },
- {
- content: '1 out',
- class: 'card-text-error'
- }
- ]);
- });
-
- it('transforms having 4 osd with 3 up, 2 in, 1 down, another 2 out', () => {
- const value = {
- osds: [
- { up: 1, in: 1, state: ['up', 'exists'] },
- { up: 1, in: 0, state: ['up', 'exists'] },
- { up: 1, in: 0, state: ['up', 'exists'] },
- { up: 0, in: 1, state: ['exists'] }
- ]
- };
- expect(pipe.transform(value)).toEqual([
- {
- content: '4 total',
- class: ''
- },
- {
- content: '',
- class: 'card-text-line-break'
- },
- {
- content: '3 up, 2 in',
- class: ''
- },
- {
- content: '',
- class: 'card-text-line-break'
- },
- {
- content: '1 down, 2 out',
- class: 'card-text-error'
- }
- ]);
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-import _ from 'lodash';
-
-@Pipe({
- name: 'osdDashboardSummary'
-})
-export class osdDashboardSummaryPipe implements PipeTransform {
- transform(value: any): any {
- if (!value) {
- return '';
- }
-
- let inCount = 0;
- let upCount = 0;
- let nearFullCount = 0;
- let fullCount = 0;
- _.each(value.osds, (osd) => {
- if (osd.in) {
- inCount++;
- }
- if (osd.up) {
- upCount++;
- }
- if (osd.state.includes('nearfull')) {
- nearFullCount++;
- }
- if (osd.state.includes('full')) {
- fullCount++;
- }
- });
-
- const osdSummary = [
- {
- content: `${value.osds.length} ${$localize`total`}`,
- class: ''
- }
- ];
- osdSummary.push({
- content: '',
- class: 'card-text-line-break'
- });
- osdSummary.push({
- content: `${upCount} ${$localize`up`}, ${inCount} ${$localize`in`}`,
- class: ''
- });
-
- const downCount = value.osds.length - upCount;
- const outCount = value.osds.length - inCount;
- if (downCount > 0 || outCount > 0) {
- osdSummary.push({
- content: '',
- class: 'card-text-line-break'
- });
-
- const downText = downCount > 0 ? `${downCount} ${$localize`down`}` : '';
- const separator = downCount > 0 && outCount > 0 ? ', ' : '';
- const outText = outCount > 0 ? `${outCount} ${$localize`out`}` : '';
- osdSummary.push({
- content: `${downText}${separator}${outText}`,
- class: 'card-text-error'
- });
- }
-
- if (nearFullCount > 0) {
- osdSummary.push(
- {
- content: '',
- class: 'card-text-line-break'
- },
- {
- content: `${nearFullCount} ${$localize`near full`}`,
- class: 'card-text-error'
- },
- {
- content: '',
- class: 'card-text-line-break'
- }
- );
- }
-
- if (fullCount > 0) {
- osdSummary.push({
- content: `${fullCount} ${$localize`full`}`,
- class: 'card-text-error'
- });
- }
-
- return osdSummary;
- }
-}
import { NotificationsSidebarComponent } from './notifications-sidebar/notifications-sidebar.component';
import { OrchestratorDocPanelComponent } from './orchestrator-doc-panel/orchestrator-doc-panel.component';
import { PwdExpirationNotificationComponent } from './pwd-expiration-notification/pwd-expiration-notification.component';
-import { RefreshSelectorComponent } from './refresh-selector/refresh-selector.component';
import { SelectBadgesComponent } from './select-badges/select-badges.component';
import { SelectComponent } from './select/select.component';
import { SparklineComponent } from './sparkline/sparkline.component';
GrafanaComponent,
SelectComponent,
BackButtonComponent,
- RefreshSelectorComponent,
ConfigOptionComponent,
AlertPanelComponent,
FormModalComponent,
LanguageSelectorComponent,
GrafanaComponent,
SelectComponent,
- RefreshSelectorComponent,
ConfigOptionComponent,
AlertPanelComponent,
PwdExpirationNotificationComponent,
+++ /dev/null
-<div class="container-fluid">
- <div class="row">
- <form>
- <div class="col-sm-1 d-flex float-end">
- <label for="refreshInterval"
- class="col-form-label my-0 mx-2 float-end"
- i18n>Refresh</label>
- <select id="refreshInterval"
- name="refreshInterval"
- class="form-select float-end"
- (change)="changeRefreshInterval($event.target.value)"
- [(ngModel)]="selectedInterval">
- <option *ngFor="let key of intervalKeys"
- [value]="intervalList[key]">{{ key }}</option>
- </select>
- </div>
- </form>
- </div>
-</div>
+++ /dev/null
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { FormsModule } from '@angular/forms';
-
-import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { RefreshSelectorComponent } from './refresh-selector.component';
-
-describe('RefreshSelectorComponent', () => {
- let component: RefreshSelectorComponent;
- let fixture: ComponentFixture<RefreshSelectorComponent>;
-
- configureTestBed({
- imports: [FormsModule],
- declarations: [RefreshSelectorComponent],
- providers: [RefreshIntervalService]
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(RefreshSelectorComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnInit } from '@angular/core';
-
-import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
-
-@Component({
- selector: 'cd-refresh-selector',
- templateUrl: './refresh-selector.component.html',
- styleUrls: ['./refresh-selector.component.scss']
-})
-export class RefreshSelectorComponent implements OnInit {
- selectedInterval: number;
- intervalList: { [key: string]: number } = {
- '5 s': 5000,
- '10 s': 10000,
- '15 s': 15000,
- '30 s': 30000,
- '1 min': 60000,
- '3 min': 180000,
- '5 min': 300000
- };
- intervalKeys = Object.keys(this.intervalList);
-
- constructor(private refreshIntervalService: RefreshIntervalService) {}
-
- ngOnInit() {
- this.selectedInterval = this.refreshIntervalService.getRefreshInterval() || 5000;
- }
-
- changeRefreshInterval(interval: number) {
- this.refreshIntervalService.setRefreshInterval(interval);
- }
-}
cephfs = true;
rgw = true;
nfs = true;
- dashboardV3 = true;
}
export type Features = keyof FeatureTogglesMap;
export type FeatureTogglesMap$ = Observable<FeatureTogglesMap>;
@import 'bootstrap/scss/bootstrap';
@import 'fork-awesome/scss/fork-awesome';
-@import 'app/ceph/dashboard/info-card/info-card-popover.scss';
@import 'app/ceph/rgw/rgw-overview-dashboard/rgw-overview-card-popover.scss';
@import './src/styles/bootstrap-extends';
@import './src/styles/ceph-custom/basics';
cephfs:
description: ''
type: boolean
- dashboard:
- description: ''
- type: boolean
iscsi:
description: ''
type: boolean
- cephfs
- rgw
- nfs
- - dashboard
type: object
description: OK
'400':
CEPHFS = 'cephfs'
RGW = 'rgw'
NFS = 'nfs'
- DASHBOARD = 'dashboard'
# if we want to add any custom warning message when enabling a feature
# we can add it here as key-value pair in warn_msg.
"cephfs": (bool, ''),
"rgw": (bool, ''),
"nfs": (bool, ''),
- "dashboard": (bool, '')
}
@APIRouter('/feature_toggles')