From 67b723a28d24e7d00404c80876a3b98a5349dee2 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 22 Mar 2026 11:16:14 +0000 Subject: [PATCH] Extract alert resource table row owner --- .../v6/internal/subsystems/alerts.md | 4 +- .../Alerts/AlertResourceTableRow.tsx | 613 ++++++++++++ .../components/Alerts/ResourceTable.test.tsx | 13 + .../src/components/Alerts/ResourceTable.tsx | 890 +----------------- .../Alerts/alertResourceTableModel.ts | 5 +- .../frontendResourceTypeBoundaries.test.ts | 15 +- 6 files changed, 681 insertions(+), 859 deletions(-) create mode 100644 frontend-modern/src/components/Alerts/AlertResourceTableRow.tsx diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index bad9e92bfb..3a1d89d17d 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -124,7 +124,9 @@ controller logic should land in those hooks rather than being rebuilt inside the table component. The alert resource thresholds editor now follows the same shape: shared metric normalization, bounds, value-resolution, and override-label logic live in -`frontend-modern/src/components/Alerts/alertResourceTableModel.ts`, while +`frontend-modern/src/components/Alerts/alertResourceTableModel.ts`, render-heavy +desktop row ownership lives in +`frontend-modern/src/components/Alerts/AlertResourceTableRow.tsx`, and selection state, delay-row toggling, and inline metric-input focus live in `frontend-modern/src/components/Alerts/useAlertResourceTableState.ts`. Future resource-table threshold semantics should land in those owners instead of diff --git a/frontend-modern/src/components/Alerts/AlertResourceTableRow.tsx b/frontend-modern/src/components/Alerts/AlertResourceTableRow.tsx new file mode 100644 index 0000000000..83c1b45f36 --- /dev/null +++ b/frontend-modern/src/components/Alerts/AlertResourceTableRow.tsx @@ -0,0 +1,613 @@ +import { For, Show } from 'solid-js'; +import RotateCcw from 'lucide-solid/icons/rotate-ccw'; + +import { TogglePrimitive } from '@/components/shared/Toggle'; +import { StatusBadge } from '@/components/shared/StatusBadge'; +import { ThresholdSlider } from '@/components/Dashboard/ThresholdSlider'; +import { TableCell, TableRow } from '@/components/shared/Table'; +import { + getAlertResourceTableCustomBadgeLabel, + getAlertResourceTableEditMetricTitle, + getAlertResourceTableMetricInputTitle, + getAlertResourceTableMetricPlaceholder, + getAlertResourceTableOfflineStateOrder, + getAlertResourceTableOfflineStatePresentation, + getAlertResourceTableOverrideNotePlaceholder, + getAlertResourceTableRevertToDefaultsLabel, +} from '@/utils/alertResourceTablePresentation'; +import { + ALERT_RESOURCE_TABLE_SLIDER_METRICS, + alertResourceSupportsMetric, + getAlertResourceLabel, + getAlertResourceMetricBounds, + getAlertResourceMetricDisplayValue, + getAlertResourceMetricStep, + isAlertResourceMetricOverridden, + normalizeAlertResourceMetricKey, + type AlertResourceThresholdMap, +} from './alertResourceTableModel'; +import type { Alert } from '@/types/api'; +import type { Resource } from '@/features/alerts/thresholds/tableTypes'; + +type OfflineState = 'off' | 'warning' | 'critical'; + +interface AlertResourceTableRowProps { + resource: Resource; + columns: string[]; + activeAlerts?: Record; + editingId: () => string | null; + editingThresholds: () => Record; + setEditingThresholds: (value: Record) => void; + formatMetricValue: (metric: string, value: number | undefined) => string; + hasActiveAlert: (resourceId: string, metric: string) => boolean; + onEdit: ( + resourceId: string, + thresholds: Record, + defaults: Record, + note: string | undefined, + ) => void; + onSaveEdit: (resourceId: string) => void; + onCancelEdit: () => void; + onRemoveOverride: (resourceId: string) => void; + onToggleDisabled?: (resourceId: string) => void; + onToggleNodeConnectivity?: (nodeId: string) => void; + showOfflineAlertsColumn?: boolean; + globalOfflineSeverity?: 'warning' | 'critical'; + onSetOfflineState?: (resourceId: string, state: OfflineState) => void; + onToggleBackup?: (resourceId: string, forceState?: boolean) => void; + onToggleSnapshot?: (resourceId: string, forceState?: boolean) => void; + globalDisableFlag?: () => boolean; + globalDisableOfflineFlag?: () => boolean; + editingNote: () => string; + setEditingNote: (value: string) => void; + activeMetricInput: () => { resourceId: string; metric: string } | null; + setActiveMetricInput: (value: { resourceId: string; metric: string } | null) => void; + showBulkSelection?: boolean; + selected?: boolean; + onToggleSelection?: (checked: boolean) => void; +} + +export function AlertResourceTableRow(props: AlertResourceTableRowProps) { + const isEditing = () => props.editingId() === props.resource.id; + const thresholds = () => (isEditing() ? props.editingThresholds() : (props.resource.thresholds ?? {})); + const resourceLabel = () => getAlertResourceLabel(props.resource); + const displayValue = (metric: string) => + getAlertResourceMetricDisplayValue( + props.resource, + metric, + props.editingThresholds(), + isEditing(), + ); + const isOverridden = (metric: string) => + isAlertResourceMetricOverridden(props.resource, metric); + + const getThresholds = (): AlertResourceThresholdMap => thresholds(); + + const startEditing = (metric?: string, event?: MouseEvent) => { + event?.stopPropagation(); + if (props.resource.editable === false) { + return; + } + if (metric) { + props.setActiveMetricInput({ resourceId: props.resource.id, metric }); + } + props.onEdit( + props.resource.id, + props.resource.thresholds ? { ...props.resource.thresholds } : {}, + props.resource.defaults ? { ...props.resource.defaults } : {}, + typeof props.resource.note === 'string' ? props.resource.note : undefined, + ); + }; + + const cancelEditing = () => { + props.onCancelEdit(); + props.setActiveMetricInput(null); + }; + + const saveEditing = () => { + props.onSaveEdit(props.resource.id); + props.setActiveMetricInput(null); + }; + + const updateEditingThreshold = (metric: string, value: number | undefined) => { + props.setEditingThresholds({ + ...props.editingThresholds(), + [metric]: value, + }); + }; + + const MetricValueWithHeat = (metricProps: { + resourceId: string; + metric: string; + value: number; + isOverridden: boolean; + }) => { + const isDisabledMetric = metricProps.value <= 0; + const displayText = isDisabledMetric + ? 'Off' + : props.formatMetricValue(metricProps.metric, metricProps.value); + + return ( +
+ + {displayText} + + +
+ +
+ ); + }; + + const renderToggleBadge = (config: { + isEnabled: boolean; + disabled?: boolean; + size?: 'sm' | 'md'; + onToggle?: () => void; + labelEnabled?: string; + labelDisabled?: string; + titleEnabled?: string; + titleDisabled?: string; + titleWhenDisabled?: string; + }) => ; + + const nextOfflineState = (state: OfflineState): OfflineState => { + const order = getAlertResourceTableOfflineStateOrder(); + const idx = order.indexOf(state); + return order[(idx + 1) % order.length]; + }; + + const renderOfflineStateButton = ( + state: OfflineState, + disabled: boolean, + onToggle: () => void, + ) => { + const config = getAlertResourceTableOfflineStatePresentation(state); + + return ( + + ); + }; + + return ( + + + + props.onToggleSelection?.(e.currentTarget.checked)} + class="rounded border-border text-sky-600 focus:ring-sky-500 transition-shadow cursor-pointer" + aria-label={`Select ${resourceLabel()}`} + /> + + + + + + {(() => { + const globallyDisabled = props.globalDisableFlag?.() ?? false; + const isChecked = !globallyDisabled && !props.resource.disabled; + return ( +
+ !globallyDisabled && props.onToggleDisabled?.(props.resource.id)} + class="my-[1px]" + title={ + globallyDisabled + ? 'Alerts disabled globally' + : props.resource.disabled + ? 'Click to enable alerts' + : 'Click to disable alerts' + } + ariaLabel={ + isChecked + ? 'Alerts enabled for this resource' + : 'Alerts disabled for this resource' + } + /> +
+ ); + })()} +
+
+ + +
+ + {resourceLabel()} + + } + > + + + + + {props.resource.node} + + + + {props.resource.subtitle as string} + + + + {getAlertResourceTableCustomBadgeLabel()} + + +
+ +
+ +