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;
|
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 {
|
.cluster-steps {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 32px;
|
gap: 32px;
|
||||||
|
|||||||
+127
-1
@@ -9,13 +9,39 @@ const navItems = [
|
|||||||
{ id: 'clusters', label: 'Clusters' },
|
{ id: 'clusters', label: 'Clusters' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
type PageId = (typeof navItems)[number]['id']
|
type PageId = (typeof navItems)[number]['id'] | 'cluster-details'
|
||||||
type Theme = 'light' | 'dark'
|
type Theme = 'light' | 'dark'
|
||||||
type TokenStatus = 'idle' | 'loading' | 'success' | 'error'
|
type TokenStatus = 'idle' | 'loading' | 'success' | 'error'
|
||||||
type RegisterStatus = 'idle' | 'loading' | 'success' | 'error'
|
type RegisterStatus = 'idle' | 'loading' | 'success' | 'error'
|
||||||
type NodesStatus = 'idle' | 'loading' | 'success' | 'error'
|
type NodesStatus = 'idle' | 'loading' | 'success' | 'error'
|
||||||
type ClusterStatus = '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 = {
|
type RegisterNodePayload = {
|
||||||
node_name: string
|
node_name: string
|
||||||
host: string
|
host: string
|
||||||
@@ -250,6 +276,13 @@ function App() {
|
|||||||
const [deletingNodeId, setDeletingNodeId] = useState<number | null>(null)
|
const [deletingNodeId, setDeletingNodeId] = useState<number | null>(null)
|
||||||
const [clusterStatus, setClusterStatus] = useState<ClusterStatus>('idle')
|
const [clusterStatus, setClusterStatus] = useState<ClusterStatus>('idle')
|
||||||
const [clusterError, setClusterError] = useState('')
|
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 nextTheme = theme === 'light' ? 'dark' : 'light'
|
||||||
const selectedClusterNodes = nodes.filter((node) =>
|
const selectedClusterNodes = nodes.filter((node) =>
|
||||||
selectedClusterNodeIds.includes(node.uuid),
|
selectedClusterNodeIds.includes(node.uuid),
|
||||||
@@ -257,6 +290,36 @@ function App() {
|
|||||||
|
|
||||||
const currentConnectionNode = selectedClusterNodes[connectionNodeIndex]
|
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 () => {
|
const loadNodeInterfaces = async () => {
|
||||||
setNodesStatus('loading')
|
setNodesStatus('loading')
|
||||||
setNodesError('')
|
setNodesError('')
|
||||||
@@ -283,6 +346,9 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (activePage === 'clusters') {
|
||||||
|
void loadClusters()
|
||||||
|
}
|
||||||
if (activePage === 'nodes' || activePage === 'clusters') {
|
if (activePage === 'nodes' || activePage === 'clusters') {
|
||||||
void loadNodeInterfaces()
|
void loadNodeInterfaces()
|
||||||
}
|
}
|
||||||
@@ -433,6 +499,7 @@ function App() {
|
|||||||
|
|
||||||
setClusterStatus('success')
|
setClusterStatus('success')
|
||||||
setIsClusterFormOpen(false)
|
setIsClusterFormOpen(false)
|
||||||
|
await loadClusters()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setClusterStatus('error')
|
setClusterStatus('error')
|
||||||
setClusterError(
|
setClusterError(
|
||||||
@@ -494,6 +561,33 @@ function App() {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className="main-content">
|
<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' && (
|
{activePage === 'dashboard' && (
|
||||||
<section className="content-panel" id="dashboard">
|
<section className="content-panel" id="dashboard">
|
||||||
<p className="eyebrow">Dashboard</p>
|
<p className="eyebrow">Dashboard</p>
|
||||||
@@ -684,6 +778,38 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 && (
|
{isClusterFormOpen && (
|
||||||
<ol className="cluster-steps" aria-label="Cluster creation progress">
|
<ol className="cluster-steps" aria-label="Cluster creation progress">
|
||||||
<li aria-current={clusterStep === 1 ? 'step' : undefined}>
|
<li aria-current={clusterStep === 1 ? 'step' : undefined}>
|
||||||
|
|||||||
Reference in new issue
Block a user