mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 18:44:53 +00:00
feat(alerts): clean alert messages + highlight all platform node tables
Backend:
- Map internal resource types to human-readable labels in alert messages
('agent-disk disk at 95.8%' -> 'Disk at 95.8%')
- Fix redundant Unraid message ('running check' -> 'check in progress')
Frontend:
- Add alert row highlighting to Docker, Kubernetes, TrueNAS, and vSphere
host/system tables (same pattern as the Proxmox nodes table fix)
This commit is contained in:
parent
5fc30e2060
commit
fa5a874986
10 changed files with 131 additions and 11 deletions
|
|
@ -7,6 +7,9 @@ import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
|
|||
import { StackedDiskBar } from '@/components/Workloads/StackedDiskBar';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { getAlertStyles } from '@/utils/alerts';
|
||||
import { useWebSocket } from '@/contexts/appRuntime';
|
||||
import { useAlertsActivation } from '@/stores/alertsActivation';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import { normalizeDiskArray } from '@/utils/format';
|
||||
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
|
||||
|
|
@ -86,6 +89,9 @@ export const DockerHostsTable: Component<{
|
|||
title?: string;
|
||||
showToolbar?: boolean;
|
||||
}> = (props) => {
|
||||
const { activeAlerts } = useWebSocket();
|
||||
const alertsActivation = useAlertsActivation();
|
||||
const alertsEnabled = createMemo(() => alertsActivation.activationState() === 'active');
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.resources,
|
||||
initialStatus: 'all' as DockerResourceStatusFilter,
|
||||
|
|
@ -250,11 +256,21 @@ export const DockerHostsTable: Component<{
|
|||
event.preventDefault();
|
||||
toggleDrawer();
|
||||
};
|
||||
const hostAlertStyles = createMemo(() =>
|
||||
getAlertStyles(host.id, activeAlerts, alertsEnabled(), name()),
|
||||
);
|
||||
const hostAlertBg = () => {
|
||||
const s = hostAlertStyles();
|
||||
if (!s.hasUnacknowledgedAlert) return '';
|
||||
return s.severity === 'critical'
|
||||
? 'bg-red-50 dark:bg-red-950'
|
||||
: 'bg-yellow-50 dark:bg-yellow-950';
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<TableRow
|
||||
class={`cursor-pointer text-[11px] outline-none sm:text-xs ${
|
||||
isSelected() ? 'bg-surface-hover' : ''
|
||||
isSelected() ? 'bg-surface-hover' : hostAlertBg()
|
||||
} focus-visible:ring-2 focus-visible:ring-blue-500/60 focus-visible:ring-offset-1 focus-visible:ring-offset-surface`}
|
||||
aria-controls={isSelected() ? detailRowId() : undefined}
|
||||
aria-expanded={isSelected() ? 'true' : 'false'}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ vi.mock('@/components/shared/responsive', () => ({
|
|||
),
|
||||
}));
|
||||
|
||||
vi.mock('@/contexts/appRuntime', () => ({
|
||||
useWebSocket: () => ({ activeAlerts: {} as Record<string, never> }),
|
||||
}));
|
||||
vi.mock('@/stores/alertsActivation', () => ({
|
||||
useAlertsActivation: () => ({ activationState: () => 'active' }),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Workloads/StackedMemoryBar', () => ({
|
||||
StackedMemoryBar: (props: { used: number; total: number; percentOnly?: number }) => (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { ResponsiveMetricCell } from '@/components/shared/responsive';
|
|||
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import { getAlertStyles } from '@/utils/alerts';
|
||||
import { useWebSocket } from '@/contexts/appRuntime';
|
||||
import { useAlertsActivation } from '@/stores/alertsActivation';
|
||||
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
|
|
@ -60,6 +63,9 @@ export const KubernetesNodesTable: Component<{
|
|||
title?: string;
|
||||
showToolbar?: boolean;
|
||||
}> = (props) => {
|
||||
const { activeAlerts } = useWebSocket();
|
||||
const alertsActivation = useAlertsActivation();
|
||||
const alertsEnabled = createMemo(() => alertsActivation.activationState() === 'active');
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.resources,
|
||||
initialStatus: 'all' as KubernetesResourceStatusFilter,
|
||||
|
|
@ -202,10 +208,20 @@ export const KubernetesNodesTable: Component<{
|
|||
const canRenderMetrics = () => indicator().variant !== 'muted';
|
||||
const detailRowId = () => drawer.detailRowId(node);
|
||||
const isExpanded = () => drawer.isExpanded(node);
|
||||
const nodeAlertStyles = createMemo(() =>
|
||||
getAlertStyles(node.id, activeAlerts, alertsEnabled(), name()),
|
||||
);
|
||||
const nodeAlertBg = () => {
|
||||
const s = nodeAlertStyles();
|
||||
if (!s.hasUnacknowledgedAlert) return '';
|
||||
return s.severity === 'critical'
|
||||
? 'bg-red-50 dark:bg-red-950'
|
||||
: 'bg-yellow-50 dark:bg-yellow-950';
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<TableRow
|
||||
class={`${getPlatformResourceDetailRowClass(isExpanded())} text-[11px] sm:text-xs`}
|
||||
class={`${getPlatformResourceDetailRowClass(isExpanded())} text-[11px] sm:text-xs ${nodeAlertBg()}`}
|
||||
aria-controls={isExpanded() ? detailRowId() : undefined}
|
||||
aria-expanded={isExpanded() ? 'true' : 'false'}
|
||||
data-kubernetes-node-row={node.id}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,16 @@ vi.mock('@/components/Workloads/StackedDiskBar', () => ({
|
|||
StackedDiskBar: () => <div data-testid="stacked-disk-bar" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/contexts/appRuntime', () => ({
|
||||
useWebSocket: () => ({ activeAlerts: {} as Record<string, never> }),
|
||||
}));
|
||||
vi.mock('@/stores/alertsActivation', () => ({
|
||||
useAlertsActivation: () => ({
|
||||
activationState: () => 'active',
|
||||
getMetricThresholds: () => ({}),
|
||||
}),
|
||||
}));
|
||||
|
||||
const makeResource = ({
|
||||
id,
|
||||
type,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { ResponsiveMetricCell } from '@/components/shared/responsive';
|
|||
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { getAlertStyles } from '@/utils/alerts';
|
||||
import { useWebSocket } from '@/contexts/appRuntime';
|
||||
import { useAlertsActivation } from '@/stores/alertsActivation';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
|
||||
import {
|
||||
|
|
@ -86,6 +89,9 @@ export const TrueNASSystemsTable: Component<{
|
|||
title?: string;
|
||||
showToolbar?: boolean;
|
||||
}> = (props) => {
|
||||
const { activeAlerts } = useWebSocket();
|
||||
const alertsActivation = useAlertsActivation();
|
||||
const alertsEnabled = createMemo(() => alertsActivation.activationState() === 'active');
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.systems,
|
||||
initialStatus: 'all' as PlatformResourceStatusFilter,
|
||||
|
|
@ -225,10 +231,20 @@ export const TrueNASSystemsTable: Component<{
|
|||
const canRenderMetrics = () => indicator().variant !== 'muted';
|
||||
const detailRowId = () => drawer.detailRowId(system);
|
||||
const isExpanded = () => drawer.isExpanded(system);
|
||||
const sysAlertStyles = createMemo(() =>
|
||||
getAlertStyles(system.id, activeAlerts, alertsEnabled(), name()),
|
||||
);
|
||||
const sysAlertBg = () => {
|
||||
const s = sysAlertStyles();
|
||||
if (!s.hasUnacknowledgedAlert) return '';
|
||||
return s.severity === 'critical'
|
||||
? 'bg-red-50 dark:bg-red-950'
|
||||
: 'bg-yellow-50 dark:bg-yellow-950';
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<TableRow
|
||||
class={`${getPlatformResourceDetailRowClass(isExpanded())} text-[11px] sm:text-xs`}
|
||||
class={`${getPlatformResourceDetailRowClass(isExpanded())} text-[11px] sm:text-xs ${sysAlertBg()}`}
|
||||
aria-controls={isExpanded() ? detailRowId() : undefined}
|
||||
aria-expanded={isExpanded() ? 'true' : 'false'}
|
||||
data-truenas-system-row={system.id}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ vi.mock('@/components/shared/responsive', () => ({
|
|||
ResponsiveMetricCell: () => <div data-testid="responsive-metric-cell" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/contexts/appRuntime', () => ({
|
||||
useWebSocket: () => ({ activeAlerts: {} as Record<string, never> }),
|
||||
}));
|
||||
vi.mock('@/stores/alertsActivation', () => ({
|
||||
useAlertsActivation: () => ({ activationState: () => 'active' }),
|
||||
}));
|
||||
|
||||
import { TrueNASSystemsTable } from '@/features/truenas/TrueNASSystemsTable';
|
||||
import type { Resource } from '@/types/resource';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { ResponsiveMetricCell } from '@/components/shared/responsive';
|
|||
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { getAlertStyles } from '@/utils/alerts';
|
||||
import { useWebSocket } from '@/contexts/appRuntime';
|
||||
import { useAlertsActivation } from '@/stores/alertsActivation';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import { formatVmwareClusterServices } from '@/utils/vmwareDisplay';
|
||||
import {
|
||||
|
|
@ -58,6 +61,9 @@ export const VsphereHostsTable: Component<{
|
|||
title?: string;
|
||||
showToolbar?: boolean;
|
||||
}> = (props) => {
|
||||
const { activeAlerts } = useWebSocket();
|
||||
const alertsActivation = useAlertsActivation();
|
||||
const alertsEnabled = createMemo(() => alertsActivation.activationState() === 'active');
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.hosts,
|
||||
initialStatus: 'all' as PlatformResourceStatusFilter,
|
||||
|
|
@ -217,10 +223,20 @@ export const VsphereHostsTable: Component<{
|
|||
const canRenderMetrics = () => indicator().variant !== 'muted';
|
||||
const detailRowId = () => drawer.detailRowId(host);
|
||||
const isExpanded = () => drawer.isExpanded(host);
|
||||
const hostAlertStyles = createMemo(() =>
|
||||
getAlertStyles(host.id, activeAlerts, alertsEnabled(), name()),
|
||||
);
|
||||
const hostAlertBg = () => {
|
||||
const s = hostAlertStyles();
|
||||
if (!s.hasUnacknowledgedAlert) return '';
|
||||
return s.severity === 'critical'
|
||||
? 'bg-red-50 dark:bg-red-950'
|
||||
: 'bg-yellow-50 dark:bg-yellow-950';
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<TableRow
|
||||
class={`${getPlatformResourceDetailRowClass(isExpanded())} text-[11px] sm:text-xs`}
|
||||
class={`${getPlatformResourceDetailRowClass(isExpanded())} text-[11px] sm:text-xs ${hostAlertBg()}`}
|
||||
aria-controls={isExpanded() ? detailRowId() : undefined}
|
||||
aria-expanded={isExpanded() ? 'true' : 'false'}
|
||||
data-vsphere-host-row={host.id}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ vi.mock('@/components/shared/responsive', () => ({
|
|||
ResponsiveMetricCell: () => <div data-testid="responsive-metric-cell" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/contexts/appRuntime', () => ({
|
||||
useWebSocket: () => ({ activeAlerts: {} as Record<string, never> }),
|
||||
}));
|
||||
vi.mock('@/stores/alertsActivation', () => ({
|
||||
useAlertsActivation: () => ({ activationState: () => 'active' }),
|
||||
}));
|
||||
|
||||
import { VsphereHostsTable } from '@/features/vmware/VsphereHostsTable';
|
||||
import type { Resource } from '@/types/resource';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package alerts
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
alertspecs "github.com/rcourtman/pulse-go-rewrite/internal/alerts/specs"
|
||||
|
|
@ -44,20 +45,44 @@ func metricClearThreshold(spec *alertspecs.MetricThresholdSpec, threshold *Hyste
|
|||
return 0
|
||||
}
|
||||
|
||||
func resourceTypeLabel(resourceType string) string {
|
||||
switch strings.TrimSpace(resourceType) {
|
||||
case "agent-disk":
|
||||
return "Disk"
|
||||
case "agent":
|
||||
return "Agent"
|
||||
case "node":
|
||||
return "Node"
|
||||
case "guest":
|
||||
return "Guest"
|
||||
case "storage":
|
||||
return "Storage"
|
||||
case "pbs":
|
||||
return "PBS"
|
||||
case "pmg":
|
||||
return "PMG"
|
||||
case "":
|
||||
return "Resource"
|
||||
default:
|
||||
return resourceType
|
||||
}
|
||||
}
|
||||
|
||||
func metricAlertMessage(resourceType, metricType string, value float64, opts *metricOptions) (string, string) {
|
||||
if opts != nil && opts.Message != "" {
|
||||
return opts.Message, ""
|
||||
}
|
||||
|
||||
label := resourceTypeLabel(resourceType)
|
||||
switch metricType {
|
||||
case "usage":
|
||||
return resourceType + " at " + formatMetricValue(value, "%"), "%"
|
||||
case "usage", "disk":
|
||||
return label + " at " + formatMetricValue(value, "%"), "%"
|
||||
case "diskRead", "diskWrite", "networkIn", "networkOut":
|
||||
return resourceType + " " + metricType + " at " + formatMetricValue(value, " MB/s"), "MB/s"
|
||||
return label + " " + metricType + " at " + formatMetricValue(value, " MB/s"), "MB/s"
|
||||
case "temperature", "disk_temperature", "diskTemperature":
|
||||
return resourceType + " " + metricType + " at " + formatMetricValue(value, "°C"), "°C"
|
||||
return label + " " + metricType + " at " + formatMetricValue(value, "°C"), "°C"
|
||||
default:
|
||||
return resourceType + " " + metricType + " at " + formatMetricValue(value, "%"), ""
|
||||
return label + " " + metricType + " at " + formatMetricValue(value, "%"), ""
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -229,9 +229,9 @@ func AssessUnraidStorage(storage models.HostUnraidStorage) Assessment {
|
|||
}
|
||||
|
||||
if action := strings.TrimSpace(storage.SyncAction); action != "" {
|
||||
summary := fmt.Sprintf("Unraid array is running %s", action)
|
||||
summary := fmt.Sprintf("array %s in progress", action)
|
||||
if storage.SyncProgress > 0 {
|
||||
summary = fmt.Sprintf("Unraid array is running %s (%.0f%%)", action, storage.SyncProgress)
|
||||
summary = fmt.Sprintf("array %s in progress (%.0f%%)", action, storage.SyncProgress)
|
||||
}
|
||||
addReason("unraid_sync_active", RiskWarning, summary)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue