From: Pedro Gonzalez Gomez Date: Tue, 14 Feb 2023 10:21:57 +0000 (+0100) Subject: mgr/dashboard: add button toggle to switch between new and old landing page X-Git-Tag: v18.1.0~211^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F50609%2Fhead;p=ceph.git mgr/dashboard: add button toggle to switch between new and old landing page Signed-off-by: Pedro Gonzalez Gomez (cherry picked from commit 21dc93ae11db02bc8e0a2175f21fa7dd5e423b00) --- diff --git a/doc/mgr/capacity-card.png b/doc/mgr/capacity-card.png new file mode 100644 index 000000000000..59a70348c358 Binary files /dev/null and b/doc/mgr/capacity-card.png differ diff --git a/doc/mgr/cluster-utilization-card.png b/doc/mgr/cluster-utilization-card.png new file mode 100644 index 000000000000..fc0fd9ed197f Binary files /dev/null and b/doc/mgr/cluster-utilization-card.png differ diff --git a/doc/mgr/dashboard-landing-page.png b/doc/mgr/dashboard-landing-page.png new file mode 100644 index 000000000000..77a1fe12b511 Binary files /dev/null and b/doc/mgr/dashboard-landing-page.png differ diff --git a/doc/mgr/dashboard.rst b/doc/mgr/dashboard.rst index 34e66cf38bd1..83f70c3ad973 100644 --- a/doc/mgr/dashboard.rst +++ b/doc/mgr/dashboard.rst @@ -127,62 +127,67 @@ The Ceph Dashboard offers the following monitoring and management capabilities: Overview of the Dashboard Landing Page ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Displays overall cluster status, performance, and capacity metrics. Shows instant -feedback for changes in the cluster and provides easy access to subpages of the -dashboard. +The landing page of Ceph Dashboard serves as the home page and features metrics +such as the overall cluster status, performance, and capacity. It provides real-time +updates on any changes in the cluster and allows quick access to other sections of the dashboard. + +.. image:: dashboard-landing-page.png + + +.. note:: + You can change the landing page to the previous version from: + ``Cluster >> Manager Modules >> Dashboard >> Edit``. + Editing the ``FEATURE_TOGGLE_DASHBOARD`` option will change the landing page, from one view to another. + + Note that the previous version of the landing page will be disabled in future releases. + +.. _dashboard-landing-page-details: + +Details +""""""" +Provides an overview of the cluster configuration, displaying various critical aspects of the cluster. + +.. image:: details-card.png .. _dashboard-landing-page-status: Status """""" +Provides a visual indication of cluster health, and displays cluster alerts grouped by severity. -* **Cluster Status**: Displays overall cluster health. In case of any error it - displays a short description of the error and provides a link to the logs. -* **Hosts**: Displays the total number of hosts associated to the cluster and - links to a subpage that lists and describes each. -* **Monitors**: Displays mons and their quorum status and - open sessions. Links to a subpage that lists and describes each. -* **OSDs**: Displays object storage daemons (ceph-osds) and - the numbers of OSDs running (up), in service - (in), and out of the cluster (out). Provides links to - subpages providing a list of all OSDs and related management actions. -* **Managers**: Displays active and standby Ceph Manager - daemons (ceph-mgr). -* **Object Gateway**: Displays active object gateways (RGWs) and - provides links to subpages that list all object gateway daemons. -* **Metadata Servers**: Displays active and standby CephFS metadata - service daemons (ceph-mds). -* **iSCSI Gateways**: Display iSCSI gateways available, - active (up), and inactive (down). Provides a link to a subpage - showing a list of all iSCSI Gateways. +.. image:: status-card-open.png .. _dashboard-landing-page-capacity: Capacity """""""" +* **Used**: Displays the used capacity out of the total physical capacity providedd by storage nodes (OSDs) +* **Warning**: Displays the `nearfull` threshold of the OSDs +* **Danger**: Displays the `full` threshold of the OSDs + +.. image:: capacity-card.png -* **Raw Capacity**: Displays the capacity used out of the total - physical capacity provided by storage nodes (OSDs). -* **Objects**: Displays the number and status of RADOS objects - including the percentages of healthy, misplaced, degraded, and unfound - objects. -* **PG Status**: Displays the total number of placement groups and - their status, including the percentage clean, working, - warning, and unknown. -* **Pools**: Displays pools and links to a subpage listing details. -* **PGs per OSD**: Displays the number of placement groups assigned to - object storage daemons. +.. _dashboard-landing-page-inventory: + +Inventory +""""""""" +An inventory for all assets within the cluster. +Provides direct access to subpages of the dashboard from each item of this card. + +.. image:: inventory-card.png .. _dashboard-landing-page-performance: -Performance -""""""""""" +Cluster Utilization +""""""""""""""""""" +* **Used Capacity**: Total capacity used of the cluster. The maximum value of the chart is the maximum capacity of the cluster. +* **IOPS (Input/Output Operations Per Second)**: Number of read and write operations. +* **Latency**: Amount of time that it takes to process a read or a write request. +* **Client Throughput**: Amount of data that clients read or write to the cluster. +* **Recovery Throughput**: Amount of recovery data that clients read or write to the cluster. + -* **Client READ/Write**: Displays an overview of - client input and output operations. -* **Client Throughput**: Displays the data transfer rates to and from Ceph clients. -* **Recovery throughput**: Displays rate of cluster healing and balancing operations. -* **Scrubbing**: Displays light and deep scrub status. +.. image:: cluster-utilization-card.png Supported Browsers ^^^^^^^^^^^^^^^^^^ diff --git a/doc/mgr/details-card.png b/doc/mgr/details-card.png new file mode 100644 index 000000000000..0c219a890b9d Binary files /dev/null and b/doc/mgr/details-card.png differ diff --git a/doc/mgr/inventory-card.png b/doc/mgr/inventory-card.png new file mode 100644 index 000000000000..54317fc9fc32 Binary files /dev/null and b/doc/mgr/inventory-card.png differ diff --git a/doc/mgr/status-card-open.png b/doc/mgr/status-card-open.png new file mode 100644 index 000000000000..4ea20921b016 Binary files /dev/null and b/doc/mgr/status-card-open.png differ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index ec30ccc72088..6880a1561c1d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -24,8 +24,7 @@ import { SilenceListComponent } from './ceph/cluster/prometheus/silence-list/sil 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 { DeprecatedDashboardComponent } from './ceph/dashboard/dashboard/dashboard.component'; -import { DashboardComponent } from './ceph/new-dashboard/dashboard/dashboard.component'; +import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component'; import { NfsFormComponent } from './ceph/nfs/nfs-form/nfs-form.component'; import { NfsListComponent } from './ceph/nfs/nfs-list/nfs-list.component'; import { PerformanceCounterComponent } from './ceph/performance-counter/performance-counter/performance-counter.component'; @@ -89,8 +88,7 @@ const routes: Routes = [ canActivate: [AuthGuardService, ChangePasswordGuardService], canActivateChild: [AuthGuardService, ChangePasswordGuardService], children: [ - { path: 'dashboard', component: DeprecatedDashboardComponent }, - { path: 'dashboard_3', component: DashboardComponent }, + { path: 'dashboard', component: DashboardComponent }, { path: 'error', component: ErrorComponent }, // Cluster diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts index 17d624697615..47772304b505 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts @@ -5,7 +5,6 @@ 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 { NewDashboardModule } from './new-dashboard/dashboard.module'; import { NfsModule } from './nfs/nfs.module'; import { PerformanceCounterModule } from './performance-counter/performance-counter.module'; @@ -14,7 +13,6 @@ import { PerformanceCounterModule } from './performance-counter/performance-coun CommonModule, ClusterModule, DashboardModule, - NewDashboardModule, PerformanceCounterModule, CephfsModule, NfsModule, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.html new file mode 100644 index 000000000000..9b7bf03e7750 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.html @@ -0,0 +1,167 @@ + + + + + + {{ data.success }} + + + {{ data.categoryPgAmount?.clean }} + + + + + + + {{ data.info }} + + + + + + + {{ data.warn }} + + + {{ data.categoryPgAmount?.warning }} + + + + + + + {{ data.error }} + + + {{ data.categoryPgAmount?.unknown }} + + + + + + + {{ data.categoryPgAmount?.working }} + + + + + + + + + {{ data.up }} + + + + + {{ data.up }} + + up + + + + {{ data.in }} + + in + + + + {{ data.down }} + + down + + + + {{ data.out }} + + out + + + + {{ data.nearfull }} + + nearfull + + {{ data.full }} + + full + + + + + + + {{ data.up }} + + + + + {{ data.down }} + + + + + + + + {{ data }} + + + + + + + {{ total }} + {{ title }} + {{ title }} + {{ title }}s + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.spec.ts new file mode 100644 index 000000000000..8932e67b29c5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CardRowComponent } from './card-row.component'; + +describe('CardRowComponent', () => { + let component: CardRowComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CardRowComponent] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CardRowComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.ts new file mode 100644 index 000000000000..90c939160eb9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.ts @@ -0,0 +1,34 @@ +import { Component, Input, OnChanges } from '@angular/core'; +import { Icons } from '~/app/shared/enum/icons.enum'; + +@Component({ + selector: 'cd-card-row', + templateUrl: './card-row.component.html', + styleUrls: ['./card-row.component.scss'] +}) +export class CardRowComponent implements OnChanges { + @Input() + title: string; + + @Input() + link: string; + + @Input() + data: any; + + @Input() + summaryType = 'default'; + + icons = Icons; + total: number; + + ngOnChanges(): void { + if (this.data.total || this.data.total === 0) { + this.total = this.data.total; + } else if (this.summaryType === 'iscsi') { + this.total = this.data.up + this.data.down || 0; + } else { + this.total = this.data; + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.html new file mode 100644 index 000000000000..35dffd46c047 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.html @@ -0,0 +1,8 @@ +
+

+ {{ title }} +

+
+ +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.scss new file mode 100644 index 000000000000..fdf19a00ec6a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.scss @@ -0,0 +1,5 @@ +.card-body { + display: flex; + flex-direction: column; + justify-content: space-evenly; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.spec.ts new file mode 100644 index 000000000000..fdc34fdf7b22 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { configureTestBed } from '~/testing/unit-test-helper'; +import { CardComponent } from './card.component'; + +describe('CardComponent', () => { + let component: CardComponent; + let fixture: ComponentFixture; + + configureTestBed({ + imports: [RouterTestingModule], + declarations: [CardComponent] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CardComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('Setting cards title makes title visible', () => { + const title = 'Card Title'; + component.title = title; + fixture.detectChanges(); + const titleDiv = fixture.debugElement.nativeElement.querySelector('.card-title'); + + expect(titleDiv.textContent).toContain(title); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.ts new file mode 100644 index 000000000000..b6bb99c66909 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'cd-card', + templateUrl: './card.component.html', + styleUrls: ['./card.component.scss'] +}) +export class CardComponent { + @Input() + title: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.html new file mode 100644 index 000000000000..6ac991fd58c5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.html @@ -0,0 +1,23 @@ +
+
+
+ {{ chartTitle }} +
+ {{currentData}} {{ currentDataUnits }} +
+ {{currentData2}} {{ currentDataUnits2 }} +
+
+
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.scss new file mode 100644 index 000000000000..12e9b9c1c6a5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.scss @@ -0,0 +1,9 @@ +.center-text { + margin-top: 1.2vw; + position: relative; +} + +.chart { + height: 8vh; + margin-top: 15px; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.spec.ts new file mode 100644 index 000000000000..0501ac75dde6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.spec.ts @@ -0,0 +1,36 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CssHelper } from '~/app/shared/classes/css-helper'; +import { DimlessBinaryPerSecondPipe } from '~/app/shared/pipes/dimless-binary-per-second.pipe'; +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 { DashboardAreaChartComponent } from './dashboard-area-chart.component'; + +describe('DashboardAreaChartComponent', () => { + let component: DashboardAreaChartComponent; + let fixture: ComponentFixture; + + configureTestBed({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [DashboardAreaChartComponent], + providers: [ + CssHelper, + DimlessBinaryPipe, + DimlessBinaryPerSecondPipe, + DimlessPipe, + FormatterService + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardAreaChartComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts new file mode 100644 index 000000000000..3da4334210de --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts @@ -0,0 +1,265 @@ +import { AfterViewInit, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core'; + +import { CssHelper } from '~/app/shared/classes/css-helper'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; +import { DimlessBinaryPerSecondPipe } from '~/app/shared/pipes/dimless-binary-per-second.pipe'; +import { FormatterService } from '~/app/shared/services/formatter.service'; +import { BaseChartDirective, PluginServiceGlobalRegistrationAndOptions } from 'ng2-charts'; +import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe'; + +@Component({ + selector: 'cd-dashboard-area-chart', + templateUrl: './dashboard-area-chart.component.html', + styleUrls: ['./dashboard-area-chart.component.scss'] +}) +export class DashboardAreaChartComponent implements OnInit, OnChanges, AfterViewInit { + @ViewChild(BaseChartDirective) chart: BaseChartDirective; + + @Input() + chartTitle: string; + @Input() + maxValue?: any; + @Input() + dataUnits: string; + @Input() + data: any; + @Input() + data2?: any; + @Input() + label: any; + @Input() + label2?: any; + + currentDataUnits: string; + currentData: number; + currentDataUnits2?: string; + currentData2?: number; + + chartData: any = { + dataset: [ + { + label: '', + data: [{ x: 0, y: 0 }], + tension: 0, + pointBackgroundColor: this.cssHelper.propertyValue('chart-color-strong-blue'), + backgroundColor: this.cssHelper.propertyValue('chart-color-translucent-blue'), + borderColor: this.cssHelper.propertyValue('chart-color-strong-blue') + }, + { + label: '', + data: [], + tension: 0, + pointBackgroundColor: this.cssHelper.propertyValue('chart-color-orange'), + backgroundColor: this.cssHelper.propertyValue('chart-color-yellow'), + borderColor: this.cssHelper.propertyValue('chart-color-orange') + } + ] + }; + + options: any = { + responsive: true, + maintainAspectRatio: false, + elements: { + point: { + radius: 0 + } + }, + legend: { + display: false + }, + tooltips: { + intersect: false, + displayColors: true, + backgroundColor: this.cssHelper.propertyValue('chart-color-tooltip-background'), + callbacks: { + title: function (tooltipItem: any): any { + return tooltipItem[0].xLabel; + } + } + }, + hover: { + intersect: false + }, + scales: { + xAxes: [ + { + display: false, + type: 'time', + gridLines: { + display: false + }, + time: { + tooltipFormat: 'YYYY/MM/DD hh:mm:ss' + } + } + ], + yAxes: [ + { + gridLines: { + display: false + }, + ticks: { + beginAtZero: true, + maxTicksLimit: 3, + callback: (value: any) => { + if (value === 0) { + return null; + } + return this.fillString(this.convertUnits(value)); + } + } + } + ] + }, + plugins: { + borderArea: true, + chartAreaBorder: { + borderColor: this.cssHelper.propertyValue('chart-color-slight-dark-gray'), + borderWidth: 2 + } + } + }; + + public chartAreaBorderPlugin: PluginServiceGlobalRegistrationAndOptions[] = [ + { + beforeDraw(chart: Chart) { + if (!chart.options.plugins.borderArea) { + return; + } + const { + ctx, + chartArea: { left, top, right, bottom } + } = chart; + ctx.save(); + ctx.strokeStyle = chart.options.plugins.chartAreaBorder.borderColor; + ctx.lineWidth = chart.options.plugins.chartAreaBorder.borderWidth; + ctx.setLineDash(chart.options.plugins.chartAreaBorder.borderDash || []); + ctx.lineDashOffset = chart.options.plugins.chartAreaBorder.borderDashOffset; + ctx.strokeRect(left, top, right - left - 1, bottom); + ctx.restore(); + } + } + ]; + + constructor( + private cssHelper: CssHelper, + private dimlessBinary: DimlessBinaryPipe, + private dimlessBinaryPerSecond: DimlessBinaryPerSecondPipe, + private dimlessPipe: DimlessPipe, + private formatter: FormatterService + ) {} + + ngOnInit(): void { + this.currentData = Number( + this.chartData.dataset[0].data[this.chartData.dataset[0].data.length - 1].y + ); + if (this.data2) { + this.currentData2 = Number( + this.chartData.dataset[1].data[this.chartData.dataset[1].data.length - 1].y + ); + } + } + + ngOnChanges(): void { + if (this.data) { + this.setChartTicks(); + this.chartData.dataset[0].data = this.formatData(this.data); + this.chartData.dataset[0].label = this.label; + [this.currentData, this.currentDataUnits] = this.convertUnits( + this.data[this.data.length - 1][1] + ).split(' '); + } + if (this.data2) { + this.chartData.dataset[1].data = this.formatData(this.data2); + this.chartData.dataset[1].label = this.label2; + [this.currentData2, this.currentDataUnits2] = this.convertUnits( + this.data2[this.data2.length - 1][1] + ).split(' '); + } + } + + ngAfterViewInit(): void { + if (this.data) { + this.setChartTicks(); + } + } + + private formatData(array: Array): any { + let formattedData = {}; + formattedData = array.map((data: any) => ({ + x: data[0] * 1000, + y: Number(this.convertUnits(data[1]).replace(/[^\d,.]+/g, '')) + })); + return formattedData; + } + + private convertUnits(data: any): any { + let dataWithUnits: string; + if (this.dataUnits === 'bytes') { + dataWithUnits = this.dimlessBinary.transform(data); + } else if (this.dataUnits === 'bytesPerSecond') { + dataWithUnits = this.dimlessBinaryPerSecond.transform(data); + } else if (this.dataUnits === 'ms') { + dataWithUnits = this.formatter.format_number(data, 1000, ['ms', 's']); + } else { + dataWithUnits = this.dimlessPipe.transform(data); + } + return dataWithUnits; + } + + private fillString(str: string): string { + let maxNumberOfChar: number = 8; + let numberOfChars: number = str.length; + if (str.length < 4) { + maxNumberOfChar = 11; + } + for (; numberOfChars < maxNumberOfChar; numberOfChars++) { + str = '\u00A0' + str; + } + return str + '\u00A0\u00A0'; + } + + private setChartTicks() { + if (this.chart && this.maxValue) { + let [maxValue, maxValueDataUnits] = this.convertUnits(this.maxValue).split(' '); + this.chart.chart.options.scales.yAxes[0].ticks.suggestedMax = maxValue; + this.chart.chart.options.scales.yAxes[0].ticks.suggestedMin = 0; + this.chart.chart.options.scales.yAxes[0].ticks.stepSize = Number((maxValue / 2).toFixed(0)); + this.chart.chart.options.scales.yAxes[0].ticks.callback = (value: any) => { + if (value === 0) { + return null; + } + return this.fillString(`${value} ${maxValueDataUnits}`); + }; + this.chart.chart.update(); + } else if (this.chart && this.data) { + let maxValue = 0, + maxValueDataUnits = ''; + let maxValueData = Math.max(...this.data.map((values: any) => values[1])); + if (this.data2) { + var maxValueData2 = Math.max(...this.data2.map((values: any) => values[1])); + [maxValue, maxValueDataUnits] = this.convertUnits( + Math.max(maxValueData, maxValueData2) + ).split(' '); + } else { + [maxValue, maxValueDataUnits] = this.convertUnits(Math.max(maxValueData)).split(' '); + } + + this.chart.chart.options.scales.yAxes[0].ticks.suggestedMax = maxValue * 1.2; + this.chart.chart.options.scales.yAxes[0].ticks.suggestedMin = 0; + this.chart.chart.options.scales.yAxes[0].ticks.stepSize = Number( + ((maxValue * 1.2) / 2).toFixed(0) + ); + this.chart.chart.options.scales.yAxes[0].ticks.callback = (value: any) => { + if (value === 0) { + return null; + } + if (!maxValueDataUnits) { + return this.fillString(`${value}`); + } + return this.fillString(`${value} ${maxValueDataUnits}`); + }; + this.chart.chart.update(); + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.html new file mode 100644 index 000000000000..ba8176beab3b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.html @@ -0,0 +1,16 @@ +
+ + +
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.scss new file mode 100644 index 000000000000..64e7a9822e22 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.scss @@ -0,0 +1,22 @@ +@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; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.spec.ts new file mode 100644 index 000000000000..892913dab6be --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.spec.ts @@ -0,0 +1,27 @@ +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 { configureTestBed } from '~/testing/unit-test-helper'; +import { DashboardPieComponent } from './dashboard-pie.component'; + +describe('DashboardPieComponent', () => { + let component: DashboardPieComponent; + let fixture: ComponentFixture; + + configureTestBed({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [DashboardPieComponent], + providers: [CssHelper, DimlessBinaryPipe] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardPieComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.ts new file mode 100644 index 000000000000..4aaabb6eb4a0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.ts @@ -0,0 +1,189 @@ +import { Component, Input, OnChanges, OnInit } from '@angular/core'; + +import * as Chart from 'chart.js'; +import _ from 'lodash'; +import { PluginServiceGlobalRegistrationAndOptions } from 'ng2-charts'; + +import { CssHelper } from '~/app/shared/classes/css-helper'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; + +@Component({ + selector: 'cd-dashboard-pie', + templateUrl: './dashboard-pie.component.html', + styleUrls: ['./dashboard-pie.component.scss'] +}) +export class DashboardPieComponent implements OnChanges, OnInit { + @Input() + data: any; + @Input() + highThreshold: number; + @Input() + lowThreshold: number; + + color: string; + + chartConfig: any = { + chartType: 'doughnut', + labels: ['', '', ''], + dataset: [ + { + label: null, + backgroundColor: [ + this.cssHelper.propertyValue('chart-color-light-gray'), + this.cssHelper.propertyValue('chart-color-slight-dark-gray'), + this.cssHelper.propertyValue('chart-color-dark-gray') + ] + }, + { + label: null, + borderWidth: 0, + backgroundColor: [ + this.cssHelper.propertyValue('chart-color-blue'), + this.cssHelper.propertyValue('chart-color-white') + ] + } + ], + options: { + cutoutPercentage: 70, + events: ['click', 'mouseout', 'touchstart'], + legend: { + display: true, + position: 'right', + labels: { + boxWidth: 10, + usePointStyle: false, + generateLabels: (chart: any) => { + const labels = { 0: {}, 1: {}, 2: {} }; + labels[0] = { + text: $localize`Used: ${chart.data.datasets[1].data[2]}`, + fillStyle: chart.data.datasets[1].backgroundColor[0], + strokeStyle: chart.data.datasets[1].backgroundColor[0] + }; + labels[1] = { + text: $localize`Warning: ${chart.data.datasets[0].data[0]}%`, + fillStyle: chart.data.datasets[0].backgroundColor[1], + strokeStyle: chart.data.datasets[0].backgroundColor[1] + }; + labels[2] = { + text: $localize`Danger: ${ + chart.data.datasets[0].data[0] + chart.data.datasets[0].data[1] + }%`, + fillStyle: chart.data.datasets[0].backgroundColor[2], + strokeStyle: chart.data.datasets[0].backgroundColor[2] + }; + + return labels; + } + } + }, + plugins: { + center_text: true + }, + 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, + filter: (tooltipItem: any) => { + return tooltipItem.datasetIndex === 1; + }, + callbacks: { + label: (item: Record, data: Record) => { + let text = data.labels[item.index]; + if (!text.includes('%')) { + text = `${text} (${data.datasets[item.datasetIndex].data[item.index]}%)`; + } + return text; + } + } + }, + title: { + display: false + } + } + }; + + public doughnutChartPlugins: PluginServiceGlobalRegistrationAndOptions[] = [ + { + id: 'center_text', + beforeDraw(chart: Chart) { + const cssHelper = new CssHelper(); + const defaultFontFamily = 'Helvetica Neue, Helvetica, Arial, sans-serif'; + Chart.defaults.global.defaultFontFamily = 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[0].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.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 cssHelper: CssHelper, private dimlessBinary: DimlessBinaryPipe) {} + + ngOnInit() { + this.prepareRawUsage(this.chartConfig, this.data); + } + + ngOnChanges() { + this.prepareRawUsage(this.chartConfig, this.data); + } + + private prepareRawUsage(chart: Record, data: Record) { + const nearFullRatioPercent = this.lowThreshold * 100; + const fullRatioPercent = this.highThreshold * 100; + const percentAvailable = this.calcPercentage(data.max - data.current, data.max); + const percentUsed = this.calcPercentage(data.current, data.max); + if (percentUsed >= fullRatioPercent) { + this.color = 'chart-color-red'; + } else if (percentUsed >= nearFullRatioPercent) { + this.color = 'chart-color-yellow'; + } else { + this.color = 'chart-color-blue'; + } + + chart.dataset[0].data = [ + Math.round(nearFullRatioPercent), + Math.round(Math.abs(nearFullRatioPercent - fullRatioPercent)), + Math.round(100 - fullRatioPercent) + ]; + + chart.dataset[1].data = [ + percentUsed, + percentAvailable, + this.dimlessBinary.transform(data.current) + ]; + chart.dataset[1].backgroundColor[0] = this.cssHelper.propertyValue(this.color); + + chart.dataset[0].label = [`${percentUsed}%\nof ${this.dimlessBinary.transform(data.max)}`]; + } + + private calcPercentage(dividend: number, divisor: number) { + if (!_.isNumber(dividend) || !_.isNumber(divisor) || divisor === 0) { + return 0; + } + return Math.ceil((dividend / divisor) * 100 * 100) / 100; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.html new file mode 100644 index 000000000000..cd960d07bd9d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.html @@ -0,0 +1,11 @@ +
+ +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.scss new file mode 100644 index 000000000000..13572dc2f0b2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.scss @@ -0,0 +1,10 @@ +select#timepicker { + border: 0; +} + +.timeSelector { + position: absolute; + right: 18px; + top: 20px; + width: 12rem; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.spec.ts new file mode 100644 index 000000000000..9aeec4dcb51e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.spec.ts @@ -0,0 +1,24 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { configureTestBed } from '~/testing/unit-test-helper'; +import { DashboardTimeSelectorComponent } from './dashboard-time-selector.component'; + +describe('DashboardTimeSelectorComponent', () => { + let component: DashboardTimeSelectorComponent; + let fixture: ComponentFixture; + + configureTestBed({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [DashboardTimeSelectorComponent] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardTimeSelectorComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.ts new file mode 100644 index 000000000000..3b0915232b66 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.ts @@ -0,0 +1,77 @@ +import { Component, EventEmitter, Output } from '@angular/core'; + +import moment from 'moment'; + +@Component({ + selector: 'cd-dashboard-time-selector', + templateUrl: './dashboard-time-selector.component.html', + styleUrls: ['./dashboard-time-selector.component.scss'] +}) +export class DashboardTimeSelectorComponent { + @Output() + selectedTime = new EventEmitter(); + + times: any; + time: any; + + constructor() { + this.times = [ + { + name: $localize`Last 5 minutes`, + value: this.timeToDate(5 * 60, 1) + }, + { + name: $localize`Last 15 minutes`, + value: this.timeToDate(15 * 60, 3) + }, + { + name: $localize`Last 30 minutes`, + value: this.timeToDate(30 * 60, 6) + }, + { + name: $localize`Last 1 hour`, + value: this.timeToDate(3600, 12) + }, + { + name: $localize`Last 3 hours`, + value: this.timeToDate(3 * 3600, 36) + }, + { + name: $localize`Last 6 hours`, + value: this.timeToDate(6 * 3600, 72) + }, + { + name: $localize`Last 12 hours`, + value: this.timeToDate(12 * 3600, 144) + }, + { + name: $localize`Last 24 hours`, + value: this.timeToDate(24 * 3600, 288) + }, + { + name: $localize`Last 2 days`, + value: this.timeToDate(48 * 3600, 576) + }, + { + name: $localize`Last 7 days`, + value: this.timeToDate(168 * 3600, 2016) + } + ]; + this.time = this.times[3].value; + } + + emitTime() { + this.selectedTime.emit(this.timeToDate(this.time.end - this.time.start, this.time.step)); + } + + private timeToDate(secondsAgo: number, step: number): any { + const date: number = moment().unix() - secondsAgo; + const dateNow: number = moment().unix(); + const formattedDate: any = { + start: date, + end: dateNow, + step: step + }; + return formattedDate; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts new file mode 100644 index 000000000000..2c3b4cc369fc --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts @@ -0,0 +1,47 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; + +import { NgbNavModule, NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { ChartsModule } from 'ng2-charts'; +import { SimplebarAngularModule } from 'simplebar-angular'; + +import { SharedModule } from '~/app/shared/shared.module'; +import { CephSharedModule } from '../shared/ceph-shared.module'; +import { CardComponent } from './card/card.component'; +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 { CardRowComponent } from './card-row/card-row.component'; +import { PgSummaryPipe } from './pg-summary.pipe'; + +@NgModule({ + imports: [ + CephSharedModule, + CommonModule, + NgbNavModule, + SharedModule, + ChartsModule, + RouterModule, + NgbPopoverModule, + NgbTooltipModule, + FormsModule, + ReactiveFormsModule, + SimplebarAngularModule + ], + + declarations: [ + DashboardV3Component, + CardComponent, + DashboardPieComponent, + CardRowComponent, + PgSummaryPipe, + DashboardAreaChartComponent, + DashboardTimeSelectorComponent + ], + + exports: [DashboardV3Component] +}) +export class DashboardV3Module {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html new file mode 100644 index 000000000000..bab03a1704c2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html @@ -0,0 +1,257 @@ +
+
+ +
+
FSID
+
{{ detailsCardData.fsid }}
+
Orchestrator
+
{{ detailsCardData.orchestrator || 'Orchestrator is not available' }}
+
Ceph version
+
{{ detailsCardData.cephVersion }}
+
+
+ + +
+ + Cluster +
+
+
+ Alerts + + + + + +
+ + +
+
+
+
+ +
+ +
+
+
+
+
+ + + + + + + +
+ +
+ + +
+ +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + + +
    + + + + + + + + + + + + + + +
    +
    +
    +
    + + + +
    +
    +
    +
    + + + + +
    +
    +
    +
    {{ alert.labels.alertname }}
    +

    +

    + Active since: {{ alert.startsAt | relativeDate }} +

    +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss new file mode 100644 index 000000000000..140f5f78fa4a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss @@ -0,0 +1,67 @@ +.alerts { + height: 17rem; + + div { + padding-top: 0; + } +} + +div { + padding-top: 20px; +} + +ngx-simplebar { + height: 18rem; +} + +hr { + margin-bottom: 2px; + margin-top: 2px; +} + +.position-right { + margin-left: auto; + order: 2; +} + +.center-content { + align-items: center; + margin-top: 30px; + position: relative; +} + +button.dropdown-toggle { + position: relative; + + &::after { + border: 0; + content: '\f054'; + font-family: 'ForkAwesome'; + font-size: 1rem; + position: absolute; + right: 20px; + transition: transform 0.3s ease-in-out; + } + + &[aria-expanded='true']::after { + transform: rotate(90deg); + } + + &:focus { + box-shadow: none; + } +} + +.list-group-item { + border: 0; +} + +dt { + font-size: larger; + margin-bottom: 0.3rem; +} + +dd { + font-size: larger; + margin-bottom: 0.8rem; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts new file mode 100644 index 000000000000..677ca35905b7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts @@ -0,0 +1,325 @@ +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 { ConfigurationService } from '~/app/shared/api/configuration.service'; +import { HealthService } from '~/app/shared/api/health.service'; +import { MgrModuleService } from '~/app/shared/api/mgr-module.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 { CardRowComponent } from '../card-row/card-row.component'; +import { CardComponent } from '../card/card.component'; +import { DashboardPieComponent } from '../dashboard-pie/dashboard-pie.component'; +import { PgSummaryPipe } from '../pg-summary.pipe'; +import { DashboardV3Component } from './dashboard-v3.component'; + +export class SummaryServiceMock { + summaryDataSource = new BehaviorSubject({ + version: + 'ceph version 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; + let configurationService: ConfigurationService; + let orchestratorService: MgrModuleService; + let getHealthSpy: jasmine.Spy; + let getAlertsSpy: jasmine.Spy; + let fakeFeatureTogglesService: jasmine.Spy; + + const healthPayload: Record = { + 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: 1, + client_perf: {}, + scrub_status: 'Inactive', + pools: [], + df: { stats: {} }, + pg_info: { object_stats: { num_objects: 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' + }, + { + 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' + }, + { + 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' + } + ]; + + const configValueData: any = { + value: [ + { + section: 'mgr', + value: 'e90a0d58-658e-4148-8f61-e896c86f0696' + } + ] + }; + + const orchData: any = { + log_level: '', + log_to_cluster: false, + log_to_cluster_level: 'info', + log_to_file: false, + orchestrator: 'cephadm' + }; + + configureTestBed({ + imports: [RouterTestingModule, HttpClientTestingModule, ToastrModule.forRoot(), SharedModule], + declarations: [ + DashboardV3Component, + CardComponent, + DashboardPieComponent, + CardRowComponent, + PgSummaryPipe + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: SummaryService, useClass: SummaryServiceMock }, + { + provide: PrometheusAlertService, + useValue: { + activeCriticalAlerts: 2, + activeWarningAlerts: 1 + } + }, + 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; + configurationService = TestBed.inject(ConfigurationService); + orchestratorService = TestBed.inject(MgrModuleService); + getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth'); + getHealthSpy.and.returnValue(of(healthPayload)); + spyOn(TestBed.inject(PrometheusService), 'ifAlertmanagerConfigured').and.callFake((fn) => fn()); + getAlertsSpy = spyOn(TestBed.inject(PrometheusService), 'getAlerts'); + getAlertsSpy.and.returnValue(of(alertsPayload)); + }); + + 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(5); + }); + + it('should get corresponding data into detailsCardData', () => { + spyOn(configurationService, 'get').and.returnValue(of(configValueData)); + spyOn(orchestratorService, 'getConfig').and.returnValue(of(orchData)); + component.ngOnInit(); + expect(component.detailsCardData.fsid).toBe('e90a0d58-658e-4148-8f61-e896c86f0696'); + 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(healthPayload); + + // HEALTH_WARN + payload.health['status'] = 'HEALTH_WARN'; + payload.health['checks'] = [ + { severity: 'HEALTH_WARN', type: 'WRN', summary: { message: 'fake warning' } } + ]; + + getHealthSpy.and.returnValue(of(payload)); + fixture.detectChanges(); + const clusterStatusCard = fixture.debugElement.query(By.css('cd-card[title="Status"] i')); + expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`); + + // HEALTH_ERR + payload.health['status'] = 'HEALTH_ERR'; + payload.health['checks'] = [ + { severity: 'HEALTH_ERR', type: 'ERR', summary: { message: 'fake error' } } + ]; + + getHealthSpy.and.returnValue(of(payload)); + fixture.detectChanges(); + expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`); + + // HEALTH_OK + payload.health['status'] = 'HEALTH_OK'; + payload.health['checks'] = [ + { severity: 'HEALTH_OK', type: 'OK', summary: { message: 'fake success' } } + ]; + + getHealthSpy.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('danger'); + 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('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), 'activeCriticalAlerts').and.returnValue(0); + spyOn(TestBed.inject(PrometheusAlertService), 'activeWarningAlerts').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); + }); + + 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(); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts new file mode 100644 index 000000000000..1aeb57fccc76 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts @@ -0,0 +1,223 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; + +import _ from 'lodash'; +import { Observable, Subscription, timer } from 'rxjs'; +import { take } from 'rxjs/operators'; +import moment from 'moment'; + +import { ClusterService } from '~/app/shared/api/cluster.service'; +import { ConfigurationService } from '~/app/shared/api/configuration.service'; +import { HealthService } from '~/app/shared/api/health.service'; +import { MgrModuleService } from '~/app/shared/api/mgr-module.service'; +import { OsdService } from '~/app/shared/api/osd.service'; +import { PrometheusService } from '~/app/shared/api/prometheus.service'; +import { Promqls as queries } from '~/app/shared/enum/dashboard-promqls.enum'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { DashboardDetails } 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'; + +@Component({ + selector: 'cd-dashboard-v3', + templateUrl: './dashboard-v3.component.html', + styleUrls: ['./dashboard-v3.component.scss'] +}) +export class DashboardV3Component extends PrometheusListHelper implements OnInit, OnDestroy { + detailsCardData: DashboardDetails = {}; + osdSettingsService: any; + osdSettings: any; + interval = new Subscription(); + permissions: Permissions; + enabledFeature$: FeatureTogglesMap$; + color: string; + capacityService: any; + capacity: any; + healthData$: Observable; + prometheusAlerts$: Observable; + + icons = Icons; + showAlerts = false; + flexHeight = true; + simplebar = { + autoHide: false + }; + textClass: string; + borderClass: string; + alertType: string; + alerts: AlertmanagerAlert[]; + healthData: any; + categoryPgAmount: Record = {}; + totalPgs = 0; + queriesResults: any = { + USEDCAPACITY: '', + IPS: '', + OPS: '', + READLATENCY: '', + WRITELATENCY: '', + READCLIENTTHROUGHPUT: '', + WRITECLIENTTHROUGHPUT: '', + RECOVERYBYTES: '' + }; + timerGetPrometheusDataSub: Subscription; + timerTime = 30000; + readonly lastHourDateObject = { + start: moment().unix() - 3600, + end: moment().unix(), + step: 12 + }; + + constructor( + private summaryService: SummaryService, + private configService: ConfigurationService, + private mgrModuleService: MgrModuleService, + private clusterService: ClusterService, + private osdService: OsdService, + private authStorageService: AuthStorageService, + private featureToggles: FeatureTogglesService, + private healthService: HealthService, + public prometheusService: PrometheusService, + private refreshIntervalService: RefreshIntervalService, + public prometheusAlertService: PrometheusAlertService + ) { + super(prometheusService); + this.permissions = this.authStorageService.getPermissions(); + this.enabledFeature$ = this.featureToggles.get(); + } + + ngOnInit() { + super.ngOnInit(); + this.interval = this.refreshIntervalService.intervalData$.subscribe(() => { + this.getHealth(); + this.getCapacityCardData(); + }); + this.getPrometheusData(this.lastHourDateObject); + this.getDetailsCardData(); + } + + ngOnDestroy() { + this.interval.unsubscribe(); + if (this.timerGetPrometheusDataSub) { + this.timerGetPrometheusDataSub.unsubscribe(); + } + } + + getHealth() { + this.healthService.getMinimalHealth().subscribe((data: any) => { + this.healthData = data; + }); + } + + toggleAlertsWindow(type: string, isToggleButton: boolean = false) { + this.triggerPrometheusAlerts(); + if (isToggleButton) { + this.showAlerts = !this.showAlerts; + this.flexHeight = !this.flexHeight; + } else if ( + !this.showAlerts || + (this.alertType === type && type !== 'danger') || + (this.alertType !== 'warning' && type === 'danger') + ) { + this.showAlerts = !this.showAlerts; + this.flexHeight = !this.flexHeight; + } + + type === 'danger' ? (this.alertType = 'critical') : (this.alertType = type); + this.textClass = `text-${type}`; + this.borderClass = `border-${type}`; + } + + getDetailsCardData() { + this.configService.get('fsid').subscribe((data) => { + this.detailsCardData.fsid = data['value'][0]['value']; + }); + this.mgrModuleService.getConfig('orchestrator').subscribe((data) => { + const orchStr = data['orchestrator']; + this.detailsCardData.orchestrator = orchStr.charAt(0).toUpperCase() + orchStr.slice(1); + }); + this.summaryService.subscribe((summary) => { + const version = summary.version.replace('ceph version ', '').split(' '); + this.detailsCardData.cephVersion = + version[0] + ' ' + version.slice(2, version.length).join(' '); + }); + } + + getCapacityCardData() { + this.osdSettingsService = this.osdService + .getOsdSettings() + .pipe(take(1)) + .subscribe((data: any) => { + this.osdSettings = data; + }); + this.capacityService = this.clusterService.getCapacity().subscribe((data: any) => { + this.capacity = data; + }); + } + + triggerPrometheusAlerts() { + this.prometheusService.ifAlertmanagerConfigured(() => { + this.prometheusService.getAlerts().subscribe((alerts) => { + this.alerts = alerts; + }); + }); + } + + getPrometheusData(selectedTime: any) { + this.prometheusService.ifPrometheusConfigured(() => { + if (this.timerGetPrometheusDataSub) { + this.timerGetPrometheusDataSub.unsubscribe(); + } + this.timerGetPrometheusDataSub = timer(0, this.timerTime).subscribe(() => { + selectedTime = this.updateTimeStamp(selectedTime); + + for (const queryName in queries) { + if (queries.hasOwnProperty(queryName)) { + const query = queries[queryName]; + let interval = selectedTime.step; + + if (query.includes('rate') && selectedTime.step < 20) { + interval = 20; + } else if (query.includes('rate')) { + interval = selectedTime.step * 2; + } + + const intervalAdjustedQuery = query.replace(/\[(.*?)\]/g, `[${interval}s]`); + + this.prometheusService + .getPrometheusData({ + params: intervalAdjustedQuery, + start: selectedTime['start'], + end: selectedTime['end'], + step: selectedTime['step'] + }) + .subscribe((data: any) => { + if (data.result.length) { + this.queriesResults[queryName] = data.result[0].values; + } + }); + } + } + }); + }); + } + + private updateTimeStamp(selectedTime: any): any { + let formattedDate = {}; + const date: number = selectedTime['start'] + this.timerTime / 1000; + const dateNow: number = selectedTime['end'] + this.timerTime / 1000; + formattedDate = { + start: date, + end: dateNow, + step: selectedTime['step'] + }; + return formattedDate; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.spec.ts new file mode 100644 index 000000000000..b467167fdce0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.spec.ts @@ -0,0 +1,36 @@ +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('OsdSummaryPipe', () => { + 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: { + 'active+clean': 241 + }, + pgs_per_osd: 241 + }; + expect(pipe.transform(value)).toEqual({ + categoryPgAmount: { + clean: 241 + }, + total: 241 + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.ts new file mode 100644 index 000000000000..a26097ee0050 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.ts @@ -0,0 +1,27 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import _ from 'lodash'; +import { PgCategoryService } from '~/app/ceph/shared/pg-category.service'; + +@Pipe({ + name: 'pgSummary' +}) +export class PgSummaryPipe implements PipeTransform { + constructor(private pgCategoryService: PgCategoryService) {} + + transform(value: any): any { + const categoryPgAmount: Record = {}; + let total = 0; + _.forEach(value.statuses, (pgAmount, pgStatesText) => { + const categoryType = this.pgCategoryService.getTypeByStates(pgStatesText); + if (_.isUndefined(categoryPgAmount[categoryType])) { + categoryPgAmount[categoryType] = 0; + } + categoryPgAmount[categoryType] += pgAmount; + total += pgAmount; + }); + return { + categoryPgAmount, + total + }; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts index 98f29e571a2c..81164d15b9d9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts @@ -7,9 +7,10 @@ import { NgbNavModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; import { ChartsModule } 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 { DeprecatedDashboardComponent } from './dashboard/dashboard.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'; @@ -29,12 +30,13 @@ import { OsdSummaryPipe } from './osd-summary.pipe'; RouterModule, NgbPopoverModule, FormsModule, - ReactiveFormsModule + ReactiveFormsModule, + DashboardV3Module ], declarations: [ HealthComponent, - DeprecatedDashboardComponent, + DashboardComponent, MonSummaryPipe, OsdSummaryPipe, MgrSummaryPipe, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html index feea20883efe..87b8c3376dc8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html @@ -1,32 +1,15 @@ -
    +
    skip to content - + + - - - -
    +
    - + + +
    diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss index 873fb5d3b802..62c4af1dd92c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss @@ -1,3 +1,3 @@ -main { +main:has(cd-health) { padding-top: 20px; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts index 86a992712919..9c20e4438f11 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts @@ -1,23 +1,26 @@ +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 { DeprecatedDashboardComponent } from './dashboard.component'; +import { DashboardComponent } from './dashboard.component'; describe('DashboardComponent', () => { - let component: DeprecatedDashboardComponent; - let fixture: ComponentFixture; + let component: DashboardComponent; + let fixture: ComponentFixture; configureTestBed({ - imports: [NgbNavModule], - declarations: [DeprecatedDashboardComponent], + imports: [NgbNavModule, HttpClientTestingModule], + declarations: [DashboardComponent], + providers: [FeatureTogglesService], schemas: [NO_ERRORS_SCHEMA] }); beforeEach(() => { - fixture = TestBed.createComponent(DeprecatedDashboardComponent); + fixture = TestBed.createComponent(DashboardComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts index b5d62a62c479..021c945a2f58 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts @@ -1,10 +1,16 @@ 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 DeprecatedDashboardComponent { - hasGrafana = false; // TODO: Temporary var, remove when grafana is implemented +export class DashboardComponent { + enabledFeature$: Observable; + + constructor(private featureToggles: FeatureTogglesService) { + this.enabledFeature$ = this.featureToggles.get(); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.html deleted file mode 100644 index 9b7bf03e7750..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.html +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - {{ data.success }} - - - {{ data.categoryPgAmount?.clean }} - - - - - - - {{ data.info }} - - - - - - - {{ data.warn }} - - - {{ data.categoryPgAmount?.warning }} - - - - - - - {{ data.error }} - - - {{ data.categoryPgAmount?.unknown }} - - - - - - - {{ data.categoryPgAmount?.working }} - - - - - - - - - {{ data.up }} - - - - - {{ data.up }} - - up - - - - {{ data.in }} - - in - - - - {{ data.down }} - - down - - - - {{ data.out }} - - out - - - - {{ data.nearfull }} - - nearfull - - {{ data.full }} - - full - - - - - - - {{ data.up }} - - - - - {{ data.down }} - - - - - - - - {{ data }} - - - - - - - {{ total }} - {{ title }} - {{ title }} - {{ title }}s - - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.spec.ts deleted file mode 100644 index 8932e67b29c5..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { CardRowComponent } from './card-row.component'; - -describe('CardRowComponent', () => { - let component: CardRowComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [CardRowComponent] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(CardRowComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.ts deleted file mode 100644 index 90c939160eb9..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Component, Input, OnChanges } from '@angular/core'; -import { Icons } from '~/app/shared/enum/icons.enum'; - -@Component({ - selector: 'cd-card-row', - templateUrl: './card-row.component.html', - styleUrls: ['./card-row.component.scss'] -}) -export class CardRowComponent implements OnChanges { - @Input() - title: string; - - @Input() - link: string; - - @Input() - data: any; - - @Input() - summaryType = 'default'; - - icons = Icons; - total: number; - - ngOnChanges(): void { - if (this.data.total || this.data.total === 0) { - this.total = this.data.total; - } else if (this.summaryType === 'iscsi') { - this.total = this.data.up + this.data.down || 0; - } else { - this.total = this.data; - } - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.html deleted file mode 100644 index 35dffd46c047..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.html +++ /dev/null @@ -1,8 +0,0 @@ -
    -

    - {{ title }} -

    -
    - -
    -
    diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.scss deleted file mode 100644 index fdf19a00ec6a..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -.card-body { - display: flex; - flex-direction: column; - justify-content: space-evenly; -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.spec.ts deleted file mode 100644 index fdc34fdf7b22..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { configureTestBed } from '~/testing/unit-test-helper'; -import { CardComponent } from './card.component'; - -describe('CardComponent', () => { - let component: CardComponent; - let fixture: ComponentFixture; - - configureTestBed({ - imports: [RouterTestingModule], - declarations: [CardComponent] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(CardComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('Setting cards title makes title visible', () => { - const title = 'Card Title'; - component.title = title; - fixture.detectChanges(); - const titleDiv = fixture.debugElement.nativeElement.querySelector('.card-title'); - - expect(titleDiv.textContent).toContain(title); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.ts deleted file mode 100644 index b6bb99c66909..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component, Input } from '@angular/core'; - -@Component({ - selector: 'cd-card', - templateUrl: './card.component.html', - styleUrls: ['./card.component.scss'] -}) -export class CardComponent { - @Input() - title: string; -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.html deleted file mode 100644 index 6ac991fd58c5..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.html +++ /dev/null @@ -1,23 +0,0 @@ -
    -
    -
    - {{ chartTitle }} -
    - {{currentData}} {{ currentDataUnits }} -
    - {{currentData2}} {{ currentDataUnits2 }} -
    -
    -
    - - -
    -
    -
    diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.scss deleted file mode 100644 index 12e9b9c1c6a5..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.scss +++ /dev/null @@ -1,9 +0,0 @@ -.center-text { - margin-top: 1.2vw; - position: relative; -} - -.chart { - height: 8vh; - margin-top: 15px; -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.spec.ts deleted file mode 100644 index 0501ac75dde6..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { CssHelper } from '~/app/shared/classes/css-helper'; -import { DimlessBinaryPerSecondPipe } from '~/app/shared/pipes/dimless-binary-per-second.pipe'; -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 { DashboardAreaChartComponent } from './dashboard-area-chart.component'; - -describe('DashboardAreaChartComponent', () => { - let component: DashboardAreaChartComponent; - let fixture: ComponentFixture; - - configureTestBed({ - schemas: [NO_ERRORS_SCHEMA], - declarations: [DashboardAreaChartComponent], - providers: [ - CssHelper, - DimlessBinaryPipe, - DimlessBinaryPerSecondPipe, - DimlessPipe, - FormatterService - ] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DashboardAreaChartComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.ts deleted file mode 100644 index 3da4334210de..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { AfterViewInit, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core'; - -import { CssHelper } from '~/app/shared/classes/css-helper'; -import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; -import { DimlessBinaryPerSecondPipe } from '~/app/shared/pipes/dimless-binary-per-second.pipe'; -import { FormatterService } from '~/app/shared/services/formatter.service'; -import { BaseChartDirective, PluginServiceGlobalRegistrationAndOptions } from 'ng2-charts'; -import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe'; - -@Component({ - selector: 'cd-dashboard-area-chart', - templateUrl: './dashboard-area-chart.component.html', - styleUrls: ['./dashboard-area-chart.component.scss'] -}) -export class DashboardAreaChartComponent implements OnInit, OnChanges, AfterViewInit { - @ViewChild(BaseChartDirective) chart: BaseChartDirective; - - @Input() - chartTitle: string; - @Input() - maxValue?: any; - @Input() - dataUnits: string; - @Input() - data: any; - @Input() - data2?: any; - @Input() - label: any; - @Input() - label2?: any; - - currentDataUnits: string; - currentData: number; - currentDataUnits2?: string; - currentData2?: number; - - chartData: any = { - dataset: [ - { - label: '', - data: [{ x: 0, y: 0 }], - tension: 0, - pointBackgroundColor: this.cssHelper.propertyValue('chart-color-strong-blue'), - backgroundColor: this.cssHelper.propertyValue('chart-color-translucent-blue'), - borderColor: this.cssHelper.propertyValue('chart-color-strong-blue') - }, - { - label: '', - data: [], - tension: 0, - pointBackgroundColor: this.cssHelper.propertyValue('chart-color-orange'), - backgroundColor: this.cssHelper.propertyValue('chart-color-yellow'), - borderColor: this.cssHelper.propertyValue('chart-color-orange') - } - ] - }; - - options: any = { - responsive: true, - maintainAspectRatio: false, - elements: { - point: { - radius: 0 - } - }, - legend: { - display: false - }, - tooltips: { - intersect: false, - displayColors: true, - backgroundColor: this.cssHelper.propertyValue('chart-color-tooltip-background'), - callbacks: { - title: function (tooltipItem: any): any { - return tooltipItem[0].xLabel; - } - } - }, - hover: { - intersect: false - }, - scales: { - xAxes: [ - { - display: false, - type: 'time', - gridLines: { - display: false - }, - time: { - tooltipFormat: 'YYYY/MM/DD hh:mm:ss' - } - } - ], - yAxes: [ - { - gridLines: { - display: false - }, - ticks: { - beginAtZero: true, - maxTicksLimit: 3, - callback: (value: any) => { - if (value === 0) { - return null; - } - return this.fillString(this.convertUnits(value)); - } - } - } - ] - }, - plugins: { - borderArea: true, - chartAreaBorder: { - borderColor: this.cssHelper.propertyValue('chart-color-slight-dark-gray'), - borderWidth: 2 - } - } - }; - - public chartAreaBorderPlugin: PluginServiceGlobalRegistrationAndOptions[] = [ - { - beforeDraw(chart: Chart) { - if (!chart.options.plugins.borderArea) { - return; - } - const { - ctx, - chartArea: { left, top, right, bottom } - } = chart; - ctx.save(); - ctx.strokeStyle = chart.options.plugins.chartAreaBorder.borderColor; - ctx.lineWidth = chart.options.plugins.chartAreaBorder.borderWidth; - ctx.setLineDash(chart.options.plugins.chartAreaBorder.borderDash || []); - ctx.lineDashOffset = chart.options.plugins.chartAreaBorder.borderDashOffset; - ctx.strokeRect(left, top, right - left - 1, bottom); - ctx.restore(); - } - } - ]; - - constructor( - private cssHelper: CssHelper, - private dimlessBinary: DimlessBinaryPipe, - private dimlessBinaryPerSecond: DimlessBinaryPerSecondPipe, - private dimlessPipe: DimlessPipe, - private formatter: FormatterService - ) {} - - ngOnInit(): void { - this.currentData = Number( - this.chartData.dataset[0].data[this.chartData.dataset[0].data.length - 1].y - ); - if (this.data2) { - this.currentData2 = Number( - this.chartData.dataset[1].data[this.chartData.dataset[1].data.length - 1].y - ); - } - } - - ngOnChanges(): void { - if (this.data) { - this.setChartTicks(); - this.chartData.dataset[0].data = this.formatData(this.data); - this.chartData.dataset[0].label = this.label; - [this.currentData, this.currentDataUnits] = this.convertUnits( - this.data[this.data.length - 1][1] - ).split(' '); - } - if (this.data2) { - this.chartData.dataset[1].data = this.formatData(this.data2); - this.chartData.dataset[1].label = this.label2; - [this.currentData2, this.currentDataUnits2] = this.convertUnits( - this.data2[this.data2.length - 1][1] - ).split(' '); - } - } - - ngAfterViewInit(): void { - if (this.data) { - this.setChartTicks(); - } - } - - private formatData(array: Array): any { - let formattedData = {}; - formattedData = array.map((data: any) => ({ - x: data[0] * 1000, - y: Number(this.convertUnits(data[1]).replace(/[^\d,.]+/g, '')) - })); - return formattedData; - } - - private convertUnits(data: any): any { - let dataWithUnits: string; - if (this.dataUnits === 'bytes') { - dataWithUnits = this.dimlessBinary.transform(data); - } else if (this.dataUnits === 'bytesPerSecond') { - dataWithUnits = this.dimlessBinaryPerSecond.transform(data); - } else if (this.dataUnits === 'ms') { - dataWithUnits = this.formatter.format_number(data, 1000, ['ms', 's']); - } else { - dataWithUnits = this.dimlessPipe.transform(data); - } - return dataWithUnits; - } - - private fillString(str: string): string { - let maxNumberOfChar: number = 8; - let numberOfChars: number = str.length; - if (str.length < 4) { - maxNumberOfChar = 11; - } - for (; numberOfChars < maxNumberOfChar; numberOfChars++) { - str = '\u00A0' + str; - } - return str + '\u00A0\u00A0'; - } - - private setChartTicks() { - if (this.chart && this.maxValue) { - let [maxValue, maxValueDataUnits] = this.convertUnits(this.maxValue).split(' '); - this.chart.chart.options.scales.yAxes[0].ticks.suggestedMax = maxValue; - this.chart.chart.options.scales.yAxes[0].ticks.suggestedMin = 0; - this.chart.chart.options.scales.yAxes[0].ticks.stepSize = Number((maxValue / 2).toFixed(0)); - this.chart.chart.options.scales.yAxes[0].ticks.callback = (value: any) => { - if (value === 0) { - return null; - } - return this.fillString(`${value} ${maxValueDataUnits}`); - }; - this.chart.chart.update(); - } else if (this.chart && this.data) { - let maxValue = 0, - maxValueDataUnits = ''; - let maxValueData = Math.max(...this.data.map((values: any) => values[1])); - if (this.data2) { - var maxValueData2 = Math.max(...this.data2.map((values: any) => values[1])); - [maxValue, maxValueDataUnits] = this.convertUnits( - Math.max(maxValueData, maxValueData2) - ).split(' '); - } else { - [maxValue, maxValueDataUnits] = this.convertUnits(Math.max(maxValueData)).split(' '); - } - - this.chart.chart.options.scales.yAxes[0].ticks.suggestedMax = maxValue * 1.2; - this.chart.chart.options.scales.yAxes[0].ticks.suggestedMin = 0; - this.chart.chart.options.scales.yAxes[0].ticks.stepSize = Number( - ((maxValue * 1.2) / 2).toFixed(0) - ); - this.chart.chart.options.scales.yAxes[0].ticks.callback = (value: any) => { - if (value === 0) { - return null; - } - if (!maxValueDataUnits) { - return this.fillString(`${value}`); - } - return this.fillString(`${value} ${maxValueDataUnits}`); - }; - this.chart.chart.update(); - } - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.html deleted file mode 100644 index ba8176beab3b..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.html +++ /dev/null @@ -1,16 +0,0 @@ -
    - - -
    -
    -
    -
    diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.scss deleted file mode 100644 index 64e7a9822e22..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.scss +++ /dev/null @@ -1,22 +0,0 @@ -@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; -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.spec.ts deleted file mode 100644 index 892913dab6be..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 { configureTestBed } from '~/testing/unit-test-helper'; -import { DashboardPieComponent } from './dashboard-pie.component'; - -describe('DashboardPieComponent', () => { - let component: DashboardPieComponent; - let fixture: ComponentFixture; - - configureTestBed({ - schemas: [NO_ERRORS_SCHEMA], - declarations: [DashboardPieComponent], - providers: [CssHelper, DimlessBinaryPipe] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DashboardPieComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.ts deleted file mode 100644 index 4aaabb6eb4a0..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { Component, Input, OnChanges, OnInit } from '@angular/core'; - -import * as Chart from 'chart.js'; -import _ from 'lodash'; -import { PluginServiceGlobalRegistrationAndOptions } from 'ng2-charts'; - -import { CssHelper } from '~/app/shared/classes/css-helper'; -import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; - -@Component({ - selector: 'cd-dashboard-pie', - templateUrl: './dashboard-pie.component.html', - styleUrls: ['./dashboard-pie.component.scss'] -}) -export class DashboardPieComponent implements OnChanges, OnInit { - @Input() - data: any; - @Input() - highThreshold: number; - @Input() - lowThreshold: number; - - color: string; - - chartConfig: any = { - chartType: 'doughnut', - labels: ['', '', ''], - dataset: [ - { - label: null, - backgroundColor: [ - this.cssHelper.propertyValue('chart-color-light-gray'), - this.cssHelper.propertyValue('chart-color-slight-dark-gray'), - this.cssHelper.propertyValue('chart-color-dark-gray') - ] - }, - { - label: null, - borderWidth: 0, - backgroundColor: [ - this.cssHelper.propertyValue('chart-color-blue'), - this.cssHelper.propertyValue('chart-color-white') - ] - } - ], - options: { - cutoutPercentage: 70, - events: ['click', 'mouseout', 'touchstart'], - legend: { - display: true, - position: 'right', - labels: { - boxWidth: 10, - usePointStyle: false, - generateLabels: (chart: any) => { - const labels = { 0: {}, 1: {}, 2: {} }; - labels[0] = { - text: $localize`Used: ${chart.data.datasets[1].data[2]}`, - fillStyle: chart.data.datasets[1].backgroundColor[0], - strokeStyle: chart.data.datasets[1].backgroundColor[0] - }; - labels[1] = { - text: $localize`Warning: ${chart.data.datasets[0].data[0]}%`, - fillStyle: chart.data.datasets[0].backgroundColor[1], - strokeStyle: chart.data.datasets[0].backgroundColor[1] - }; - labels[2] = { - text: $localize`Danger: ${ - chart.data.datasets[0].data[0] + chart.data.datasets[0].data[1] - }%`, - fillStyle: chart.data.datasets[0].backgroundColor[2], - strokeStyle: chart.data.datasets[0].backgroundColor[2] - }; - - return labels; - } - } - }, - plugins: { - center_text: true - }, - 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, - filter: (tooltipItem: any) => { - return tooltipItem.datasetIndex === 1; - }, - callbacks: { - label: (item: Record, data: Record) => { - let text = data.labels[item.index]; - if (!text.includes('%')) { - text = `${text} (${data.datasets[item.datasetIndex].data[item.index]}%)`; - } - return text; - } - } - }, - title: { - display: false - } - } - }; - - public doughnutChartPlugins: PluginServiceGlobalRegistrationAndOptions[] = [ - { - id: 'center_text', - beforeDraw(chart: Chart) { - const cssHelper = new CssHelper(); - const defaultFontFamily = 'Helvetica Neue, Helvetica, Arial, sans-serif'; - Chart.defaults.global.defaultFontFamily = 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[0].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.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 cssHelper: CssHelper, private dimlessBinary: DimlessBinaryPipe) {} - - ngOnInit() { - this.prepareRawUsage(this.chartConfig, this.data); - } - - ngOnChanges() { - this.prepareRawUsage(this.chartConfig, this.data); - } - - private prepareRawUsage(chart: Record, data: Record) { - const nearFullRatioPercent = this.lowThreshold * 100; - const fullRatioPercent = this.highThreshold * 100; - const percentAvailable = this.calcPercentage(data.max - data.current, data.max); - const percentUsed = this.calcPercentage(data.current, data.max); - if (percentUsed >= fullRatioPercent) { - this.color = 'chart-color-red'; - } else if (percentUsed >= nearFullRatioPercent) { - this.color = 'chart-color-yellow'; - } else { - this.color = 'chart-color-blue'; - } - - chart.dataset[0].data = [ - Math.round(nearFullRatioPercent), - Math.round(Math.abs(nearFullRatioPercent - fullRatioPercent)), - Math.round(100 - fullRatioPercent) - ]; - - chart.dataset[1].data = [ - percentUsed, - percentAvailable, - this.dimlessBinary.transform(data.current) - ]; - chart.dataset[1].backgroundColor[0] = this.cssHelper.propertyValue(this.color); - - chart.dataset[0].label = [`${percentUsed}%\nof ${this.dimlessBinary.transform(data.max)}`]; - } - - private calcPercentage(dividend: number, divisor: number) { - if (!_.isNumber(dividend) || !_.isNumber(divisor) || divisor === 0) { - return 0; - } - return Math.ceil((dividend / divisor) * 100 * 100) / 100; - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.html deleted file mode 100644 index cd960d07bd9d..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.html +++ /dev/null @@ -1,11 +0,0 @@ -
    - -
    diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.scss deleted file mode 100644 index 13572dc2f0b2..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.scss +++ /dev/null @@ -1,10 +0,0 @@ -select#timepicker { - border: 0; -} - -.timeSelector { - position: absolute; - right: 18px; - top: 20px; - width: 12rem; -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.spec.ts deleted file mode 100644 index 9aeec4dcb51e..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { configureTestBed } from '~/testing/unit-test-helper'; -import { DashboardTimeSelectorComponent } from './dashboard-time-selector.component'; - -describe('DashboardTimeSelectorComponent', () => { - let component: DashboardTimeSelectorComponent; - let fixture: ComponentFixture; - - configureTestBed({ - schemas: [NO_ERRORS_SCHEMA], - declarations: [DashboardTimeSelectorComponent] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DashboardTimeSelectorComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.ts deleted file mode 100644 index 3b0915232b66..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Component, EventEmitter, Output } from '@angular/core'; - -import moment from 'moment'; - -@Component({ - selector: 'cd-dashboard-time-selector', - templateUrl: './dashboard-time-selector.component.html', - styleUrls: ['./dashboard-time-selector.component.scss'] -}) -export class DashboardTimeSelectorComponent { - @Output() - selectedTime = new EventEmitter(); - - times: any; - time: any; - - constructor() { - this.times = [ - { - name: $localize`Last 5 minutes`, - value: this.timeToDate(5 * 60, 1) - }, - { - name: $localize`Last 15 minutes`, - value: this.timeToDate(15 * 60, 3) - }, - { - name: $localize`Last 30 minutes`, - value: this.timeToDate(30 * 60, 6) - }, - { - name: $localize`Last 1 hour`, - value: this.timeToDate(3600, 12) - }, - { - name: $localize`Last 3 hours`, - value: this.timeToDate(3 * 3600, 36) - }, - { - name: $localize`Last 6 hours`, - value: this.timeToDate(6 * 3600, 72) - }, - { - name: $localize`Last 12 hours`, - value: this.timeToDate(12 * 3600, 144) - }, - { - name: $localize`Last 24 hours`, - value: this.timeToDate(24 * 3600, 288) - }, - { - name: $localize`Last 2 days`, - value: this.timeToDate(48 * 3600, 576) - }, - { - name: $localize`Last 7 days`, - value: this.timeToDate(168 * 3600, 2016) - } - ]; - this.time = this.times[3].value; - } - - emitTime() { - this.selectedTime.emit(this.timeToDate(this.time.end - this.time.start, this.time.step)); - } - - private timeToDate(secondsAgo: number, step: number): any { - const date: number = moment().unix() - secondsAgo; - const dateNow: number = moment().unix(); - const formattedDate: any = { - start: date, - end: dateNow, - step: step - }; - return formattedDate; - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard.module.ts deleted file mode 100644 index 466da5625695..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard.module.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { RouterModule } from '@angular/router'; - -import { NgbNavModule, NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; -import { ChartsModule } from 'ng2-charts'; -import { SimplebarAngularModule } from 'simplebar-angular'; - -import { SharedModule } from '~/app/shared/shared.module'; -import { CephSharedModule } from '../shared/ceph-shared.module'; -import { CardComponent } from './card/card.component'; -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 { DashboardComponent } from './dashboard/dashboard.component'; -import { CardRowComponent } from './card-row/card-row.component'; -import { PgSummaryPipe } from './pg-summary.pipe'; - -@NgModule({ - imports: [ - CephSharedModule, - CommonModule, - NgbNavModule, - SharedModule, - ChartsModule, - RouterModule, - NgbPopoverModule, - NgbTooltipModule, - FormsModule, - ReactiveFormsModule, - SimplebarAngularModule - ], - - declarations: [ - DashboardComponent, - CardComponent, - DashboardPieComponent, - CardRowComponent, - PgSummaryPipe, - DashboardAreaChartComponent, - DashboardTimeSelectorComponent - ] -}) -export class NewDashboardModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.html deleted file mode 100644 index bab03a1704c2..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.html +++ /dev/null @@ -1,257 +0,0 @@ -
    -
    - -
    -
    FSID
    -
    {{ detailsCardData.fsid }}
    -
    Orchestrator
    -
    {{ detailsCardData.orchestrator || 'Orchestrator is not available' }}
    -
    Ceph version
    -
    {{ detailsCardData.cephVersion }}
    -
    -
    - - -
    - - Cluster -
    -
    -
    - Alerts - - - - - -
    - - -
    -
    -
    -
    - -
    - -
    -
    -
    -
    -
    - - - - - - - -
    - -
    - - -
    - -
  • - -
  • -
    - -
  • - -
  • -
    - -
  • - -
  • -
    - -
  • - -
  • -
    - -
  • - -
  • -
    - -
  • - -
  • -
    - -
  • - -
  • -
    - -
  • - -
  • -
    - -
  • - -
  • -
    - - -
    - - - - - - - - - - - - - - -
    -
    -
    -
    - - - -
    -
    -
    -
    - - - - -
    -
    -
    -
    {{ alert.labels.alertname }}
    -

    -

    - Active since: {{ alert.startsAt | relativeDate }} -

    -
    -
    -
    -
    -
    -
    -
    -
    diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.scss deleted file mode 100644 index 140f5f78fa4a..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.scss +++ /dev/null @@ -1,67 +0,0 @@ -.alerts { - height: 17rem; - - div { - padding-top: 0; - } -} - -div { - padding-top: 20px; -} - -ngx-simplebar { - height: 18rem; -} - -hr { - margin-bottom: 2px; - margin-top: 2px; -} - -.position-right { - margin-left: auto; - order: 2; -} - -.center-content { - align-items: center; - margin-top: 30px; - position: relative; -} - -button.dropdown-toggle { - position: relative; - - &::after { - border: 0; - content: '\f054'; - font-family: 'ForkAwesome'; - font-size: 1rem; - position: absolute; - right: 20px; - transition: transform 0.3s ease-in-out; - } - - &[aria-expanded='true']::after { - transform: rotate(90deg); - } - - &:focus { - box-shadow: none; - } -} - -.list-group-item { - border: 0; -} - -dt { - font-size: larger; - margin-bottom: 0.3rem; -} - -dd { - font-size: larger; - margin-bottom: 0.8rem; -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts deleted file mode 100644 index fa24090976e1..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts +++ /dev/null @@ -1,325 +0,0 @@ -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 { ConfigurationService } from '~/app/shared/api/configuration.service'; -import { HealthService } from '~/app/shared/api/health.service'; -import { MgrModuleService } from '~/app/shared/api/mgr-module.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 { CardRowComponent } from '../card-row/card-row.component'; -import { CardComponent } from '../card/card.component'; -import { DashboardPieComponent } from '../dashboard-pie/dashboard-pie.component'; -import { PgSummaryPipe } from '../pg-summary.pipe'; -import { DashboardComponent } from './dashboard.component'; - -export class SummaryServiceMock { - summaryDataSource = new BehaviorSubject({ - version: - 'ceph version 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: DashboardComponent; - let fixture: ComponentFixture; - let configurationService: ConfigurationService; - let orchestratorService: MgrModuleService; - let getHealthSpy: jasmine.Spy; - let getAlertsSpy: jasmine.Spy; - let fakeFeatureTogglesService: jasmine.Spy; - - const healthPayload: Record = { - 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: 1, - client_perf: {}, - scrub_status: 'Inactive', - pools: [], - df: { stats: {} }, - pg_info: { object_stats: { num_objects: 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' - }, - { - 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' - }, - { - 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' - } - ]; - - const configValueData: any = { - value: [ - { - section: 'mgr', - value: 'e90a0d58-658e-4148-8f61-e896c86f0696' - } - ] - }; - - const orchData: any = { - log_level: '', - log_to_cluster: false, - log_to_cluster_level: 'info', - log_to_file: false, - orchestrator: 'cephadm' - }; - - configureTestBed({ - imports: [RouterTestingModule, HttpClientTestingModule, ToastrModule.forRoot(), SharedModule], - declarations: [ - DashboardComponent, - CardComponent, - DashboardPieComponent, - CardRowComponent, - PgSummaryPipe - ], - schemas: [NO_ERRORS_SCHEMA], - providers: [ - { provide: SummaryService, useClass: SummaryServiceMock }, - { - provide: PrometheusAlertService, - useValue: { - activeCriticalAlerts: 2, - activeWarningAlerts: 1 - } - }, - 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(DashboardComponent); - component = fixture.componentInstance; - configurationService = TestBed.inject(ConfigurationService); - orchestratorService = TestBed.inject(MgrModuleService); - getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth'); - getHealthSpy.and.returnValue(of(healthPayload)); - spyOn(TestBed.inject(PrometheusService), 'ifAlertmanagerConfigured').and.callFake((fn) => fn()); - getAlertsSpy = spyOn(TestBed.inject(PrometheusService), 'getAlerts'); - getAlertsSpy.and.returnValue(of(alertsPayload)); - }); - - 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(5); - }); - - it('should get corresponding data into detailsCardData', () => { - spyOn(configurationService, 'get').and.returnValue(of(configValueData)); - spyOn(orchestratorService, 'getConfig').and.returnValue(of(orchData)); - component.ngOnInit(); - expect(component.detailsCardData.fsid).toBe('e90a0d58-658e-4148-8f61-e896c86f0696'); - 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(healthPayload); - - // HEALTH_WARN - payload.health['status'] = 'HEALTH_WARN'; - payload.health['checks'] = [ - { severity: 'HEALTH_WARN', type: 'WRN', summary: { message: 'fake warning' } } - ]; - - getHealthSpy.and.returnValue(of(payload)); - fixture.detectChanges(); - const clusterStatusCard = fixture.debugElement.query(By.css('cd-card[title="Status"] i')); - expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`); - - // HEALTH_ERR - payload.health['status'] = 'HEALTH_ERR'; - payload.health['checks'] = [ - { severity: 'HEALTH_ERR', type: 'ERR', summary: { message: 'fake error' } } - ]; - - getHealthSpy.and.returnValue(of(payload)); - fixture.detectChanges(); - expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`); - - // HEALTH_OK - payload.health['status'] = 'HEALTH_OK'; - payload.health['checks'] = [ - { severity: 'HEALTH_OK', type: 'OK', summary: { message: 'fake success' } } - ]; - - getHealthSpy.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('danger'); - 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('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), 'activeCriticalAlerts').and.returnValue(0); - spyOn(TestBed.inject(PrometheusAlertService), 'activeWarningAlerts').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); - }); - - describe('features disabled', () => { - beforeEach(() => { - fakeFeatureTogglesService.and.returnValue( - of({ - rbd: false, - mirroring: false, - iscsi: false, - cephfs: false, - rgw: false - }) - ); - fixture = TestBed.createComponent(DashboardComponent); - 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(); - }); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts deleted file mode 100644 index 8615444665a1..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; - -import _ from 'lodash'; -import { Observable, Subscription, timer } from 'rxjs'; -import { take } from 'rxjs/operators'; -import moment from 'moment'; - -import { ClusterService } from '~/app/shared/api/cluster.service'; -import { ConfigurationService } from '~/app/shared/api/configuration.service'; -import { HealthService } from '~/app/shared/api/health.service'; -import { MgrModuleService } from '~/app/shared/api/mgr-module.service'; -import { OsdService } from '~/app/shared/api/osd.service'; -import { PrometheusService } from '~/app/shared/api/prometheus.service'; -import { Promqls as queries } from '~/app/shared/enum/dashboard-promqls.enum'; -import { Icons } from '~/app/shared/enum/icons.enum'; -import { DashboardDetails } 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'; - -@Component({ - selector: 'cd-dashboard', - templateUrl: './dashboard.component.html', - styleUrls: ['./dashboard.component.scss'] -}) -export class DashboardComponent extends PrometheusListHelper implements OnInit, OnDestroy { - detailsCardData: DashboardDetails = {}; - osdSettingsService: any; - osdSettings: any; - interval = new Subscription(); - permissions: Permissions; - enabledFeature$: FeatureTogglesMap$; - color: string; - capacityService: any; - capacity: any; - healthData$: Observable; - prometheusAlerts$: Observable; - - icons = Icons; - showAlerts = false; - flexHeight = true; - simplebar = { - autoHide: false - }; - textClass: string; - borderClass: string; - alertType: string; - alerts: AlertmanagerAlert[]; - healthData: any; - categoryPgAmount: Record = {}; - totalPgs = 0; - queriesResults: any = { - USEDCAPACITY: '', - IPS: '', - OPS: '', - READLATENCY: '', - WRITELATENCY: '', - READCLIENTTHROUGHPUT: '', - WRITECLIENTTHROUGHPUT: '', - RECOVERYBYTES: '' - }; - timerGetPrometheusDataSub: Subscription; - timerTime = 30000; - readonly lastHourDateObject = { - start: moment().unix() - 3600, - end: moment().unix(), - step: 12 - }; - - constructor( - private summaryService: SummaryService, - private configService: ConfigurationService, - private mgrModuleService: MgrModuleService, - private clusterService: ClusterService, - private osdService: OsdService, - private authStorageService: AuthStorageService, - private featureToggles: FeatureTogglesService, - private healthService: HealthService, - public prometheusService: PrometheusService, - private refreshIntervalService: RefreshIntervalService, - public prometheusAlertService: PrometheusAlertService - ) { - super(prometheusService); - this.permissions = this.authStorageService.getPermissions(); - this.enabledFeature$ = this.featureToggles.get(); - } - - ngOnInit() { - super.ngOnInit(); - this.interval = this.refreshIntervalService.intervalData$.subscribe(() => { - this.getHealth(); - this.getCapacityCardData(); - }); - this.getPrometheusData(this.lastHourDateObject); - this.getDetailsCardData(); - } - - ngOnDestroy() { - this.interval.unsubscribe(); - } - - getHealth() { - this.healthService.getMinimalHealth().subscribe((data: any) => { - this.healthData = data; - }); - } - - toggleAlertsWindow(type: string, isToggleButton: boolean = false) { - this.triggerPrometheusAlerts(); - if (isToggleButton) { - this.showAlerts = !this.showAlerts; - this.flexHeight = !this.flexHeight; - } else if ( - !this.showAlerts || - (this.alertType === type && type !== 'danger') || - (this.alertType !== 'warning' && type === 'danger') - ) { - this.showAlerts = !this.showAlerts; - this.flexHeight = !this.flexHeight; - } - - type === 'danger' ? (this.alertType = 'critical') : (this.alertType = type); - this.textClass = `text-${type}`; - this.borderClass = `border-${type}`; - } - - getDetailsCardData() { - this.configService.get('fsid').subscribe((data) => { - this.detailsCardData.fsid = data['value'][0]['value']; - }); - this.mgrModuleService.getConfig('orchestrator').subscribe((data) => { - const orchStr = data['orchestrator']; - this.detailsCardData.orchestrator = orchStr.charAt(0).toUpperCase() + orchStr.slice(1); - }); - this.summaryService.subscribe((summary) => { - const version = summary.version.replace('ceph version ', '').split(' '); - this.detailsCardData.cephVersion = - version[0] + ' ' + version.slice(2, version.length).join(' '); - }); - } - - getCapacityCardData() { - this.osdSettingsService = this.osdService - .getOsdSettings() - .pipe(take(1)) - .subscribe((data: any) => { - this.osdSettings = data; - }); - this.capacityService = this.clusterService.getCapacity().subscribe((data: any) => { - this.capacity = data; - }); - } - - triggerPrometheusAlerts() { - this.prometheusService.ifAlertmanagerConfigured(() => { - this.prometheusService.getAlerts().subscribe((alerts) => { - this.alerts = alerts; - }); - }); - } - - getPrometheusData(selectedTime: any) { - this.prometheusService.ifPrometheusConfigured(() => { - if (this.timerGetPrometheusDataSub) { - this.timerGetPrometheusDataSub.unsubscribe(); - } - this.timerGetPrometheusDataSub = timer(0, this.timerTime).subscribe(() => { - selectedTime = this.updateTimeStamp(selectedTime); - - for (const queryName in queries) { - if (queries.hasOwnProperty(queryName)) { - const query = queries[queryName]; - let interval = selectedTime.step; - - if (query.includes('rate') && selectedTime.step < 20) { - interval = 20; - } else if (query.includes('rate')) { - interval = selectedTime.step * 2; - } - - const intervalAdjustedQuery = query.replace(/\[(.*?)\]/g, `[${interval}s]`); - - this.prometheusService - .getPrometheusData({ - params: intervalAdjustedQuery, - start: selectedTime['start'], - end: selectedTime['end'], - step: selectedTime['step'] - }) - .subscribe((data: any) => { - if (data.result.length) { - this.queriesResults[queryName] = data.result[0].values; - } - }); - } - } - }); - }); - } - - private updateTimeStamp(selectedTime: any): any { - let formattedDate = {}; - const date: number = selectedTime['start'] + this.timerTime / 1000; - const dateNow: number = selectedTime['end'] + this.timerTime / 1000; - formattedDate = { - start: date, - end: dateNow, - step: selectedTime['step'] - }; - return formattedDate; - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.spec.ts deleted file mode 100644 index b467167fdce0..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -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('OsdSummaryPipe', () => { - 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: { - 'active+clean': 241 - }, - pgs_per_osd: 241 - }; - expect(pipe.transform(value)).toEqual({ - categoryPgAmount: { - clean: 241 - }, - total: 241 - }); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.ts deleted file mode 100644 index a26097ee0050..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import _ from 'lodash'; -import { PgCategoryService } from '~/app/ceph/shared/pg-category.service'; - -@Pipe({ - name: 'pgSummary' -}) -export class PgSummaryPipe implements PipeTransform { - constructor(private pgCategoryService: PgCategoryService) {} - - transform(value: any): any { - const categoryPgAmount: Record = {}; - let total = 0; - _.forEach(value.statuses, (pgAmount, pgStatesText) => { - const categoryType = this.pgCategoryService.getTypeByStates(pgStatesText); - if (_.isUndefined(categoryPgAmount[categoryType])) { - categoryPgAmount[categoryType] = 0; - } - categoryPgAmount[categoryType] += pgAmount; - total += pgAmount; - }); - return { - categoryPgAmount, - total - }; - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.scss index fc12d8f49a8e..9cc5b5d1ac19 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.scss @@ -100,6 +100,10 @@ color: vv.$gray-200; } + .no-hover:hover { + background-color: vv.$secondary !important; + } + @media (min-width: vv.$screen-md-min) { .cd-navbar-utility { border-bottom: 0; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.ts index bb7f2a0d6145..03577681e1a7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.ts @@ -12,6 +12,7 @@ export class FeatureTogglesMap { cephfs = true; rgw = true; nfs = true; + dashboardV3 = true; } export type Features = keyof FeatureTogglesMap; export type FeatureTogglesMap$ = Observable; diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index e74fb384c120..303596a8373e 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -2858,6 +2858,9 @@ paths: cephfs: description: '' type: boolean + dashboard: + description: '' + type: boolean iscsi: description: '' type: boolean @@ -2880,6 +2883,7 @@ paths: - cephfs - rgw - nfs + - dashboard type: object description: OK '400': diff --git a/src/pybind/mgr/dashboard/plugins/feature_toggles.py b/src/pybind/mgr/dashboard/plugins/feature_toggles.py index 4e23e1707591..f16b2e68c4d6 100644 --- a/src/pybind/mgr/dashboard/plugins/feature_toggles.py +++ b/src/pybind/mgr/dashboard/plugins/feature_toggles.py @@ -25,6 +25,7 @@ class Features(Enum): CEPHFS = 'cephfs' RGW = 'rgw' NFS = 'nfs' + DASHBOARD = 'dashboard' PREDISABLED_FEATURES = set() # type: Set[str] @@ -140,7 +141,8 @@ class FeatureToggles(I.CanMgr, I.Setupable, I.HasOptions, "iscsi": (bool, ''), "cephfs": (bool, ''), "rgw": (bool, ''), - "nfs": (bool, '') + "nfs": (bool, ''), + "dashboard": (bool, '') } @APIRouter('/feature_toggles')