]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: carbonize RGW overview dashboard 68986/head
authorAfreen Misbah <afreen@ibm.com>
Tue, 19 May 2026 00:09:47 +0000 (05:39 +0530)
committerAfreen Misbah <afreen@ibm.com>
Tue, 19 May 2026 09:20:49 +0000 (14:50 +0530)
Fixes: https://tracker.ceph.com/issues/76684
Signed-off-by: Afreen Misbah <afreen23.git@gmail.com>
Assisted-by: Claude
14 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/api/performance-card.service.ts

index 69ab46002049f09079065a1d48cc54ce5c5cda5a..60d6331a7d48f4bf9350b819b9a60fffff33a495 100644 (file)
-<div class="container-fluid">
-  <div class="row">
-    <cd-card cardTitle="Inventory"
-             i18n-title
-             class="col-sm-3 px-3 d-flex"
-             aria-label="Inventory card">
-
-      <cd-card-row [data]="rgwDaemonCount"
-                   link="/rgw/daemon"
-                   title="Gateway"
-                   summaryType="simplified"
-                   *ngIf="rgwDaemonCount != null"></cd-card-row>
-
-      <cd-card-row [data]="rgwRealmCount"
-                   link="/rgw/multisite"
-                   title="Realm"
-                   summaryType="simplified"
-                   *ngIf="rgwRealmCount != null"></cd-card-row>
-
-      <cd-card-row [data]="rgwZonegroupCount"
-                   link="/rgw/multisite"
-                   title="Zonegroup"
-                   summaryType="simplified"
-                   *ngIf="rgwZonegroupCount != null"></cd-card-row>
-
-      <cd-card-row [data]="rgwZoneCount"
-                   link="/rgw/multisite"
-                   title="Zone"
-                   summaryType="simplified"
-                   *ngIf="rgwZoneCount != null"></cd-card-row>
-
-      <cd-card-row [data]="rgwBucketCount"
-                   link="/rgw/bucket"
-                   title="Bucket"
-                   summaryType="simplified"
-                   *ngIf="rgwBucketCount != null"></cd-card-row>
-
-      <cd-card-row [data]="UserCount"
-                   link="/rgw/user"
-                   title="User"
-                   summaryType="simplified"
-                   *ngIf="UserCount != null"></cd-card-row>
+<main cdsGrid
+      [fullWidth]="true"
+      [narrow]="true"
+      class="rgw-overview cds-mt-5">
+  <div cdsRow
+       class="cds-mb-5">
+    <div cdsCol
+         [columnNumbers]="{lg: 16}">
+      <cd-productive-card
+        [applyShadow]="false">
+        <ng-template #header>
+          <h2 class="cds--type-heading-compact-02"
+              i18n>Overview</h2>
+        </ng-template>
+        <div class="rgw-overview-grid">
+          <div class="rgw-overview-grid__item">
+            <span class="cds--type-label-01"
+                  i18n>Gateway</span>
+            <a class="cds--link cds--type-body-compact-01"
+               routerLink="/rgw/daemon">{{ rgwDaemonCount }}</a>
+          </div>
+          <div class="rgw-overview-grid__item">
+            <span class="cds--type-label-01"
+                  i18n>Realm</span>
+            <a class="cds--link cds--type-body-compact-01"
+               routerLink="/rgw/multisite">{{ rgwRealmCount }}</a>
+          </div>
+          <div class="rgw-overview-grid__item">
+            <span class="cds--type-label-01"
+                  i18n>Zonegroup</span>
+            <a class="cds--link cds--type-body-compact-01"
+               routerLink="/rgw/multisite">{{ rgwZonegroupCount }}</a>
+          </div>
+          <div class="rgw-overview-grid__item">
+            <span class="cds--type-label-01"
+                  i18n>Zone</span>
+            <a class="cds--link cds--type-body-compact-01"
+               routerLink="/rgw/multisite">{{ rgwZoneCount }}</a>
+          </div>
+          <div class="rgw-overview-grid__item">
+            <span class="cds--type-label-01"
+                  i18n>Bucket</span>
+            <a class="cds--link cds--type-body-compact-01"
+               routerLink="/rgw/bucket">{{ rgwBucketCount }}</a>
+          </div>
+          <div class="rgw-overview-grid__item">
+            <span class="cds--type-label-01"
+                  i18n>User</span>
+            <a class="cds--link cds--type-body-compact-01"
+               routerLink="/rgw/user">{{ UserCount }}</a>
+          </div>
+          <div class="rgw-overview-grid__item">
+            <span class="cds--type-label-01"
+                  i18n>Object</span>
+            <span class="cds--type-body-compact-01">{{ objectCount }}</span>
+          </div>
+          <div class="rgw-overview-grid__item">
+            <span class="cds--type-label-01"
+                  i18n>Used Capacity</span>
+            <span class="cds--type-body-compact-01">{{ totalPoolUsedBytes | dimlessBinary }}</span>
+          </div>
+          <div class="rgw-overview-grid__item">
+            <span class="cds--type-label-01"
+                  i18n>Average Object Size</span>
+            <span class="cds--type-body-compact-01">{{ averageObjectSize | dimlessBinary }}</span>
+          </div>
+        </div>
+      </cd-productive-card>
+    </div>
+  </div>
 
