From ff1ceb99ee769aab8597cb262f28cccc1c4dcb7e Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 7 Oct 2025 19:46:54 +0000 Subject: [PATCH] Refine alerts thresholds layout --- .../src/components/Alerts/ResourceTable.tsx | 288 +++++++++--------- .../src/components/Alerts/ThresholdsTable.tsx | 198 ++++++++---- 2 files changed, 270 insertions(+), 216 deletions(-) diff --git a/frontend-modern/src/components/Alerts/ResourceTable.tsx b/frontend-modern/src/components/Alerts/ResourceTable.tsx index b781004ad..c7a3fb6e2 100644 --- a/frontend-modern/src/components/Alerts/ResourceTable.tsx +++ b/frontend-modern/src/components/Alerts/ResourceTable.tsx @@ -107,9 +107,6 @@ export function ResourceTable(props: ResourceTableProps) { )} - - ({meta.rawName}) - {meta.clusterName} @@ -153,6 +150,61 @@ export function ResourceTable(props: ResourceTableProps) { ); }; + const renderToggleBadge = (config: { + isEnabled: boolean; + disabled?: boolean; + size?: 'sm' | 'md'; + onToggle?: () => void; + labelEnabled?: string; + labelDisabled?: string; + titleEnabled?: string; + titleDisabled?: string; + titleWhenDisabled?: string; + }) => { + const { + isEnabled, + disabled = false, + size = 'sm', + onToggle, + labelEnabled = 'Enabled', + labelDisabled = 'Disabled', + titleEnabled, + titleDisabled, + titleWhenDisabled, + } = config; + + const basePadding = size === 'md' ? 'px-2.5 py-1' : 'px-2 py-0.5'; + const baseClasses = `inline-flex items-center justify-center ${basePadding} text-xs font-semibold rounded-full border transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-blue-400`; + const stateClasses = isEnabled + ? 'bg-emerald-500/10 text-emerald-700 border-emerald-500/50 hover:bg-emerald-500/15 dark:bg-emerald-500/20 dark:text-emerald-200 dark:border-emerald-400/40' + : 'bg-rose-500/10 text-rose-700 border-rose-500/50 hover:bg-rose-500/15 dark:bg-rose-500/20 dark:text-rose-200 dark:border-rose-400/40'; + const disabledClasses = disabled + ? 'opacity-60 cursor-not-allowed hover:bg-transparent dark:hover:bg-transparent' + : ''; + + const title = disabled + ? titleWhenDisabled ?? titleDisabled ?? titleEnabled ?? '' + : isEnabled + ? titleEnabled ?? '' + : titleDisabled ?? ''; + + return ( + + ); + }; + return ( Resource - - Type - {(column) => ( @@ -215,7 +264,7 @@ export function ResourceTable(props: ResourceTableProps) { - + Global Defaults @@ -325,21 +374,19 @@ export function ResourceTable(props: ResourceTableProps) { -}> -
- { + {(() => { + const offlineDisabled = props.globalDisableOfflineFlag?.() ?? false; + return renderToggleBadge({ + isEnabled: !offlineDisabled, + size: 'md', + onToggle: () => { props.onToggleGlobalDisableOffline?.(); props.setHasUnsavedChanges?.(true); - }} - checkedClass="bg-emerald-500/80 border-emerald-600/70 dark:bg-emerald-500/60 dark:border-emerald-500/70" - uncheckedClass="bg-rose-500/80 border-rose-600/70 dark:bg-rose-500/60 dark:border-rose-500/70" - class="my-[1px]" - title="Global offline alerts toggle" - ariaLabel="Global offline alerts toggle" - /> -
+ }, + titleEnabled: 'Offline alerts currently enabled globally. Click to disable.', + titleDisabled: 'Offline alerts currently disabled globally. Click to enable.', + }); + })()}
@@ -362,7 +409,7 @@ export function ResourceTable(props: ResourceTableProps) { {/* Node group header */} {renderGroupHeader(nodeName, headerMeta)} @@ -452,15 +499,6 @@ export function ResourceTable(props: ResourceTableProps) { > {resource.name} - - ({resource.vmid}) - - - on {resource.node} - - - on {resource.node} - Custom @@ -473,7 +511,9 @@ export function ResourceTable(props: ResourceTableProps) { - {resource.displayName || resource.name} + {resource.type === 'node' + ? resource.name + : resource.displayName || resource.name} }> {(host) => ( @@ -489,13 +529,12 @@ export function ResourceTable(props: ResourceTableProps) { }`} title={`Open ${resource.displayName || resource.name} web interface`} > - {resource.displayName || resource.name} + {resource.type === 'node' + ? resource.name + : resource.displayName || resource.name} )} - - ({resource.rawName}) - {resource.clusterName} @@ -509,29 +548,8 @@ export function ResourceTable(props: ResourceTableProps) { - - - {resource.resourceType} - - - {/* Metric columns - dynamically rendered based on resource type */} - + {/* Metric columns - dynamically rendered based on resource type */} + {(column) => { const normalizedColumn = column.trim().toLowerCase(); const metric = ( @@ -686,33 +704,24 @@ export function ResourceTable(props: ResourceTableProps) { - - !props.globalDisableFlag?.() && - !props.globalDisableOfflineFlag?.() && - props.onToggleNodeConnectivity?.(resource.id) - } - checkedClass="bg-emerald-500/80 border-emerald-600/70 dark:bg-emerald-500/60 dark:border-emerald-500/70" - uncheckedClass="bg-rose-500/80 border-rose-600/70 dark:bg-rose-500/60 dark:border-rose-500/70" - disabledClass="bg-slate-400/60 border-slate-500/70 dark:bg-slate-600/60 dark:border-slate-600/70 cursor-not-allowed opacity-60" - class="mx-auto my-[1px]" - title={ - props.globalDisableFlag?.() - ? 'Alerts disabled globally' - : props.globalDisableOfflineFlag?.() - ? 'Offline alerts disabled globally' - : resource.disableConnectivity - ? 'Offline alerts disabled for this resource' - : 'Offline alerts enabled' - } - ariaLabel={resource.disableConnectivity ? 'Offline alerts disabled for this resource' : 'Offline alerts enabled for this resource'} - /> - - - + {(() => { + const disabledGlobally = props.globalDisableFlag?.() || props.globalDisableOfflineFlag?.(); + const globalOfflineDisabled = props.globalDisableOfflineFlag?.() ?? false; + return renderToggleBadge({ + isEnabled: !globalOfflineDisabled && !resource.disableConnectivity, + disabled: disabledGlobally, + onToggle: () => { + if (disabledGlobally) return; + props.onToggleNodeConnectivity?.(resource.id); + }, + titleEnabled: 'Offline alerts enabled. Click to disable for this resource.', + titleDisabled: 'Offline alerts disabled. Click to enable for this resource.', + titleWhenDisabled: 'Offline alerts controlled globally', + }); + })()} + + + {/* Actions column */} @@ -900,15 +909,6 @@ export function ResourceTable(props: ResourceTableProps) { > {resource.name} - - ({resource.vmid}) - - - on {resource.node} - - - on {resource.node} - Custom @@ -921,7 +921,9 @@ export function ResourceTable(props: ResourceTableProps) { - {resource.displayName || resource.name} + {resource.type === 'node' + ? resource.name + : resource.displayName || resource.name} }> {(host) => ( @@ -937,13 +939,12 @@ export function ResourceTable(props: ResourceTableProps) { }`} title={`Open ${resource.displayName || resource.name} web interface`} > - {resource.displayName || resource.name} + {resource.type === 'node' + ? resource.name + : resource.displayName || resource.name} )} - - ({resource.rawName}) - {resource.clusterName} @@ -957,27 +958,6 @@ export function ResourceTable(props: ResourceTableProps) { - - - {resource.resourceType} - - {/* Metric columns - dynamically rendered based on resource type */} {(column) => { @@ -1116,35 +1096,43 @@ export function ResourceTable(props: ResourceTableProps) { -
- - !props.globalDisableFlag?.() && - !props.globalDisableOfflineFlag?.() && - props.onToggleNodeConnectivity?.(resource.id) - } - checkedClass="bg-emerald-500/80 border-emerald-600/70 dark:bg-emerald-500/60 dark:border-emerald-500/70" - uncheckedClass="bg-rose-500/80 border-rose-600/70 dark:bg-rose-500/60 dark:border-rose-500/70" - disabledClass="bg-slate-400/60 border-slate-500/70 dark:bg-slate-600/60 dark:border-slate-600/70 cursor-not-allowed opacity-60" - class="my-[1px]" - title={ - props.globalDisableFlag?.() - ? 'Alerts disabled globally' - : props.globalDisableOfflineFlag?.() - ? 'Offline alerts disabled globally' - : resource.disableConnectivity - ? 'Offline alerts disabled for this resource' - : 'Offline alerts enabled' - } - ariaLabel={resource.disableConnectivity ? 'Offline alerts disabled for this resource' : 'Offline alerts enabled for this resource'} - /> -
+ {(() => { + const disabledGlobally = props.globalDisableFlag?.() || props.globalDisableOfflineFlag?.(); + const isEnabled = !(resource.disableConnectivity || props.globalDisableOfflineFlag?.()); + const label = isEnabled ? 'Enabled' : 'Disabled'; + const baseClasses = + 'inline-flex items-center justify-center px-2 py-0.5 text-xs font-semibold rounded-full border transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-blue-400'; + const stateClasses = isEnabled + ? 'bg-emerald-500/10 text-emerald-700 border-emerald-500/50 hover:bg-emerald-500/15 dark:bg-emerald-500/20 dark:text-emerald-200 dark:border-emerald-400/40' + : 'bg-rose-500/10 text-rose-700 border-rose-500/50 hover:bg-rose-500/15 dark:bg-rose-500/20 dark:text-rose-200 dark:border-rose-400/40'; + const disabledClasses = disabledGlobally + ? 'opacity-60 cursor-not-allowed hover:bg-transparent dark:hover:bg-transparent' + : ''; + return ( + + ); + })()}
- -
+ + {/* Actions column */} @@ -1241,7 +1229,7 @@ export function ResourceTable(props: ResourceTableProps) { > No {props.title.toLowerCase()} found @@ -1252,7 +1240,7 @@ export function ResourceTable(props: ResourceTableProps) { {props.emptyMessage || 'No resources available.'} diff --git a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx index 764226340..61934d129 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx @@ -208,6 +208,76 @@ export function ThresholdsTable(props: ThresholdsTableProps) { }; // Process nodes with their overrides +const getFriendlyNodeName = (value: string, clusterName?: string): string => { + if (!value) return value; + + const clusterLower = clusterName?.toLowerCase().trim(); + + const normalizeToken = (token?: string | null): string => { + if (!token) return ''; + let result = token.replace(/\(.*?\)/g, ' ').replace(/\s+/g, ' ').trim(); + if (clusterLower) { + result = result + .split(' ') + .filter((part) => part.toLowerCase() !== clusterLower) + .join(' ') + .trim(); + } + if (!result) return ''; + const firstWord = result.split(/\s+/)[0] || result; + const withoutDomain = firstWord.includes('.') ? firstWord.split('.')[0] ?? firstWord : firstWord; + return withoutDomain.trim(); + }; + + const parentheticalMatch = value.match(/\(([^)]+)\)/); + const parentheticalRaw = parentheticalMatch?.[1]?.trim(); + + let base = normalizeToken(value); + if (!base) { + base = value.trim(); + } + + const parenthetical = normalizeToken(parentheticalRaw); + if (parenthetical && parenthetical.toLowerCase() !== base.toLowerCase()) { + return parenthetical; + } + + return base; +}; + +const buildNodeHeaderMeta = (node: Node) => { + const originalDisplayName = node.displayName?.trim() || node.name; + const friendlyName = getFriendlyNodeName(originalDisplayName, node.clusterName); + const hostValue = node.host?.trim(); + let host: string | undefined; + if (hostValue && hostValue !== '') { + host = hostValue.startsWith('http') + ? hostValue + : `https://${hostValue.includes(':') ? hostValue : `${hostValue}:8006`}`; + } else if (node.name) { + host = `https://${node.name.includes(':') ? node.name : `${node.name}:8006`}`; + } + + const headerMeta: GroupHeaderMeta = { + type: 'node', + displayName: friendlyName, + rawName: originalDisplayName, + host, + status: node.status, + clusterName: node.isClusterMember ? node.clusterName?.trim() || 'Cluster' : undefined, + isClusterMember: node.isClusterMember ?? false, + }; + + const keys = new Set(); + [node.name, originalDisplayName, friendlyName].forEach((value) => { + if (value && value.trim()) { + keys.add(value.trim()); + } + }); + + return { headerMeta, keys }; +}; + const nodesWithOverrides = createMemo((prev = []) => { // If we're currently editing, return the previous value to avoid re-renders if (editingId()) { @@ -231,8 +301,10 @@ export function ThresholdsTable(props: ThresholdsTableProps) { ); }); - const displayName = node.displayName?.trim() || node.name; + const originalDisplayName = node.displayName?.trim() || node.name; + const friendlyName = getFriendlyNodeName(originalDisplayName, node.clusterName); const rawName = node.name; + const sanitizedName = friendlyName || originalDisplayName || rawName.split('.')[0] || rawName; // Build a best-effort management URL for the node const hostValue = node.host?.trim() || rawName; const normalizedHost = hostValue.startsWith('http://') || hostValue.startsWith('https://') @@ -241,9 +313,9 @@ export function ThresholdsTable(props: ThresholdsTableProps) { return { id: node.id, - name: displayName, - displayName, - rawName, + name: sanitizedName, + displayName: sanitizedName, + rawName: originalDisplayName, host: normalizedHost, type: 'node' as const, resourceType: 'Node', @@ -550,36 +622,12 @@ const dockerContainersGroupedByHost = createMemo>((pr const guestGroupHeaderMeta = createMemo>(() => { const meta: Record = {}; - const nodeLookup = new Map((props.nodes ?? []).map((node) => [node.name, node])); - - Object.keys(guestsGroupedByNode() ?? {}).forEach((nodeName) => { - const node = nodeLookup.get(nodeName); - if (!node) { - return; - } - - const displayName = node.displayName?.trim() || node.name; - const hostValue = node.host?.trim(); - let host: string | undefined; - if (hostValue && hostValue !== '') { - host = hostValue.startsWith('http') - ? hostValue - : `https://${hostValue.includes(':') ? hostValue : `${hostValue}:8006`}`; - } else if (node.name) { - host = `https://${node.name.includes(':') ? node.name : `${node.name}:8006`}`; - } - - meta[nodeName] = { - type: 'node', - displayName, - rawName: node.name, - host, - status: node.status, - clusterName: node.isClusterMember ? node.clusterName?.trim() || 'Cluster' : undefined, - isClusterMember: node.isClusterMember ?? false, - }; + (props.nodes ?? []).forEach((node) => { + const { headerMeta, keys } = buildNodeHeaderMeta(node); + keys.forEach((key) => { + meta[key] = headerMeta; + }); }); - return meta; }); @@ -690,6 +738,23 @@ const dockerContainersGroupedByHost = createMemo>((pr return storageDevices; }, []); + const storageGroupedByNode = createMemo>(() => { + const grouped: Record = {}; + storageWithOverrides().forEach((storage) => { + const key = storage.node?.trim() || 'Unassigned'; + if (!grouped[key]) { + grouped[key] = []; + } + grouped[key].push(storage); + }); + + Object.values(grouped).forEach((resources) => { + resources.sort((a, b) => a.name.localeCompare(b.name)); + }); + + return grouped; + }); + const summaryItems = createMemo(() => { try { const items = [ @@ -1175,11 +1240,44 @@ const dockerContainersGroupedByHost = createMemo>((pr + +
+ props.setDisableAllGuests(!props.disableAllGuests())} + globalDisableOfflineFlag={props.disableAllGuestsOffline} + onToggleGlobalDisableOffline={() => props.setDisableAllGuestsOffline(!props.disableAllGuestsOffline())} + /> +
+
+
>((pr />
- - -
- props.setDisableAllGuests(!props.disableAllGuests())} - globalDisableOfflineFlag={props.disableAllGuestsOffline} - onToggleGlobalDisableOffline={() => props.setDisableAllGuestsOffline(!props.disableAllGuestsOffline())} - /> -
-