Status Monitoring
blockdevopsUptime page. Banner derives All systems operational / Degraded performance / Partial outage from the newest day of each service. Per-service rows render a 90-segment day bar (success/warning/destructive/info/muted per status) with hover tooltips and a right-aligned uptime percentage, followed by an incident timeline with severity badges. Pass `services` (name + days array of up/degraded/down/maintenance/none) and `incidents` to replace the stubs.
Installation
$pnpm dlx uipkge-ng@latest add status-monitoring$npx uipkge-ng@latest add status-monitoring$yarn dlx uipkge-ng@latest add status-monitoring$bunx uipkge-ng@latest add status-monitoringManifest:
https://uipkge.dev/r/angular/status-monitoring.jsonInstalls to:src/app/components/blocks/status-monitoring/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ServiceStatusinterface ServiceStatus {
name: string
/** One entry per day, oldest first. Expected length: 90. */
days: DayStatus[]
}MonitoringIncidentinterface MonitoringIncident {
id: string
date: Date
title: string
description: string
duration: string
severity: IncidentSeverity
resolved?: boolean
}npm dependencies
Includes
Files installed (2)
src/app/components/blocks/status-monitoring/status-monitoring.component.ts10.7 kB
// Uptime page. Banner derives All systems operational / Degraded performance / // Partial outage from the newest day of each service. Per-service rows render a // 90-segment day bar with hover tooltips and a right-aligned uptime percentage, // followed by an incident timeline with severity badges. // Port of the Vue block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiSectionCardComponent } from '@/ui/section-card/section-card.component' export type DayStatus = 'up' | 'degraded' | 'down' | 'maintenance' | 'none' export type IncidentSeverity = 'minor' | 'major' | 'critical' export interface ServiceStatus { name: string /** One entry per day, oldest first. Expected length: 90. */ days: DayStatus[] } export interface MonitoringIncident { id: string date: Date title: string description: string duration: string severity: IncidentSeverity resolved?: boolean } export type OverallState = 'operational' | 'degraded' | 'outage' const DAY_COUNT = 90 // Deterministic stubs (no Math.random) so SSR and client markup match exactly. function buildDays(blips: Array<{ ago: number; length: number; status: DayStatus }> = []): DayStatus[] { const days: DayStatus[] = Array.from({ length: DAY_COUNT }, () => 'up') for (const blip of blips) { for (let k = 0; k < blip.length; k++) { const idx = DAY_COUNT - 1 - blip.ago - k if (idx >= 0 && idx < DAY_COUNT) days[idx] = blip.status } } return days } const now = new Date() const daysAgoDate = (d: number) => new Date(now.getTime() - d * 86_400_000) const stubServices: ServiceStatus[] = [ { name: 'API Gateway', days: buildDays([ { ago: 34, length: 1, status: 'down' }, { ago: 33, length: 1, status: 'degraded' }, ]), }, { name: 'Web application', days: buildDays() }, { name: 'Webhooks', days: buildDays([{ ago: 61, length: 1, status: 'degraded' }]) }, ] const stubIncidents: MonitoringIncident[] = [ { id: 'i1', date: daysAgoDate(33), title: 'Elevated error rates on API Gateway', description: 'A bad deploy caused intermittent 502s for roughly forty minutes. Rolled back; error rates recovered.', duration: 'Lasted 42 minutes', severity: 'major', resolved: true, }, { id: 'i2', date: daysAgoDate(34), title: 'API Gateway unavailable', description: 'Provider network maintenance dropped traffic between 02:14 and 02:51 UTC.', duration: 'Lasted 37 minutes', severity: 'critical', resolved: true, }, { id: 'i3', date: daysAgoDate(61), title: 'Delayed webhook deliveries', description: 'A queue backlog delayed deliveries by up to six minutes. The backlog drained on its own.', duration: 'Lasted 1 hour, 12 minutes', severity: 'minor', resolved: true, }, ] const bannerMeta: Record<OverallState, { label: string; box: string; text: string; dot: string }> = { operational: { label: 'All systems operational', box: 'border-success/25 bg-success/10', text: 'text-success', dot: 'bg-success', }, degraded: { label: 'Degraded performance', box: 'border-warning/25 bg-warning/10', text: 'text-warning', dot: 'bg-warning', }, outage: { label: 'Partial outage', box: 'border-destructive/25 bg-destructive/10', text: 'text-destructive', dot: 'bg-destructive', }, } const dayClass: Record<DayStatus, string> = { up: 'bg-success', degraded: 'bg-warning', down: 'bg-destructive', maintenance: 'bg-info', none: 'bg-muted', } const dayLabel: Record<DayStatus, string> = { up: 'Operational', degraded: 'Degraded', down: 'Outage', maintenance: 'Maintenance', none: 'No data', } const severityVariant: Record<IncidentSeverity, 'info' | 'warning' | 'destructive'> = { minor: 'info', major: 'warning', critical: 'destructive', } // Built once per module load so 90 segments × N services never rebuild Date objects during render. const dayStamps = Array.from({ length: DAY_COUNT }, (_, i) => daysAgoDate(DAY_COUNT - 1 - i).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }), ) const updatedStamp = now.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-status-monitoring, [ui-status-monitoring]', standalone: true, // Vue renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [UiBadgeComponent, UiSectionCardComponent], template: ` <div data-slot="status-monitoring" [class]="rootClass"> <div role="status" [class]="bannerClass"> <span class="relative flex size-2.5 shrink-0"> <span [class]="bannerDotPingClass"></span> <span [class]="bannerDotClass"></span> </span> <p [class]="bannerTextClass">{{ banner().label }}</p> <span class="text-muted-foreground ml-auto text-xs whitespace-nowrap">Updated {{ updatedStamp }}</span> </div> <ui-section-card title="Service status" description="Daily availability over the last 90 days."> <ul slot="header-action" class="text-muted-foreground hidden flex-wrap items-center gap-3 text-xs md:flex"> @for (item of legend; track item.status) { <li class="flex shrink-0 items-center gap-1.5 whitespace-nowrap"> <span [class]="legendDotClass(item.status)"></span> {{ item.label }} </li> } </ul> <ul class="divide-y"> @for (s of servicesList(); track s.name; let si = $index) { <li [class]="serviceRowClass(si)"> <div class="flex flex-wrap items-baseline justify-between gap-4"> <p class="text-sm font-medium">{{ s.name }}</p> <p class="text-muted-foreground text-sm tabular-nums">{{ uptimePct(s.days) }}</p> </div> <div class="flex w-full gap-px overflow-hidden sm:gap-[2px]" role="img" [attr.aria-label]="s.name + ' uptime ' + uptimePct(s.days) + ' over the last 90 days'" > @for (d of s.days; track $index; let di = $index) { <span [class]="daySegmentClass(d)" [attr.title]="tooltipFor(s.days, di)"></span> } </div> </li> } </ul> <p class="text-muted-foreground mt-4 flex justify-between text-xs"> <span>90 days ago</span> <span>Today</span> </p> </ui-section-card> <ui-section-card title="Incident history" description="Reported incidents over the same period."> @if (incidentList().length) { <ul class="divide-y"> @for (inc of incidentList(); track inc.id; let ii = $index) { <li [class]="ii === 0 ? 'py-4 pt-0' : 'py-4'"> <p class="text-muted-foreground text-xs font-medium">{{ formatDay(inc.date) }}</p> <div class="mt-1.5 flex flex-wrap items-center gap-2"> <span ui-badge [variant]="incidentBadgeVariant(inc)" class="capitalize"> {{ inc.resolved ? 'Resolved' : inc.severity }} </span> <p class="text-sm font-medium">{{ inc.title }}</p> <span class="text-muted-foreground ml-auto text-xs whitespace-nowrap">{{ inc.duration }}</span> </div> <p class="text-muted-foreground mt-1 text-sm leading-relaxed">{{ inc.description }}</p> </li> } </ul> } @else { <div class="flex flex-col items-center justify-center px-6 py-10 text-center"> <p class="text-sm font-medium">No incidents reported</p> <p class="text-muted-foreground mt-0.5 text-xs">Every day in this window has been quiet.</p> </div> } </ui-section-card> </div> `, }) export class UiStatusMonitoringComponent { protected readonly legend = (Object.keys(dayLabel) as DayStatus[]).map((status) => ({ status, label: dayLabel[status], })) protected readonly updatedStamp = updatedStamp @Input() set services(v: ServiceStatus[] | undefined) { this.servicesInput.set(v) } @Input() set incidents(v: MonitoringIncident[] | undefined) { this.incidentsInput.set(v) } @Input('class') className?: string private readonly servicesInput = signal<ServiceStatus[] | undefined>(undefined) private readonly incidentsInput = signal<MonitoringIncident[] | undefined>(undefined) readonly servicesList = computed(() => this.servicesInput() ?? stubServices) readonly incidentList = computed(() => [...(this.incidentsInput() ?? stubIncidents)].sort((a, b) => b.date.getTime() - a.date.getTime()), ) readonly overall = computed<OverallState>(() => { const latest = this.servicesList().map((s) => [...s.days].reverse().find((d) => d !== 'none')) if (latest.includes('down')) return 'outage' if (latest.includes('degraded')) return 'degraded' return 'operational' }) readonly banner = computed(() => bannerMeta[this.overall()]) get rootClass(): string { return cn('space-y-4', this.className) } get bannerClass(): string { return cn('flex items-center gap-3 rounded-xl border px-4 py-3', this.banner().box) } get bannerTextClass(): string { return cn('text-sm font-medium', this.banner().text) } get bannerDotClass(): string { return cn('relative inline-flex size-2.5 rounded-full', this.banner().dot) } get bannerDotPingClass(): string { return cn('absolute inline-flex h-full w-full rounded-full opacity-60', this.banner().dot) } legendDotClass(status: DayStatus): string { return cn('size-2 rounded-full', dayClass[status]) } daySegmentClass(status: DayStatus): string { return cn('h-6 w-full min-w-px flex-1 rounded-sm transition-transform hover:scale-y-125', dayClass[status]) } serviceRowClass(index: number): string { return cn('space-y-2 py-4', index === 0 && 'pt-0') } tooltipFor(days: DayStatus[], i: number): string { return `${dayStamps[i]} · ${dayLabel[days[i]]}` } uptimePct(days: DayStatus[]): string { const counted = days.filter((d) => d !== 'none') if (counted.length === 0) return '—' const up = counted.filter((d) => d === 'up').length return `${((up / counted.length) * 100).toFixed(2)}%` } formatDay(d: Date): string { return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) } incidentBadgeVariant(inc: MonitoringIncident): 'success' | 'info' | 'warning' | 'destructive' { if (inc.resolved) return 'success' return severityVariant[inc.severity] } }src/app/components/blocks/status-monitoring/index.ts0.2 kB
export { UiStatusMonitoringComponent, type DayStatus, type IncidentSeverity, type ServiceStatus, type MonitoringIncident, type OverallState, } from './status-monitoring.component'
Raw manifest:https://uipkge.dev/r/angular/status-monitoring.json