-<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 <cd-doc section="multisite"></cd-doc> 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>
-@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 {
flex-direction: column;
list-style-type: none;
}
-
-.align-primary-zone {
- padding-left: 4em;
-}
-
-.border-left {
- border-left: 1px solid vv.$chart-color-border;
-}
});
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', () => {
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([
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',
AVG_GET_LATENCY: [],
AVG_PUT_LATENCY: []
};
+ requestsChartData: ChartPoint[] = [];
+ latencyChartData: ChartPoint[] = [];
+ bandwidthChartData: ChartPoint[] = [];
timerGetPrometheusDataSub: Subscription;
chartTitles = ['Metadata Sync', 'Data Sync'];
realm: string;
private rgwBucketService: RgwBucketService,
private prometheusService: PrometheusService,
private rgwMultisiteService: RgwMultisiteService,
- private notificationService: NotificationService
+ private notificationService: NotificationService,
+ private performanceCardService: PerformanceCardService
) {
this.permissions = this.authStorageService.getPermissions();
}
.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')
+ );
});
}
-<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>
@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;
+}
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');
]
};
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');
});
});
-<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>
+}
@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;
+}
};
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');
]
};
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');
});
});
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';
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';
RadioModule,
SelectModule,
LayoutModule,
- SkeletonModule
+ SkeletonModule,
+ TilesModule,
+ ProductiveCardComponent,
+ TimePickerComponent,
+ AreaChartComponent
],
exports: [
RgwDaemonDetailsComponent,
<!-- 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>
@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 {
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'
};
}
- 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) {
}
}
- return [...map.values()].sort((a, b) => a.timestamp - b.timestamp);
+ return [...map.values()].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
}
}