diff --git a/src/App.css b/src/App.css index 090697e..ffea33e 100644 --- a/src/App.css +++ b/src/App.css @@ -468,6 +468,37 @@ white-space: nowrap; } +.cluster-steps { + display: flex; + gap: 32px; + flex-wrap: wrap; + margin: 24px 0; + padding: 0; + list-style: none; +} + +.cluster-steps li { + display: flex; + align-items: center; + gap: 10px; + opacity: 0.5; +} + +.cluster-steps li[aria-current='step'] { + color: var(--text-h); + font-weight: 600; + opacity: 1; +} + +.cluster-step-number { + display: grid; + place-items: center; + width: 30px; + height: 30px; + border: 1px solid currentColor; + border-radius: 50%; +} + .communication-rules { min-width: 0; margin: 0; diff --git a/src/App.tsx b/src/App.tsx index c79802b..ced07c6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,6 @@ 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 CommunicationDirection = 'none' | 'forward' | 'reverse' | 'both' type RegisterNodePayload = { node_name: string @@ -56,16 +55,9 @@ type DeleteNodeInterfaceResponse = { type CreateClusterPayload = { cluster_name: string - nodes: string[] - connections: { - publisher_node: string - subscriber_node: string - }[] + links: [string, string][] } -const getCommunicationKey = (sourceUuid: string, targetUuid: string) => - `${sourceUuid}__${targetUuid}` - const getSavedTheme = (): Theme => { try { return localStorage.getItem('theme') === 'dark' ? 'dark' : 'light' @@ -86,6 +78,8 @@ 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' @@ -203,13 +197,34 @@ const deleteNodeInterface = async ( } const createCluster = async (payload: CreateClusterPayload) => { - console.log('Cluster creation payload:', payload) + console.log(JSON.stringify(payload, null, 2)) - await new Promise((resolve) => { - window.setTimeout(resolve, 500) + 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), }) - return { status: 'success' } + 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() { @@ -218,11 +233,13 @@ function App() { 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 [clusterCommunicationDirections, setClusterCommunicationDirections] = - 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') @@ -238,6 +255,8 @@ function App() { selectedClusterNodeIds.includes(node.uuid), ) + const currentConnectionNode = selectedClusterNodes[connectionNodeIndex] + const loadNodeInterfaces = async () => { setNodesStatus('loading') setNodesError('') @@ -369,81 +388,40 @@ function App() { : currentNodeIds.filter((currentNodeId) => currentNodeId !== nodeUuid), ) - if (!isSelected) { - setClusterCommunicationDirections((currentDirections) => - Object.fromEntries( - Object.entries(currentDirections).filter( - ([key]) => !key.includes(nodeUuid), - ), - ), - ) - } } - const handleClusterDirectionChange = ( - pairKey: string, - direction: CommunicationDirection, - ) => { - setClusterCommunicationDirections((currentDirections) => ({ - ...currentDirections, - [pairKey]: direction, - })) - } - - const handleCreateCluster = async ( - event: React.FormEvent, - ) => { + const handleClusterDetailsNext = (event: React.FormEvent) => { event.preventDefault() - const form = event.currentTarget - const formData = new FormData(form) - const connections = selectedClusterNodes.flatMap((sourceNode, sourceIndex) => - selectedClusterNodes.slice(sourceIndex + 1).flatMap((targetNode) => { - const pairKey = getCommunicationKey(sourceNode.uuid, targetNode.uuid) - const direction = clusterCommunicationDirections[pairKey] ?? 'both' + const formData = new FormData(event.currentTarget) + const name = String(formData.get('cluster_name') ?? '').trim() - if (direction === 'both') { - return [ - { - publisher_node: sourceNode.uuid, - subscriber_node: targetNode.uuid, - }, - { - publisher_node: targetNode.uuid, - subscriber_node: sourceNode.uuid, - }, - ] - } - - if (direction === 'forward') { - return [ - { - publisher_node: sourceNode.uuid, - subscriber_node: targetNode.uuid, - }, - ] - } - - if (direction === 'reverse') { - return [ - { - publisher_node: targetNode.uuid, - subscriber_node: sourceNode.uuid, - }, - ] - } - - return [] - }), - ) - const payload: CreateClusterPayload = { - cluster_name: String(formData.get('cluster_name') ?? ''), - nodes: selectedClusterNodeIds, - connections, + if (!name || selectedClusterNodeIds.length < 2) { + setClusterStatus('error') + setClusterError(!name ? 'Enter a cluster name' : 'Select at least two nodes for the cluster') + return } - if (payload.nodes.length < 2) { - setClusterStatus('error') - setClusterError('Select at least two nodes for the cluster') + 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 } @@ -451,16 +429,9 @@ function App() { setClusterError('') try { - const clusterResponse = await createCluster(payload) - - if (clusterResponse.status !== 'success') { - throw new Error('Failed to create cluster') - } + await createCluster(payload) setClusterStatus('success') - form.reset() - setSelectedClusterNodeIds([]) - setClusterCommunicationDirections({}) setIsClusterFormOpen(false) } catch (error) { setClusterStatus('error') @@ -701,10 +672,11 @@ function App() { className="primary-button" onClick={() => { setIsClusterFormOpen(true) + setClusterStep(1) setClusterStatus('idle') setClusterError('') setSelectedClusterNodeIds([]) - setClusterCommunicationDirections({}) + setNodeConnections({}) }} disabled={clusterStatus === 'loading'} > @@ -712,6 +684,19 @@ function App() { + {isClusterFormOpen && ( +
    +
  1. + 1 + Cluster name and nodes +
  2. +
  3. + 2 + Node connections +
  4. +
+ )} + {clusterStatus === 'success' && (

Cluster created successfully.

)} @@ -724,8 +709,52 @@ function App() {

{nodesError}

)} - {isClusterFormOpen ? ( -
+ {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 ? ( +
@@ -864,7 +801,7 @@ function App() { onClick={() => { setIsClusterFormOpen(false) setSelectedClusterNodeIds([]) - setClusterCommunicationDirections({}) + setNodeConnections({}) }} > Cancel @@ -874,7 +811,7 @@ function App() { className="primary-button" disabled={clusterStatus === 'loading'} > - {clusterStatus === 'loading' ? 'Creating' : 'Create'} + {clusterStatus === 'loading' ? 'Continuing' : 'Next'}