@let hwEnabled = enabled$ | async;
@let hwData = hardwareData$ | async;
@let telemetryEnabled = telemetryEnabled$ | async;
+@let pgAlertCount = pgAlertCount$ | async;
@let colorClass = 'overview-health-card-status--' + vm?.clusterHealth?.icon;
<p class="overview-health-card-secondary-text cds--type-label-01">
{{ vm?.resiliencyHealth?.description }}
</p>
- <button
- cdsButton="tertiary"
- size="sm"
- type="button"
- (click)="onViewPGStatesClick()"
+ <div
+ cdsStack="horizontal"
+ [gap]="4"
+ class="cds-mt-4"
>
- <span
- i18n
- class="cds-ml-3"
- >See all PGs states</span
+ @if (pgAlertCount > 0) {
+ <a
+ cdsButton="primary"
+ size="sm"
+ class="overview-health-card-resiliency-btn"
+ [routerLink]="['/monitoring/active-alerts']"
+ >
+ <span i18n>View alerts ({{ pgAlertCount }})</span>
+ <cd-icon type="arrowRight"></cd-icon>
+ </a>
+ }
+ <button
+ cdsButton="tertiary"
+ size="sm"
+ class="overview-health-card-resiliency-btn"
+ type="button"
+ (click)="onViewPGStatesClick()"
>
- <cd-icon type="arrowUpRight"></cd-icon>
- </button>
+ <span i18n>See all PGs states</span>
+ <cd-icon type="arrowUpRight"></cd-icon>
+ </button>
+ </div>
</div>
@if (vm?.pgs?.activeCleanChartData && vm?.pgs?.activeCleanChartOptions) {
<div class="overview-health-card-tab-content-item-row">
<div class="overview-health-card-resiliency-chart-text">
<p
i18n
- class="cds--type-helper-text-01 overview-health-card-secondary-text"
+ class="cds--type-helper-text-01 overview-health-card-secondary-text cds-mt-2"
>
<em>Data resiliency</em> reflects data availability and replication (% of placement
groups that are active and clean).
) {
@if (item.count) {
<p
+ [class.cds-mb-2]="!isLast"
[class.cds-mb-0]="isLast"
- class="cds-mb-2"
>
<span class="cds--type-label-01 cds-mr-1">{{ item?.state }}:</span>
<span class="cds--type-label-01 overview-health-card-bold"
justify-content: space-between;
}
+ &-resiliency-btn {
+ gap: var(--cds-spacing-05);
+ padding-inline: var(--cds-spacing-05);
+ }
+
&-resiliency-tab-content {
display: flex;
}
}
&-resiliency-chart-text {
- width: 13rem;
+ width: 15rem;
margin-left: var(--cds-spacing-04);
}
import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
import { GaugeChartComponent } from '@carbon/charts-angular';
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
type OverviewHealthData = {
summary: Summary;
private readonly mgrModuleService = inject(MgrModuleService);
private readonly refreshIntervalService = inject(RefreshIntervalService);
private readonly authStorageService = inject(AuthStorageService);
+ private readonly prometheusAlertService = inject(PrometheusAlertService);
@Input({ required: true }) vm!: HealthCardVM;
@Output() viewIncidents = new EventEmitter<void>();
shareReplay({ bufferSize: 1, refCount: true })
);
+ readonly pgAlertCount$ = this.prometheusAlertService.pgAlerts$.pipe(startWith(0));
+
readonly telemetryEnabled$: Observable<boolean> = this.healthService.getTelemetryStatus().pipe(
map((enabled: any) => !!enabled),
catchError(() => of(false)),
import VmdkDisk16 from '@carbon/icons/es/vmdk-disk/16';
import WarningAlt16 from '@carbon/icons/es/warning--alt/16';
import CheckMarkOutline16 from '@carbon/icons/es/checkmark--outline/16';
+import ArrowRight16 from '@carbon/icons/es/arrow--right/16';
import ArrowUpRight16 from '@carbon/icons/es/arrow--up-right/16';
import InProgress16 from '@carbon/icons/es/in-progress/16';
import ArrowDown16 from '@carbon/icons/es/arrow--down/16';
VmdkDisk16,
WarningAlt16,
CheckMarkOutline16,
+ ArrowRight16,
ArrowUpRight16,
InProgress16,
ArrowDown16,
vmdkDisk = 'vmdk-disk',
checkMarkOutline = 'checkmark--outline',
warningAlt = 'warning--alt',
+ arrowRight = 'arrow--right',
arrowUpRight = 'arrow--up-right',
inProgress = 'in-progress',
arrowDown = 'arrow--down',
vmdkDisk: 'vmdk-disk',
warningAlt: 'warning--alt',
checkMarkOutline: 'checkmark--outline',
+ arrowRight: 'arrow--right',
arrowUpRight: ' arrow--up-right',
inProgress: 'in-progress',
arrowDown: 'arrow--down',
--- /dev/null
+import { buildHealthCardVM } from './overview';
+import { HealthSnapshotMap } from './health.interface';
+
+function makeSnapshot(checks: Record<string, any> = {}): HealthSnapshotMap {
+ return {
+ fsid: 'test-fsid',
+ health: { status: 'HEALTH_OK', checks, mutes: [] },
+ monmap: { num_mons: 3 },
+ osdmap: { in: 3, up: 3, num_osds: 3 },
+ pgmap: {
+ pgs_by_state: [{ state_name: 'active+clean', count: 100 }],
+ num_pools: 1,
+ bytes_used: 0,
+ bytes_total: 1000,
+ num_pgs: 100,
+ write_bytes_sec: 0,
+ read_bytes_sec: 0,
+ recovering_bytes_per_sec: 0
+ },
+ mgrmap: { num_active: 1, num_standbys: 1 },
+ fsmap: { num_active: 0, num_standbys: 0 },
+ num_rgw_gateways: 0,
+ num_iscsi_gateways: { up: 0, down: 0 },
+ num_hosts: 3
+ } as any;
+}
+
+describe('buildHealthCardVM', () => {
+ it('should not include pgAlertCount in the VM', () => {
+ const vm = buildHealthCardVM(makeSnapshot());
+ expect('pgAlertCount' in vm).toBe(false);
+ });
+
+ it('should build resiliencyHealth from health checks', () => {
+ const vm = buildHealthCardVM(
+ makeSnapshot({
+ PG_DEGRADED: {
+ severity: 'HEALTH_WARN',
+ summary: { message: 'degraded', count: 5 },
+ muted: false
+ }
+ })
+ );
+ expect(vm.resiliencyHealth.severity).toBe('warn');
+ });
+
+ it('should report ok resiliency when no PG checks', () => {
+ const vm = buildHealthCardVM(makeSnapshot());
+ expect(vm.resiliencyHealth.severity).toBe('ok');
+ });
+});
export const ACTIVE_CLEAN_CHART_OPTIONS: GaugeChartOptions = {
resizable: true,
- height: '100px',
- width: '100px',
+ height: '130px',
+ width: '130px',
gauge: { type: 'full' },
toolbar: {
enabled: false
private warningSubject = new BehaviorSubject<number>(0);
readonly warningAlerts$ = this.warningSubject.asObservable();
+ private pgAlertsSubject = new BehaviorSubject<number>(0);
+ readonly pgAlerts$ = this.pgAlertsSubject.asObservable();
+
constructor(
private alertFormatter: PrometheusAlertFormatter,
private prometheusService: PrometheusService
0
);
+ const activePgAlerts = alerts.filter(
+ (alert) =>
+ alert.status.state === AlertState.ACTIVE &&
+ alert.labels.alertname?.startsWith('CephPG')
+ ).length;
+
this.totalSubject.next(this.activeAlerts);
this.criticalSubject.next(this.activeCriticalAlerts);
this.warningSubject.next(this.activeWarningAlerts);
+ this.pgAlertsSubject.next(activePgAlerts);
this.alerts = alerts
.reverse()