]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: dashboard-v3: landing-page revamp
authorbryanmontalvan <68972382+bryanmontalvan@users.noreply.github.com>
Thu, 7 Jul 2022 16:36:15 +0000 (12:36 -0400)
committerPedro Gonzalez Gomez <pegonzal@redhat.com>
Wed, 15 Feb 2023 12:17:16 +0000 (13:17 +0100)
This commit add the following
- A new dashboard component, which will exist in parallel with in the
  current landing-page
- Created a route for this dashboard `/dashboard_3`
- Created a bare-bones bootstrap grid with mock-up card components

Signed-off-by: bryanmontalvan <bmontalv@redhat.com>
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
mgr/dashboard: changes to first layout

CHANGES:

 - Renamed dashboardcomponents

 - Removed unnecesary styling

 - Added unit tests

Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
Moved router.url logic inside html template

This commit removes the `this.router.url` logic which was located in the
`workbench-layout.component.ts` file and moved it into the HTML template
section.

Signed-off-by: bryanmontalvan <bmontalv@redhat.com>
mgr/dashboard: syntax changes from bootstrap 4 to 5

Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
mgr/dashboard: small fixes and improvements over all cards and layout

- all cards placed evenly with the same height
- increased font size on details card and adjusted margin
- changed capacity card legend to: "Used"
- adjusted cluster utilization card margins and increased graphs height
- improved status card toggle
- changed orchestror to orchestrator
- switched IPS/OPS graph colors

Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
mgr/dashboard: added recovery throghput graph, improve promqls, fix inventory's card lines

Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
22 files changed:
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts [new file with mode: 0644]

