--Added UI feature for adding connections
This commit is contained in:
george committed 2026-09-15 17:51:08 +05:30
1 parent 95bb3cd050
commit 0832a2318a
2 files changed
+157 -189

No files matched your search

+31
View File
@@ -468,6 +468,37 @@
white-space: nowrap; 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 { .communication-rules {
min-width: 0; min-width: 0;
margin: 0; margin: 0;
+126 -189
View File
@@ -15,7 +15,6 @@ 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 CommunicationDirection = 'none' | 'forward' | 'reverse' | 'both'
type RegisterNodePayload = { type RegisterNodePayload = {
node_name: string node_name: string
@@ -56,16 +55,9 @@ type DeleteNodeInterfaceResponse = {
type CreateClusterPayload = { type CreateClusterPayload = {
cluster_name: string cluster_name: string
nodes: string[] links: [string, string][]
connections: {
publisher_node: string
subscriber_node: string
}[]
} }
const getCommunicationKey = (sourceUuid: string, targetUuid: string) =>
`${sourceUuid}__${targetUuid}`
const getSavedTheme = (): Theme => { const getSavedTheme = (): Theme => {
try { try {
return localStorage.getItem('theme') === 'dark' ? 'dark' : 'light' return localStorage.getItem('theme') === 'dark' ? 'dark' : 'light'
@@ -86,6 +78,8 @@ const getSavedPage = (): PageId => {
try { try {
const savedPage = localStorage.getItem('activePage') const savedPage = localStorage.getItem('activePage')
if (savedPage === 'cluster-details') return 'clusters'
return navItems.some((item) => item.id === savedPage) return navItems.some((item) => item.id === savedPage)
? (savedPage as PageId) ? (savedPage as PageId)
: 'dashboard' : 'dashboard'
@@ -203,13 +197,34 @@ const deleteNodeInterface = async (
} }
const createCluster = async (payload: CreateClusterPayload) => { const createCluster = async (payload: CreateClusterPayload) => {
console.log('Cluster creation payload:', payload) console.log(JSON.stringify(payload, null, 2))
await new Promise((resolve) => { const accessToken = localStorage.getItem('access_token')
window.setTimeout(resolve, 500) const headers: Record<string, string> = {
'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() { function App() {
@@ -218,11 +233,13 @@ function App() {
const [theme, setTheme] = useState<Theme>(getSavedTheme) const [theme, setTheme] = useState<Theme>(getSavedTheme)
const [isRegisterFormOpen, setIsRegisterFormOpen] = useState(false) const [isRegisterFormOpen, setIsRegisterFormOpen] = useState(false)
const [isClusterFormOpen, setIsClusterFormOpen] = useState(false) const [isClusterFormOpen, setIsClusterFormOpen] = useState(false)
const [clusterStep, setClusterStep] = useState<1 | 2>(1)
const [selectedClusterNodeIds, setSelectedClusterNodeIds] = useState<string[]>( const [selectedClusterNodeIds, setSelectedClusterNodeIds] = useState<string[]>(
[], [],
) )
const [clusterCommunicationDirections, setClusterCommunicationDirections] = const [clusterName, setClusterName] = useState('')
useState<Record<string, CommunicationDirection>>({}) const [connectionNodeIndex, setConnectionNodeIndex] = useState(0)
const [nodeConnections, setNodeConnections] = useState<Record<string, string[]>>({})
const [tokenStatus, setTokenStatus] = useState<TokenStatus>('idle') const [tokenStatus, setTokenStatus] = useState<TokenStatus>('idle')
const [tokenError, setTokenError] = useState('') const [tokenError, setTokenError] = useState('')
const [registerStatus, setRegisterStatus] = useState<RegisterStatus>('idle') const [registerStatus, setRegisterStatus] = useState<RegisterStatus>('idle')
@@ -238,6 +255,8 @@ function App() {
selectedClusterNodeIds.includes(node.uuid), selectedClusterNodeIds.includes(node.uuid),
) )
const currentConnectionNode = selectedClusterNodes[connectionNodeIndex]
const loadNodeInterfaces = async () => { const loadNodeInterfaces = async () => {
setNodesStatus('loading') setNodesStatus('loading')
setNodesError('') setNodesError('')
@@ -369,81 +388,40 @@ function App() {
: currentNodeIds.filter((currentNodeId) => currentNodeId !== nodeUuid), : currentNodeIds.filter((currentNodeId) => currentNodeId !== nodeUuid),
) )
if (!isSelected) {
setClusterCommunicationDirections((currentDirections) =>
Object.fromEntries(
Object.entries(currentDirections).filter(
([key]) => !key.includes(nodeUuid),
),
),
)
}
} }
const handleClusterDirectionChange = ( const handleClusterDetailsNext = (event: React.FormEvent<HTMLFormElement>) => {
pairKey: string,
direction: CommunicationDirection,
) => {
setClusterCommunicationDirections((currentDirections) => ({
...currentDirections,
[pairKey]: direction,
}))
}
const handleCreateCluster = async (
event: React.FormEvent<HTMLFormElement>,
) => {
event.preventDefault() event.preventDefault()
const form = event.currentTarget const formData = new FormData(event.currentTarget)
const formData = new FormData(form) const name = String(formData.get('cluster_name') ?? '').trim()
const connections = selectedClusterNodes.flatMap((sourceNode, sourceIndex) =>
selectedClusterNodes.slice(sourceIndex + 1).flatMap((targetNode) => {
const pairKey = getCommunicationKey(sourceNode.uuid, targetNode.uuid)
const direction = clusterCommunicationDirections[pairKey] ?? 'both'
if (direction === 'both') { if (!name || selectedClusterNodeIds.length < 2) {
return [ setClusterStatus('error')
{ setClusterError(!name ? 'Enter a cluster name' : 'Select at least two nodes for the cluster')
publisher_node: sourceNode.uuid, return
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 (payload.nodes.length < 2) { console.log(JSON.stringify({ cluster_name: name, links: [] }, null, 2))
setClusterStatus('error') setClusterName(name)
setClusterError('Select at least two nodes for the cluster') 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 return
} }
@@ -451,16 +429,9 @@ function App() {
setClusterError('') setClusterError('')
try { try {
const clusterResponse = await createCluster(payload) await createCluster(payload)
if (clusterResponse.status !== 'success') {
throw new Error('Failed to create cluster')
}
setClusterStatus('success') setClusterStatus('success')
form.reset()
setSelectedClusterNodeIds([])
setClusterCommunicationDirections({})
setIsClusterFormOpen(false) setIsClusterFormOpen(false)
} catch (error) { } catch (error) {
setClusterStatus('error') setClusterStatus('error')
@@ -701,10 +672,11 @@ function App() {
className="primary-button" className="primary-button"
onClick={() => { onClick={() => {
setIsClusterFormOpen(true) setIsClusterFormOpen(true)
setClusterStep(1)
setClusterStatus('idle') setClusterStatus('idle')
setClusterError('') setClusterError('')
setSelectedClusterNodeIds([]) setSelectedClusterNodeIds([])
setClusterCommunicationDirections({}) setNodeConnections({})
}} }}
disabled={clusterStatus === 'loading'} disabled={clusterStatus === 'loading'}
> >
@@ -712,6 +684,19 @@ function App() {
</button> </button>
</div> </div>
{isClusterFormOpen && (
<ol className="cluster-steps" aria-label="Cluster creation progress">
<li aria-current={clusterStep === 1 ? 'step' : undefined}>
<span className="cluster-step-number">1</span>
Cluster name and nodes
</li>
<li aria-current={clusterStep === 2 ? 'step' : undefined}>
<span className="cluster-step-number">2</span>
Node connections
</li>
</ol>
)}
{clusterStatus === 'success' && ( {clusterStatus === 'success' && (
<p className="status-message success">Cluster created successfully.</p> <p className="status-message success">Cluster created successfully.</p>
)} )}
@@ -724,8 +709,52 @@ function App() {
<p className="status-message error">{nodesError}</p> <p className="status-message error">{nodesError}</p>
)} )}
{isClusterFormOpen ? ( {isClusterFormOpen && clusterStep === 2 && currentConnectionNode && (
<form className="register-form" onSubmit={handleCreateCluster}> <div className="register-form">
<p className="eyebrow">
Node {connectionNodeIndex + 1} of {selectedClusterNodes.length}
</p>
<fieldset className="form-field node-picker" key={currentConnectionNode.uuid}>
<legend>Connect {currentConnectionNode.if_name} to</legend>
<div className="node-choice-list">
{selectedClusterNodes
.filter((node) => node.uuid !== currentConnectionNode.uuid)
.map((node) => (
<label className="node-choice" key={node.uuid}>
<input
type="checkbox"
checked={(nodeConnections[currentConnectionNode.uuid] ?? []).includes(node.uuid)}
disabled={clusterStatus === 'loading'}
onChange={(event) => {
const checked = event.currentTarget.checked
setNodeConnections((current) => ({
...current,
[currentConnectionNode.uuid]: checked
? [...(current[currentConnectionNode.uuid] ?? []), node.uuid]
: (current[currentConnectionNode.uuid] ?? []).filter((uuid) => uuid !== node.uuid),
}))
}}
/>
<span>{node.if_name}</span>
</label>
))}
</div>
</fieldset>
<div className="form-actions">
<button
type="button"
className="primary-button"
disabled={clusterStatus === 'loading'}
onClick={() => void handleConnectionNext()}
>
{clusterStatus === 'loading' ? 'Creating' : connectionNodeIndex === selectedClusterNodes.length - 1 ? 'Finish' : 'Next'}
</button>
</div>
</div>
)}
{isClusterFormOpen && clusterStep === 1 ? (
<form className="register-form" onSubmit={handleClusterDetailsNext}>
<div className="form-grid cluster-form-grid"> <div className="form-grid cluster-form-grid">
<label className="form-field"> <label className="form-field">
<span>Cluster Name</span> <span>Cluster Name</span>
@@ -763,98 +792,6 @@ function App() {
)} )}
</fieldset> </fieldset>
{selectedClusterNodes.length > 1 ? (
<fieldset className="form-field communication-rules">
<legend>Communication</legend>
<div className="communication-list">
{selectedClusterNodes.map((sourceNode, sourceIndex) =>
selectedClusterNodes
.slice(sourceIndex + 1)
.map((targetNode) => {
const pairKey = getCommunicationKey(
sourceNode.uuid,
targetNode.uuid,
)
const direction =
clusterCommunicationDirections[pairKey] ??
'both'
return (
<div className="communication-card" key={pairKey}>
<span className="communication-title">
{sourceNode.if_name} - {targetNode.if_name}
</span>
<div className="direction-options">
<label>
<input
name={`direction-${pairKey}`}
type="radio"
checked={direction === 'both'}
onChange={() =>
handleClusterDirectionChange(
pairKey,
'both',
)
}
/>
<span>Both directions</span>
</label>
<label>
<input
name={`direction-${pairKey}`}
type="radio"
checked={direction === 'forward'}
onChange={() =>
handleClusterDirectionChange(
pairKey,
'forward',
)
}
/>
<span>
{sourceNode.if_name} to{' '}
{targetNode.if_name}
</span>
</label>
<label>
<input
name={`direction-${pairKey}`}
type="radio"
checked={direction === 'reverse'}
onChange={() =>
handleClusterDirectionChange(
pairKey,
'reverse',
)
}
/>
<span>
{targetNode.if_name} to{' '}
{sourceNode.if_name}
</span>
</label>
<label>
<input
name={`direction-${pairKey}`}
type="radio"
checked={direction === 'none'}
onChange={() =>
handleClusterDirectionChange(
pairKey,
'none',
)
}
/>
<span>No publishing</span>
</label>
</div>
</div>
)
}),
)}
</div>
</fieldset>
) : null}
</div> </div>
<div className="form-actions"> <div className="form-actions">
@@ -864,7 +801,7 @@ function App() {
onClick={() => { onClick={() => {
setIsClusterFormOpen(false) setIsClusterFormOpen(false)
setSelectedClusterNodeIds([]) setSelectedClusterNodeIds([])
setClusterCommunicationDirections({}) setNodeConnections({})
}} }}
> >
Cancel Cancel
@@ -874,7 +811,7 @@ function App() {
className="primary-button" className="primary-button"
disabled={clusterStatus === 'loading'} disabled={clusterStatus === 'loading'}
> >
{clusterStatus === 'loading' ? 'Creating' : 'Create'} {clusterStatus === 'loading' ? 'Continuing' : 'Next'}
</button> </button>
</div> </div>
</form> </form>