]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add button toggle to switch between new and old landing page 50608/head
authorPedro Gonzalez Gomez <pegonzal@redhat.com>
Tue, 14 Feb 2023 10:21:57 +0000 (11:21 +0100)
committerNizamudeen A <nia@redhat.com>
Tue, 21 Mar 2023 16:10:40 +0000 (21:40 +0530)
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
(cherry picked from commit 21dc93ae11db02bc8e0a2175f21fa7dd5e423b00)

72 files changed:
doc/mgr/capacity-card.png [new file with mode: 0644]
doc/mgr/cluster-utilization-card.png [new file with mode: 0644]
doc/mgr/dashboard-landing-page.png [new file with mode: 0644]
doc/mgr/dashboard.rst
doc/mgr/details-card.png [new file with mode: 0644]
doc/mgr/inventory-card.png [new file with mode: 0644]
doc/mgr/status-card-open.png [new file with mode: 0644]
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-v3/card-row/card-row.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-pie/dashboard-pie.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-time-selector/dashboard-time-selector.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss
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-row/card-row.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard.module.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles.service.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/plugins/feature_toggles.py

diff --git a/doc/mgr/capacity-card.png b/doc/mgr/capacity-card.png
new file mode 100644 (file)
index 0000000..59a7034
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 (file)
index 0000000..fc0fd9e
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 (file)
index 0000000..77a1fe1
Binary files /dev/null and b/doc/mgr/dashboard-landing-page.png differ
index faf4b4a1e6d3973a656e387def975a251242612d..047b7db3412c5f6e1c2b27082447d1bda2e84c7b 100644 (file)
@@ -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 (file)
index 0000000..0c219a8
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 (file)
index 0000000..54317fc
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 (file)
index 0000000..4ea2092
Binary files /dev/null and b/doc/mgr/status-card-open.png differ
index a33467dd0e840f128d4e9297cc6b1e4dd9dc2dd8..4a490728b7cb41980d47e4ebe8e36b77c9996b88 100644 (file)
@@ -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';
@@ -87,8 +86,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
index 17d6246976153694d1d750d44e66c491fb6e35e3..47772304b505ffa50bbbd0072c076e9bdb4fa994 100644 (file)
@@ -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 (file)
index 0000000..9b7bf03
--- /dev/null
@@ -0,0 +1,167 @@
+<div class="d-flex pl-1 pb-2 pt-2">
+  <div class="ms-2 me-auto">
+    <a [routerLink]="link"
+       *ngIf="link && total > 0; else noLinkTitle"
+       [ngPlural]="total"
+       i18n>
+        {{ total }}
+      <ng-template ngPluralCase="=0">{{ title }}</ng-template>
+      <ng-template ngPluralCase="=1">{{ title }}</ng-template>
+      <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+    </a>
+  </div>
+
+  <ng-container [ngSwitch]="summaryType">
+    <ng-container *ngSwitchCase="'iscsi'">
+      <ng-container *ngTemplateOutlet="iscsiSummary"></ng-container>
+    </ng-container>
+    <ng-container *ngSwitchCase="'osd'">
+      <ng-container *ngTemplateOutlet="osdSummary"></ng-container>
+    </ng-container>
+    <ng-container *ngSwitchCase="'simplified'">
+      <ng-container *ngTemplateOutlet="simplifiedSummary"></ng-container>
+    </ng-container>
+    <ng-container *ngSwitchDefault>
+      <ng-container *ngTemplateOutlet="defaultSummary"></ng-container>
+    </ng-container>
+  </ng-container>
+</div>
+
+<ng-template #defaultSummary>
+  <span *ngIf="data.success || data.categoryPgAmount?.clean || (data.success === 0 && data.total === 0)">
+    <span *ngIf="data.success || (data.success === 0 && data.total === 0)">
+      {{ data.success }}
+    </span>
+    <span *ngIf="data.categoryPgAmount?.clean">
+      {{ data.categoryPgAmount?.clean }}
+    </span>
+    <i class="text-success"
+       [ngClass]="[icons.success]">
+    </i>
+  </span>
+  <span *ngIf="data.info"
+        class="ms-2">
+    <span *ngIf="data.info">
+      {{ data.info }}
+    </span>
+    <i class="text-info"
+       [ngClass]="[icons.danger]">
+    </i>
+  </span>
+  <span *ngIf="data.warn || data.categoryPgAmount?.warning"
+        class="ms-2">
+    <span *ngIf="data.warn">
+      {{ data.warn }}
+    </span>
+    <span *ngIf="data.categoryPgAmount?.warning">
+      {{ data.categoryPgAmount?.warning }}
+    </span>
+    <i class="text-warning"
+       [ngClass]="[icons.warning]">
+    </i>
+  </span>
+  <span *ngIf="data.error || data.categoryPgAmount?.unknown"
+        class="ms-2">
+    <span *ngIf="data.error">
+      {{ data.error }}
+    </span>
+    <span *ngIf="data.categoryPgAmount?.unknown">
+      {{ data.categoryPgAmount?.unknown }}
+    </span>
+    <i class="text-danger"
+       [ngClass]="[icons.danger]">
+    </i>
+  </span>
+  <span *ngIf="data.categoryPgAmount?.working"
+        class="ms-2">
+    <span *ngIf="data.categoryPgAmount?.working">
+      {{ data.categoryPgAmount?.working }}
+    </span>
+    <i class="text-warning"
+       [ngClass]="[icons.spinner, icons.spin]">
+    </i>
+  </span>
+</ng-template>
+
+<ng-template #osdSummary>
+  <span *ngIf="data.up === data.in">
+    {{ data.up }}
+    <i class="text-success"
+       [ngClass]="[icons.success]">
+    </i>
+  </span>
+  <span *ngIf="data.up !== data.in">
+    {{ data.up }}
+    <span class="fw-bold text-success">
+        up
+    </span>
+  </span>
+  <span *ngIf="data.in !== data.up"
+        class="ms-2">
+    {{ data.in }}
+    <span class="fw-bold text-success">
+        in
+    </span>
+  </span>
+  <span *ngIf="data.down"
+        class="ms-2">
+    {{ data.down }}
+    <span class="fw-bold text-danger me-2">
+        down
+    </span>
+  </span>
+  <span *ngIf="data.out"
+        class="ms-2">
+    {{ data.out }}
+    <span class="fw-bold text-danger me-2">
+        out
+    </span>
+  </span>
+  <span *ngIf="data.nearfull"
+        class="ms-2">
+        {{ data.nearfull }}
+    <span class="fw-bold text-warning me-2">
+      nearfull</span></span>
+  <span *ngIf="data.full"
+        class="ms-2">
+        {{ data.full }}
+    <span class="fw-bold text-danger">
+      full
+    </span>
+  </span>
+</ng-template>
+
+<ng-template #iscsiSummary>
+  <span>
+    {{ data.up }}
+    <i class="text-success"
+       *ngIf="data.up || data.up === 0"
+       [ngClass]="[icons.success]">
+    </i>
+  </span>
+  <span *ngIf="data.down"
+        class="ms-2">
+        {{ data.down }}
+    <i class="text-danger"
+       [ngClass]="[icons.danger]">
+    </i>
+  </span>
+</ng-template>
+
+<ng-template #simplifiedSummary>
+  <span>
+    {{ data }}
+    <i class="text-success"
+       [ngClass]="[icons.success]"></i>
+  </span>
+</ng-template>
+
+<ng-template #noLinkTitle>
+  <span *ngIf="total || total === 0"
+        [ngPlural]="total">
+    {{ total }}
+    <ng-template ngPluralCase="=0">{{ title }}</ng-template>
+    <ng-template ngPluralCase="=1">{{ title }}</ng-template>
+    <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+  </span>
+</ng-template>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..8932e67
--- /dev/null
@@ -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<CardRowComponent>;
+
+  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 (file)
index 0000000..90c9391
--- /dev/null
@@ -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 (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/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 (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/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 (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/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 (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/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 (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/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 (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/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 (file)
index 0000000..0501ac7
--- /dev/null
@@ -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<DashboardAreaChartComponent>;
+
+  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 (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/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 (file)
index 0000000..ba8176b
--- /dev/null
@@ -0,0 +1,16 @@
+<div class="chart-container">
+  <canvas baseChart
+          #chartCanvas
+          [datasets]="chartConfig.dataset"
+          [chartType]="chartConfig.chartType"
+          [options]="chartConfig.options"
+          [labels]="chartConfig.labels"
+          [colors]="chartConfig.colors"
+          [plugins]="doughnutChartPlugins"
+          class="chart-canvas">
+  </canvas>
+  <div class="chartjs-tooltip"
+       #chartTooltip>
+    <table></table>
+  </div>
+</div>
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 (file)
index 0000000..64e7a98
--- /dev/null
@@ -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 (file)
index 0000000..892913d
--- /dev/null
@@ -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<DashboardPieComponent>;
+
+  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 (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/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 (file)
index 0000000..cd960d0
--- /dev/null
@@ -0,0 +1,11 @@
+<div class="timeSelector">
+  <select id="timepicker"
+          name="timepicker"
+          [(ngModel)]="time"
+          (ngModelChange)="emitTime()"
+          class="form-select">
+    <option *ngFor="let key of times"
+            [ngValue]="key.value">{{ key.name }}
+    </option>
+  </select>
+</div>
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 (file)
index 0000000..13572dc
--- /dev/null
@@ -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 (file)
index 0000000..9aeec4d
--- /dev/null
@@ -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<DashboardTimeSelectorComponent>;
+
+  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 (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/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 (file)
index 0000000..2c3b4cc
--- /dev/null
@@ -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 (file)
index 0000000..bab03a1
--- /dev/null
@@ -0,0 +1,257 @@
+<div class="container-fluid"
+     *ngIf="healthData && enabledFeature$ | async as enabledFeature">
+  <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 && (prometheusAlertService.activeCriticalAlerts || prometheusAlertService.activeWarningAlerts)">
+        <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="prometheusAlertService?.activeCriticalAlerts > 0">
+            <i [ngClass]="[icons.danger]"></i>
+            <span>{{ prometheusAlertService.activeCriticalAlerts }}</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="prometheusAlertService?.activeWarningAlerts > 0">
+            <i [ngClass]="[icons.infoCircle]"></i>
+            <span>{{ prometheusAlertService.activeWarningAlerts }}</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="capacity && osdSettings">
+        <cd-dashboard-pie [data]="{max: capacity.total_bytes, current: capacity.total_used_raw_bytes}"
+                          [lowThreshold]="osdSettings.nearfull_ratio"
+                          [highThreshold]="osdSettings.full_ratio">
+        </cd-dashboard-pie>
+      </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="Cluster 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/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 (file)
index 0000000..140f5f7
--- /dev/null
@@ -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 (file)
index 0000000..677ca35
--- /dev/null
@@ -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<DashboardV3Component>;
+  let configurationService: ConfigurationService;
+  let orchestratorService: MgrModuleService;
+  let getHealthSpy: jasmine.Spy;
+  let getAlertsSpy: jasmine.Spy;
+  let fakeFeatureTogglesService: jasmine.Spy;
+
+  const healthPayload: Record<string, any> = {
+    health: { status: 'HEALTH_OK' },
+    mon_status: { monmap: { mons: [] }, quorum: [] },
+    osd_map: { osds: [] },
+    mgr_map: { standbys: [] },
+    hosts: 0,
+    rgw: 0,
+    fs_map: { filesystems: [], standbys: [] },
+    iscsi_daemons: 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 (file)
index 0000000..1aeb57f
--- /dev/null
@@ -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<Object>;
+  prometheusAlerts$: Observable<AlertmanagerAlert[]>;
+
+  icons = Icons;
+  showAlerts = false;
+  flexHeight = true;
+  simplebar = {
+    autoHide: false
+  };
+  textClass: string;
+  borderClass: string;
+  alertType: string;
+  alerts: AlertmanagerAlert[];
+  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,
+    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 (file)
index 0000000..b467167
--- /dev/null
@@ -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 (file)
index 0000000..a26097e
--- /dev/null
@@ -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<string, number> = {};
+    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
+    };
+  }
+}
index 98f29e571a2c66364b407c05364647e96b5e0dce..81164d15b9d90567ca09d79208ff84c44a7f23c7 100644 (file)
@@ -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,
index feea20883efeb6c6bff69152511170ead290fa3a..87b8c3376dc8114e5cd9f9202d348fe49a2c59d7 100644 (file)
@@ -1,32 +1,15 @@
-<main aria-label="Dashboard" >
+<main aria-label="Dashboard">
   <a href="#main"
      class="sr-only">skip to content</a>
 
-  <cd-refresh-selector></cd-refresh-selector>
+  <ng-container *ngIf="(enabledFeature$ | async)?.dashboard === false; else dashboardV3"
+                class="main-padding">
+    <cd-refresh-selector></cd-refresh-selector>
 
-  <ng-container *ngIf="hasGrafana">
-    <nav ngbNav
-         #nav="ngbNav"
-         class="nav-tabs">
-      <ng-container ngbNavItem>
-        <a ngbNavLink
-           i18n>Health</a>
-        <ng-template ngbNavContent>
-          <cd-health></cd-health>
-        </ng-template>
-      </ng-container>
-      <ng-container ngbNavItem>
-        <a ngbNavLink
-           i18n>Statistics</a>
-        <ng-template ngbNavContent>
-        </ng-template>
-      </ng-container>
-
-    </nav>
-
-    <div [ngbNavOutlet]="nav"></div>
+    <cd-health id="main"></cd-health>
   </ng-container>
 
-  <cd-health id="main"
-             *ngIf="!hasGrafana"></cd-health>
+  <ng-template #dashboardV3>
+    <cd-dashboard-v3></cd-dashboard-v3>
+  </ng-template>
 </main>
index 86a9927129197356693e4d6fde58b7771db3b83a..9c20e4438f11c73048e334ed5b7f1a43fc53e41e 100644 (file)
@@ -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<DeprecatedDashboardComponent>;
+  let component: DashboardComponent;
+  let fixture: ComponentFixture<DashboardComponent>;
 
   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();
   });
index b5d62a62c479733c1c2da6270e19149caed18cea..021c945a2f5897936fa339ebf770ca54e2b30f61 100644 (file)
@@ -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<Object>;
+
+  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 (file)
index 9b7bf03..0000000
+++ /dev/null
@@ -1,167 +0,0 @@
-<div class="d-flex pl-1 pb-2 pt-2">
-  <div class="ms-2 me-auto">
-    <a [routerLink]="link"
-       *ngIf="link && total > 0; else noLinkTitle"
-       [ngPlural]="total"
-       i18n>
-        {{ total }}
-      <ng-template ngPluralCase="=0">{{ title }}</ng-template>
-      <ng-template ngPluralCase="=1">{{ title }}</ng-template>
-      <ng-template ngPluralCase="other">{{ title }}s</ng-template>
-    </a>
-  </div>
-
-  <ng-container [ngSwitch]="summaryType">
-    <ng-container *ngSwitchCase="'iscsi'">
-      <ng-container *ngTemplateOutlet="iscsiSummary"></ng-container>
-    </ng-container>
-    <ng-container *ngSwitchCase="'osd'">
-      <ng-container *ngTemplateOutlet="osdSummary"></ng-container>
-    </ng-container>
-    <ng-container *ngSwitchCase="'simplified'">
-      <ng-container *ngTemplateOutlet="simplifiedSummary"></ng-container>
-    </ng-container>
-    <ng-container *ngSwitchDefault>
-      <ng-container *ngTemplateOutlet="defaultSummary"></ng-container>
-    </ng-container>
-  </ng-container>
-</div>
-
-<ng-template #defaultSummary>
-  <span *ngIf="data.success || data.categoryPgAmount?.clean || (data.success === 0 && data.total === 0)">
-    <span *ngIf="data.success || (data.success === 0 && data.total === 0)">
-      {{ data.success }}
-    </span>
-    <span *ngIf="data.categoryPgAmount?.clean">
-      {{ data.categoryPgAmount?.clean }}
-    </span>
-    <i class="text-success"
-       [ngClass]="[icons.success]">
-    </i>
-  </span>
-  <span *ngIf="data.info"
-        class="ms-2">
-    <span *ngIf="data.info">
-      {{ data.info }}
-    </span>
-    <i class="text-info"
-       [ngClass]="[icons.danger]">
-    </i>
-  </span>
-  <span *ngIf="data.warn || data.categoryPgAmount?.warning"
-        class="ms-2">
-    <span *ngIf="data.warn">
-      {{ data.warn }}
-    </span>
-    <span *ngIf="data.categoryPgAmount?.warning">
-      {{ data.categoryPgAmount?.warning }}
-    </span>
-    <i class="text-warning"
-       [ngClass]="[icons.warning]">
-    </i>
-  </span>
-  <span *ngIf="data.error || data.categoryPgAmount?.unknown"
-        class="ms-2">
-    <span *ngIf="data.error">
-      {{ data.error }}
-    </span>
-    <span *ngIf="data.categoryPgAmount?.unknown">
-      {{ data.categoryPgAmount?.unknown }}
-    </span>
-    <i class="text-danger"
-       [ngClass]="[icons.danger]">
-    </i>
-  </span>
-  <span *ngIf="data.categoryPgAmount?.working"
-        class="ms-2">
-    <span *ngIf="data.categoryPgAmount?.working">
-      {{ data.categoryPgAmount?.working }}
-    </span>
-    <i class="text-warning"
-       [ngClass]="[icons.spinner, icons.spin]">
-    </i>
-  </span>
-</ng-template>
-
-<ng-template #osdSummary>
-  <span *ngIf="data.up === data.in">
-    {{ data.up }}
-    <i class="text-success"
-       [ngClass]="[icons.success]">
-    </i>
-  </span>
-  <span *ngIf="data.up !== data.in">
-    {{ data.up }}
-    <span class="fw-bold text-success">
-        up
-    </span>
-  </span>
-  <span *ngIf="data.in !== data.up"
-        class="ms-2">
-    {{ data.in }}
-    <span class="fw-bold text-success">
-        in
-    </span>
-  </span>
-  <span *ngIf="data.down"
-        class="ms-2">
-    {{ data.down }}
-    <span class="fw-bold text-danger me-2">
-        down
-    </span>
-  </span>
-  <span *ngIf="data.out"
-        class="ms-2">
-    {{ data.out }}
-    <span class="fw-bold text-danger me-2">
-        out
-    </span>
-  </span>
-  <span *ngIf="data.nearfull"
-        class="ms-2">
-        {{ data.nearfull }}
-    <span class="fw-bold text-warning me-2">
-      nearfull</span></span>
-  <span *ngIf="data.full"
-        class="ms-2">
-        {{ data.full }}
-    <span class="fw-bold text-danger">
-      full
-    </span>
-  </span>
-</ng-template>
-
-<ng-template #iscsiSummary>
-  <span>
-    {{ data.up }}
-    <i class="text-success"
-       *ngIf="data.up || data.up === 0"
-       [ngClass]="[icons.success]">
-    </i>
-  </span>
-  <span *ngIf="data.down"
-        class="ms-2">
-        {{ data.down }}
-    <i class="text-danger"
-       [ngClass]="[icons.danger]">
-    </i>
-  </span>
-</ng-template>
-
-<ng-template #simplifiedSummary>
-  <span>
-    {{ data }}
-    <i class="text-success"
-       [ngClass]="[icons.success]"></i>
-  </span>
-</ng-template>
-
-<ng-template #noLinkTitle>
-  <span *ngIf="total || total === 0"
-        [ngPlural]="total">
-    {{ total }}
-    <ng-template ngPluralCase="=0">{{ title }}</ng-template>
-    <ng-template ngPluralCase="=1">{{ title }}</ng-template>
-    <ng-template ngPluralCase="other">{{ title }}s</ng-template>
-  </span>
-</ng-template>
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 (file)
index e69de29..0000000
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 (file)
index 8932e67..0000000
+++ /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<CardRowComponent>;
-
-  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 (file)
index 90c9391..0000000
+++ /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 (file)
index 35dffd4..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<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
deleted file mode 100644 (file)
index fdf19a0..0000000
+++ /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 (file)
index fdc34fd..0000000
+++ /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<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
deleted file mode 100644 (file)
index b6bb99c..0000000
+++ /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 (file)
index 6ac991f..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<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
deleted file mode 100644 (file)
index 12e9b9c..0000000
+++ /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 (file)
index 0501ac7..0000000
+++ /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<DashboardAreaChartComponent>;
-
-  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 (file)
index 3da4334..0000000
+++ /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>): 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 (file)
index ba8176b..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-<div class="chart-container">
-  <canvas baseChart
-          #chartCanvas
-          [datasets]="chartConfig.dataset"
-          [chartType]="chartConfig.chartType"
-          [options]="chartConfig.options"
-          [labels]="chartConfig.labels"
-          [colors]="chartConfig.colors"
-          [plugins]="doughnutChartPlugins"
-          class="chart-canvas">
-  </canvas>
-  <div class="chartjs-tooltip"
-       #chartTooltip>
-    <table></table>
-  </div>
-</div>
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 (file)
index 64e7a98..0000000
+++ /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 (file)
index 892913d..0000000
+++ /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<DashboardPieComponent>;
-
-  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 (file)
index 4aaabb6..0000000
+++ /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<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.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.html
deleted file mode 100644 (file)
index cd960d0..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<div class="timeSelector">
-  <select id="timepicker"
-          name="timepicker"
-          [(ngModel)]="time"
-          (ngModelChange)="emitTime()"
-          class="form-select">
-    <option *ngFor="let key of times"
-            [ngValue]="key.value">{{ key.name }}
-    </option>
-  </select>
-</div>
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 (file)
index 13572dc..0000000
+++ /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 (file)
index 9aeec4d..0000000
+++ /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<DashboardTimeSelectorComponent>;
-
-  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 (file)
index 3b09152..0000000
+++ /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<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
deleted file mode 100644 (file)
index 466da56..0000000
+++ /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 (file)
index bab03a1..0000000
+++ /dev/null
@@ -1,257 +0,0 @@
-<div class="container-fluid"
-     *ngIf="healthData && enabledFeature$ | async as enabledFeature">
-  <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 && (prometheusAlertService.activeCriticalAlerts || prometheusAlertService.activeWarningAlerts)">
-        <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="prometheusAlertService?.activeCriticalAlerts > 0">
-            <i [ngClass]="[icons.danger]"></i>
-            <span>{{ prometheusAlertService.activeCriticalAlerts }}</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="prometheusAlertService?.activeWarningAlerts > 0">
-            <i [ngClass]="[icons.infoCircle]"></i>
-            <span>{{ prometheusAlertService.activeWarningAlerts }}</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="capacity && osdSettings">
-        <cd-dashboard-pie [data]="{max: capacity.total_bytes, current: capacity.total_used_raw_bytes}"
-                          [lowThreshold]="osdSettings.nearfull_ratio"
-                          [highThreshold]="osdSettings.full_ratio">
-        </cd-dashboard-pie>
-      </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="Cluster 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
deleted file mode 100644 (file)
index 140f5f7..0000000
+++ /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 (file)
index fa24090..0000000
+++ /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<DashboardComponent>;
-  let configurationService: ConfigurationService;
-  let orchestratorService: MgrModuleService;
-  let getHealthSpy: jasmine.Spy;
-  let getAlertsSpy: jasmine.Spy;
-  let fakeFeatureTogglesService: jasmine.Spy;
-
-  const healthPayload: Record<string, any> = {
-    health: { status: 'HEALTH_OK' },
-    mon_status: { monmap: { mons: [] }, quorum: [] },
-    osd_map: { osds: [] },
-    mgr_map: { standbys: [] },
-    hosts: 0,
-    rgw: 0,
-    fs_map: { filesystems: [], standbys: [] },
-    iscsi_daemons: 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 (file)
index 8615444..0000000
+++ /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<Object>;
-  prometheusAlerts$: Observable<AlertmanagerAlert[]>;
-
-  icons = Icons;
-  showAlerts = false;
-  flexHeight = true;
-  simplebar = {
-    autoHide: false
-  };
-  textClass: string;
-  borderClass: string;
-  alertType: string;
-  alerts: AlertmanagerAlert[];
-  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,
-    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 (file)
index b467167..0000000
+++ /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 (file)
index a26097e..0000000
+++ /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<string, number> = {};
-    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
-    };
-  }
-}
index fc12d8f49a8e7bf6e030a24337a0da391cee9e62..9cc5b5d1ac1910c6ae0c3bd205072dabf6037ac2 100644 (file)
     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;
index bb7f2a0d6145386a8b2249d99b501a84c542242c..03577681e1a786a6e6d978b2c769677de4daf490 100644 (file)
@@ -12,6 +12,7 @@ export class FeatureTogglesMap {
   cephfs = true;
   rgw = true;
   nfs = true;
+  dashboardV3 = true;
 }
 export type Features = keyof FeatureTogglesMap;
 export type FeatureTogglesMap$ = Observable<FeatureTogglesMap>;
index b780e6fd4cdad0a0f68995f9dcc7b037d970640f..20698c695af7276b13ffc55a09e6d3f90b40ee3a 100644 (file)
@@ -2782,6 +2782,9 @@ paths:
                   cephfs:
                     description: ''
                     type: boolean
+                  dashboard:
+                    description: ''
+                    type: boolean
                   iscsi:
                     description: ''
                     type: boolean
@@ -2804,6 +2807,7 @@ paths:
                 - cephfs
                 - rgw
                 - nfs
+                - dashboard
                 type: object
           description: OK
         '400':
index 4e23e170759141a882d8753ae7f39aea625e9c3e..f16b2e68c4d692c3b06f7d97d97105251ebd2de8 100644 (file)
@@ -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')