index 6880a1561c1d9d281d264a41dfd286be8344d484..ec30ccc720885430a59a5568783ca2e86efa2bae 100644 (file)
@@ -24,7 +24,8 @@ 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 { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component';
+import { DeprecatedDashboardComponent } from './ceph/dashboard/dashboard/dashboard.component';
+import { DashboardComponent } from './ceph/new-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';
@@ -88,7 +89,8 @@ const routes: Routes = [
     canActivate: [AuthGuardService, ChangePasswordGuardService],
     canActivateChild: [AuthGuardService, ChangePasswordGuardService],
     children: [
-      { path: 'dashboard', component: DashboardComponent },
+      { path: 'dashboard', component: DeprecatedDashboardComponent },
+      { path: 'dashboard_3', component: DashboardComponent },
       { path: 'error', component: ErrorComponent },
 
       // Cluster
index 47772304b505ffa50bbbd0072c076e9bdb4fa994..17d6246976153694d1d750d44e66c491fb6e35e3 100644 (file)
@@ -5,6 +5,7 @@ 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';
 
@@ -13,6 +14,7 @@ import { PerformanceCounterModule } from './performance-counter/performance-coun
     CommonModule,
     ClusterModule,
     DashboardModule,
+    NewDashboardModule,
     PerformanceCounterModule,
     CephfsModule,
     NfsModule,
index 4bdfd50a51bfd25d5125c89acbae7811303e85d5..98f29e571a2c66364b407c05364647e96b5e0dce 100644 (file)
@@ -9,7 +9,7 @@ import { ChartsModule } from 'ng2-charts';
 import { SharedModule } from '~/app/shared/shared.module';
 import { CephSharedModule } from '../shared/ceph-shared.module';
 import { FeedbackComponent } from '../shared/feedback/feedback.component';
-import { DashboardComponent } from './dashboard/dashboard.component';
+import { DeprecatedDashboardComponent } 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';
@@ -34,7 +34,7 @@ import { OsdSummaryPipe } from './osd-summary.pipe';
 
   declarations: [
     HealthComponent,
-    DashboardComponent,
+    DeprecatedDashboardComponent,
     MonSummaryPipe,
     OsdSummaryPipe,
     MgrSummaryPipe,
index 7bc4980bb82be3945ebb9197f4edd0088536941b..86a9927129197356693e4d6fde58b7771db3b83a 100644 (file)
@@ -4,20 +4,20 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
 
 import { configureTestBed } from '~/testing/unit-test-helper';
-import { DashboardComponent } from './dashboard.component';
+import { DeprecatedDashboardComponent } from './dashboard.component';
 
 describe('DashboardComponent', () => {
-  let component: DashboardComponent;
-  let fixture: ComponentFixture<DashboardComponent>;
+  let component: DeprecatedDashboardComponent;
+  let fixture: ComponentFixture<DeprecatedDashboardComponent>;
 
   configureTestBed({
     imports: [NgbNavModule],
-    declarations: [DashboardComponent],
+    declarations: [DeprecatedDashboardComponent],
     schemas: [NO_ERRORS_SCHEMA]
   });
 
   beforeEach(() => {
-    fixture = TestBed.createComponent(DashboardComponent);
+    fixture = TestBed.createComponent(DeprecatedDashboardComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();
   });
index 354e3890359657279488c0be4bb92c5c9d0ba5e3..b5d62a62c479733c1c2da6270e19149caed18cea 100644 (file)
@@ -5,6 +5,6 @@ import { Component } from '@angular/core';
   templateUrl: './dashboard.component.html',
   styleUrls: ['./dashboard.component.scss']
 })
-export class DashboardComponent {
+export class DeprecatedDashboardComponent {
   hasGrafana = false; // TODO: Temporary var, remove when grafana is implemented
 }
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
new file mode 100644 (file)
index 0000000..35dffd4
--- /dev/null
@@ -0,0 +1,8 @@
+<div class="card shadow-sm flex-fill">
+  <h4 class="card-title mt-4 ms-4 mb-0">
+    {{ title }}
+  </h4>
+  <div class="card-body ps-0 pe-0">
+    <ng-content></ng-content>
+  </div>
+</div>
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
new file mode 100644 (file)
index 0000000..fdf19a0
--- /dev/null
@@ -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/new-dashboard/card/card.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.spec.ts
new file mode 100644 (file)
index 0000000..fdc34fd
--- /dev/null
@@ -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<CardComponent>;
+
+  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
new file mode 100644 (file)
index 0000000..b6bb99c
--- /dev/null
@@ -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/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
new file mode 100644 (file)
index 0000000..6ac991f
--- /dev/null
@@ -0,0 +1,23 @@
+<div class="row">
+  <div class="col-3 center-text">
+    <br>
+    <b class="chartTitle"
+       i18n>{{ chartTitle }}</b>
+    <br>
+    <span [ngbTooltip]="label"
+          i18n>{{currentData}} {{ currentDataUnits }}</span>
+    <br>
+    <span [ngbTooltip]="label2"
+          i18n>{{currentData2}} {{ currentDataUnits2 }}</span>
+  </div>
+  <div class="col-9">
+    <div class="chart">
+      <canvas baseChart
+              [datasets]="chartData.dataset"
+              [options]="options"
+              [chartType]="'line'"
+              [plugins]="chartAreaBorderPlugin">
+      </canvas>
+    </div>
+  </div>
+</div>
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
new file mode 100644 (file)
index 0000000..12e9b9c
--- /dev/null
@@ -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/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
new file mode 100644 (file)
index 0000000..3da4334
--- /dev/null
@@ -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>): 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.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.ts
new file mode 100644 (file)
index 0000000..4aaabb6
--- /dev/null
@@ -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<string, any>, data: Record<string, any>) => {
+            let text = data.labels[item.index];
+            if (!text.includes('%')) {
+              text = `${text} (${data.datasets[item.datasetIndex].data[item.index]}%)`;
+            }
+            return text;
+          }
+        }
+      },
+      title: {
+        display: false
+      }
+    }
+  };
+
+  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<string, any>, data: Record<string, any>) {
+    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.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.ts
new file mode 100644 (file)
index 0000000..3b09152
--- /dev/null
@@ -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<any>();
+
+  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
new file mode 100644 (file)
index 0000000..27ed0f2
--- /dev/null
@@ -0,0 +1,29 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import { NgbNavModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+import { ChartsModule } from 'ng2-charts';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { CephSharedModule } from '../shared/ceph-shared.module';
+import { CardComponent } from './card/card.component';
+import { DashboardComponent } from './dashboard/dashboard.component';
+
+@NgModule({
+  imports: [
+    CephSharedModule,
+    CommonModule,
+    NgbNavModule,
+    SharedModule,
+    ChartsModule,
+    RouterModule,
+    NgbPopoverModule,
+    FormsModule,
+    ReactiveFormsModule
+  ],
+
+  declarations: [DashboardComponent, CardComponent]
+})
+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
new file mode 100644 (file)
index 0000000..8065bb8
--- /dev/null
@@ -0,0 +1,258 @@
+<div class="container-fluid">
+  <div class="row mx-0">
+    <cd-card title="Details"
+             i18n-title
+             class="col-sm-3 px-3"
+             [ngClass]="{'d-flex': flexHeight}">
+      <dl class="ms-4 me-4">
+        <dt>FSID</dt>
+        <dd>{{ detailsCardData.fsid }}</dd>
+        <dt>Orchestrator</dt>
+        <dd i18n>{{ detailsCardData.orchestrator || 'Orchestrator is not available' }}</dd>
+        <dt>Ceph version</dt>
+        <dd>{{ detailsCardData.cephVersion }}</dd>
+      </dl>
+    </cd-card>
+
+    <cd-card title="Status"
+             i18n-title
+             class="col-sm-6 px-3 d-flex">
+      <div class="d-flex ms-4 me-4 mb-5 center-content">
+        <i *ngIf="healthData.health?.status"
+           [ngClass]="[healthData.health.status | healthIcon, icons.large2x]"
+           [ngStyle]="healthData.health.status | healthColor"
+           [title]="healthData.health.status"></i>
+        <span class="ms-2 mt-n1 lead"
+              i18n>Cluster</span>
+      </div>
+      <section class="border-top mt-5"
+               *ngIf="isAlertmanagerConfigured && (crticialActiveAlerts || warningActiveAlerts)">
+        <div class="d-flex flex-wrap ms-4 me-4">
+          <span class="pt-2"
+                i18n>Alerts</span>
+          <!-- Potentially make widget component -->
+          <button class="btn btn-outline-danger rounded-pill ms-2"
+                  [ngClass]="{'active': showAlerts && alertType === 'critical'}"
+                  title="Danger"
+                  (click)="toggleAlertsWindow('danger')"
+                  id="dangerAlerts"
+                  i18n-title
+                  *ngIf="crticialActiveAlerts">
+            <i [ngClass]="[icons.danger]"></i>
+            <span>{{ crticialActiveAlerts }}</span>
+          </button>
+
+          <button class="btn btn-outline-warning rounded-pill ms-2"
+                  [ngClass]="{'active': showAlerts && alertType === 'warning'}"
+                  title="Warning"
+                  (click)="toggleAlertsWindow('warning')"
+                  id="warningAlerts"
+                  i18n-title
+                  *ngIf="warningActiveAlerts">
+            <i [ngClass]="[icons.infoCircle]"></i>
+            <span>{{ warningActiveAlerts }}</span>
+          </button>
+
+          <div class="pt-0 position-right">
+            <button class="btn btn-block dropdown-toggle"
+                    data-toggle="collapse"
+                    aria-label="toggle alert window"
+                    [attr.aria-expanded]="showAlerts"
+                    (click)="toggleAlertsWindow('danger', 'true')"></button>
+
+          </div>
+        </div>
+        <div class="alerts pt-0"
+             *ngIf="showAlerts">
+          <hr class="mt-4">
+          <ngx-simplebar [options]="simplebar">
+            <div class="card-body ps-0 pe-1 pt-1">
+              <ng-container *ngTemplateOutlet="alertsCard"></ng-container>
+            </div>
+          </ngx-simplebar>
+        </div>
+      </section>
+    </cd-card>
+
+    <cd-card title="Capacity"
+             i18n-title
+             class="col-sm-3 px-3"
+             [ngClass]="{'d-flex': flexHeight}">
+      <ng-container class="ms-4 me-4"
+                    *ngIf="{osdSettings: osdSettings$ | async, capacity: capacity$ | async} as values">
+        <ng-container *ngIf="values.osdSettings && values.capacity">
+          <cd-dashboard-pie [data]="{max: values.capacity.total_bytes, current: values.capacity.total_used_raw_bytes}"
+                            [lowThreshold]="values.osdSettings.nearfull_ratio"
+                            [highThreshold]="values.osdSettings.full_ratio">
+          </cd-dashboard-pie>
+        </ng-container>
+      </ng-container>
+    </cd-card>
+  </div>
+  <!-- Second row -->
+  <div class="row mx-0">
+    <!-- Inventory Card -->
+    <cd-card title="Inventory"
+             i18n-title
+             class="col-sm-3 px-3 d-flex">
+      <hr>
+      <!-- Hosts -->
+      <li class="list-group-item">
+        <cd-card-row [data]="healthData.hosts"
+                     link="/hosts"
+                     title="Host"
+                     summaryType="simplified"
+                     *ngIf="healthData.hosts != null"></cd-card-row>
+      </li>
+      <hr>
+      <!-- Monitors -->
+      <li class="list-group-item">
+        <cd-card-row [data]="healthData.mon_status.monmap.mons.length"
+                     link="/monitor"
+                     title="Monitor"
+                     summaryType="simplified"
+                     *ngIf="healthData.mon_status"></cd-card-row>
+      </li>
+      <hr>
+      <!-- Managers -->
+      <li *ngIf="healthData.mgr_map"
+          class="list-group-item">
+        <cd-card-row [data]="healthData.mgr_map | mgrSummary"
+                     link="/manager"
+                     title="Manager"
+                     *ngIf="healthData.mgr_map"></cd-card-row>
+      </li>
+      <hr>
+      <!-- OSDs -->
+      <li class="list-group-item">
+        <cd-card-row [data]="healthData.osd_map | osdSummary"
+                     link="/osd"
+                     title="OSD"
+                     summaryType="osd"
+                     *ngIf="healthData.osd_map"></cd-card-row>
+      </li>
+      <hr>
+      <!-- Pools -->
+      <li *ngIf="healthData.pools"
+          class="list-group-item">
+        <cd-card-row [data]="healthData.pools.length"
+                     link="/pool"
+                     title="Pool"
+                     summaryType="simplified"
+                     *ngIf="healthData.pools"></cd-card-row>
+      </li>
+      <hr>
+      <!-- PG Info -->
+      <li class="list-group-item">
+        <cd-card-row [data]="healthData.pg_info | pgSummary"
+                     title="PG"
+                     *ngIf="healthData.pg_info"></cd-card-row>
+      </li>
+      <hr>
+      <!-- Object gateways -->
+      <li *ngIf="enabledFeature.rgw && healthData.rgw != null"
+          class="list-group-item"
+          id="rgw-item">
+        <cd-card-row [data]="healthData.rgw"
+                     link="/rgw/daemon"
+                     title="Object Gateway"
+                     summaryType="simplified"
+                     *ngIf="healthData.rgw || healthData.rgw === 0 "></cd-card-row>
+      </li>
+      <hr>
+      <!-- Metadata Servers -->
+      <li *ngIf="enabledFeature.cephfs && healthData.fs_map"
+          class="list-group-item"
+          id="mds-item">
+        <cd-card-row [data]="healthData.fs_map | mdsSummary"
+                     title="Metadata Server"
+                     *ngIf="healthData.fs_map"></cd-card-row>
+      </li>
+      <hr>
+      <!-- iSCSI Gateways -->
+      <li *ngIf="enabledFeature.iscsi && healthData.iscsi_daemons != null"
+          class="list-group-item"
+          id="iscsi-item">
+        <cd-card-row [data]="healthData.iscsi_daemons"
+                     link="/iscsi/daemon"
+                     title="iSCSI Gateway"
+                     summaryType="iscsi"
+                     *ngIf="healthData.iscsi_daemons"></cd-card-row>
+      </li>
+    </cd-card>
+
+    <cd-card title="Capacity utilization"
+             i18n-title
+             class="col-sm-9 px-3 d-flex">
+      <div class="ms-4 me-4 mt-0">
+        <cd-dashboard-time-selector (selectedTime)="getPrometheusData($event)">
+        </cd-dashboard-time-selector>
+        <ng-container *ngIf="capacity">
+          <cd-dashboard-area-chart chartTitle="Used Capacity"
+                                   [maxValue]="capacity.total_bytes"
+                                   dataUnits="bytes"
+                                   label="Used Capacity"
+                                   [data]="queriesResults.USEDCAPACITY">
+          </cd-dashboard-area-chart>
+        </ng-container>
+        <cd-dashboard-area-chart chartTitle="IOPS"
+                                 dataUnits="none"
+                                 label="OPS"
+                                 label2="IPS"
+                                 [data]="queriesResults.OPS"
+                                 [data2]="queriesResults.IPS">
+        </cd-dashboard-area-chart>
+        <cd-dashboard-area-chart chartTitle="Latency"
+                                 dataUnits="ms"
+                                 label="Read"
+                                 label2="Write"
+                                 [data]="queriesResults.READLATENCY"
+                                 [data2]="queriesResults.WRITELATENCY">
+        </cd-dashboard-area-chart>
+        <cd-dashboard-area-chart chartTitle="Client Throughput"
+                                 dataUnits="bytesPerSecond"
+                                 label="Read"
+                                 label2="Write"
+                                 [data]="queriesResults.READCLIENTTHROUGHPUT"
+                                 [data2]="queriesResults.WRITECLIENTTHROUGHPUT">
+        </cd-dashboard-area-chart>
+        <cd-dashboard-area-chart chartTitle="Recovery Throughput"
+                                 dataUnits="bytesPerSecond"
+                                 label="Recovery Throughput"
+                                 [data]="queriesResults.RECOVERYBYTES">
+        </cd-dashboard-area-chart>
+      </div>
+    </cd-card>
+  </div>
+</div>
+
+<ng-template #alertsCard>
+  <ng-container *ngFor="let alert of alerts; let i = index">
+    <div [ngClass]="borderClass"
+         *ngIf="alertType === alert.labels.severity">
+      <div class="card tc_alerts border-0 pt-3">
+        <div class="row no-gutters">
+          <div class="col-sm-1 text-center">
+            <span [ngClass]="[icons.stack, icons.large, textClass]">
+              <i [ngClass]="[icons.circle, icons.stack2x]"></i>
+              <i [ngClass]="[icons.stack1x, icons.inverse, icons.warning]"></i>
+            </span>
+          </div>
+          <div class="col-md-11">
+            <div class="card-body ps-0 pe-1 pt-1">
+              <h6 class="card-title bold">{{ alert.labels.alertname }}</h6>
+              <p class="card-text me-3"
+                 [innerHtml]="alert.annotations.summary"></p>
+              <p class="card-text text-muted me-3">
+                <small class="date"
+                       [title]="alert.startsAt | cdDate"
+                       i18n>Active since: {{ alert.startsAt  | relativeDate }}</small>
+              </p>
+            </div>
+          </div>
+        </div>
+      </div>
+      <hr>
+    </div>
+  </ng-container>
+</ng-template>
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
new file mode 100644 (file)
index 0000000..50789c8
--- /dev/null
@@ -0,0 +1,59 @@
+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
new file mode 100644 (file)
index 0000000..cf3f518
--- /dev/null
@@ -0,0 +1,30 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CardComponent } from '../card/card.component';
+import { DashboardComponent } from './dashboard.component';
+
+describe('CardComponent', () => {
+  let component: DashboardComponent;
+  let fixture: ComponentFixture<DashboardComponent>;
+
+  configureTestBed({
+    imports: [RouterTestingModule],
+    declarations: [DashboardComponent, CardComponent]
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(DashboardComponent);
+    component = fixture.componentInstance;
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should render all cards', () => {
+    const dashboardCards = fixture.debugElement.nativeElement.querySelectorAll('cd-card');
+    expect(dashboardCards.length).toBe(5);
+  });
+});
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
new file mode 100644 (file)
index 0000000..843bc86
--- /dev/null
@@ -0,0 +1,202 @@
+import { Component } from '@angular/core';
+
+@Component({
+  selector: 'cd-dashboard',
+  templateUrl: './dashboard.component.html',
+  styleUrls: ['./dashboard.component.scss']
+})
+export class DashboardComponent implements OnInit, OnDestroy {
+  detailsCardData: DashboardDetails = {};
+  osdSettingsService: any;
+  osdSettings: any;
+  interval = new Subscription();
+  permissions: Permissions;
+  enabledFeature$: FeatureTogglesMap$;
+  color: string;
+  capacityService: any;
+  capacity: any;
+  healthData$: Observable<Object>;
+  prometheusAlerts$: Observable<AlertmanagerAlert[]>;
+
+  isAlertmanagerConfigured = false;
+  icons = Icons;
+  showAlerts = false;
+  flexHeight = true;
+  simplebar = {
+    autoHide: false
+  };
+  textClass: string;
+  borderClass: string;
+  alertType: string;
+  alerts: AlertmanagerAlert[];
+  crticialActiveAlerts: number;
+  warningActiveAlerts: number;
+  healthData: any;
+  categoryPgAmount: Record<string, number> = {};
+  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
+  ) {
+    this.permissions = this.authStorageService.getPermissions();
+    this.enabledFeature$ = this.featureToggles.get();
+  }
+
+  ngOnInit() {
+    this.interval = this.refreshIntervalService.intervalData$.subscribe(() => {
+      this.getHealth();
+      this.triggerPrometheusAlerts();
+      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) {
+    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.isAlertmanagerConfigured = true;
+
+      this.prometheusService.getAlerts().subscribe((alerts) => {
+        this.alerts = alerts;
+        this.crticialActiveAlerts = alerts.filter(
+          (alert: AlertmanagerAlert) =>
+            alert.status.state === 'active' && alert.labels.severity === 'critical'
+        ).length;
+        this.warningActiveAlerts = alerts.filter(
+          (alert: AlertmanagerAlert) =>
+            alert.status.state === 'active' && alert.labels.severity === 'warning'
+        ).length;
+      });
+    });
+  }
+
+  getPrometheusData(selectedTime: any) {
+    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;
+  }
+}
index 3979ad7a4a95e20cdcc26b3145fc7dfd7b7535bf..d8c1891fc2129f79c88885d536f395a8dc3def63 100644 (file)
@@ -1,8 +1,8 @@
 <block-ui>
   <cd-navigation>
     <div class="container-fluid h-100"
