mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
feat(alerts): severity-first sorting, severity badges, threshold context
- Sort active alerts by severity (critical before warning) then recency - Add explicit Critical/Warning text badge on each alert card - Show 'limit: 90%' annotation for threshold-based alerts - Add critical count breakdown to stats card Makes alerts more scannable and actionable at a glance.
This commit is contained in:
parent
6c5139c16d
commit
dcce40706e
3 changed files with 44 additions and 11 deletions
|
|
@ -67,6 +67,19 @@ export function AlertOverviewAlertCard(props: AlertOverviewAlertCardProps) {
|
|||
{props.alert.resourceName}
|
||||
</span>
|
||||
<span class="text-xs text-muted">({alertTypeDisplayLabel(props.alert.type)})</span>
|
||||
<Show when={!props.alert.acknowledged}>
|
||||
<span
|
||||
class="shrink-0 rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
|
||||
classList={{
|
||||
'bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300':
|
||||
props.alert.level === 'critical',
|
||||
'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300':
|
||||
props.alert.level !== 'critical',
|
||||
}}
|
||||
>
|
||||
{props.alert.level === 'critical' ? 'Critical' : 'Warning'}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={props.alert.node}>
|
||||
<span class="text-xs text-muted">
|
||||
{getAlertOverviewNodeLabel(props.alert.nodeDisplayName || props.alert.node)}
|
||||
|
|
@ -79,9 +92,19 @@ export function AlertOverviewAlertCard(props: AlertOverviewAlertCardProps) {
|
|||
</Show>
|
||||
</div>
|
||||
<p class="text-sm text-base-content mt-1 break-words">{props.alert.message}</p>
|
||||
<p class={getAlertOverviewStartedAtClass()}>
|
||||
{getAlertOverviewStartedAtLabel(new Date(props.alert.startTime).toLocaleString())}
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-0.5 mt-1">
|
||||
<p class={getAlertOverviewStartedAtClass()}>
|
||||
{getAlertOverviewStartedAtLabel(new Date(props.alert.startTime).toLocaleString())}
|
||||
</p>
|
||||
<Show when={props.alert.threshold > 0}>
|
||||
<span class="text-xs text-muted">
|
||||
limit: {props.alert.threshold}
|
||||
{props.alert.type === 'temperature' || props.alert.type === 'diskTemperature'
|
||||
? '°C'
|
||||
: '%'}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1.5 sm:gap-2 mt-3 sm:mt-0 sm:ml-4 self-end sm:self-start justify-end">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Show } from 'solid-js';
|
||||
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { Table, TableBody, TableCell, TableRow } from '@/components/shared/Table';
|
||||
|
|
@ -43,6 +45,11 @@ export function AlertOverviewStatsCards(props: AlertOverviewStatsCardsProps) {
|
|||
<TableCell class={labelCellClass}>{labels().last24Hours}</TableCell>
|
||||
<TableCell class={valueCellClass} data-testid="alert-overview-stat-value">
|
||||
{props.state.alertStats().total24h}
|
||||
<Show when={props.state.alertStats().critical24h > 0}>
|
||||
<span class="ml-1.5 text-[10px] font-normal text-red-600 dark:text-red-400">
|
||||
{props.state.alertStats().critical24h} critical
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
|
|
|
|||
|
|
@ -35,15 +35,17 @@ export function useAlertOverviewState(props: UseAlertOverviewStateProps) {
|
|||
|
||||
const alertStats = createMemo(() => {
|
||||
const alerts = effectiveAlerts();
|
||||
const recent = alerts.filter((alert) => {
|
||||
const ts = new Date(alert.startTime).getTime();
|
||||
if (Number.isNaN(ts)) return true;
|
||||
const age = tick() - ts;
|
||||
return age >= 0 && age < 86_400_000;
|
||||
});
|
||||
return {
|
||||
active: alerts.filter((alert) => !alert.acknowledged).length,
|
||||
acknowledged: alerts.filter((alert) => alert.acknowledged).length,
|
||||
total24h: alerts.filter((alert) => {
|
||||
const ts = new Date(alert.startTime).getTime();
|
||||
if (Number.isNaN(ts)) return true;
|
||||
const age = tick() - ts;
|
||||
return age >= 0 && age < 86_400_000;
|
||||
}).length,
|
||||
total24h: recent.length,
|
||||
critical24h: recent.filter((alert) => alert.level === 'critical').length,
|
||||
overrides: props.overrides().length,
|
||||
};
|
||||
});
|
||||
|
|
@ -55,10 +57,11 @@ export function useAlertOverviewState(props: UseAlertOverviewStateProps) {
|
|||
if (a.acknowledged !== b.acknowledged) {
|
||||
return a.acknowledged ? 1 : -1;
|
||||
}
|
||||
const severityRank = (level: string) => (level === 'critical' ? 0 : 1);
|
||||
const severityDiff = severityRank(a.level) - severityRank(b.level);
|
||||
if (severityDiff !== 0) return severityDiff;
|
||||
const timeDiff = new Date(b.startTime).getTime() - new Date(a.startTime).getTime();
|
||||
if (timeDiff !== 0) return timeDiff;
|
||||
// Stable id tiebreaker: alerts fired in the same polling cycle share a
|
||||
// startTime and would otherwise scramble visually on re-render (#1218).
|
||||
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue