diff --git a/src/App.tsx b/src/App.tsx index 59ff600..d0c5a44 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -616,8 +616,8 @@ function Workspace({ page: activePage }: { page: PageId | 'node-details' }) { Back to cluster - {clusterDetails.nodes.some((node) => node.uuid === nodeId) && ( - + {nodeId && clusterDetails.nodes.some((node) => node.uuid === nodeId) && ( + )} )} diff --git a/src/NodeConnections.tsx b/src/NodeConnections.tsx index 45297b3..6f15c68 100644 --- a/src/NodeConnections.tsx +++ b/src/NodeConnections.tsx @@ -25,71 +25,105 @@ function isReplicationHealth(value: unknown): value is ReplicationHealth { }) } -export default function NodeConnections() { +export default function NodeConnections({ nodeId }: { nodeId: string }) { const [health, setHealth] = useState(null) - const [status, setStatus] = useState('Connecting to replication health…') + const [status, setStatus] = useState('Loading node connection details…') const [error, setError] = useState('') const [attempt, setAttempt] = useState(0) useEffect(() => { let active = true - let socket: WebSocket + let socket: WebSocket | undefined + const controller = new AbortController() - try { - const baseUrl = import.meta.env.VITE_WS_BASE_URL - if (!baseUrl) throw new Error('VITE_WS_BASE_URL is not configured.') - socket = new WebSocket(`${baseUrl.replace(/\/$/, '')}/ws/node/replication-health`) - } catch (cause) { - setStatus('') - setError(cause instanceof Error ? cause.message : 'Unable to open replication health connection.') - return - } - - socket.onopen = () => { - if (!active) return - setStatus('Waiting for replication health…') - // Temporary connection details until the API supports the selected node. - socket.send(JSON.stringify({ - node_name: 'node3', - host: '172.16.10.178', - port: 5444, - dbname: 'postgres', - user: 'postgres', - password: 'postgres', - })) - } - socket.onmessage = (event) => { - if (!active) return + const connect = async () => { try { - const data: unknown = JSON.parse(event.data) - if (!isReplicationHealth(data)) throw new Error('Invalid replication health response.') - setHealth(data) - setError('') - setStatus('Live updates connected') - } catch { - setHealth(null) + const apiBaseUrl = import.meta.env.VITE_API_BASE_URL + const baseUrl = import.meta.env.VITE_WS_BASE_URL + if (!apiBaseUrl) throw new Error('VITE_API_BASE_URL is not configured.') + if (!baseUrl) throw new Error('VITE_WS_BASE_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(response.status === 404 + ? 'Node not found.' + : `Failed to load node connection details (${response.status}).`) + } + const data = await response.json() + const node = data?.node + if (data?.status !== 'success' || !node + || node.uuid !== nodeId + || typeof node.if_name !== 'string' + || typeof node.host !== 'string' + || typeof node.port !== 'number' + || typeof node.dbname !== 'string' + || typeof node.user !== 'string') { + throw new Error('Invalid node connection details response.') + } + if (!active) return + + setStatus('Connecting to replication health…') + const connection = new WebSocket(`${baseUrl.replace(/\/$/, '')}/ws/node/replication-health`) + socket = connection + connection.onopen = () => { + if (!active) return + setStatus('Waiting for replication health…') + connection.send(JSON.stringify({ + node_name: node.if_name, + host: node.host, + port: node.port, + dbname: node.dbname, + user: node.user, + password: 'postgres', + })) + } + connection.onmessage = (event) => { + if (!active) return + try { + const data: unknown = JSON.parse(event.data) + if (!isReplicationHealth(data)) throw new Error('Invalid replication health response.') + setHealth(data) + setError('') + setStatus('Live updates connected') + } catch { + setHealth(null) + setStatus('') + setError('Received an invalid replication health response.') + } + } + connection.onerror = () => { + if (!active) return + setHealth(null) + setStatus('') + setError('Unable to receive replication health. Check that the WebSocket server is running.') + } + connection.onclose = () => { + if (!active) return + setHealth(null) + setStatus('') + setError((current) => current || 'Replication health connection closed. Reconnect to receive updates.') + } + } catch (cause) { + if (!active) return setStatus('') - setError('Received an invalid replication health response.') + setError(cause instanceof Error ? cause.message : 'Unable to load node connection details.') } } - socket.onerror = () => { - if (!active) return - setHealth(null) - setStatus('') - setError('Unable to receive replication health. Check that the WebSocket server is running.') - } - socket.onclose = () => { - if (!active) return - setHealth(null) - setStatus('') - setError((current) => current || 'Replication health connection closed. Reconnect to receive updates.') - } + void connect() return () => { active = false - socket.close() + controller.abort() + socket?.close() } - }, [attempt]) + }, [attempt, nodeId]) return (
@@ -101,7 +135,7 @@ export default function NodeConnections() {