--ADDED details to node cards in cluster
This commit is contained in:
george committed 2026-09-23 13:20:14 +05:30
1 parent 08d79770cf
commit 0d46483c39
4 files changed
+162 -8

No files matched your search

+26 -1
View File
@@ -484,7 +484,9 @@
}
.cluster-node-card {
display: block;
display: flex;
flex-direction: column;
gap: 12px;
text-decoration: none;
border: 1px solid var(--border);
border-radius: 8px;
@@ -495,6 +497,29 @@
overflow-wrap: anywhere;
}
.cluster-node-name {
font-size: 1.1rem;
}
.cluster-node-database,
.cluster-node-uptime > span {
color: var(--text);
font-size: 0.85rem;
font-weight: 400;
}
.cluster-node-uptime {
display: flex;
flex-direction: column;
gap: 4px;
border-top: 1px solid var(--border);
padding-top: 12px;
}
.cluster-node-uptime strong {
font-size: 1.25rem;
}
.cluster-node-card:hover {
border-color: var(--accent-border);
background: var(--accent-bg);
+3 -7
View File
@@ -1,8 +1,9 @@
import { useEffect, useState } from 'react'
import { Link, Navigate, Route, Routes, useMatch, useNavigate, useParams } from 'react-router-dom'
import { Navigate, Route, Routes, useMatch, useNavigate, useParams } from 'react-router-dom'
import './App.css'
import NodeConnections from './NodeConnections'
import NodeHardware from './NodeHardware'
import ClusterNodeCard from './ClusterNodeCard'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
@@ -639,12 +640,7 @@ function Workspace({ page: activePage }: { page: PageId | 'node-details' }) {
<ul className="cluster-node-list" aria-label="Cluster nodes">
{clusterDetails.nodes.map((node) => (
<li key={node.uuid}>
<Link
className="cluster-node-card"
to={`/clusters/${encodeURIComponent(clusterId!)}/nodes/${encodeURIComponent(node.uuid)}`}
>
{node.node_name}
</Link>
<ClusterNodeCard node={node} clusterId={clusterId!} />
</li>
))}
</ul>
+26
View File
@@ -0,0 +1,26 @@
import { Link } from 'react-router-dom'
import useDatabaseStatus from './useDatabaseStatus'
export default function ClusterNodeCard({ node, clusterId }: {
node: { uuid: string; node_name: string }
clusterId: string
}) {
const { data: databaseStatus, connection: databaseConnection } = useDatabaseStatus(node.uuid, node.node_name)
return (
<Link
className="cluster-node-card"
to={`/clusters/${encodeURIComponent(clusterId)}/nodes/${encodeURIComponent(node.uuid)}`}
>
<span className="cluster-node-name">{node.node_name}</span>
<span className={`connection-status ${databaseStatus?.status === 'online' ? 'connected' : databaseStatus?.status === 'offline' ? 'disconnected' : ''}`}>
<span className="connection-status-dot" aria-hidden="true" />
{databaseStatus ? databaseStatus.status.charAt(0).toUpperCase() + databaseStatus.status.slice(1) : databaseConnection}
</span>
<span className="cluster-node-database">Database: {databaseStatus?.database_name ?? '—'}</span>
<span className="cluster-node-uptime">
<span>Uptime</span>
<strong>{databaseStatus ? `${(databaseStatus.uptime_seconds / 3600).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} hours` : '—'}</strong>
</span>
</Link>
)
}
+107
View File
@@ -0,0 +1,107 @@
import { useEffect, useState } from 'react'
type DatabaseStatus = {
database_name: string
status: string
uptime_seconds: number
}
function isDatabaseStatus(value: unknown): value is DatabaseStatus {
if (!value || typeof value !== 'object') return false
const data = value as Record<string, unknown>
return typeof data.database_name === 'string'
&& typeof data.status === 'string' && data.status.trim().length > 0
&& typeof data.uptime_seconds === 'number'
&& Number.isFinite(data.uptime_seconds) && data.uptime_seconds >= 0
}
export default function useDatabaseStatus(nodeId: string, nodeName: string) {
const [data, setData] = useState<DatabaseStatus | null>(null)
const [connection, setConnection] = useState('Connecting…')
useEffect(() => {
let active = true
const controller = new AbortController()
let socket: WebSocket | undefined
let retry: ReturnType<typeof setTimeout> | undefined
const connect = async () => {
if (!active) return
setData(null)
setConnection('Connecting…')
const disconnected = () => {
if (!active) return
setData(null)
setConnection('Status unavailable · reconnecting…')
clearTimeout(retry)
retry = setTimeout(connect, 5000)
}
try {
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL
const wsBaseUrl = import.meta.env.VITE_WS_BASE_URL
if (!wsBaseUrl) throw new Error('VITE_WS_BASE_URL is not configured')
if (!apiBaseUrl) throw new Error('API URL is not configured')
setConnection('Loading node details…')
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 || node.uuid !== nodeId
|| typeof node.host !== 'string' || typeof node.port !== 'number'
|| typeof node.dbname !== 'string' || typeof node.user !== 'string') {
throw new Error('Invalid node details')
}
if (!active) return
setConnection('Connecting…')
const current = new WebSocket(`${wsBaseUrl.replace(/\/$/, '')}/ws/database/status`)
socket = current
current.onopen = () => {
if (!active) return
current.send(JSON.stringify({
node_name: nodeName,
host: node.host,
port: node.port,
dbname: node.dbname,
user: node.user,
password: 'postgres',
}))
setConnection('Waiting for status…')
}
current.onmessage = (event) => {
if (!active) return
try {
const message: unknown = JSON.parse(event.data)
if (!isDatabaseStatus(message)) throw new Error('Invalid database status')
setData({ ...message, status: message.status.trim().toLowerCase() })
setConnection('Live')
} catch {
setData(null)
setConnection('Invalid status received')
}
}
current.onerror = () => current.close()
current.onclose = disconnected
} catch {
disconnected()
}
}
void connect()
return () => {
active = false
controller.abort()
clearTimeout(retry)
socket?.close()
}
}, [nodeId, nodeName])
return { data, connection }
}