-      <cd-card-row [data]="objectCount"
-                   title="Object"
-                   summaryType="simplified"
-                   *ngIf="objectCount != null"></cd-card-row>
-    </cd-card>
-    <cd-card cardTitle="Performance Statistics"
-             i18n-title
-             class="col-sm-6 d-flex"
-             ria-label="Performance Statistics card">
-      <div class="ms-4 me-4 mt-0">
-        <cd-dashboard-time-selector (selectedTime)="getPrometheusData($event)">
-        </cd-dashboard-time-selector>
-        <cd-dashboard-area-chart chartTitle="Requests/sec"
-                                 dataUnits=""
-                                 [labelsArray]="['Requests/sec']"
-                                 [dataArray]="[queriesResults.RGW_REQUEST_PER_SECOND]">
-        </cd-dashboard-area-chart>
-        <cd-dashboard-area-chart chartTitle="Latency"
-                                 dataUnits="ms"
-                                 decimals="2"
-                                 [labelsArray]="['GET', 'PUT']"
-                                 [dataArray]="[queriesResults.AVG_GET_LATENCY, queriesResults.AVG_PUT_LATENCY]">
-        </cd-dashboard-area-chart>
-        <cd-dashboard-area-chart chartTitle="Bandwidth"
-                                 dataUnits="B"
-                                 [labelsArray]="['GET', 'PUT']"
-                                 [dataArray]="[queriesResults.GET_BANDWIDTH, queriesResults.PUT_BANDWIDTH]">
-        </cd-dashboard-area-chart>
-      </div>
-    </cd-card>
-    <div class="col-lg-3">
-      <cd-card cardTitle="Used Capacity"
-               i18n-title
-               class="col-sm-2 d-flex w-100 h-50 pb-3"
-               aria-label="Used Capacity"
-               [alignItemsCenter]="true"
-               [justifyContentCenter]="true">
-        <span  class="ms-4 me-4 text-center">
-          <h1>{{ totalPoolUsedBytes | dimlessBinary}}</h1>
-        </span>
-      </cd-card>
-      <cd-card cardTitle="Average Object Size"
-               i18n-title
-               class="col-sm-2 d-flex w-100 h-50 pt-3"
-               aria-label="Avg Object Size"
-               [alignItemsCenter]="true"
-               [justifyContentCenter]="true">
-        <span class="ms-4 me-4 text-center">
-          <h1>{{ averageObjectSize | dimlessBinary}}</h1>
-        </span>
-      </cd-card>
+  <div cdsRow
+       class="cds-mb-5">
+    <div cdsCol
+         [columnNumbers]="{lg: 16}">
+      <cd-productive-card
+        [applyShadow]="false">
+        <ng-template #header>
+          <h2 class="cds--type-heading-compact-02"
+              i18n>Performance Statistics</h2>
+          <cd-time-picker
+            [dropdownSize]="'sm'"
+            [label]="'Time Span'"
+            (selectedTime)="getPrometheusData($event)"
+          ></cd-time-picker>
+        </ng-template>
+        <div cdsGrid
+             [narrow]="true"
+             [condensed]="false"
+             [fullWidth]="true">
+          <div cdsRow
+               [narrow]="true">
+            <div cdsCol
+                 class="cds-mb-5"
+                 [columnNumbers]="{lg: 5, md: 8, sm: 12}">
+              <cd-area-chart
+                chartTitle="Requests/sec"
+                i18n-chartTitle
+                [dataUnit]="''"
+                [rawData]="requestsChartData">
+              </cd-area-chart>
+            </div>
+            <div cdsCol
+                 class="cds-mb-5"
+                 [columnNumbers]="{lg: 5, md: 8, sm: 12}">
+              <cd-area-chart
+                chartTitle="Latency"
+                i18n-chartTitle
+                [dataUnit]="'ms'"
+                [decimals]="2"
+                [rawData]="latencyChartData">
+              </cd-area-chart>
+            </div>
+            <div cdsCol
+                 class="cds-mb-5"
+                 [columnNumbers]="{lg: 5, md: 8, sm: 12}">
+              <cd-area-chart
+                chartTitle="Bandwidth"
+                i18n-chartTitle
+                [dataUnit]="'B'"
+                [rawData]="bandwidthChartData">
+              </cd-area-chart>
+            </div>
+          </div>
+        </div>
+      </cd-productive-card>
     </div>
   </div>
 
-  <div class="row pt-4 pb-4">
-    <cd-card cardTitle="Multi-Site Sync Status"
-             i18n-title>
-      <ng-template #notConfigured>
-        <span class="pe-5 ps-5">
+  <div cdsRow>
+    <div cdsCol
+         [columnNumbers]="{lg: 16}">
+      <cd-productive-card
+        [applyShadow]="false">
+        <ng-template #header>
+          <h2 class="cds--type-heading-compact-02"
+              i18n>Multi-Site Sync Status</h2>
+        </ng-template>
+        <ng-template #notConfigured>
           <cd-alert-panel type="info"
                           i18n>
             Multi-site needs to be configured in order to see the multi-site sync status.
             Please consult the&nbsp;<cd-doc section="multisite"></cd-doc>&nbsp;on how to configure and enable the multi-site functionality.
           </cd-alert-panel>
