From 48d563509535f8d889242f94eaf966ba3d67d872 Mon Sep 17 00:00:00 2001 From: George Date: Thu, 8 Oct 2026 11:07:00 +0530 Subject: [PATCH] ADDED : --NodeDetailsDashboard.tsx --- src/App.css | 71 +++++++++++++++++++- src/App.tsx | 16 ++++- src/NodeDetailsDashboard.tsx | 2 + src/NodeDockerContainers.tsx | 126 +++++++++++++++++++++++++++++++++++ 4 files changed, 211 insertions(+), 4 deletions(-) create mode 100644 src/NodeDockerContainers.tsx diff --git a/src/App.css b/src/App.css index debf14f..c27f7b6 100644 --- a/src/App.css +++ b/src/App.css @@ -1100,28 +1100,93 @@ font-variant-numeric: tabular-nums; } +.node-docker-panel { + grid-column: 1 / -1; + min-width: 0; +} + +.node-metrics-dashboard > .node-docker-panel { + padding: 0.8% 1.2%; +} + +.node-docker-panel code { + font-size: inherit; + padding: 0.3% 1%; + max-width: 100%; + box-sizing: border-box; + overflow-wrap: anywhere; +} + +.node-docker-panel .page-header { + align-items: center; + gap: 0 2%; + margin-bottom: 0.5%; + padding-bottom: 0.5%; +} + +.node-docker-panel h2 { + margin: 0; +} + +.node-docker-panel .page-actions { + flex: 1 1 60%; + gap: 0 2%; +} + +.node-docker-panel .page-actions > * { + margin: 0; +} + +.node-docker-panel .secondary-button { + min-height: 0; + padding: 0.5% 1.2%; + flex-shrink: 0; + line-height: 1.2; +} + +.node-docker-panel .connection-status { + gap: 0; +} + +.node-docker-panel .docker-view-all { + margin: 0.6% 0 0; +} + .node-replication-panel { grid-column: span 2; } -.node-replication-panel .nodes-table-wrap { +.node-replication-panel .nodes-table-wrap, +.node-docker-panel .nodes-table-wrap { margin-top: 1%; } -.node-replication-panel .nodes-table { +.node-replication-panel .nodes-table, +.node-docker-panel .nodes-table { min-width: 0; font-size: clamp(0.625rem, 0.8cqw, 0.7rem); } .node-replication-panel .nodes-table th, .node-replication-panel .nodes-table td, -.node-replication-panel .nodes-table caption { +.node-replication-panel .nodes-table caption, +.node-docker-panel .nodes-table th, +.node-docker-panel .nodes-table td { padding: 1% 1.5%; font-size: inherit; white-space: normal; overflow-wrap: anywhere; } +.node-docker-panel .nodes-table-wrap { + margin-top: 0; +} + +.node-docker-panel .nodes-table th, +.node-docker-panel .nodes-table td { + padding: 0.4% 1%; +} + .node-metrics-dashboard .status-message { box-sizing: border-box; max-width: 100%; diff --git a/src/App.tsx b/src/App.tsx index 04c91ec..4e9cdca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import NodeDetailsDashboard from './NodeDetailsDashboard' import NodeDeadTuples from './NodeDeadTuples' import NodeLargestTables from './NodeLargestTables' import NodeHardware from './NodeHardware' +import NodeDockerContainers from './NodeDockerContainers' import ClusterNodeCard from './ClusterNodeCard' const API_BASE_URL = import.meta.env.VITE_API_BASE_URL @@ -269,13 +270,14 @@ function App() { } /> } /> } /> + } /> } /> } /> ) } -function Workspace({ page: activePage }: { page: PageId | 'node-details' | 'largest-tables' | 'dead-tuples' }) { +function Workspace({ page: activePage }: { page: PageId | 'node-details' | 'largest-tables' | 'dead-tuples' | 'docker-containers' }) { const navigate = useNavigate() const { clusterId, nodeId } = useParams() const isClusterPage = Boolean(useMatch('/clusters/*')) @@ -632,6 +634,18 @@ function Workspace({ page: activePage }: { page: PageId | 'node-details' | 'larg )} )} + {activePage === 'docker-containers' && clusterDetails && ( +
+
+

{clusterDetails.nodes.find((node) => node.uuid === nodeId)?.node_name ?? 'Node not found'}

+
+ {nodeId && clusterDetails.nodes.some((node) => node.uuid === nodeId) && ( +
+ +
+ )} +
+ )} {activePage === 'largest-tables' && clusterDetails && (
{nodeId && clusterDetails.nodes.some((node) => node.uuid === nodeId) ? ( diff --git a/src/NodeDetailsDashboard.tsx b/src/NodeDetailsDashboard.tsx index 05c29d6..9f545c0 100644 --- a/src/NodeDetailsDashboard.tsx +++ b/src/NodeDetailsDashboard.tsx @@ -8,6 +8,7 @@ import NodeWalSize from './NodeWalSize' import NodeDeadTuples from './NodeDeadTuples' import NodeLocksDeadlocks from './NodeLocksDeadlocks' import NodeConnections from './NodeConnections' +import NodeDockerContainers from './NodeDockerContainers' export default function NodeDetailsDashboard(props: { nodeId: string; nodeName: string; clusterId: string }) { return ( @@ -21,6 +22,7 @@ export default function NodeDetailsDashboard(props: { nodeId: string; nodeName: +
diff --git a/src/NodeDockerContainers.tsx b/src/NodeDockerContainers.tsx new file mode 100644 index 0000000..dff9a2b --- /dev/null +++ b/src/NodeDockerContainers.tsx @@ -0,0 +1,126 @@ +import { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' + +type Container = { id: string; name: string; status: string } + +function isContainerResponse(value: unknown): value is { containers: Container[] } { + if (!value || typeof value !== 'object') return false + const containers = (value as Record).containers + return Array.isArray(containers) && containers.every((container: unknown) => { + if (!container || typeof container !== 'object') return false + const item = container as Record + return typeof item.id === 'string' && typeof item.name === 'string' && typeof item.status === 'string' + }) +} + +export default function NodeDockerContainers({ nodeId, clusterId, detailed = false }: { + nodeId: string; clusterId: string; detailed?: boolean +}) { + const [containers, setContainers] = useState(null) + const [status, setStatus] = useState('Connecting…') + const [attempt, setAttempt] = useState(0) + + useEffect(() => { + let active = true + const controller = new AbortController() + let socket: WebSocket | undefined + const disconnected = () => { + if (!active) return + setContainers(null) + setStatus('Docker feed disconnected. Reconnect to try again.') + } + const connect = async () => { + setContainers(null) + setStatus('Loading node details…') + try { + const apiBaseUrl = import.meta.env.VITE_API_BASE_URL + if (!apiBaseUrl) throw new Error('API URL is not configured.') + const token = localStorage.getItem('access_token') + const response = await fetch( + `${apiBaseUrl.replace(/\/$/, '')}/spock/node-interfaces/${encodeURIComponent(nodeId)}`, + { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + signal: controller.signal, + }, + ) + if (!response.ok) throw new Error('Failed to load node details.') + const result = await response.json() + const node = result?.node + if (result?.status !== 'success' || node?.uuid !== nodeId + || typeof node.host !== 'string' || !node.host.trim()) { + throw new Error('Invalid node host received.') + } + if (!active) return + const host = node.host.trim() + const hostname = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host + setStatus('Connecting…') + socket = new WebSocket(`ws://${hostname}:9100/ws/docker/containers`) + socket.onopen = () => { + if (active) setStatus('Waiting for containers…') + } + socket.onmessage = (event) => { + if (!active) return + try { + const message: unknown = JSON.parse(event.data) + if (!isContainerResponse(message)) throw new Error('Invalid container data') + setContainers(message.containers) + setStatus('Live') + } catch { + setContainers(null) + setStatus('Invalid container data received.') + } + } + socket.onerror = disconnected + socket.onclose = disconnected + } catch (error) { + if (!active) return + setStatus(error instanceof Error ? error.message : 'Unable to connect to the Docker feed.') + } + } + void connect() + return () => { + active = false + controller.abort() + socket?.close() + } + }, [attempt, nodeId]) + + const nodePath = `/clusters/${encodeURIComponent(clusterId)}/nodes/${encodeURIComponent(nodeId)}` + const visibleContainers = detailed ? containers : containers?.slice(0, 3) + + return ( +
+
+

Docker containers

+
+ {detailed && Back to node} +

{status}

+ +
+
+ {containers !== null && (containers.length === 0 ?

No containers found.

: ( +
+ + + + {visibleContainers?.map((container) => ( + + + + + + ))} + +
NameContainer IDStatus
{container.name}{container.id}{container.status}
+
+ ))} + {!detailed && containers && containers.length > 3 && ( +

View all {containers.length} containers

+ )} +
+ ) +}