-         [ngClass]="{'dashboard':isDashboardPage()} ">
-      <cd-context></cd-context>
+         [ngClass]="{'dashboard': (router.url == '/dashboard' || router.url == '/dashboard_3')}">
+    <cd-context></cd-context>
       <cd-breadcrumbs></cd-breadcrumbs>
       <router-outlet></router-outlet>
     </div>
index f2070be5fe0e9f2f2cf838d722aad48a2f47dd5b..afc7a83bb277e5d1fdf9156eec57b22062c98701 100644 (file)
@@ -17,7 +17,7 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy {
   private subs = new Subscription();
 
   constructor(
-    private router: Router,
+    public router: Router,
     private summaryService: SummaryService,
     private taskManagerService: TaskManagerService,
     private faviconService: FaviconService
@@ -32,8 +32,4 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy {
   ngOnDestroy() {
     this.subs.unsubscribe();
   }
-
-  isDashboardPage() {
-    return this.router.url === '/dashboard';
-  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts
new file mode 100644 (file)
index 0000000..7afd069
--- /dev/null
@@ -0,0 +1,10 @@
+export enum Promqls {
+  USEDCAPACITY = 'ceph_cluster_total_used_bytes',
+  IPS = 'sum(rate(ceph_osd_op_w_in_bytes[$interval]))',
+  OPS = 'sum(rate(ceph_osd_op_r_out_bytes[$interval]))',
+  READLATENCY = 'avg_over_time(ceph_osd_apply_latency_ms[$interval])',
+  WRITELATENCY = 'avg_over_time(ceph_osd_commit_latency_ms[$interval])',
+  READCLIENTTHROUGHPUT = 'sum(rate(ceph_pool_rd_bytes[$interval]))',
+  WRITECLIENTTHROUGHPUT = 'sum(rate(ceph_pool_wr_bytes[$interval]))',
+  RECOVERYBYTES = 'sum(rate(ceph_osd_recovery_bytes[$interval]))'
+}