-        </span>
-      </ng-template>
-      <span *ngIf="loading"
-            class="d-flex justify-content-center">
-        <i [ngClass]="[icons.large3x, icons.spinner, icons.spin]"></i>
-      </span>
-      <div class="row"
-           *ngIf="multisiteSyncStatus$ | async">
-        <div class="row pt-2"
-             *ngIf="showMultisiteCard; else notConfigured">
-          <cd-card cardTitle="Primary Source Zone"
-                   class="col-lg-3 d-flex justify-content-center align-primary-zone"
-                   [alignItemsCenter]="true"
-                   [justifyContentCenter]="true">
-            <span *ngIf="loading"
-                  class="d-flex justify-content-center">
-              <i [ngClass]="[icons.large3x, icons.spinner, icons.spin]"></i>
-            </span>
-            <span *ngIf="!loading"
-                  class="d-flex justify-content-center">
-              <cd-rgw-sync-primary-zone [realm]="realm"
-                                        [zonegroup]="zonegroup"
-                                        [zone]="zone">
-              </cd-rgw-sync-primary-zone>
-            </span>
-          </cd-card>
-          <div class="col-lg-9">
-            <cd-card cardTitle="Source Zones"
-                     class="d-flex h-100">
-              <span *ngIf="loading"
-                    class="d-flex justify-content-center">
-                <i [ngClass]="[icons.large3x, icons.spinner, icons.spin]"></i>
-              </span>
-              <div class="row"
-                   *ngIf="!loading">
-                <cd-card *ngFor="let zone of replicaZonesInfo; trackBy: trackByFn"
-                         cardTitle="{{zone.name}}"
-                         cardType="zone"
-                         shadowClass="true"
-                         i18n-title
-                         class="col-sm-9 col-lg-6 align-replica-zones d-flex pt-4"
-                         aria-label="Source Zones Card">
-                  <div class="row pb-4 ps-3 pe-3">
-                    <cd-card *ngFor="let title of chartTitles"
-                             [cardTitle]="title"
-                             i18n-title
-                             cardType="syncCards"
-                             removeBorder="true"
-                             class="col-sm-9 col-lg-6"
-                             [ngClass]="{ 'border-left': title === 'Data Sync' }"
-                             aria-label="Charts Card"
-                             [alignItemsCenter]="true"
-                             [justifyContentCenter]="true">
-                      <span class="me-2 text-center"
-                            *ngIf="title === 'Metadata Sync'">
-                        <cd-rgw-sync-metadata-info [metadataSyncInfo]="metadataSyncInfo">
-                        </cd-rgw-sync-metadata-info>
-                      </span>
-                      <span class="me-2"
-                            *ngIf="title === 'Data Sync'">
-                        <cd-rgw-sync-data-info [zone]="zone">
-                        </cd-rgw-sync-data-info>
-                      </span>
-                    </cd-card>
+        </ng-template>
+        @if (loading) {
+          <div class="multisite-sync__loading">
+            <cds-loading [isActive]="true"
+                         size="sm"></cds-loading>
+          </div>
+        }
+        @if (multisiteSyncStatus$ | async) {
+          @if (showMultisiteCard) {
+            <div class="multisite-sync__layout">
+              <div class="multisite-sync__primary">
+                <h4 class="cds--type-heading-compact-01 multisite-sync__section-title"
+                    i18n>Primary Source Zone</h4>
+                <div class="multisite-sync__primary-content">
+                  @if (loading) {
+                    <div class="multisite-sync__loading">
+                      <cds-loading [isActive]="true"
+                                   size="sm"></cds-loading>
+                    </div>
+                  } @else {
+                    <cd-rgw-sync-primary-zone [realm]="realm"
+                                              [zonegroup]="zonegroup"
+                                              [zone]="zone">
+                    </cd-rgw-sync-primary-zone>
+                  }
+                </div>
+              </div>
+              <div class="multisite-sync__source-zones">
+                <h4 class="cds--type-heading-compact-01 multisite-sync__section-title multisite-sync__section-title--centered"
+                    i18n>Source Zones</h4>
+                @if (loading) {
+                  <div class="multisite-sync__loading">
+                    <cds-loading [isActive]="true"
+                                 size="sm"></cds-loading>
+                  </div>
+                } @else {
+                  <div class="multisite-sync__zones-grid">
+                    @for (zone of replicaZonesInfo; track zone) {
+                      <cds-tile class="multisite-sync__zone-tile">
+                        <h5 class="multisite-sync__zone-header">
+                          <svg [cdsIcon]="icons.deploy"
+                               [size]="icons.size20"></svg>
+                          <cds-tag class="tag-info">{{ zone.name }}</cds-tag>
+                        </h5>
+                        <div class="multisite-sync__sync-columns">
+                          @for (title of chartTitles; track title) {
+                            <div class="multisite-sync__sync-item"
+                                 [class.multisite-sync__sync-item--bordered]="title === 'Data Sync'">
+                              <span class="cds--type-body-compact-01 multisite-sync__sync-title">{{ title }}</span>
+                              @if (title === 'Metadata Sync') {
+                                <cd-rgw-sync-metadata-info [metadataSyncInfo]="metadataSyncInfo">
+                                </cd-rgw-sync-metadata-info>
+                              }
+                              @if (title === 'Data Sync') {
+                                <cd-rgw-sync-data-info [zone]="zone">
+                                </cd-rgw-sync-data-info>
+                              }
+                            </div>
+                          }
+                        </div>
+                      </cds-tile>
+                    }
                   </div>
-                </cd-card>
+                }
               </div>
-            </cd-card>
-          </div>
-        </div>
-      </div>
-    </cd-card>
+            </div>
+          } @else {
+            <ng-container *ngTemplateOutlet="notConfigured"></ng-container>
+          }
+        }
+      </cd-productive-card>
+    </div>
   </div>
-</div>
+</main>
 
index b735edde21f176fb5ea1900bc43339818995f7fe..c9f980ae4b4788930ed1d0ebc59541b6fa1e1795 100644 (file)
@@ -1,19 +1,89 @@
-@use './src/styles/vendor/variables' as vv;
+.rgw-overview-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+  row-gap: var(--cds-spacing-06);
+  column-gap: var(--cds-spacing-07);
 
-hr {
-  margin-bottom: 2px;
-  margin-top: 2px;
+  &__item {
+    display: flex;
+    flex-direction: column;
+    gap: var(--cds-spacing-02);
+  }
 }
 
-.list-group-item {
-  border: 0;
-}
+.multisite-sync {
+  &__loading {
+    display: flex;
+    justify-content: center;
+    padding: var(--cds-spacing-05);
+  }
+
+  &__layout {
+    display: flex;
+    align-items: flex-start;
+  }
+
+  &__primary {
+    flex: 0 0 250px;
+    text-align: center;
+  }
+
+  &__primary-content {
+    border-inline-end: 1px solid var(--cds-border-subtle);
+    padding-inline-end: var(--cds-spacing-05);
+  }
+
+  &__section-title {
+    margin-block-end: var(--cds-spacing-05);
+
+    &--centered {
+      text-align: center;
+    }
+  }
+
+  &__source-zones {
+    flex: 1;
+    padding-inline-start: var(--cds-spacing-05);
+  }
+
+  &__zones-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+    gap: var(--cds-spacing-05);
+  }
+
+  &__zone-tile {
+    height: 100%;
+    padding-block-start: 0;
+  }
 
