import { useEffect, useState } from 'react' import { Navigate, Route, Routes, useMatch, useNavigate, useParams } from 'react-router-dom' import './App.css' import NodeDetailsDashboard from './NodeDetailsDashboard' import NodeDeadTuples from './NodeDeadTuples' import NodeLargestTables from './NodeLargestTables' import NodeHardware from './NodeHardware' import ClusterNodeCard from './ClusterNodeCard' const API_BASE_URL = import.meta.env.VITE_API_BASE_URL const navItems = [ { id: 'dashboard', label: 'Dashboard' }, { id: 'nodes', label: 'Nodes' }, { id: 'clusters', label: 'Clusters' }, ] as const type PageId = (typeof navItems)[number]['id'] | 'cluster-details' type Theme = 'light' | 'dark' type TokenStatus = 'idle' | 'loading' | 'success' | 'error' type RegisterStatus = 'idle' | 'loading' | 'success' | 'error' type NodesStatus = 'idle' | 'loading' | 'success' | 'error' type ClusterStatus = 'idle' | 'loading' | 'success' | 'error' type Cluster = { uuid: string cluster_name: string } type ClusterNodesResponse = { cluster_name: string nodes: { node_name: string uuid: string }[] } const getClusterResource = async (path: string) => { const accessToken = localStorage.getItem('access_token') const response = await fetch(`${API_BASE_URL}/spock/${path}`, { headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {}, }) if (!response.ok) { throw new Error('Failed to load cluster data') } return response } type RegisterNodePayload = { node_name: string host: string port: number dbname: string user: string password: string } type RegisterNodeResponse = { status: string message?: string } type NodeInterface = { if_id: number if_name: string if_nodeid: number host: string port: number dbname: string user: string uuid: string } type NodeInterfacesResponse = { status: string count: number nodes: NodeInterface[] message?: string } type DeleteNodeInterfaceResponse = { status: string message?: string } type CreateClusterPayload = { cluster_name: string links: [string, string][] } const getSavedTheme = (): Theme => { try { return localStorage.getItem('theme') === 'dark' ? 'dark' : 'light' } catch { return 'light' } } const saveTheme = (theme: Theme) => { try { localStorage.setItem('theme', theme) } catch { return } } const getSavedPage = (): PageId => { try { const savedPage = localStorage.getItem('activePage') if (savedPage === 'cluster-details') return 'clusters' return navItems.some((item) => item.id === savedPage) ? (savedPage as PageId) : 'dashboard' } catch { return 'dashboard' } } const savePage = (page: PageId) => { try { localStorage.setItem('activePage', page) } catch { return } } const getSessionToken = async () => { const response = await fetch(`${API_BASE_URL}/get-token`, { method: 'POST', }) if (!response.ok) { throw new Error('Failed to get token') } const data = (await response.json()) as { access_token: string token_type: string } localStorage.setItem('access_token', data.access_token) const storedToken = localStorage.getItem('access_token') if (!storedToken) { throw new Error('Token was not stored') } console.log('Token successfully stored:', storedToken) return storedToken } const registerNode = async ( payload: RegisterNodePayload, ): Promise => { const accessToken = localStorage.getItem('access_token') const headers: Record = { 'Content-Type': 'application/json', } if (accessToken) { headers.Authorization = `Bearer ${accessToken}` } const response = await fetch(`${API_BASE_URL}/register-node`, { method: 'POST', headers, body: JSON.stringify(payload), }) if (!response.ok) { throw new Error('Failed to register node') } return response.json() as Promise } const getNodeInterfaces = async (): Promise => { const accessToken = localStorage.getItem('access_token') const headers: Record = {} if (accessToken) { headers.Authorization = `Bearer ${accessToken}` } const response = await fetch(`${API_BASE_URL}/spock/node-interfaces`, { method: 'GET', headers, }) if (!response.ok) { throw new Error('Failed to get node interfaces') } return response.json() as Promise } const deleteNodeInterface = async ( node: NodeInterface, ): Promise => { const accessToken = localStorage.getItem('access_token') const headers: Record = {} const params = new URLSearchParams({ uuid: node.uuid, }) if (accessToken) { headers.Authorization = `Bearer ${accessToken}` } const response = await fetch( `${API_BASE_URL}/spock/del-node-interfaces?${params.toString()}`, { method: 'DELETE', headers, }, ) if (!response.ok) { throw new Error('Failed to delete node interface') } return response.json() as Promise } const createCluster = async (payload: CreateClusterPayload) => { console.log(JSON.stringify(payload, null, 2)) const accessToken = localStorage.getItem('access_token') const headers: Record = { 'Content-Type': 'application/json', } if (accessToken) { headers.Authorization = `Bearer ${accessToken}` } const response = await fetch(`${API_BASE_URL}/spock/create_cluster`, { method: 'POST', headers, body: JSON.stringify(payload), }) const result = await response.json().catch(() => null) as { status?: string message?: string detail?: string } | null if (!response.ok || (result?.status && result.status !== 'success')) { throw new Error( result?.message || result?.detail || 'Failed to create cluster', ) } } function App() { return ( } /> } /> } /> } /> } /> } /> } /> } /> } /> ) } function Workspace({ page: activePage }: { page: PageId | 'node-details' | 'largest-tables' | 'dead-tuples' }) { const navigate = useNavigate() const { clusterId, nodeId } = useParams() const isClusterPage = Boolean(useMatch('/clusters/*')) const [isCollapsed, setIsCollapsed] = useState(false) const [theme, setTheme] = useState(getSavedTheme) const [isRegisterFormOpen, setIsRegisterFormOpen] = useState(false) const [isClusterFormOpen, setIsClusterFormOpen] = useState(false) const [clusterStep, setClusterStep] = useState<1 | 2>(1) const [selectedClusterNodeIds, setSelectedClusterNodeIds] = useState( [], ) const [clusterName, setClusterName] = useState('') const [connectionNodeIndex, setConnectionNodeIndex] = useState(0) const [nodeConnections, setNodeConnections] = useState>({}) const [tokenStatus, setTokenStatus] = useState('idle') const [tokenError, setTokenError] = useState('') const [registerStatus, setRegisterStatus] = useState('idle') const [registerError, setRegisterError] = useState('') const [nodes, setNodes] = useState([]) const [nodesStatus, setNodesStatus] = useState('idle') const [nodesError, setNodesError] = useState('') const [deletingNodeId, setDeletingNodeId] = useState(null) const [clusterStatus, setClusterStatus] = useState('idle') const [clusterError, setClusterError] = useState('') const [clusters, setClusters] = useState([]) const [clustersStatus, setClustersStatus] = useState('idle') const [clustersError, setClustersError] = useState('') const [clusterNodesStatus, setClusterNodesStatus] = useState('idle') const [clusterNodesError, setClusterNodesError] = useState('') const [selectedCluster, setSelectedCluster] = useState(null) const [clusterDetails, setClusterDetails] = useState(null) const nextTheme = theme === 'light' ? 'dark' : 'light' const selectedClusterNodes = nodes.filter((node) => selectedClusterNodeIds.includes(node.uuid), ) const currentConnectionNode = selectedClusterNodes[connectionNodeIndex] const loadClusters = async () => { setClustersStatus('loading') setClustersError('') try { const response = await getClusterResource('clusters') setClusters(await response.json() as Cluster[]) setClustersStatus('success') } catch (error) { setClustersStatus('error') setClustersError(error instanceof Error ? error.message : 'Failed to load clusters') } } const handleClusterClick = (cluster: Cluster) => { setSelectedCluster(cluster) navigate(`/clusters/${encodeURIComponent(cluster.uuid)}`) } useEffect(() => { if (!clusterId) return let cancelled = false setClusterDetails(null) setClusterNodesStatus('loading') setClusterNodesError('') void getClusterResource(`cluster-nodes/${encodeURIComponent(clusterId)}`) .then((response) => response.json() as Promise) .then((details) => { if (cancelled) return setClusterDetails(details) setClusterNodesStatus('success') }) .catch((error: unknown) => { if (cancelled) return setClusterNodesStatus('error') setClusterNodesError(error instanceof Error ? error.message : 'Failed to load cluster nodes') }) return () => { cancelled = true } }, [clusterId]) const loadNodeInterfaces = async () => { setNodesStatus('loading') setNodesError('') try { const nodeInterfacesResponse = await getNodeInterfaces() if (nodeInterfacesResponse.status !== 'success') { throw new Error( nodeInterfacesResponse.message ?? 'Failed to get node interfaces', ) } setNodes(nodeInterfacesResponse.nodes) setNodesStatus('success') } catch (error) { setNodesStatus('error') setNodesError( error instanceof Error ? error.message : 'Failed to get node interfaces', ) } } useEffect(() => { if (activePage === 'clusters') { void loadClusters() } if (activePage === 'dashboard' || activePage === 'nodes' || activePage === 'clusters') { void loadNodeInterfaces() } }, [activePage]) const selectPage = (page: PageId) => { navigate(`/${page}`) savePage(page) } const toggleTheme = () => { setTheme((currentTheme) => { const updatedTheme = currentTheme === 'light' ? 'dark' : 'light' saveTheme(updatedTheme) return updatedTheme }) } const handleTempButtonClick = async () => { setTokenStatus('loading') setTokenError('') try { await getSessionToken() setTokenStatus('success') await loadNodeInterfaces() } catch (error) { setTokenStatus('error') setTokenError( error instanceof Error ? error.message : 'Failed to get token', ) } } const handleRegisterNode = async ( event: React.FormEvent, ) => { event.preventDefault() const form = event.currentTarget setRegisterStatus('loading') setRegisterError('') const formData = new FormData(form) const payload: RegisterNodePayload = { node_name: String(formData.get('node_name') ?? ''), host: String(formData.get('host') ?? ''), port: Number(formData.get('port')), dbname: String(formData.get('dbname') ?? ''), user: String(formData.get('user') ?? ''), password: String(formData.get('password') ?? ''), } try { const registerResponse = await registerNode(payload) console.log('Node registration response:', registerResponse) if (registerResponse.status !== 'success') { throw new Error(registerResponse.message ?? 'Failed to register node') } setRegisterStatus('success') form.reset() setIsRegisterFormOpen(false) window.location.reload() } catch (error) { setRegisterStatus('error') setRegisterError( error instanceof Error ? error.message : 'Failed to register node', ) } } const handleDeleteNodeClick = async (node: NodeInterface) => { setDeletingNodeId(node.if_id) setNodesError('') try { const deleteResponse = await deleteNodeInterface(node) if (deleteResponse.status !== 'success') { throw new Error(deleteResponse.message ?? 'Failed to delete node') } await loadNodeInterfaces() } catch (error) { setNodesStatus('error') setNodesError( error instanceof Error ? error.message : 'Failed to delete node', ) } finally { setDeletingNodeId(null) } } const handleClusterNodeToggle = (nodeUuid: string, isSelected: boolean) => { setSelectedClusterNodeIds((currentNodeIds) => isSelected ? [...currentNodeIds, nodeUuid] : currentNodeIds.filter((currentNodeId) => currentNodeId !== nodeUuid), ) } const handleClusterDetailsNext = (event: React.FormEvent) => { event.preventDefault() const formData = new FormData(event.currentTarget) const name = String(formData.get('cluster_name') ?? '').trim() if (!name || selectedClusterNodeIds.length < 2) { setClusterStatus('error') setClusterError(!name ? 'Enter a cluster name' : 'Select at least two nodes for the cluster') return } console.log(JSON.stringify({ cluster_name: name, links: [] }, null, 2)) setClusterName(name) setConnectionNodeIndex(0) setClusterStatus('idle') setClusterError('') setClusterStep(2) } const handleConnectionNext = async () => { const payload: CreateClusterPayload = { cluster_name: clusterName, links: selectedClusterNodes.flatMap((node) => selectedClusterNodes .filter((target) => (nodeConnections[node.uuid] ?? []).includes(target.uuid)) .map((target): [string, string] => [node.if_name, target.if_name]), ), } if (connectionNodeIndex < selectedClusterNodes.length - 1) { console.log(JSON.stringify(payload, null, 2)) setConnectionNodeIndex((index) => index + 1) return } setClusterStatus('loading') setClusterError('') try { await createCluster(payload) setClusterStatus('success') setIsClusterFormOpen(false) await loadClusters() } catch (error) { setClusterStatus('error') setClusterError( error instanceof Error ? error.message : 'Failed to create cluster', ) } } return (
{clusterId && ( <> {clusterNodesStatus === 'loading' &&

Loading...

} {clusterNodesStatus === 'error' && (

{clusterNodesError}

)} )} {activePage === 'node-details' && clusterDetails && (

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

{nodeId && clusterDetails.nodes.some((node) => node.uuid === nodeId) && ( node.uuid === nodeId)!.node_name} /> )}
)} {activePage === 'largest-tables' && clusterDetails && (
{nodeId && clusterDetails.nodes.some((node) => node.uuid === nodeId) ? ( node.uuid === nodeId)!.node_name} detailed /> ) :

Node not found

}
)} {activePage === 'dead-tuples' && clusterDetails && (
{nodeId && clusterDetails.nodes.some((node) => node.uuid === nodeId) ? ( node.uuid === nodeId)!.node_name} detailed /> ) :

Node not found

}
)} {activePage === 'cluster-details' && clusterDetails && (

{clusterDetails.cluster_name}

Nodes

{clusterDetails.nodes.length > 0 ? (
    {clusterDetails.nodes.map((node) => (
  • ))}
) : (

No nodes found in this cluster.

)}
)} {activePage === 'dashboard' && (

Dashboard

Hardware usage

Live CPU, memory, and disk usage for each registered node.

{(nodesStatus === 'idle' || nodesStatus === 'loading') &&

Loading nodes…

} {nodesStatus === 'error' &&

{nodesError}

} {nodesStatus === 'success' && (nodes.length === 0 ?

No registered nodes. Add a node from the Nodes page to monitor hardware usage.

:
{nodes.map((node) => )}
)}
)} {activePage === 'nodes' && (

Nodes

{tokenStatus === 'success' && (

Token successfully stored.

)} {tokenStatus === 'error' && (

{tokenError}

)} {registerStatus === 'success' && (

Node registered successfully.

)} {registerStatus === 'error' && (

{registerError}

)} {nodesStatus === 'error' && (

{nodesError}

)} {isRegisterFormOpen ? (
) : null} {!isRegisterFormOpen && (
{nodesStatus === 'loading' ? (

Loading nodes...

) : nodes.length > 0 ? ( {nodes.map((node) => ( ))}
Node ID Node Name Host Port DB Name DB Username Actions
{node.if_nodeid} {node.if_name} {node.host} {node.port} {node.dbname} {node.user}
) : (

No nodes found.

)}
)}
)} {activePage === 'clusters' && (

Clusters

{!isClusterFormOpen && (
{clustersStatus === 'loading' &&

Loading clusters...

} {clustersStatus === 'error' && (

{clustersError}

)} {clustersStatus === 'success' && ( clusters.length > 0 ? (
    {clusters.map((cluster) => (
  • ))}
) :

No clusters found.

)} {clusterNodesStatus === 'loading' &&

Loading cluster nodes...

} {clusterNodesStatus === 'error' && (

{clusterNodesError}

)}
)} {isClusterFormOpen && (
  1. 1 Cluster name and nodes
  2. 2 Node connections
)} {clusterStatus === 'success' && (

Cluster created successfully.

)} {clusterStatus === 'error' && (

{clusterError}

)} {nodesStatus === 'error' && (

{nodesError}

)} {isClusterFormOpen && clusterStep === 2 && currentConnectionNode && (

Node {connectionNodeIndex + 1} of {selectedClusterNodes.length}

Connect {currentConnectionNode.if_name} to
{selectedClusterNodes .filter((node) => node.uuid !== currentConnectionNode.uuid) .map((node) => ( ))}
)} {isClusterFormOpen && clusterStep === 1 ? (
Nodes {nodesStatus === 'loading' ? (

Loading nodes...

) : nodes.length > 0 ? (
{nodes.map((node) => ( ))}
) : (

No nodes found.

)}
) : null}
)}
) } export default App