From 2f677c36b3c87075a0d561e49a787c758c5c42c5 Mon Sep 17 00:00:00 2001 From: George Date: Wed, 16 Sep 2026 15:57:04 +0530 Subject: [PATCH] feat: --Added cluster view page --Added cluster node view page --- src/App.css | 37 +++++++++++++++ src/App.tsx | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 164 insertions(+), 1 deletion(-) diff --git a/src/App.css b/src/App.css index ffea33e..c018f89 100644 --- a/src/App.css +++ b/src/App.css @@ -468,6 +468,43 @@ white-space: nowrap; } +.cluster-list { + display: grid; + gap: 10px; + padding: 0; + list-style: none; +} + +.cluster-node-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr)); + gap: 14px; + padding: 0; + list-style: none; +} + +.cluster-node-card { + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + color: var(--text-h); + padding: 20px; + font-weight: 600; + overflow-wrap: anywhere; +} + +.cluster-list .secondary-button { + width: 100%; + justify-content: flex-start; + text-align: left; + overflow-wrap: anywhere; +} + +.cluster-list .secondary-button[aria-pressed='true'] { + border-color: var(--accent); + background: var(--accent-bg); +} + .cluster-steps { display: flex; gap: 32px; diff --git a/src/App.tsx b/src/App.tsx index ced07c6..cd5375a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,13 +9,39 @@ const navItems = [ { id: 'clusters', label: 'Clusters' }, ] as const -type PageId = (typeof navItems)[number]['id'] +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 @@ -250,6 +276,13 @@ function App() { 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), @@ -257,6 +290,36 @@ function App() { 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 = async (cluster: Cluster) => { + setSelectedCluster(cluster) + setClusterNodesStatus('loading') + setClusterNodesError('') + + try { + const response = await getClusterResource(`cluster-nodes/${encodeURIComponent(cluster.uuid)}`) + setClusterDetails(await response.json() as ClusterNodesResponse) + setClusterNodesStatus('success') + selectPage('cluster-details') + } catch (error) { + setClusterNodesStatus('error') + setClusterNodesError(error instanceof Error ? error.message : 'Failed to load cluster nodes') + } + } + const loadNodeInterfaces = async () => { setNodesStatus('loading') setNodesError('') @@ -283,6 +346,9 @@ function App() { } useEffect(() => { + if (activePage === 'clusters') { + void loadClusters() + } if (activePage === 'nodes' || activePage === 'clusters') { void loadNodeInterfaces() } @@ -433,6 +499,7 @@ function App() { setClusterStatus('success') setIsClusterFormOpen(false) + await loadClusters() } catch (error) { setClusterStatus('error') setClusterError( @@ -494,6 +561,33 @@ function App() {
+ {activePage === 'cluster-details' && clusterDetails && ( +
+
+

{clusterDetails.cluster_name}

+ +
+

Nodes

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

No nodes found in this cluster.

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

Dashboard

@@ -684,6 +778,38 @@ function App() { + {!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 && (