--Added form and node connection in create cluster
This commit is contained in:
george committed 2026-09-11 16:46:02 +05:30
1 parent 9d58606491
commit 95bb3cd050
2 files changed
+414 -9

No files matched your search

+129
View File
@@ -416,6 +416,127 @@
color: var(--text);
}
.cluster-form-grid {
grid-template-columns: 1fr;
}
.node-picker {
min-width: 0;
margin: 0;
border: 0;
padding: 0;
}
.node-picker legend {
margin-bottom: 8px;
color: var(--text-h);
font-size: 14px;
font-weight: 600;
}
.node-choice-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.node-choice {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
min-height: 40px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: var(--text-h);
font-size: 15px;
padding: 0 12px;
}
.node-choice input {
width: 16px;
height: 16px;
flex: 0 0 16px;
margin: 0;
accent-color: var(--accent);
}
.node-choice span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.communication-rules {
min-width: 0;
margin: 0;
border: 0;
padding: 0;
}
.communication-rules legend {
margin-bottom: 8px;
color: var(--text-h);
font-size: 14px;
font-weight: 600;
}
.communication-list {
display: grid;
gap: 12px;
}
.communication-card {
display: grid;
gap: 12px;
min-width: 0;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
padding: 14px;
}
.communication-title {
overflow: hidden;
color: var(--text-h);
font-size: 15px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.direction-options {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.direction-options label {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
color: var(--text-h);
font-size: 14px;
}
.direction-options input {
width: 15px;
height: 15px;
flex: 0 0 15px;
margin: 0;
accent-color: var(--accent);
}
.direction-options span {
overflow: hidden;
font-size: 14px;
font-weight: 500;
text-overflow: ellipsis;
white-space: nowrap;
}
.form-actions {
display: flex;
justify-content: flex-end;
@@ -504,4 +625,12 @@
.form-grid {
grid-template-columns: 1fr;
}
.node-choice-list {
grid-template-columns: 1fr;
}
.direction-options {
grid-template-columns: 1fr;
}
}
+285 -9
View File
@@ -15,6 +15,7 @@ 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
@@ -53,6 +54,18 @@ type DeleteNodeInterfaceResponse = {
message?: string
}
type CreateClusterPayload = {
cluster_name: string
nodes: string[]
connections: {
publisher_node: string
subscriber_node: string
}[]
}
const getCommunicationKey = (sourceUuid: string, targetUuid: string) =>
`${sourceUuid}__${targetUuid}`
const getSavedTheme = (): Theme => {
try {
return localStorage.getItem('theme') === 'dark' ? 'dark' : 'light'
@@ -189,7 +202,9 @@ const deleteNodeInterface = async (
return response.json() as Promise<DeleteNodeInterfaceResponse>
}
const createCluster = async () => {
const createCluster = async (payload: CreateClusterPayload) => {
console.log('Cluster creation payload:', payload)
await new Promise((resolve) => {
window.setTimeout(resolve, 500)
})
@@ -202,6 +217,12 @@ function App() {
const [activePage, setActivePage] = useState<PageId>(getSavedPage)
const [theme, setTheme] = useState<Theme>(getSavedTheme)
const [isRegisterFormOpen, setIsRegisterFormOpen] = useState(false)
const [isClusterFormOpen, setIsClusterFormOpen] = useState(false)
const [selectedClusterNodeIds, setSelectedClusterNodeIds] = useState<string[]>(
[],
)
const [clusterCommunicationDirections, setClusterCommunicationDirections] =
useState<Record<string, CommunicationDirection>>({})
const [tokenStatus, setTokenStatus] = useState<TokenStatus>('idle')
const [tokenError, setTokenError] = useState('')
const [registerStatus, setRegisterStatus] = useState<RegisterStatus>('idle')
@@ -213,6 +234,9 @@ function App() {
const [clusterStatus, setClusterStatus] = useState<ClusterStatus>('idle')
const [clusterError, setClusterError] = useState('')
const nextTheme = theme === 'light' ? 'dark' : 'light'
const selectedClusterNodes = nodes.filter((node) =>
selectedClusterNodeIds.includes(node.uuid),
)
const loadNodeInterfaces = async () => {
setNodesStatus('loading')
@@ -240,7 +264,7 @@ function App() {
}
useEffect(() => {
if (activePage === 'nodes') {
if (activePage === 'nodes' || activePage === 'clusters') {
void loadNodeInterfaces()
}
}, [activePage])
@@ -338,18 +362,106 @@ function App() {
}
}
const handleCreateClusterClick = async () => {
const handleClusterNodeToggle = (nodeUuid: string, isSelected: boolean) => {
setSelectedClusterNodeIds((currentNodeIds) =>
isSelected
? [...currentNodeIds, nodeUuid]
: 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<HTMLFormElement>,
) => {
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'
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 (payload.nodes.length < 2) {
setClusterStatus('error')
setClusterError('Select at least two nodes for the cluster')
return
}
setClusterStatus('loading')
setClusterError('')
try {
const clusterResponse = await createCluster()
const clusterResponse = await createCluster(payload)
if (clusterResponse.status !== 'success') {
throw new Error('Failed to create cluster')
}
setClusterStatus('success')
form.reset()
setSelectedClusterNodeIds([])
setClusterCommunicationDirections({})
setIsClusterFormOpen(false)
} catch (error) {
setClusterStatus('error')
setClusterError(
@@ -587,22 +699,186 @@ function App() {
<button
type="button"
className="primary-button"
onClick={handleCreateClusterClick}
onClick={() => {
setIsClusterFormOpen(true)
setClusterStatus('idle')
setClusterError('')
setSelectedClusterNodeIds([])
setClusterCommunicationDirections({})
}}
disabled={clusterStatus === 'loading'}
>
{clusterStatus === 'loading'
? 'Creating Cluster'
: 'Create Cluster'}
Create Cluster
</button>
</div>
{clusterStatus === 'success' && (
<p className="status-message success">Dummy API triggered.</p>
<p className="status-message success">Cluster created successfully.</p>
)}
{clusterStatus === 'error' && (
<p className="status-message error">{clusterError}</p>
)}
{nodesStatus === 'error' && (
<p className="status-message error">{nodesError}</p>
)}
{isClusterFormOpen ? (
<form className="register-form" onSubmit={handleCreateCluster}>
<div className="form-grid cluster-form-grid">
<label className="form-field">
<span>Cluster Name</span>
<input name="cluster_name" type="text" required />
</label>
<fieldset className="form-field node-picker">
<legend>Nodes</legend>
{nodesStatus === 'loading' ? (
<p className="table-state">Loading nodes...</p>
) : nodes.length > 0 ? (
<div className="node-choice-list">
{nodes.map((node) => (
<label className="node-choice" key={node.if_id}>
<input
name="nodes"
type="checkbox"
value={node.uuid}
checked={selectedClusterNodeIds.includes(
node.uuid,
)}
onChange={(event) =>
handleClusterNodeToggle(
node.uuid,
event.currentTarget.checked,
)
}
/>
<span>{node.if_name}</span>
</label>
))}
</div>
) : (
<p className="table-state">No nodes found.</p>
)}
</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 className="form-actions">
<button
type="button"
className="secondary-button"
onClick={() => {
setIsClusterFormOpen(false)
setSelectedClusterNodeIds([])
setClusterCommunicationDirections({})
}}
>
Cancel
</button>
<button
type="submit"
className="primary-button"
disabled={clusterStatus === 'loading'}
>
{clusterStatus === 'loading' ? 'Creating' : 'Create'}
</button>
</div>
</form>
) : null}
</section>
)}
</main>