feat:
--Added cluster view page --Added cluster node view page
This commit is contained in:
1 parent
0832a2318a
commit
2f677c36b3
2 files changed
+164
-1
No files matched your search
+37
@@ -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;
|
||||
|
||||
+127
-1
@@ -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<number | null>(null)
|
||||
const [clusterStatus, setClusterStatus] = useState<ClusterStatus>('idle')
|
||||
const [clusterError, setClusterError] = useState('')
|
||||
const [clusters, setClusters] = useState<Cluster[]>([])
|
||||
const [clustersStatus, setClustersStatus] = useState<ClusterStatus>('idle')
|
||||
const [clustersError, setClustersError] = useState('')
|
||||
const [clusterNodesStatus, setClusterNodesStatus] = useState<ClusterStatus>('idle')
|
||||
const [clusterNodesError, setClusterNodesError] = useState('')
|
||||
const [selectedCluster, setSelectedCluster] = useState<Cluster | null>(null)
|
||||
const [clusterDetails, setClusterDetails] = useState<ClusterNodesResponse | null>(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() {
|
||||
</aside>
|
||||
|
||||
<main className="main-content">
|
||||
{activePage === 'cluster-details' && clusterDetails && (
|
||||
<section className="content-panel" id="cluster-details">
|
||||
<div className="page-header">
|
||||
<h1>{clusterDetails.cluster_name}</h1>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => selectPage('clusters')}
|
||||
>
|
||||
Back to clusters
|
||||
</button>
|
||||
</div>
|
||||
<h2>Nodes</h2>
|
||||
{clusterDetails.nodes.length > 0 ? (
|
||||
<ul className="cluster-node-list" aria-label="Cluster nodes">
|
||||
{clusterDetails.nodes.map((node) => (
|
||||
<li className="cluster-node-card" key={node.uuid}>
|
||||
{node.node_name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>No nodes found in this cluster.</p>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activePage === 'dashboard' && (
|
||||
<section className="content-panel" id="dashboard">
|
||||
<p className="eyebrow">Dashboard</p>
|
||||
@@ -684,6 +778,38 @@ function App() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!isClusterFormOpen && (
|
||||
<div>
|
||||
{clustersStatus === 'loading' && <p role="status">Loading clusters...</p>}
|
||||
{clustersStatus === 'error' && (
|
||||
<p className="status-message error" role="alert">{clustersError}</p>
|
||||
)}
|
||||
{clustersStatus === 'success' && (
|
||||
clusters.length > 0 ? (
|
||||
<ul className="cluster-list">
|
||||
{clusters.map((cluster) => (
|
||||
<li key={cluster.uuid}>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => void handleClusterClick(cluster)}
|
||||
disabled={clusterNodesStatus === 'loading'}
|
||||
aria-pressed={selectedCluster?.uuid === cluster.uuid}
|
||||
>
|
||||
{cluster.cluster_name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : <p>No clusters found.</p>
|
||||
)}
|
||||
{clusterNodesStatus === 'loading' && <p role="status">Loading cluster nodes...</p>}
|
||||
{clusterNodesStatus === 'error' && (
|
||||
<p className="status-message error" role="alert">{clusterNodesError}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isClusterFormOpen && (
|
||||
<ol className="cluster-steps" aria-label="Cluster creation progress">
|
||||
<li aria-current={clusterStep === 1 ? 'step' : undefined}>
|
||||
|
||||
Reference in new issue
Block a user