import { PageHelper } from '../page-helper.po';
export class DashboardV3PageHelper extends PageHelper {
- pages = { index: { url: '#/overview', id: 'cd-dashboard-v3' } };
+ pages = { index: { url: '#/overview', id: 'cd-overview' } };
cardTitle(index: number) {
return cy.get('.card-title').its(index).text();
import { ServiceFormComponent } from './ceph/cluster/services/service-form/service-form.component';
import { ServicesComponent } from './ceph/cluster/services/services.component';
import { TelemetryComponent } from './ceph/cluster/telemetry/telemetry.component';
-import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component';
import { NfsFormComponent } from './ceph/nfs/nfs-form/nfs-form.component';
import { PerformanceCounterComponent } from './ceph/performance-counter/performance-counter/performance-counter.component';
import { LoginPasswordFormComponent } from './core/auth/login-password-form/login-password-form.component';
import { NotificationsPageComponent } from './core/navigation/notification-panel/notifications-page/notifications-page.component';
import { CephfsMirroringWizardComponent } from './ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component';
import { CephfsMirroringErrorComponent } from './ceph/cephfs/cephfs-mirroring-error/cephfs-mirroring-error.component';
+import { OverviewComponent } from './ceph/overview/overview.component';
@Injectable()
export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver {
canActivate: [AuthGuardService, ChangePasswordGuardService],
canActivateChild: [AuthGuardService, ChangePasswordGuardService],
children: [
- { path: 'overview', component: DashboardComponent },
+ { path: 'overview', component: OverviewComponent },
{ path: 'error', component: ErrorComponent },
{
path: 'cephfs/mirroring/error',
import { SharedModule } from '../shared/shared.module';
import { CephfsModule } from './cephfs/cephfs.module';
import { ClusterModule } from './cluster/cluster.module';
-import { DashboardModule } from './dashboard/dashboard.module';
import { NfsModule } from './nfs/nfs.module';
import { PerformanceCounterModule } from './performance-counter/performance-counter.module';
import { SmbModule } from './smb/smb.module';
imports: [
CommonModule,
ClusterModule,
- DashboardModule,
PerformanceCounterModule,
CephfsModule,
NfsModule,
import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
import { InventoryDevicesComponent } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component';
-import { DashboardModule } from '~/app/ceph/dashboard/dashboard.module';
import { HostService } from '~/app/shared/api/host.service';
import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
SharedModule,
RouterTestingModule,
ReactiveFormsModule,
- ToastrModule.forRoot(),
- DashboardModule
+ ToastrModule.forRoot()
],
declarations: [OsdFormComponent, OsdDevicesSelectionGroupsComponent, InventoryDevicesComponent]
});
import { CephModule } from '~/app/ceph/ceph.module';
import { ClusterModule } from '~/app/ceph/cluster/cluster.module';
-import { DashboardModule } from '~/app/ceph/dashboard/dashboard.module';
import { CoreModule } from '~/app/core/core.module';
import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
import { SharedModule } from '~/app/shared/shared.module';
ToastrModule.forRoot(),
SharedModule,
ClusterModule,
- DashboardModule,
CephModule,
CoreModule
]
import { DashboardAreaChartComponent } from './dashboard-area-chart/dashboard-area-chart.component';
import { DashboardPieComponent } from './dashboard-pie/dashboard-pie.component';
import { DashboardTimeSelectorComponent } from './dashboard-time-selector/dashboard-time-selector.component';
-import { DashboardV3Component } from './dashboard/dashboard-v3.component';
-import { PgSummaryPipe } from './pg-summary.pipe';
import { InlineLoadingModule, ToggletipModule, TagModule } from 'carbon-components-angular';
import { ProductiveCardComponent } from '~/app/shared/components/productive-card/productive-card.component';
ProductiveCardComponent
],
declarations: [
- DashboardV3Component,
DashboardPieComponent,
- PgSummaryPipe,
DashboardAreaChartComponent,
DashboardTimeSelectorComponent
],
- exports: [
- DashboardV3Component,
- DashboardAreaChartComponent,
- DashboardTimeSelectorComponent,
- DashboardPieComponent
- ],
+ exports: [DashboardAreaChartComponent, DashboardTimeSelectorComponent, DashboardPieComponent],
providers: [provideCharts(withDefaultRegisterables())]
})
export class DashboardV3Module {}
+++ /dev/null
-<div class="container-fluid p-4">
- <div class="row d-flex flex-row ps-3">
- <!-- First Grid to hold Details and Inventory Card-->
- <div class="col-sm-3 d-flex flex-column ps-2 details-card">
- <cd-productive-card headerTitle="Details"
- i18n-headerTitle>
- <dl>
- <dt>Cluster ID</dt>
- <dd>{{ detailsCardData.fsid }}</dd>
- <dt>Orchestrator</dt>
- <dd i18n>{{ detailsCardData.orchestrator || 'Orchestrator is not available' }}</dd>
- <dt>Ceph version</dt>
- <dd>
- {{ detailsCardData.cephVersion }}
- <cd-upgradable></cd-upgradable>
- </dd>
- <dt>Cluster API</dt>
- <dd>
- <a routerLink="/api-docs"
- target="_blank">
- {{ origin }}/api-docs
- <i class="fa fa-external-link"></i>
- </a>
- </dd>
- <ng-container>
- <dt>Telemetry Dashboard
- <cds-tag *ngIf="telemetryEnabled !== null"
- [class]="telemetryEnabled ? 'tag-success' : 'tag-secondary'"
- [size]="'md'"
- [ngbTooltip]="getTelemetryText()">
- {{ telemetryEnabled ? 'Active' : 'Inactive' }}
- </cds-tag>
- </dt>
- <dd>
- <a target="_blank"
- [href]="telemetryURL">
- {{ telemetryURL }}
- <i class="fa fa-external-link"></i>
- </a>
- </dd>
- </ng-container>
- <ng-container *ngIf="managedByConfig$ | async as managedByConfig">
- <span *ngIf="managedByConfig['MANAGED_BY_CLUSTERS'].length > 0">
- <dt>Managed By</dt>
- <dd>
- <a target="_blank"
- [href]="managedByConfig['MANAGED_BY_CLUSTERS'][0]['url']">
- {{ managedByConfig['MANAGED_BY_CLUSTERS'][0]['fsid'] }}
- <i class="fa fa-external-link"></i>
- </a>
- </dd>
- </span>
- </ng-container>
- </dl>
- </cd-productive-card>
-
- <!-- Inventory Card-->
- <cd-card cardTitle="Inventory"
- i18n-title
- class="pt-4"
- aria-label="Inventory card">
- <ng-container *ngIf="enabledFeature$ | async as enabledFeature">
- <!-- Hosts -->
- <cd-card-row [data]="hostsCount"
- link="/hosts"
- title="Host"
- summaryType="simplified"
- [dropdownData]="(isHardwareEnabled$ | async) && (hardwareSummary$ | async)">
- </cd-card-row>
- <!-- Monitors -->
- <cd-card-row [data]="monCount"
- link="/monitor"
- title="Monitor"
- summaryType="simplified"></cd-card-row>
- <!-- Managers -->
- <cd-card-row [data]="mgrStatus"
- title="Manager"></cd-card-row>
-
- <!-- OSDs -->
- <cd-card-row [data]="osdCount"
- link="/osd"
- title="OSD"
- summaryType="osd"></cd-card-row>
-
- <!-- Pools -->
- <cd-card-row [data]="poolCount"
- link="/pool"
- title="Pool"
- summaryType="simplified"></cd-card-row>
-
- <!-- PG Info -->
- <cd-card-row [data]="pgStatus | pgSummary"
- title="PG"></cd-card-row>
-
- <!-- Object gateways -->
- <cd-card-row [data]="rgwCount"
- link="/rgw/daemon"
- title="Object Gateway"
- summaryType="simplified"
- id="rgw-item"
- *ngIf="enabledFeature.rgw"></cd-card-row>
-
- <!-- Metadata Servers -->
- <cd-card-row [data]="mdsStatus"
- title="Metadata Server"
- id="mds-item"
- *ngIf="enabledFeature.cephfs"></cd-card-row>
- <!-- iSCSI Gateways -->
- <cd-card-row [data]="iscsiMap"
- link="/iscsi/daemon"
- title="iSCSI Gateway"
- summaryType="iscsi"
- id="iscsi-item"
- *ngIf="enabledFeature.iscsi"></cd-card-row>
- </ng-container>
- </cd-card>
- </div>
-
- <!-- Second Grid to hold Status Capacity and Cluster Utilization Cards-->
- <div class="col-sm-9 ps-0">
- <div class="row">
- <!-- This column will hold Status and Capacity cards-->
- <div class="col-sm-8">
- <cd-card cardTitle="Status"
- i18n-title
- aria-label="Status card"
- class="status"
- [alignItemsCenter]="true"
- [cardFooter]="isAlertmanagerConfigured && prometheusAlertService.alerts.length"
- [fullHeight]="true">
- <div class="viewAlert"
- *ngIf="prometheusAlertService.alerts.length">
- <a href="#/monitoring/active-alerts"
- i18n>
- View alerts
- </a>
- </div>
- <div class="d-flex flex-column ms-4 me-4 mt-4 mb-4">
- <div class="d-flex flex-row col-md-3 ms-4">
- <i *ngIf="healthCardData?.status else loadingTpl"
- [ngClass]="[healthCardData.status | healthIcon, icons.large2x]"
- [ngStyle]="healthCardData.status | healthColor"
- [title]="healthCardData.status">
- </i>
- <span class="ms-2 mt-n1 lead"
- *ngIf="!hasHealthChecks"
- i18n>Cluster</span>
- <cds-toggletip [dropShadow]="true"
- [autoAlign]="true">
- <div cdsToggletipButton>
- <a class="ms-2 mt-n1 lead text-primary"
- popoverClass="info-card-popover-cluster-status"
- *ngIf="hasHealthChecks"
- i18n>Cluster
- </a>
- </div>
- <div cdsToggletipContent
- #healthCheck>
- <div class="cds--popover-scroll-container">
- <cd-health-checks *ngIf="hasHealthChecks"
- [healthData]="healthCardData.checks">
- </cd-health-checks>
- </div>
- </div>
- </cds-toggletip>
- </div>
- </div>
-
- <div class="d-flex flex-column col-md-3">
- <div *ngIf="hasHardwareError"
- class="d-flex flex-row">
- <i class="text-danger"
- [ngClass]="[icons.danger, icons.large2x]"></i>
- <span class="ms-2 mt-n1 lead"
- i18n>Hardware</span>
- </div>
- </div>
- <section class="footer alerts"
- *ngIf="isAlertmanagerConfigured && prometheusAlertService.alerts.length">
- <div class="d-flex flex-wrap ms-4 me-4 mb-3 mt-3">
- <span class="pt-2"
- i18n>Alerts</span>
-
- <!-- Potentially make widget component -->
- <button class="btn btn-outline-danger rounded-pill ms-2"
- [ngClass]="{'active': true && alertType === 'critical'}"
- title="Danger"
- (click)="toggleAlertsWindow('critical')"
- id="dangerAlerts"
- i18n-title
- *ngIf="prometheusAlertService?.activeCriticalAlerts">
- <i [ngClass]="[icons.danger]"></i>
- <span>{{ prometheusAlertService.activeCriticalAlerts }}</span>
- </button>
-
- <button class="btn btn-outline-warning rounded-pill ms-2"
- [ngClass]="{'active': true && alertType === 'warning'}"
- title="Warning"
- (click)="toggleAlertsWindow('warning')"
- id="warningAlerts"
- i18n-title
- *ngIf="prometheusAlertService?.activeWarningAlerts">
- <i [ngClass]="[icons.infoCircle]"></i>
- <span>{{ prometheusAlertService.activeWarningAlerts }}</span>
- </button>
- </div>
-
- <div class="alerts-section pt-0">
- <hr class="mt-1 mb-0">
- <ngx-simplebar [options]="simplebar">
- <div class="card-body p-0">
- <ng-container *ngTemplateOutlet="alertsCard"></ng-container>
- </div>
- </ngx-simplebar>
- </div>
- </section>
- </cd-card>
- </div>
- <div class="col-sm-4 ps-0">
- <cd-card cardTitle="Capacity"
- i18n-title
- [fullHeight]="true"
- aria-label="Capacity card">
- <ng-container class="ms-4 me-4"
- *ngIf="totalCapacity && usedCapacity">
- <cd-dashboard-pie [data]="{max: totalCapacity, current: usedCapacity}"
- [lowThreshold]="capacityCardData.osdNearfull"
- [highThreshold]="capacityCardData.osdFull">
- </cd-dashboard-pie>
- </ng-container>
- </cd-card>
- </div>
-
- <!-- This column will hold Cluster Utlization card -->
- <div class="col-sm-12 d-flex flex-column pt-4">
- <cd-card cardTitle="Cluster Utilization"
- i18n-title
- aria-label="Cluster utilization card">
- <div class="ms-4 me-4 mt-0">
- <cd-dashboard-time-selector (selectedTime)="getPrometheusData($event)">
- </cd-dashboard-time-selector>
- <ng-container *ngIf="totalCapacity">
- <cd-dashboard-area-chart chartTitle="Used Capacity (RAW)"
- [maxValue]="totalCapacity"
- dataUnits="B"
- [labelsArray]="['Used Capacity']"
- [dataArray]="[queriesResults.USEDCAPACITY]">
- </cd-dashboard-area-chart>
- </ng-container>
- <cd-dashboard-area-chart chartTitle="IOPS"
- dataUnits=""
- decimals="0"
- [labelsArray]="['Reads', 'Writes']"
- [dataArray]="[queriesResults.READIOPS, queriesResults.WRITEIOPS]">
- </cd-dashboard-area-chart>
- <cd-dashboard-area-chart chartTitle="OSD Latencies"
- dataUnits="ms"
- decimals="2"
- [labelsArray]="['Apply', 'Commit']"
- [dataArray]="[queriesResults.READLATENCY, queriesResults.WRITELATENCY]">
- </cd-dashboard-area-chart>
- <cd-dashboard-area-chart chartTitle="Client Throughput"
- dataUnits="B/s"
- decimals="2"
- [labelsArray]="['Reads', 'Writes']"
- [dataArray]="[queriesResults.READCLIENTTHROUGHPUT, queriesResults.WRITECLIENTTHROUGHPUT]">
- </cd-dashboard-area-chart>
- <cd-dashboard-area-chart chartTitle="Recovery Throughput"
- dataUnits="B/s"
- decimals="2"
- [labelsArray]="['Recovery Throughput']"
- [dataArray]="[queriesResults.RECOVERYBYTES]">
- </cd-dashboard-area-chart>
- </div>
- </cd-card>
- </div>
- </div>
- </div>
- </div>
-</div>
-
-<ng-template #alertsCard>
- <ng-container *ngFor="let alert of prometheusAlertService.alerts; let i = index; trackBy: trackByFn">
- <div [ngClass]="['border-'+alertClass[alert.labels.severity]]"
- *ngIf="alert.status.state === 'active' &&
- (alert.labels.severity === alertType ||
- !alertType)">
- <div class="card tc_alerts border-0 pt-3">
- <div class="row no-gutters ps-2">
- <div class="col-sm-1 text-center">
- <span [ngClass]="[icons.stack, icons.large, 'text-'+alertClass[alert.labels.severity]]">
- <i [ngClass]="[icons.circle, icons.stack2x]"></i>
- <i [ngClass]="[icons.stack1x, icons.inverse, icons.warning]"></i>
- </span>
- </div>
- <div class="col-md-11 ps-0">
- <div class="card-body ps-0 pe-1 pb-1 pt-0">
- <h6 class="card-title bold">{{ alert.labels.alertname }}</h6>
- <p class="card-text me-3 mb-0 text-truncate"
- [innerHtml]="alert.annotations.summary"
- [ngbTooltip]="alert.annotations.summary"></p>
- <p class="card-text text-muted me-3">
- <small class="date"
- [title]="alert.startsAt | cdDate"
- i18n>Active since: {{ alert.startsAt | relativeDate }}</small>
- <small class="alert_count"
- *ngIf="alert.alert_count > 1"
- [title]="alert.alert_count"
- i18n>Total occurrences: {{ alert.alert_count }}</small>
- </p>
- </div>
- </div>
- </div>
- </div>
- <hr class="mt-0 mb-0">
- </div>
- </ng-container>
-</ng-template>
-
-<ng-template #loadingTpl>
- <cds-inline-loading></cds-inline-loading>
-</ng-template>
+++ /dev/null
-@use './src/styles/vendor/variables' as vv;
-@use '@carbon/layout';
-
-.details {
- font-size: larger;
-
- dt {
- margin-bottom: 0.3rem;
- }
-
- dd {
- margin-bottom: 0.8rem;
- }
-}
-
-.status {
- .viewAlert {
- position: absolute;
- right: 2rem;
- top: 2rem;
- }
-}
-
-.alerts {
- ngx-simplebar {
- height: 13.5rem;
- overflow-x: hidden;
- }
-
- .text-truncate {
- -webkit-box-orient: vertical; /* stylelint-disable-line property-no-vendor-prefix */
- display: -webkit-box; /* stylelint-disable-line value-no-vendor-prefix */
- -webkit-line-clamp: 2;
- white-space: normal;
- }
-
- .card-text .date {
- display: inline-block;
- min-width: layout.rem(220px);
- }
-
- .card-text .alert_count {
- display: inline-block;
- }
-}
-
-.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;
- }
-}
-
-.details-card {
- dl,
- dd:last-of-type {
- margin-bottom: 0;
- }
-}
+++ /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 { RouterTestingModule } from '@angular/router/testing';
-
-import _ from 'lodash';
-import { ToastrModule } from 'ngx-toastr';
-import { BehaviorSubject, of } from 'rxjs';
-
-import { HealthService } from '~/app/shared/api/health.service';
-import { PrometheusService } from '~/app/shared/api/prometheus.service';
-import { CssHelper } from '~/app/shared/classes/css-helper';
-import { AlertmanagerAlert } from '~/app/shared/models/prometheus-alerts';
-import { FeatureTogglesService } from '~/app/shared/services/feature-toggles.service';
-import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
-import { SummaryService } from '~/app/shared/services/summary.service';
-import { SharedModule } from '~/app/shared/shared.module';
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { PgCategoryService } from '../../shared/pg-category.service';
-import { DashboardPieComponent } from '../dashboard-pie/dashboard-pie.component';
-import { PgSummaryPipe } from '../pg-summary.pipe';
-import { DashboardV3Component } from './dashboard-v3.component';
-import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
-import { AlertClass } from '~/app/shared/enum/health-icon.enum';
-import { HealthSnapshotMap } from '~/app/shared/models/health.interface';
-import { VERSION_PREFIX } from '~/app/shared/constants/app.constants';
-
-export class SummaryServiceMock {
- summaryDataSource = new BehaviorSubject({
- version:
- `${VERSION_PREFIX} 17.0.0-12222-gcd0cd7cb ` +
- '(b8193bb4cda16ccc5b028c3e1df62bc72350a15d) quincy (dev)'
- });
- summaryData$ = this.summaryDataSource.asObservable();
-
- subscribe(call: any) {
- return this.summaryData$.subscribe(call);
- }
-}
-
-describe('Dashbord Component', () => {
- let component: DashboardV3Component;
- let fixture: ComponentFixture<DashboardV3Component>;
- let orchestratorService: OrchestratorService;
- let getHealthStatusSpy: jasmine.Spy;
- let getAlertsSpy: jasmine.Spy;
- let fakeFeatureTogglesService: jasmine.Spy;
-
- const healthStatusPayload: HealthSnapshotMap = {
- fsid: '7d0cc9da-ca8d-4539-a953-ab062139c26a',
- health: {
- status: 'HEALTH_WARN',
- checks: {
- DASHBOARD_DEBUG: {
- severity: 'HEALTH_WARN',
- summary: {
- message: 'Dashboard debug mode is enabled',
- count: 0
- },
- muted: false
- }
- },
- mutes: []
- },
- monmap: {
- num_mons: 3
- },
- osdmap: {
- in: 3,
- up: 3,
- num_osds: 3
- },
- pgmap: {
- pgs_by_state: [
- {
- state_name: 'active+clean',
- count: 497
- }
- ],
- num_pools: 14,
- bytes_used: 3236978688,
- bytes_total: 325343772672,
- num_pgs: 497,
- write_bytes_sec: 0,
- read_bytes_sec: 0,
- recovering_bytes_per_sec: 0
- },
- mgrmap: {
- num_active: 1,
- num_standbys: 0
- },
- fsmap: {
- num_standbys: 2,
- num_active: 1
- },
- num_rgw_gateways: 3,
- num_iscsi_gateways: {
- up: 0,
- down: 0
- },
- num_hosts: 1
- };
-
- const alertsPayload: AlertmanagerAlert[] = [
- {
- labels: {
- alertname: 'CephMgrPrometheusModuleInactive',
- instance: 'ceph2:9283',
- job: 'ceph',
- severity: 'critical'
- },
- annotations: {
- description: 'The mgr/prometheus module at ceph2:9283 is unreachable.',
- summary: 'The mgr/prometheus module is not available'
- },
- startsAt: '2022-09-28T08:23:41.152Z',
- endsAt: '2022-09-28T15:28:01.152Z',
- generatorURL: 'http://prometheus:9090/testUrl',
- status: {
- state: 'active',
- silencedBy: null,
- inhibitedBy: null
- },
- receivers: ['ceph2'],
- fingerprint: 'fingerprint',
- alert_count: 1
- },
- {
- labels: {
- alertname: 'CephOSDDownHigh',
- instance: 'ceph:9283',
- job: 'ceph',
- severity: 'critical'
- },
- annotations: {
- description: '66.67% or 2 of 3 OSDs are down (>= 10%).',
- summary: 'More than 10% of OSDs are down'
- },
- startsAt: '2022-09-28T14:17:22.665Z',
- endsAt: '2022-09-28T15:28:32.665Z',
- generatorURL: 'http://prometheus:9090/testUrl',
- status: {
- state: 'active',
- silencedBy: null,
- inhibitedBy: null
- },
- receivers: ['default'],
- fingerprint: 'fingerprint',
- alert_count: 1
- },
- {
- labels: {
- alertname: 'CephHealthWarning',
- instance: 'ceph:9283',
- job: 'ceph',
- severity: 'warning'
- },
- annotations: {
- description: 'The cluster state has been HEALTH_WARN for more than 15 minutes.',
- summary: 'Ceph is in the WARNING state'
- },
- startsAt: '2022-09-28T08:41:38.454Z',
- endsAt: '2022-09-28T15:28:38.454Z',
- generatorURL: 'http://prometheus:9090/testUrl',
- status: {
- state: 'active',
- silencedBy: null,
- inhibitedBy: null
- },
- receivers: ['ceph'],
- fingerprint: 'fingerprint',
- alert_count: 1
- }
- ];
-
- const orchName: any = 'Cephadm';
-
- configureTestBed({
- imports: [RouterTestingModule, HttpClientTestingModule, ToastrModule.forRoot(), SharedModule],
- declarations: [DashboardV3Component, DashboardPieComponent, PgSummaryPipe],
- schemas: [NO_ERRORS_SCHEMA],
- providers: [
- { provide: SummaryService, useClass: SummaryServiceMock },
- PrometheusAlertService,
- CssHelper,
- PgCategoryService
- ]
- });
-
- beforeEach(() => {
- fakeFeatureTogglesService = spyOn(TestBed.inject(FeatureTogglesService), 'get').and.returnValue(
- of({
- rbd: true,
- mirroring: true,
- iscsi: true,
- cephfs: true,
- rgw: true
- })
- );
- fixture = TestBed.createComponent(DashboardV3Component);
- component = fixture.componentInstance;
- orchestratorService = TestBed.inject(OrchestratorService);
- getHealthStatusSpy = spyOn(TestBed.inject(HealthService), 'getHealthSnapshot');
- getHealthStatusSpy.and.returnValue(of(healthStatusPayload));
- getAlertsSpy = spyOn(TestBed.inject(PrometheusService), 'getAlerts');
- getAlertsSpy.and.returnValue(of(alertsPayload));
- component.prometheusAlertService.alerts = alertsPayload;
- component.isAlertmanagerConfigured = true;
- let prometheusAlertService = TestBed.inject(PrometheusAlertService);
- spyOn(prometheusAlertService, 'getGroupedAlerts').and.callFake(() => of([]));
- prometheusAlertService.activeCriticalAlerts = 2;
- prometheusAlertService.activeWarningAlerts = 1;
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('should render all cards', () => {
- fixture.detectChanges();
- const dashboardCards = fixture.debugElement.nativeElement.querySelectorAll('cd-card');
- expect(dashboardCards.length).toBe(4);
- });
-
- it('should get corresponding data into detailsCardData', () => {
- spyOn(orchestratorService, 'getName').and.returnValue(of(orchName));
- component.ngOnInit();
- expect(component.detailsCardData.fsid).toBe(healthStatusPayload['fsid']);
- expect(component.detailsCardData.orchestrator).toBe('Cephadm');
- expect(component.detailsCardData.cephVersion).toBe('17.0.0-12222-gcd0cd7cb quincy (dev)');
- });
-
- it('should check if the respective icon is shown for each status', () => {
- const payload = _.cloneDeep(healthStatusPayload);
-
- // HEALTH_WARN
- payload.health['status'] = 'HEALTH_WARN';
- payload.health['checks'] = {
- FAKE_CHECK: {
- severity: 'HEALTH_WARN',
- summary: { message: 'fake warning', count: 1 },
- muted: false
- }
- };
-
- getHealthStatusSpy.and.returnValue(of(payload));
- fixture.detectChanges();
- const clusterStatusCard = fixture.debugElement.query(By.css('cd-card[cardTitle="Status"] i'));
- expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
-
- // HEALTH_ERR
- payload.health['status'] = 'HEALTH_ERR';
- payload.health['checks'] = {
- FAKE_CHECK: {
- severity: 'HEALTH_ERR',
- summary: { message: 'fake error', count: 1 },
- muted: false
- }
- };
-
- getHealthStatusSpy.and.returnValue(of(payload));
- fixture.detectChanges();
- expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
-
- // HEALTH_OK
- payload.health['status'] = 'HEALTH_OK';
- payload.health['checks'] = {};
-
- getHealthStatusSpy.and.returnValue(of(payload));
- fixture.detectChanges();
- expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
- });
-
- it('should show the actual alert count on each alerts pill', () => {
- fixture.detectChanges();
-
- const warningAlerts = fixture.debugElement.query(By.css('button[id=warningAlerts] span'));
-
- const dangerAlerts = fixture.debugElement.query(By.css('button[id=dangerAlerts] span'));
-
- expect(warningAlerts.nativeElement.textContent).toBe('1');
- expect(dangerAlerts.nativeElement.textContent).toBe('2');
- });
-
- it('should show the critical alerts window and its content', () => {
- const payload = _.cloneDeep(alertsPayload[0]);
- component.toggleAlertsWindow(AlertClass[0]);
- fixture.detectChanges();
-
- const cardTitle = fixture.debugElement.query(By.css('.tc_alerts h6.card-title'));
-
- expect(cardTitle.nativeElement.textContent).toBe(payload.labels.alertname);
- expect(component.alertType).not.toBe('warning');
- });
-
- it('should show the warning alerts window and its content', () => {
- const payload = _.cloneDeep(alertsPayload[2]);
- component.toggleAlertsWindow(AlertClass.warning);
- fixture.detectChanges();
-
- const cardTitle = fixture.debugElement.query(By.css('.tc_alerts h6.card-title'));
-
- expect(cardTitle.nativeElement.textContent).toBe(payload.labels.alertname);
- expect(component.alertType).not.toBe('critical');
- });
-
- it('should only show the pills when the alerts are not empty', () => {
- spyOn(TestBed.inject(PrometheusAlertService), 'alerts').and.returnValue(0);
- fixture.detectChanges();
-
- const warningAlerts = fixture.debugElement.query(By.css('button[id=warningAlerts]'));
-
- const dangerAlerts = fixture.debugElement.query(By.css('button[id=dangerAlerts]'));
-
- expect(warningAlerts).toBe(null);
- expect(dangerAlerts).toBe(null);
- });
-
- it('should render "Status" card text that is not clickable', () => {
- const payload = _.cloneDeep(healthStatusPayload);
- payload.health['status'] = 'HEALTH_OK';
- payload.health['checks'] = null;
-
- getHealthStatusSpy.and.returnValue(of(payload));
- fixture.detectChanges();
- const clusterStatusCard = fixture.debugElement.query(By.css('cd-card[cardTitle="Status"]'));
- const clickableContent = clusterStatusCard.query(By.css('.lead.text-primary'));
- expect(clickableContent).toBeNull();
- });
-
- it('should render "Status" card text that is clickable (popover)', () => {
- const payload = _.cloneDeep(healthStatusPayload);
- payload.health['status'] = 'HEALTH_WARN';
- payload.health['checks'] = {
- FAKE_CHECK: {
- severity: 'HEALTH_WARN',
- summary: { message: 'fake warning', count: 1 },
- muted: false
- }
- };
-
- getHealthStatusSpy.and.returnValue(of(payload));
- fixture.detectChanges();
-
- const clusterStatusCard = fixture.debugElement.query(By.css('cd-card[cardTitle="Status"]'));
- const clickableContent = clusterStatusCard.query(By.css('.lead.text-primary'));
- expect(clickableContent).not.toBeNull();
- });
-
- describe('features disabled', () => {
- beforeEach(() => {
- fakeFeatureTogglesService.and.returnValue(
- of({
- rbd: false,
- mirroring: false,
- iscsi: false,
- cephfs: false,
- rgw: false
- })
- );
- fixture = TestBed.createComponent(DashboardV3Component);
- component = fixture.componentInstance;
- });
-
- it('should not render items related to disabled features', () => {
- fixture.detectChanges();
-
- const iscsiCard = fixture.debugElement.query(By.css('li[id=iscsi-item]'));
- const rgwCard = fixture.debugElement.query(By.css('li[id=rgw-item]'));
- const mds = fixture.debugElement.query(By.css('li[id=mds-item]'));
-
- expect(iscsiCard).toBeFalsy();
- expect(rgwCard).toBeFalsy();
- expect(mds).toBeFalsy();
- });
- });
-});
+++ /dev/null
-import { Component, OnDestroy, OnInit } from '@angular/core';
-
-import _ from 'lodash';
-import { BehaviorSubject, EMPTY, Observable, Subject, Subscription, of } from 'rxjs';
-import { catchError, exhaustMap, switchMap, takeUntil } from 'rxjs/operators';
-
-import { HealthService } from '~/app/shared/api/health.service';
-import { PrometheusService, PromqlGuageMetric } from '~/app/shared/api/prometheus.service';
-import {
- CapacityCardQueries,
- UtilizationCardQueries
-} from '~/app/shared/enum/dashboard-promqls.enum';
-import { Icons } from '~/app/shared/enum/icons.enum';
-import {
- CapacityCardDetails,
- DashboardDetails,
- InventoryCommonDetail,
- InventoryDetails
-} from '~/app/shared/models/cd-details';
-import { Permissions } from '~/app/shared/models/permissions';
-import { AlertmanagerAlert } from '~/app/shared/models/prometheus-alerts';
-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';
-import { SummaryService } from '~/app/shared/services/summary.service';
-import { PrometheusListHelper } from '~/app/shared/helpers/prometheus-list-helper';
-import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
-import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
-import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
-import { AlertClass } from '~/app/shared/enum/health-icon.enum';
-import { HardwareService } from '~/app/shared/api/hardware.service';
-import { SettingsService } from '~/app/shared/api/settings.service';
-import {
- Health,
- HealthSnapshotMap,
- IscsiMap,
- PgStateCount
-} from '~/app/shared/models/health.interface';
-import { VERSION_PREFIX } from '~/app/shared/constants/app.constants';
-
-@Component({
- selector: 'cd-dashboard-v3',
- templateUrl: './dashboard-v3.component.html',
- styleUrls: ['./dashboard-v3.component.scss'],
- standalone: false
-})
-export class DashboardV3Component extends PrometheusListHelper implements OnInit, OnDestroy {
- telemetryURL = 'https://telemetry-public.ceph.com/';
- origin = window.location.origin;
- icons = Icons;
-
- permissions: Permissions;
-
- hardwareSubject = new BehaviorSubject<any>([]);
- private subs = new Subscription();
- private destroy$ = new Subject<void>();
-
- enabledFeature$: FeatureTogglesMap$;
- prometheusAlerts$: Observable<AlertmanagerAlert[]>;
- isHardwareEnabled$: Observable<boolean>;
- hardwareSummary$: Observable<any>;
- managedByConfig$: Observable<any>;
-
- color: string;
- flexHeight = true;
- simplebar = {
- autoHide: true
- };
- borderClass: string;
- alertType: string;
- alertClass = AlertClass;
-
- queriesResults: Record<string, [number, string][]> = {
- USEDCAPACITY: [],
- IPS: [],
- OPS: [],
- READLATENCY: [],
- WRITELATENCY: [],
- READCLIENTTHROUGHPUT: [],
- WRITECLIENTTHROUGHPUT: [],
- RECOVERYBYTES: [],
- READIOPS: [],
- WRITEIOPS: []
- };
-
- telemetryEnabled: boolean;
- detailsCardData: DashboardDetails = {};
- capacityCardData: CapacityCardDetails = {
- osdNearfull: null,
- osdFull: null
- };
- healthCardData: Health;
- hasHealthChecks: boolean;
- hardwareHealth: any;
- hardwareEnabled: boolean = false;
- hasHardwareError: boolean = false;
- totalCapacity: number = null;
- usedCapacity: number = null;
- hostsCount: number = null;
- monCount: number = null;
- poolCount: number = null;
- rgwCount: number = null;
- osdCount: { in: number; out: number; up: number; down: number } & InventoryCommonDetail = null;
- pgStatus: { statuses: PgStateCount[] } & InventoryCommonDetail = null;
- mgrStatus: InventoryDetails = null;
- mdsStatus: InventoryDetails = null;
- iscsiMap: IscsiMap = null;
-
- constructor(
- private summaryService: SummaryService,
- private orchestratorService: OrchestratorService,
- private authStorageService: AuthStorageService,
- private featureToggles: FeatureTogglesService,
- private healthService: HealthService,
- private settingsService: SettingsService,
- public prometheusService: PrometheusService,
- private mgrModuleService: MgrModuleService,
- private refreshIntervalService: RefreshIntervalService,
- public prometheusAlertService: PrometheusAlertService,
- private hardwareService: HardwareService
- ) {
- super(prometheusService);
- this.permissions = this.authStorageService.getPermissions();
- this.enabledFeature$ = this.featureToggles.get();
- }
-
- ngOnInit() {
- super.ngOnInit();
- if (this.permissions.configOpt.read) {
- this.isHardwareEnabled$ = this.getHardwareConfig();
- this.hardwareSummary$ = this.hardwareSubject.pipe(
- switchMap(() =>
- this.hardwareService.getSummary().pipe(
- switchMap((data: any) => {
- this.hasHardwareError = data.host.flawed;
- return of(data);
- })
- )
- )
- );
- this.managedByConfig$ = this.settingsService.getValues('MANAGED_BY_CLUSTERS');
- }
-
- this.loadInventories();
- this.getPrometheusData(this.prometheusService.lastHourDateObject);
- this.getDetailsCardData();
- this.getTelemetryReport();
- this.getCapacityCardData();
- this.prometheusAlertService.getGroupedAlerts(true);
- }
-
- getTelemetryText(): string {
- return this.telemetryEnabled
- ? $localize`Cluster telemetry is active`
- : $localize`Cluster telemetry is inactive. To Activate the Telemetry, \
- click settings icon on top navigation bar and select \
- Telemetry configration.`;
- }
-
- ngOnDestroy() {
- this.prometheusService.unsubscribe();
- this.subs?.unsubscribe();
- this.destroy$.next();
- this.destroy$.complete();
- }
-
- toggleAlertsWindow(type: AlertClass) {
- this.alertType === type ? (this.alertType = null) : (this.alertType = type);
- }
-
- getDetailsCardData() {
- this.orchestratorService.getName().subscribe((data: string) => {
- this.detailsCardData.orchestrator = data;
- });
- this.subs.add(
- this.summaryService.subscribe((summary) => {
- const version = summary.version.replace(`${VERSION_PREFIX} `, '').split(' ');
- this.detailsCardData.cephVersion =
- version[0] + ' ' + version.slice(2, version.length).join(' ');
- })
- );
- }
-
- public getPrometheusData(selectedTime: any) {
- this.prometheusService
- .getRangeQueriesData(selectedTime, UtilizationCardQueries, true)
- .pipe(takeUntil(this.destroy$))
- .subscribe((results) => {
- this.queriesResults = results;
- });
- }
-
- getCapacityQueryValues(data: PromqlGuageMetric['result']) {
- let osdFull = null;
- let osdNearfull = null;
- if (data?.[0]?.metric?.['__name__'] === CapacityCardQueries.OSD_FULL) {
- osdFull = data[0]?.value?.[1];
- osdNearfull = data[1]?.value?.[1];
- } else {
- osdFull = data?.[1]?.value?.[1];
- osdNearfull = data?.[0]?.value?.[1];
- }
- return [osdFull, osdNearfull];
- }
-
- getCapacityCardData() {
- const CAPACITY_QUERY = `{__name__=~"${CapacityCardQueries.OSD_FULL}|${CapacityCardQueries.OSD_NEARFULL}"}`;
- this.prometheusService
- .getGaugeQueryData(CAPACITY_QUERY)
- .subscribe((data: PromqlGuageMetric) => {
- const [osdFull, osdNearfull] = this.getCapacityQueryValues(data?.result);
- this.capacityCardData.osdFull = this.prometheusService.formatGuageMetric(osdFull);
- this.capacityCardData.osdNearfull = this.prometheusService.formatGuageMetric(osdNearfull);
- });
- }
-
- private getTelemetryReport() {
- this.healthService.getTelemetryStatus().subscribe((enabled: boolean) => {
- this.telemetryEnabled = enabled;
- });
- }
-
- trackByFn(index: any) {
- return index;
- }
-
- getHardwareConfig(): Observable<any> {
- return this.mgrModuleService.getConfig('cephadm').pipe(
- switchMap((resp: any) => {
- this.hardwareEnabled = resp?.hw_monitoring;
- return of(resp?.hw_monitoring);
- })
- );
- }
-
- refreshIntervalObs(fn: Function) {
- return this.refreshIntervalService.intervalData$.pipe(
- exhaustMap(() => fn().pipe(catchError(() => EMPTY))),
- takeUntil(this.destroy$)
- );
- }
-
- private safeSum(a: number, b: number): number | null {
- return a != null && b != null ? a + b : null;
- }
-
- private safeDifference(a: number, b: number): number | null {
- return a != null && b != null ? a - b : null;
- }
-
- loadInventories() {
- this.refreshIntervalObs(() => this.healthService.getHealthSnapshot()).subscribe({
- next: (data: HealthSnapshotMap) => {
- this.detailsCardData.fsid = data?.fsid;
- this.healthCardData = data?.health;
- this.hasHealthChecks = !!Object.keys(this.healthCardData?.checks ?? {})?.length;
- this.monCount = data?.monmap?.num_mons;
-
- const osdMap = data?.osdmap;
- const osdIn = osdMap?.in;
- const osdUp = osdMap?.up;
- const osdTotal = osdMap?.num_osds;
-
- this.osdCount = {
- in: osdIn,
- up: osdUp,
- total: osdTotal,
- down: this.safeDifference(osdTotal, osdUp),
- out: this.safeDifference(osdTotal, osdIn)
- };
-
- const pgmap = data?.pgmap;
- this.poolCount = pgmap?.num_pools;
- this.usedCapacity = pgmap?.bytes_used;
- this.totalCapacity = pgmap?.bytes_total;
- this.pgStatus = {
- statuses: pgmap?.pgs_by_state,
- total: pgmap?.num_pgs
- };
-
- const mgrmap = data?.mgrmap;
- const mgrInfo = mgrmap?.num_standbys;
- const mgrSuccess = mgrmap?.num_active;
-
- this.mgrStatus = {
- info: mgrInfo,
- success: mgrSuccess,
- total: this.safeSum(mgrInfo, mgrSuccess)
- };
-
- const mdsInfo = data?.fsmap?.num_standbys;
- const mdsSuccess = data?.fsmap?.num_active;
-
- this.mdsStatus = {
- info: mdsInfo,
- success: mdsSuccess,
- total: this.safeSum(mdsInfo, mdsSuccess)
- };
-
- this.rgwCount = data?.num_rgw_gateways;
- this.iscsiMap = data?.num_iscsi_gateways;
- this.hostsCount = data?.num_hosts;
- this.enabledFeature$ = this.featureToggles.get();
- }
- });
- }
-}
+++ /dev/null
-import { TestBed } from '@angular/core/testing';
-
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { PgCategoryService } from '../shared/pg-category.service';
-import { PgSummaryPipe } from './pg-summary.pipe';
-
-describe('PgSummaryPipe', () => {
- let pipe: PgSummaryPipe;
-
- configureTestBed({
- providers: [PgSummaryPipe, PgCategoryService]
- });
-
- beforeEach(() => {
- pipe = TestBed.inject(PgSummaryPipe);
- });
-
- it('create an instance', () => {
- expect(pipe).toBeTruthy();
- });
-
- it('tranforms value', () => {
- const value = {
- statuses: [
- {
- state_name: 'active+clean',
- count: 497
- }
- ],
- total: 497
- };
- expect(pipe.transform(value)).toEqual({
- categoryPgAmount: {
- clean: 497
- },
- total: 497
- });
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-import { PgCategoryService } from '~/app/ceph/shared/pg-category.service';
-import { PgStateCount } from '~/app/shared/models/health.interface';
-
-@Pipe({
- name: 'pgSummary',
- standalone: false
-})
-export class PgSummaryPipe implements PipeTransform {
- constructor(private pgCategoryService: PgCategoryService) {}
-
- transform(value: any): any {
- if (!value) return null;
- const categoryPgAmount: Record<string, number> = {};
- value.statuses.forEach((status: PgStateCount) => {
- const categoryType = this.pgCategoryService.getTypeByStates(status?.state_name);
- if (!categoryPgAmount?.[categoryType]) {
- categoryPgAmount[categoryType] = 0;
- }
- categoryPgAmount[categoryType] += status?.count;
- });
- return {
- categoryPgAmount,
- total: value.total
- };
- }
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { FormsModule, ReactiveFormsModule } from '@angular/forms';
-import { RouterModule } from '@angular/router';
-
-import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
-import { provideCharts, withDefaultRegisterables, BaseChartDirective } from 'ng2-charts';
-
-import { SharedModule } from '~/app/shared/shared.module';
-import { DashboardV3Module } from '../dashboard-v3/dashboard-v3.module';
-import { CephSharedModule } from '../shared/ceph-shared.module';
-import { FeedbackComponent } from '../shared/feedback/feedback.component';
-import { DashboardComponent } from './dashboard/dashboard.component';
-import { HealthPieComponent } from './health-pie/health-pie.component';
-import { OverviewComponent } from '../overview/overview.component';
-import {
- InputModule,
- ModalModule,
- SelectModule,
- ThemeModule,
- ToggletipModule
-} from 'carbon-components-angular';
-
-@NgModule({
- imports: [
- CephSharedModule,
- CommonModule,
- NgbNavModule,
- SharedModule,
- RouterModule,
- FormsModule,
- ReactiveFormsModule,
- DashboardV3Module,
- BaseChartDirective,
- ToggletipModule,
- ModalModule,
- InputModule,
- SelectModule,
- OverviewComponent,
- ThemeModule
- ],
- exports: [OverviewComponent],
- declarations: [DashboardComponent, HealthPieComponent, FeedbackComponent],
- providers: [provideCharts(withDefaultRegisterables())]
-})
-export class DashboardModule {}
+++ /dev/null
-<main aria-label="Overview">
-@if (enabledFeature$ | async; as features) {
-@if (features.dashboard) {
-<!-- OLD OVERVIEW -->
-<cd-dashboard-v3 data-testid="cd-dashboard-v3"></cd-dashboard-v3>
-} @else {
-<!-- NEWER OVERVIEW -->
-<cd-pwd-expiration-notification></cd-pwd-expiration-notification>
-<cd-telemetry-notification></cd-telemetry-notification>
-<cd-motd></cd-motd>
-<cd-overview data-testid="cd-overview"></cd-overview>
-}
-}
-</main>
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
-import { FeatureTogglesService } from '~/app/shared/services/feature-toggles.service';
-
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { DashboardComponent } from './dashboard.component';
-
-describe('DashboardComponent', () => {
- let component: DashboardComponent;
- let fixture: ComponentFixture<DashboardComponent>;
-
- configureTestBed({
- imports: [NgbNavModule, HttpClientTestingModule],
- declarations: [DashboardComponent],
- providers: [FeatureTogglesService],
- schemas: [NO_ERRORS_SCHEMA]
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(DashboardComponent);
- component = fixture.componentInstance;
- });
-
- it('should create', () => {
- fixture.detectChanges();
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, inject, OnInit } from '@angular/core';
-import { Observable } from 'rxjs';
-import {
- FeatureTogglesMap,
- FeatureTogglesService
-} from '~/app/shared/services/feature-toggles.service';
-
-@Component({
- selector: 'cd-dashboard',
- templateUrl: './dashboard.component.html',
- styleUrls: ['./dashboard.component.scss'],
- standalone: false
-})
-export class DashboardComponent implements OnInit {
- enabledFeature$: Observable<FeatureTogglesMap>;
-
- private featureToggles = inject(FeatureTogglesService);
-
- ngOnInit() {
- this.enabledFeature$ = this.featureToggles.get();
- }
-}
+++ /dev/null
-<div class="chart-container">
- <canvas baseChart
- #chartCanvas
- [datasets]="chartConfig.dataset"
- [type]="chartConfig.chartType"
- [options]="chartConfig.options"
- [labels]="chartConfig.labels"
- [plugins]="doughnutChartPlugins"
- class="chart-canvas">
- </canvas>
- <div class="chartjs-tooltip"
- #chartTooltip>
- </div>
-</div>
+++ /dev/null
-@use './src/styles/chart-tooltip';
-
-$canvas-width: 100%;
-$canvas-height: 100%;
-
-.chart-container {
- height: $canvas-height;
- margin-left: auto;
- margin-right: auto;
- position: unset;
- width: $canvas-width;
-}
-
-.chart-canvas {
- height: $canvas-height;
- margin-left: auto;
- margin-right: auto;
- max-height: $canvas-height;
- max-width: $canvas-width;
- position: unset;
- width: $canvas-width;
-}
+++ /dev/null
-import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { CssHelper } from '~/app/shared/classes/css-helper';
-import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
-import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
-import { FormatterService } from '~/app/shared/services/formatter.service';
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { HealthPieComponent } from './health-pie.component';
-
-describe('HealthPieComponent', () => {
- let component: HealthPieComponent;
- let fixture: ComponentFixture<HealthPieComponent>;
-
- configureTestBed({
- schemas: [NO_ERRORS_SCHEMA],
- declarations: [HealthPieComponent],
- providers: [DimlessBinaryPipe, DimlessPipe, FormatterService, CssHelper]
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(HealthPieComponent);
- component = fixture.componentInstance;
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('Add slice border if there is more than one slice with numeric non zero value', () => {
- component.chartConfig.dataset[0].data = [48, 0, 1, 0];
- component.ngOnChanges();
-
- expect(component.chartConfig.dataset[0].borderWidth).toEqual(1);
- });
-
- it('Remove slice border if there is only one slice with numeric non zero value', () => {
- component.chartConfig.dataset[0].data = [48, 0, undefined, 0];
- component.ngOnChanges();
-
- expect(component.chartConfig.dataset[0].borderWidth).toEqual(0);
- });
-
- it('Remove slice border if there is no slice with numeric non zero value', () => {
- component.chartConfig.dataset[0].data = [undefined, 0];
- component.ngOnChanges();
-
- expect(component.chartConfig.dataset[0].borderWidth).toEqual(0);
- });
-
- it('should not hide any slice if there is no user click on legend item', () => {
- const initialData = [8, 15];
- component.chartConfig.dataset[0].data = initialData;
- component.ngOnChanges();
-
- expect(component.chartConfig.dataset[0].data).toEqual(initialData);
- });
-
- describe('tooltip body', () => {
- const tooltipBody = ['text: 10000'];
-
- it('should return amount converted to appropriate units', () => {
- component.isBytesData = false;
- expect(component['getChartTooltipBody'](tooltipBody)).toEqual('text: 10 k');
-
- component.isBytesData = true;
- expect(component['getChartTooltipBody'](tooltipBody)).toEqual('text: 9.8 KiB');
- });
-
- it('should not return amount when showing label as tooltip', () => {
- component.showLabelAsTooltip = true;
- expect(component['getChartTooltipBody'](tooltipBody)).toEqual('text');
- });
- });
-});
+++ /dev/null
-import {
- Component,
- ElementRef,
- EventEmitter,
- Input,
- OnChanges,
- OnInit,
- Output,
- ViewChild
-} from '@angular/core';
-
-import * as Chart from 'chart.js';
-import _ from 'lodash';
-
-import { CssHelper } from '~/app/shared/classes/css-helper';
-import { ChartTooltip } from '~/app/shared/models/chart-tooltip';
-import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
-import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
-
-@Component({
- selector: 'cd-health-pie',
- templateUrl: './health-pie.component.html',
- styleUrls: ['./health-pie.component.scss'],
- standalone: false
-})
-export class HealthPieComponent implements OnChanges, OnInit {
- @ViewChild('chartCanvas', { static: true })
- chartCanvasRef: ElementRef;
- @ViewChild('chartTooltip', { static: true })
- chartTooltipRef: ElementRef;
-
- @Input()
- data: any;
- @Input()
- config = {};
- @Input()
- isBytesData = false;
- @Input()
- tooltipFn: any;
- @Input()
- showLabelAsTooltip = false;
- @Output()
- prepareFn = new EventEmitter();
-
- chartConfig: any;
-
- public doughnutChartPlugins: any[] = [
- {
- id: 'center_text',
- beforeDraw(chart: any) {
- const cssHelper = new CssHelper();
- const defaultFontFamily = 'Helvetica Neue, Helvetica, Arial, sans-serif';
- Chart.defaults.font.family = defaultFontFamily;
- const ctx = chart.ctx;
- if (!chart.options.plugins.center_text || !chart.data.datasets[0].label) {
- return;
- }
-
- ctx.save();
- const label = chart.data.datasets[0].label.split('\n');
-
- const centerX = (chart.chartArea.left + chart.chartArea.right) / 2;
- const centerY = (chart.chartArea.top + chart.chartArea.bottom) / 2;
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
-
- ctx.font = `24px ${defaultFontFamily}`;
- ctx.fillStyle = cssHelper.propertyValue('chart-color-center-text');
- ctx.fillText(label[0], centerX, centerY - 10);
-
- if (label.length > 1) {
- ctx.font = `14px ${defaultFontFamily}`;
- ctx.fillStyle = cssHelper.propertyValue('chart-color-center-text-description');
- ctx.fillText(label[1], centerX, centerY + 10);
- }
- ctx.restore();
- }
- }
- ];
-
- constructor(
- private dimlessBinary: DimlessBinaryPipe,
- private dimless: DimlessPipe,
- private cssHelper: CssHelper
- ) {
- this.chartConfig = {
- chartType: 'doughnut',
- dataset: [
- {
- label: null,
- borderWidth: 0,
- backgroundColor: [
- this.cssHelper.propertyValue('chart-color-green'),
- this.cssHelper.propertyValue('chart-color-yellow'),
- this.cssHelper.propertyValue('chart-color-orange'),
- this.cssHelper.propertyValue('chart-color-red'),
- this.cssHelper.propertyValue('chart-color-blue')
- ]
- }
- ],
- options: {
- cutout: '90%',
- events: ['click', 'mouseout', 'touchstart'],
- aspectRatio: 2,
- plugins: {
- center_text: true,
- legend: {
- display: true,
- position: 'right',
- labels: {
- boxWidth: 10,
- usePointStyle: false
- }
- },
- tooltips: {
- enabled: true,
- displayColors: false,
- backgroundColor: this.cssHelper.propertyValue('chart-color-tooltip-background'),
- cornerRadius: 0,
- bodyFontSize: 14,
- bodyFontStyle: '600',
- position: 'nearest',
- xPadding: 12,
- yPadding: 12,
- callbacks: {
- label: (item: Record<string, any>, data: Record<string, any>) => {
- let text = data.labels[item.index];
- if (!text.includes('%')) {
- text = `${text} (${data.datasets[item.datasetIndex].data[item.index]}%)`;
- }
- return text;
- }
- }
- },
- title: {
- display: false
- }
- }
- }
- };
- }
-
- ngOnInit() {
- const getStyleTop = (tooltip: any, positionY: number) => {
- return positionY + tooltip.caretY - tooltip.height - 10 + 'px';
- };
-
- const getStyleLeft = (tooltip: any, positionX: number) => {
- return positionX + tooltip.caretX + 'px';
- };
-
- const chartTooltip = new ChartTooltip(
- this.chartCanvasRef,
- this.chartTooltipRef,
- getStyleLeft,
- getStyleTop
- );
-
- chartTooltip.getBody = (body: any) => {
- return this.getChartTooltipBody(body);
- };
-
- _.merge(this.chartConfig, this.config);
-
- this.prepareFn.emit([this.chartConfig, this.data]);
- }
-
- ngOnChanges() {
- this.prepareFn.emit([this.chartConfig, this.data]);
- this.setChartSliceBorderWidth();
- }
-
- private getChartTooltipBody(body: string[]) {
- const bodySplit = body[0].split(': ');
-
- if (this.showLabelAsTooltip) {
- return bodySplit[0];
- }
-
- bodySplit[1] = this.isBytesData
- ? this.dimlessBinary.transform(bodySplit[1])
- : this.dimless.transform(bodySplit[1]);
-
- return bodySplit.join(': ');
- }
-
- private setChartSliceBorderWidth() {
- let nonZeroValueSlices = 0;
- _.forEach(this.chartConfig.dataset[0].data, function (slice) {
- if (slice > 0) {
- nonZeroValueSlices += 1;
- }
- });
-
- this.chartConfig.dataset[0].borderWidth = nonZeroValueSlices > 1 ? 1 : 0;
- }
-}
+<!-- BANNERS -->
+
+<cd-pwd-expiration-notification></cd-pwd-expiration-notification>
+<cd-telemetry-notification></cd-telemetry-notification>
+<cd-motd></cd-motd>
+
+<!-- OVEVRIEW -->
+
@let storageCard = (storageCardVm$ | async);
@let health = (healthCardVm$ | async);
<div cdsGrid
import { DeviceListComponent } from './device-list/device-list.component';
import { SmartListComponent } from './smart-list/smart-list.component';
import { HealthChecksComponent } from './health-checks/health-checks.component';
+import { InputModule, ModalModule, SelectModule } from 'carbon-components-angular';
+import { FeedbackComponent } from './feedback/feedback.component';
+import { ReactiveFormsModule } from '@angular/forms';
+import { ComponentsModule } from '~/app/shared/components/components.module';
@NgModule({
- imports: [CommonModule, DataTableModule, SharedModule, NgbNavModule, PipesModule, RouterModule],
- exports: [DeviceListComponent, SmartListComponent, HealthChecksComponent],
- declarations: [DeviceListComponent, SmartListComponent, HealthChecksComponent]
+ imports: [
+ CommonModule,
+ DataTableModule,
+ SharedModule,
+ NgbNavModule,
+ PipesModule,
+ RouterModule,
+ ModalModule,
+ ReactiveFormsModule,
+ InputModule,
+ SelectModule,
+ ComponentsModule
+ ],
+ exports: [DeviceListComponent, SmartListComponent, HealthChecksComponent, FeedbackComponent],
+ declarations: [DeviceListComponent, SmartListComponent, HealthChecksComponent, FeedbackComponent]
})
export class CephSharedModule {}
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { DashboardModule } from '~/app/ceph/dashboard/dashboard.module';
import { SharedModule } from '~/app/shared/shared.module';
import { configureTestBed } from '~/testing/unit-test-helper';
import { MotdComponent } from './motd.component';
let fixture: ComponentFixture<MotdComponent>;
configureTestBed({
- imports: [DashboardModule, HttpClientTestingModule, SharedModule]
+ imports: [HttpClientTestingModule, SharedModule]
});
beforeEach(() => {
cephfs = true;
rgw = true;
nfs = true;
- dashboard = true;
}
export type Features = keyof FeatureTogglesMap;
export type FeatureTogglesMap$ = Observable<FeatureTogglesMap>;
cephfs:
description: ''
type: boolean
- dashboard:
- description: ''
- type: boolean
iscsi:
description: ''
type: boolean
- cephfs
- rgw
- nfs
- - dashboard
type: object
application/vnd.ceph.api.v1.0+json:
schema:
cephfs:
description: ''
type: boolean
- dashboard:
- description: ''
- type: boolean
iscsi:
description: ''
type: boolean
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')