import { useEffect, useState } from 'react' import './App.css' 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'] type Theme = 'light' | 'dark' type TokenStatus = 'idle' | 'loading' | 'success' | 'error' type RegisterStatus = 'idle' | 'loading' | 'success' | 'error' type NodesStatus = 'idle' | 'loading' | 'success' | 'error' 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 } 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') 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 } function App() { const [isCollapsed, setIsCollapsed] = useState(false) const [activePage, setActivePage] = useState(getSavedPage) const [theme, setTheme] = useState(getSavedTheme) const [isRegisterFormOpen, setIsRegisterFormOpen] = useState(false) 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 nextTheme = theme === 'light' ? 'dark' : 'light' 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 === 'nodes') { void loadNodeInterfaces() } }, [activePage]) const selectPage = (page: PageId) => { setActivePage(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) } } return (
{activePage === 'dashboard' && (

Dashboard

Welcome back

Your main workspace is ready. The left navigation can collapse now, and more sections can be added to the nav list as the app grows.

)} {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

)}
) } export default App