-.align-replica-zones {
-  margin-left: auto;
-  margin-right: auto;
-  padding-left: 2em;
-  padding-right: 2em;
+  &__zone-header {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: var(--cds-spacing-02);
+    margin-block-end: var(--cds-spacing-04);
+  }
+
+  &__sync-columns {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+  }
+
+  &__sync-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+
+    &--bordered {
+      border-inline-start: 1px solid var(--cds-border-subtle);
+    }
+  }
+
+  &__sync-title {
+    text-align: center;
+    margin-block-end: var(--cds-spacing-03);
+  }
 }
 
 ul {
@@ -22,11 +92,3 @@ ul {
   flex-direction: column;
   list-style-type: none;
 }
-
-.align-primary-zone {
-  padding-left: 4em;
-}
-
-.border-left {
-  border-left: 1px solid vv.$chart-color-border;
-}
index 941eabb426f9f2419f8f7a6ba106320d2f99a34f..3fde88d9c12a61109a579ebe1a69954a27883172 100644 (file)
@@ -119,8 +119,10 @@ describe('RgwOverviewDashboardComponent', () => {
   });
 
   it('should render all cards', () => {
-    const dashboardCards = fixture.debugElement.nativeElement.querySelectorAll('cd-card');
-    expect(dashboardCards.length).toBe(5);
+    const productiveCards = fixture.debugElement.nativeElement.querySelectorAll(
+      'cd-productive-card'
+    );
+    expect(productiveCards.length).toBe(3);
   });
 
   it('should get data for Realms', () => {
@@ -138,6 +140,62 @@ describe('RgwOverviewDashboardComponent', () => {
     expect(component.rgwZoneCount).toEqual(4);
   });
 
+  it('should transform prometheus data to chart format', () => {
+    const mockResults: Record<string, [number, string][]> = {
+      RGW_REQUEST_PER_SECOND: [
+        [1700000000, '10'],
+        [1700000060, '20']
+      ],
+      AVG_GET_LATENCY: [
+        [1700000000, '1.5'],
+        [1700000060, '2.0']
+      ],
+      AVG_PUT_LATENCY: [
+        [1700000000, '3.0'],
+        [1700000060, '4.0']
+      ],
+      GET_BANDWIDTH: [
+        [1700000000, '1024'],
+        [1700000060, '2048']
+      ],
+      PUT_BANDWIDTH: [
+        [1700000000, '512'],
+        [1700000060, '768']
+      ]
+    };
+
+    const perfService = component['performanceCardService'];
+    component['getPrometheusData'] = function (_selectedTime: any) {
+      this.queriesResults = mockResults;
+      this.requestsChartData = perfService.toSeries(
+        mockResults.RGW_REQUEST_PER_SECOND,
+        'Requests/sec'
+      );
+      this.latencyChartData = perfService.mergeSeries(
+        perfService.toSeries(mockResults.AVG_GET_LATENCY, 'GET'),
+        perfService.toSeries(mockResults.AVG_PUT_LATENCY, 'PUT')
+      );
+      this.bandwidthChartData = perfService.mergeSeries(
+        perfService.toSeries(mockResults.GET_BANDWIDTH, 'GET'),
+        perfService.toSeries(mockResults.PUT_BANDWIDTH, 'PUT')
+      );
+    };
+
+    component['getPrometheusData']({});
+
+    expect(component.requestsChartData.length).toBe(2);
+    expect(component.requestsChartData[0].values['Requests/sec']).toBe(10);
+    expect(component.requestsChartData[0].timestamp).toEqual(new Date(1700000000 * 1000));
+
+    expect(component.latencyChartData.length).toBe(2);
+    expect(component.latencyChartData[0].values['GET']).toBe(1.5);
+    expect(component.latencyChartData[0].values['PUT']).toBe(3.0);
+
+    expect(component.bandwidthChartData.length).toBe(2);
+    expect(component.bandwidthChartData[0].values['GET']).toBe(1024);
+    expect(component.bandwidthChartData[0].values['PUT']).toBe(512);
+  });
+
   it('should set component properties from services using combineLatest', fakeAsync(() => {
     component.interval = of(null).subscribe(() => {
       component.fetchDataSub = combineLatest([
index 61f93c9fda13c4c065db365281c0adabf9de3bae..3ab594b37c4c9fc325dd5228e7cefa4951657f4e 100644 (file)
@@ -16,9 +16,11 @@ import { PrometheusService } from '~/app/shared/api/prometheus.service';
 import { RgwPromqls as queries } from '~/app/shared/enum/dashboard-promqls.enum';
 import { Icons } from '~/app/shared/enum/icons.enum';
 import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
+import { ChartPoint } from '~/app/shared/models/area-chart-point';
 import { catchError, shareReplay, switchMap, takeUntil, tap } from 'rxjs/operators';
 import { NotificationService } from '~/app/shared/services/notification.service';
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { PerformanceCardService } from '~/app/shared/api/performance-card.service';
 
 @Component({
   selector: 'cd-rgw-overview-dashboard',
@@ -51,6 +53,9 @@ export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
     AVG_GET_LATENCY: [],
     AVG_PUT_LATENCY: []
   };
+  requestsChartData: ChartPoint[] = [];
+  latencyChartData: ChartPoint[] = [];
+  bandwidthChartData: ChartPoint[] = [];
   timerGetPrometheusDataSub: Subscription;
   chartTitles = ['Metadata Sync', 'Data Sync'];
   realm: string;
@@ -76,7 +81,8 @@ export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
     private rgwBucketService: RgwBucketService,
     private prometheusService: PrometheusService,
     private rgwMultisiteService: RgwMultisiteService,
-    private notificationService: NotificationService
+    private notificationService: NotificationService,
+    private performanceCardService: PerformanceCardService
   ) {
     this.permissions = this.authStorageService.getPermissions();
   }
@@ -158,6 +164,18 @@ export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
       .pipe(takeUntil(this.destroy$))
       .subscribe((results) => {
         this.queriesResults = results;
+        this.requestsChartData = this.performanceCardService.toSeries(
+          results?.RGW_REQUEST_PER_SECOND || [],
+          $localize`Requests/sec`
+        );
+        this.latencyChartData = this.performanceCardService.mergeSeries(
+          this.performanceCardService.toSeries(results?.AVG_GET_LATENCY || [], 'GET'),
+          this.performanceCardService.toSeries(results?.AVG_PUT_LATENCY || [], 'PUT')
+        );
+        this.bandwidthChartData = this.performanceCardService.mergeSeries(
+          this.performanceCardService.toSeries(results?.GET_BANDWIDTH || [], 'GET'),
+          this.performanceCardService.toSeries(results?.PUT_BANDWIDTH || [], 'PUT')
+        );
       });
   }
 
index bef3e5c67b6f95454381b4a191fd2061d386cb90..2e2db07843cac8f2b1560d9d87035e38b5909f47 100644 (file)
@@ -1,58 +1,63 @@
-<ul class="me-2">
-  <ng-template #upToDateTpl>
-    <li i18n>
-      <cds-tag class="tag-success">Up to Date</cds-tag>
-    </li>
-  </ng-template>
-  <cds-toggletip  [dropShadow]="true"
-                  [align]="align">
-    <div  cdsToggletipButton
-          class="toggleTipBtn">
-    <ng-template #showStatus>
-      <a  *ngIf="zone.syncstatus !== 'Not Syncing From Zone'"
-          class="lead text-primary"
-          i18n>{{ zone.syncstatus | titlecase }}</a>
-      <a  *ngIf="zone.syncstatus === 'Not Syncing From Zone'"
-          class="lead text-primary"
-          i18n>Not Syncing</a>
-    </ng-template>
-    <li><b>Status:</b></li>
-    <li *ngIf="zone.syncstatus?.includes('failed') || zone.syncstatus?.includes('error'); else showStatus">
-      <svg  [cdsIcon]="icons.danger"
-            [size]="icons.size16"
-            class="cds-danger-color"></svg>
-      <a  class="lead text-danger"
-          i18n>Error</a>
-    </li>
-  </div>
-  <div cdsToggletipContent>
-    <div class="cds--popover-scroll-container">
-      <ul class="text-center">
-        <li><h5><b i18n>Sync Status:</b></h5></li>
-        <li *ngFor="let status of zone.fullSyncStatus">
-          <span *ngIf="!status?.includes(zone.name) && !status?.includes(zone.syncstatus) && !status?.includes('failed') && !status?.includes('error')">
-            <span *ngIf="status?.includes(':')">
-              <b>{{ status.split(': ')[0] | titlecase }}</b>:{{ status.split(': ')[1] | titlecase}}
-            </span>
-            <span *ngIf="!status?.includes(':')">
-              <b>{{ status | titlecase }}</b>
-            </span>
-          </span>
-          <span *ngIf="status?.includes('failed') || status?.includes('error')">
-            {{ status | titlecase }}
-          </span>
-        </li>
-      </ul>
+<div class="sync-info">
+  <cds-toggletip [dropShadow]="true"
+                 [align]="align">
+    <div cdsToggletipButton
+         class="toggleTipBtn">
+      <span class="cds--type-label-01" i18n>Status:</span>
+      @if (zone.syncstatus?.includes('failed') || zone.syncstatus?.includes('error')) {
+        <span class="sync-info__status">
+          <svg [cdsIcon]="icons.danger"
+               [size]="icons.size16"
+               class="cds-danger-color"></svg>
+          <a class="cds--link cds--type-label-01 cds-danger-color sync-status-link"
+             i18n>Error</a>
+        </span>
+      } @else {
+        @if (zone.syncstatus === 'Not Syncing From Zone') {
+          <a class="cds--link cds--type-label-01 sync-status-link"
+             i18n>Not Syncing</a>
+        } @else {
+          <a class="cds--link cds--type-label-01 sync-status-link"
+             i18n>{{ zone.syncstatus | titlecase }}</a>
+        }
+      }
+    </div>
+    <div cdsToggletipContent>
+      <div class="cds--popover-scroll-container">
+        <div class="toggletip-content">
+            @for (status of zone.fullSyncStatus; track status) {
+              @if (status?.includes('failed') || status?.includes('error')) {
+                <div class="toggletip-content__item">
+                  <span class="cds--type-body-compact-01 cds-danger-color">{{ status | titlecase }}</span>
+                </div>
+              } @else if (!status?.includes(zone.name) && !status?.includes(zone.syncstatus) && status?.trim()) {
+                @if (status?.includes(':')) {
+                  <div class="toggletip-content__item">
+                    <span class="cds--type-label-01">{{ status.split(': ')[0] | titlecase }}</span>
+                    <span class="cds--type-body-compact-01">{{ status.split(': ')[1] | titlecase }}</span>
+                  </div>
+                } @else {
+                  <div class="toggletip-content__item">
+                    <span class="cds--type-label-01" i18n>Summary</span>
+                    <span class="cds--type-body-compact-01">{{ status | titlecase }}</span>
+                  </div>
+                }
+              }
+            }
+        </div>
+      </div>
     </div>
-  </div>
   </cds-toggletip>
-  <li class="mt-4 fw-bold"
-      i18n>
-      Last Synced:
-  </li>
-  <li *ngIf="zone.timestamp; else upToDateTpl">
-    <cds-tag class="tag-info">
-      {{ zone.timestamp | relativeDate }}
-    </cds-tag>
-  </li>
-</ul>
+  <div class="sync-info__last-synced cds-mt-3">
+    <span class="cds--type-label-01" i18n>Last Synced:</span>
+    @if (zone.timestamp) {
+      <cds-tag class="tag-info" size="sm">
+        {{ zone.timestamp | relativeDate }}
+      </cds-tag>
+    } @else {
+      <cds-tag class="tag-success"
+               size="sm"
+               i18n>Up to Date</cds-tag>
+    }
+  </div>
+</div>
index dbb6a0cedf1acc740332287ad3a9d2f97a893440..1988b39b0bffa67125636d6367e6226049b89fe2 100644 (file)
@@ -1,13 +1,42 @@
 @use './src/styles/vendor/variables' as vv;
 
-ul {
-  align-items: center;
+.sync-info {
   display: flex;
   flex-direction: column;
-  list-style-type: none;
+  align-items: center;
+
+  &__status {
+    display: flex;
+    align-items: center;
+    gap: var(--cds-spacing-02);
+  }
+
+  &__last-synced {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+  }
 }
 
 .toggleTipBtn {
   flex-direction: column;
   text-align: center;
 }
+
+.toggletip-content {
+  display: flex;
+  flex-direction: column;
+  gap: var(--cds-spacing-04);
+
+  &__item {
+    display: flex;
+    flex-direction: column;
+    gap: var(--cds-spacing-01);
+  }
+}
+
+.sync-status-link {
+  text-decoration: underline dotted;
+  text-underline-offset: 3px;
+  cursor: pointer;
+}
index 94e886f148319d19e9d3f3b43f8bb668e6a49f4c..0614d778446d4571428472cf68c35a8acb09bd51 100644 (file)
@@ -39,10 +39,10 @@ describe('RgwSyncDataInfoComponent', () => {
     component.zone = { syncstatus: 'Syncing', timestamp: new Date(Date.now() - 10 * 60 * 1000) };
     fixture.detectChanges();
 
-    const statusElement = fixture.debugElement.query(By.css('li b'));
+    const statusElement = fixture.debugElement.query(By.css('.cds--type-label-01'));
     expect(statusElement.nativeElement.textContent).toContain('Status:');
 
-    const lastSyncedElement = fixture.debugElement.query(By.css('li.mt-4.fw-bold'));
+    const lastSyncedElement = fixture.debugElement.query(By.css('.sync-info__last-synced'));
     expect(lastSyncedElement.nativeElement.textContent).toContain('Last Synced:');
     const lastSyncedTimestamp = fixture.debugElement.query(By.css('cds-tag.tag-info'));
     expect(lastSyncedTimestamp.nativeElement.textContent.trim()).toEqual('10 minutes ago');
@@ -59,16 +59,16 @@ describe('RgwSyncDataInfoComponent', () => {
       ]
     };
     fixture.detectChanges();
-    const syncStatus = fixture.debugElement.query(By.css('.text-primary'));
+    const syncStatus = fixture.debugElement.query(By.css('.cds--link'));
     expect(syncStatus).toBeTruthy();
     expect(syncStatus.nativeElement.textContent).toEqual('Syncing');
     const syncPopover = fixture.nativeElement.querySelector('a');
     syncPopover.dispatchEvent(new Event('click'));
     fixture.detectChanges();
     expect(syncPopover).toBeTruthy();
-    const syncPopoverText = fixture.debugElement.query(By.css('.text-center'));
-    expect(syncPopoverText.nativeElement.textContent).toEqual(
-      'Sync Status:Full Sync:0/128 Shards Incremental Sync:128/128 Shards: Data Is Behind On 31 Shards'
-    );
+    const syncPopoverText = fixture.debugElement.query(By.css('.toggletip-content'));
+    expect(syncPopoverText.nativeElement.textContent).toContain('Full Sync');
+    expect(syncPopoverText.nativeElement.textContent).toContain('0/128 Shards');
+    expect(syncPopoverText.nativeElement.textContent).toContain('Data Is Behind On 31 Shards');
   });
 });
index 6f923e9eff06625c8205233c9e1bb43fa3ea27c5..6db4caf3b37b9c436662d9f830237b971daebf6b 100644 (file)
@@ -1,69 +1,71 @@
-<span *ngIf="metadataSyncInfo === 'no sync (zone is master)'">
-  <ul class="me-2">
-    <li><b i18n>Status:</b></li>
-    <li>No Sync</li>
-  </ul>
-</span>
-<span *ngIf="metadataSyncInfo !== 'no sync (zone is master)'">
-  <ul class="me-2">
-    <ng-template #upToDateTpl>
-      <li i18n>
-        <cds-tag class="tag-success">
-          Up to Date
-        </cds-tag>
-      </li>
-    </ng-template>
-    <cds-toggletip  [dropShadow]="true"
-                    [align]="align">
+@if (metadataSyncInfo === 'no sync (zone is master)') {
+  <div class="sync-info">
+    <span class="cds--type-label-01" i18n>Status:</span>
+    <span class="cds--type-label-01"
+          style="color: var(--cds-text-secondary)">No Sync</span>
+  </div>
+} @else {
+  <div class="sync-info">
+    <cds-toggletip [dropShadow]="true"
+                   [align]="align">
       <div cdsToggletipButton
            class="toggleTipBtn">
-        <ng-template #showMetadataStatus>
-          <a  *ngIf="metadataSyncInfo.syncstatus !== 'Not Syncing From Zone'"
-              class="lead text-primary"
-              popoverClass="rgw-overview-card-popover"
-              i18n>{{ metadataSyncInfo.syncstatus | titlecase }}</a>
-          <a  *ngIf="metadataSyncInfo.syncstatus === 'Not Syncing From Zone'"
-              class="lead text-primary"
-              i18n>Not Syncing</a>
-        </ng-template>
-        <li><b i18n>Status:</b></li>
-        <li *ngIf="metadataSyncInfo.syncstatus?.includes('failed') || metadataSyncInfo.syncstatus?.includes('error'); else showMetadataStatus">
-          <svg  [cdsIcon]="icons.danger"
-                [size]="icons.size16"
-                class="cds-danger-color"></svg>
-          <a class="lead text-danger"
-             i18n>Error</a>
-        </li>
+        <span class="cds--type-label-01" i18n>Status:</span>
+        @if (metadataSyncInfo.syncstatus?.includes('failed') || metadataSyncInfo.syncstatus?.includes('error')) {
+          <span class="sync-info__status">
+            <svg [cdsIcon]="icons.danger"
+                 [size]="icons.size16"
+                 class="cds-danger-color"></svg>
+            <a class="cds--link cds--type-label-01 cds-danger-color sync-status-link"
+               i18n>Error</a>
+          </span>
+        } @else {
+          @if (metadataSyncInfo.syncstatus === 'Not Syncing From Zone') {
+            <a class="cds--link cds--type-label-01 sync-status-link"
+               i18n>Not Syncing</a>
+          } @else {
+            <a class="cds--link cds--type-label-01 sync-status-link"
+               i18n>{{ metadataSyncInfo.syncstatus | titlecase }}</a>
+          }
+        }
       </div>
       <div cdsToggletipContent>
         <div class="cds--popover-scroll-container">
-          <ul class="text-center">
-            <li><h5><b i18n>Metadata Sync Status:</b></h5></li>
-            <li *ngFor="let status of metadataSyncInfo.fullSyncStatus">
-              <span *ngIf="!status?.includes(metadataSyncInfo.syncstatus) && !status?.includes('failed') && !status?.includes('error')">
-                <span *ngIf="status?.includes(':')">
-                  <b>{{ status.split(':')[0] | titlecase }}</b>:{{ status.split(':')[1] | titlecase}}
-                </span>
-                <span *ngIf="!status?.includes(':')">
-                  <b>{{ status | titlecase }}</b>
-                </span>
-              </span>
-              <span *ngIf="status?.includes('failed') || status?.includes('error')">
-                  {{ status | titlecase }}
-              </span>
-            </li>
-          </ul>
+          <div class="toggletip-content">
+              @for (status of metadataSyncInfo.fullSyncStatus; track status; let last = $last) {
+                @if (status?.includes('failed') || status?.includes('error')) {
+                  <div class="toggletip-content__item">
+                    <span class="cds--type-body-compact-01 cds-danger-color">{{ status | titlecase }}</span>
+                  </div>
+                } @else if (!status?.includes(metadataSyncInfo.syncstatus) && status?.trim()) {
+                  @if (status?.includes(':')) {
+                    <div class="toggletip-content__item">
+                      <span class="cds--type-label-01">{{ status.split(':')[0] | titlecase }}</span>
+                      <span class="cds--type-body-compact-01">{{ status.split(':')[1] | titlecase }}</span>
+                    </div>
+                  } @else {
+                    <div class="toggletip-content__item">
+                      <span class="cds--type-label-01" i18n>Summary</span>
+                      <span class="cds--type-body-compact-01">{{ status | titlecase }}</span>
+                    </div>
+                  }
+                }
+              }
+          </div>
         </div>
       </div>
     </cds-toggletip>
-    <li class="mt-4 fw-bold"
-        i18n>
-        Last Synced:
-    </li>
-    <li *ngIf="metadataSyncInfo.timestamp; else upToDateTpl">
-      <cds-tag class="tag-info">
-        {{ metadataSyncInfo.timestamp | relativeDate }}
-      </cds-tag>
-    </li>
-  </ul>
-</span>
+    <div class="sync-info__last-synced cds-mt-3">
+      <span class="cds--type-label-01" i18n>Last Synced:</span>
+      @if (metadataSyncInfo.timestamp) {
+        <cds-tag class="tag-info" size="sm">
+          {{ metadataSyncInfo.timestamp | relativeDate }}
+        </cds-tag>
+      } @else {
+        <cds-tag class="tag-success"
+                 size="sm"
+                 i18n>Up to Date</cds-tag>
+      }
+    </div>
+  </div>
+}
index 91dc6e03739497ffd762f9aea09e39e59ea09056..225afd402f5777c80a59bab739e72bc1dbb590de 100644 (file)
@@ -1,12 +1,41 @@
 @use './src/styles/vendor/variables' as vv;
 
-ul {
-  align-items: center;
+.sync-info {
   display: flex;
   flex-direction: column;
-  list-style-type: none;
+  align-items: center;
+
+  &__status {
+    display: flex;
+    align-items: center;
+    gap: var(--cds-spacing-02);
+  }
+
+  &__last-synced {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+  }
 }
 
 .toggleTipBtn {
   flex-direction: column;
 }
+
+.toggletip-content {
+  display: flex;
+  flex-direction: column;
+  gap: var(--cds-spacing-04);
+
+  &__item {
+    display: flex;
+    flex-direction: column;
+    gap: var(--cds-spacing-01);
+  }
+}
+
+.sync-status-link {
+  text-decoration: underline dotted;
+  text-underline-offset: 3px;
+  cursor: pointer;
+}
index 1b4155a01a05ce95ca9e44fdb41a335392134f8a..ec6e9a38081b0880e642441abb0ff6b5f4a635c3 100644 (file)
@@ -43,10 +43,10 @@ describe('RgwSyncMetadataInfoComponent', () => {
     };
     fixture.detectChanges();
 
-    const statusElement = fixture.debugElement.query(By.css('li b'));
+    const statusElement = fixture.debugElement.query(By.css('.cds--type-label-01'));
     expect(statusElement.nativeElement.textContent).toContain('Status:');
 
-    const lastSyncedElement = fixture.debugElement.query(By.css('li.mt-4.fw-bold'));
+    const lastSyncedElement = fixture.debugElement.query(By.css('.sync-info__last-synced'));
     expect(lastSyncedElement.nativeElement.textContent).toContain('Last Synced:');
     const lastSyncedTimestamp = fixture.debugElement.query(By.css('cds-tag'));
     expect(lastSyncedTimestamp.nativeElement.classList).toContain('tag-info');
@@ -64,16 +64,16 @@ describe('RgwSyncMetadataInfoComponent', () => {
       ]
     };
     fixture.detectChanges();
-    const syncStatus = fixture.debugElement.query(By.css('.text-primary'));
+    const syncStatus = fixture.debugElement.query(By.css('.cds--link'));
     expect(syncStatus).toBeTruthy();
     expect(syncStatus.nativeElement.textContent).toEqual('Syncing');
     const syncPopover = fixture.nativeElement.querySelector('a');
     syncPopover.dispatchEvent(new Event('click'));
     fixture.detectChanges();
     expect(syncPopover).toBeTruthy();
-    const syncPopoverText = fixture.debugElement.query(By.css('.text-center'));
-    expect(syncPopoverText.nativeElement.textContent).toEqual(
-      'Metadata Sync Status:Full Sync:0/128 Shards Incremental Sync:128/128 Shards Data Is Behind On 31 Shards'
-    );
+    const syncPopoverText = fixture.debugElement.query(By.css('.toggletip-content'));
+    expect(syncPopoverText.nativeElement.textContent).toContain('Full Sync');
+    expect(syncPopoverText.nativeElement.textContent).toContain('0/128 Shards');
+    expect(syncPopoverText.nativeElement.textContent).toContain('Data Is Behind On 31 Shards');
   });
 });
index dd1f0c9d2b2b4a78306332a69541d8d848f01423..9a5e465d3ee8a9e902356efddf3dfc9885b55c5a 100644 (file)
@@ -85,7 +85,8 @@ import {
   ToggletipModule,
   IconService,
   LayoutModule,
-  SkeletonModule
+  SkeletonModule,
+  TilesModule
 } from 'carbon-components-angular';
 import EditIcon from '@carbon/icons/es/edit/16';
 import ScalesIcon from '@carbon/icons/es/scales/20';
@@ -100,6 +101,9 @@ import ToolsIcon from '@carbon/icons/es/tools/32';
 import ParentChild from '@carbon/icons/es/parent-child/20';
 import UserAccessLocked from '@carbon/icons/es/user--access-locked/16';
 
+import { ProductiveCardComponent } from '~/app/shared/components/productive-card/productive-card.component';
+import { TimePickerComponent } from '~/app/shared/components/time-picker/time-picker.component';
+import { AreaChartComponent } from '~/app/shared/components/area-chart/area-chart.component';
 import { CephSharedModule } from '../shared/ceph-shared.module';
 import { RgwUserAccountsComponent } from './rgw-user-accounts/rgw-user-accounts.component';
 import { RgwUserAccountsFormComponent } from './rgw-user-accounts-form/rgw-user-accounts-form.component';
@@ -154,7 +158,11 @@ import { RgwNotificationFormComponent } from './rgw-notification-form/rgw-notifi
     RadioModule,
     SelectModule,
     LayoutModule,
-    SkeletonModule
+    SkeletonModule,
+    TilesModule,
+    ProductiveCardComponent,
+    TimePickerComponent,
+    AreaChartComponent
   ],
   exports: [
     RgwDaemonDetailsComponent,
index aeafc41c44708c93296d706ae5c23541495134f9..95d610d26d3a7360095780f48cb9c7154c57bf77 100644 (file)
@@ -6,7 +6,7 @@
       <!-- ALERTS BANNER     -->
       <!-- ************************ -->
       <div class="cd-alert-container"
-           [ngClass]="{'ms-4 me-4': (router.url == '/overview' || router.url == '/dashboard_3' || router.url == '/multi-cluster/overview'), 'm-3': (router.url == '/rgw/overview')}">
+           [ngClass]="{'ms-4 me-4': (router.url == '/overview' || router.url == '/dashboard_3' || router.url == '/multi-cluster/overview' || router.url == '/rgw/overview')}">
         @if (enabledFeature$ | async; as features) {
         <cd-pwd-expiration-notification></cd-pwd-expiration-notification>
         <cd-motd></cd-motd>
index 5fe0558f2db3de505dd61554c8ddae0cee9afcbe..7eae41be97ccac2e3f8c523ef42a752495fe32ca 100644 (file)
@@ -1,8 +1,13 @@
 @use './src/styles/vendor/variables' as vv;
 
-.overview {
+.overview,
+.rgw-dashboard {
   margin: 0;
   padding: 0;
+
+  .breadcrumbs--padding {
+    padding-left: var(--cds-spacing-07);
+  }
 }
 
 .container-fluid {
index 41f01496b3a28c101d3b47b79706b6bbe2a2ffe9..0d5e409c06a9ae502d7a0ba30396cf237d029973 100644 (file)
@@ -4,6 +4,7 @@ import { PerformanceData } from '../models/performance-data';
 import { AllStoragetypesQueries } from '../enum/dashboard-promqls.enum';
 import { map } from 'rxjs/operators';
 import { Observable } from 'rxjs';
+import { ChartPoint } from '../models/area-chart-point';
 
 @Injectable({
   providedIn: 'root'
@@ -50,15 +51,15 @@ export class PerformanceCardService {
     };
   }
 
-  private toSeries(metric: [number, string][], label: string) {
+  toSeries(metric: [number, string][], label: string): ChartPoint[] {
     return metric.map(([ts, val]) => ({
       timestamp: new Date(ts * 1000),
       values: { [label]: Number(val) }
     }));
   }
 
-  private mergeSeries(...series: any[]) {
-    const map = new Map<number, any>();
+  mergeSeries(...series: ChartPoint[][]): ChartPoint[] {
+    const map = new Map<number, ChartPoint>();
 
     for (const items of series) {
       for (const item of items) {
@@ -75,6 +76,6 @@ export class PerformanceCardService {
       }
     }
 
-    return [...map.values()].sort((a, b) => a.timestamp - b.timestamp);
+    return [...map.values()].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
   }
 }