From 35bc44bc3295f37bc614d63492f634a00cb2c8e4 Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Sun, 9 Nov 2025 00:42:38 +0000 Subject: [PATCH] Simplify detection UI. --- src/components/ClusterNodesComponent.tsx | 133 ++++++++++------------- src/components/nodes/NodeForm.tsx | 119 ++++++++++---------- src/components/nodes/NodeFormDrawer.tsx | 1 - src/router/pages/ClusterHealthPage.tsx | 4 +- src/router/pages/DashboardPage.tsx | 2 +- src/services/api/types/cluster.ts | 2 +- 6 files changed, 123 insertions(+), 138 deletions(-) diff --git a/src/components/ClusterNodesComponent.tsx b/src/components/ClusterNodesComponent.tsx index 6ab7d4f..59a9133 100644 --- a/src/components/ClusterNodesComponent.tsx +++ b/src/components/ClusterNodesComponent.tsx @@ -24,7 +24,6 @@ export function ClusterNodesComponent() { addNode, addError, deleteNode, - isDeleting, deleteError, discover, isDiscovering, @@ -50,7 +49,6 @@ export function ClusterNodesComponent() { const { data: clusterStatusData } = useClusterStatus(currentInstance || ''); - const [discoverSubnet, setDiscoverSubnet] = useState(''); const [addNodeIp, setAddNodeIp] = useState(''); const [discoverError, setDiscoverError] = useState(null); const [detectError, setDetectError] = useState(null); @@ -182,7 +180,6 @@ export function ClusterNodesComponent() { role: data.role, disk: data.disk, target_ip: data.targetIp, - current_ip: data.currentIp, interface: data.interface, schematic_id: data.schematicId, maintenance: data.maintenance, @@ -198,9 +195,7 @@ export function ClusterNodesComponent() { nodeName: drawerState.node.hostname, updates: { role: data.role, - disk: data.disk, target_ip: data.targetIp, - current_ip: data.currentIp, interface: data.interface, schematic_id: data.schematicId, maintenance: data.maintenance, @@ -231,8 +226,8 @@ export function ClusterNodesComponent() { const handleDiscover = () => { setDiscoverError(null); setDiscoverSuccess(null); - // Pass subnet only if it's not empty, otherwise auto-detect - discover(discoverSubnet || undefined); + // Always use auto-detect to scan all local networks + discover(undefined); }; @@ -268,7 +263,9 @@ export function ClusterNodesComponent() { // Check if cluster is already bootstrapped using cluster status // The backend checks for kubeconfig existence and cluster connectivity - const hasBootstrapped = clusterStatus?.ready === true; + // Status is "not_bootstrapped" when kubeconfig doesn't exist + // Any other status (ready, degraded, unreachable) means cluster is bootstrapped + const hasBootstrapped = clusterStatus?.status !== 'not_bootstrapped'; return hasReadyControlPlane && !hasBootstrapped; }, [assignedNodes, clusterStatus]); @@ -433,26 +430,21 @@ export function ClusterNodesComponent() { )} - {/* DISCOVERY SECTION - Scan subnet for nodes */} + {/* ADD NODES SECTION - Discovery and manual add combined */}

- Discover Nodes on Network + Add Nodes to Cluster

- Scan a specific subnet or leave empty to auto-detect all local networks + Discover nodes on the network or manually add by IP address

-
- setDiscoverSubnet(e.target.value)} - placeholder="192.168.1.0/24 (optional - leave empty to auto-detect)" - className="flex-1" - /> + {/* Discovery button */} +
{(isDiscovering || discoveryStatus?.active) && ( @@ -475,69 +467,60 @@ export function ClusterNodesComponent() { )}
+ {/* Discovered nodes display */} {discoveryStatus?.nodes_found && discoveryStatus.nodes_found.length > 0 && ( -
-

- Discovered {discoveryStatus.nodes_found.length} node(s) -

-
- {discoveryStatus.nodes_found.map((discovered) => ( -
-
-
-

{discovered.ip}

+
+ {discoveryStatus.nodes_found.map((discovered) => ( +
+
+
+

{discovered.ip}

+ {discovered.version && discovered.version !== 'maintenance' && (

- Maintenance mode{discovered.version ? ` • Talos ${discovered.version}` : ''} + {discovered.version}

- {discovered.hostname && ( -

{discovered.hostname}

- )} -
- + )}
+
- ))} -
+
+ ))}
)} -
- {/* ADD NODE SECTION - Add single node by IP */} -
-

- Add Single Node -

-

- Add a node by IP address to detect hardware and configure -

- -
- setAddNodeIp(e.target.value)} - placeholder="192.168.8.128" - className="flex-1" - /> - + {/* Manual add by IP - styled like a list item */} +
+
+ setAddNodeIp(e.target.value)} + placeholder="192.168.8.128" + className="flex-1 font-mono" + /> + +
+

+ Add a node by IP address if not discovered automatically +

diff --git a/src/components/nodes/NodeForm.tsx b/src/components/nodes/NodeForm.tsx index 867f4c8..9518e88 100644 --- a/src/components/nodes/NodeForm.tsx +++ b/src/components/nodes/NodeForm.tsx @@ -17,7 +17,6 @@ export interface NodeFormData { role: 'controlplane' | 'worker'; disk: string; targetIp: string; - currentIp?: string; interface?: string; schematicId?: string; maintenance: boolean; @@ -111,8 +110,7 @@ function getInitialValues( hostname: initial?.hostname || defaultHostname, role, disk: defaultDisk, - targetIp: initial?.targetIp || '', // Don't auto-fill targetIp from detection - currentIp: initial?.currentIp || detection?.ip || '', // Auto-fill from detection + targetIp: initial?.targetIp || detection?.ip || '', // Auto-fill from detection interface: defaultInterface, schematicId: initial?.schematicId || '', maintenance: initial?.maintenance ?? true, @@ -152,15 +150,24 @@ export function NodeForm({ const role = watch('role'); const hostname = watch('hostname'); - // Reset form when initialValues change (e.g., switching to configure a different node) + // Reset form when switching between different nodes in configure mode // This ensures select boxes and all fields show the current values - // Use a ref to track the hostname to avoid infinite loops from object reference changes + // Use refs to track both the hostname and mode to avoid unnecessary resets const prevHostnameRef = useRef(undefined); + const prevModeRef = useRef<'add' | 'configure' | undefined>(undefined); + useEffect(() => { const currentHostname = initialValues?.hostname; - // Only reset if the hostname actually changed (switching between nodes) - if (currentHostname !== prevHostnameRef.current) { + const currentMode = initialValues?.hostname ? 'configure' : 'add'; + + // Only reset if we're actually switching between different nodes in configure mode + // or switching from add to configure mode (or vice versa) + const modeChanged = currentMode !== prevModeRef.current; + const hostnameChanged = currentMode === 'configure' && currentHostname !== prevHostnameRef.current; + + if (modeChanged || hostnameChanged) { prevHostnameRef.current = currentHostname; + prevModeRef.current = currentMode; const newValues = getInitialValues(initialValues, detection, nodes, hostnamePrefix); reset(newValues); } @@ -291,6 +298,13 @@ export function NodeForm({ // Skip if this is an existing node (configure mode) if (initialValues?.targetIp) return; + // Skip if there's a detection IP (hardware detection provides the actual IP) + if (detection?.ip) return; + + // Skip if there's already a targetIp from detection + const currentTargetIp = watch('targetIp'); + if (currentTargetIp && role === 'worker') return; // For workers, keep any existing value + const clusterConfig = instanceConfig?.cluster as any; const vip = clusterConfig?.nodes?.control?.vip as string | undefined; @@ -342,8 +356,8 @@ export function NodeForm({ // Set the calculated IP setValue('targetIp', `${vipPrefix}.${nextOctet}`); - } else if (role === 'worker') { - // For new worker nodes, clear target IP (let user set if needed) + } else if (role === 'worker' && !detection?.ip) { + // For worker nodes without detection, only clear if it looks like an auto-calculated control plane IP const currentTargetIp = watch('targetIp'); // Only clear if it looks like an auto-calculated IP (matches VIP pattern) if (currentTargetIp && vip) { @@ -353,7 +367,7 @@ export function NodeForm({ } } } - }, [role, instanceConfig, nodes, setValue, watch, initialValues?.targetIp]); + }, [role, instanceConfig, nodes, setValue, watch, initialValues?.targetIp, detection?.ip]); // Build disk options from both detection and initial values const diskOptions = (() => { @@ -433,21 +447,25 @@ export function NodeForm({ name="disk" control={control} rules={{ required: 'Disk is required' }} - render={({ field }) => ( - - )} + render={({ field }) => { + // Ensure we have a value - use the field value or fall back to first option + const value = field.value || (diskOptions.length > 0 ? diskOptions[0].path : ''); + return ( + + ); + }} /> ) : ( -
- - - {errors.currentIp && ( -

{errors.currentIp.message}

- )} - {detection?.ip && ( -

- Auto-detected from hardware (read-only) -

- )} -
-
{interfaceOptions.length > 0 ? ( ( - - )} + render={({ field }) => { + // Ensure we have a value - use the field value or fall back to first option + const value = field.value || (interfaceOptions.length > 0 ? interfaceOptions[0] : ''); + return ( + + ); + }} /> ) : ( ) : status ? (
- - {status.ready ? 'Ready' : 'Not Ready'} + + {status.status === 'ready' ? 'Ready' : 'Not Ready'}

{status.nodes} nodes total diff --git a/src/router/pages/DashboardPage.tsx b/src/router/pages/DashboardPage.tsx index 13b7b98..6284784 100644 --- a/src/router/pages/DashboardPage.tsx +++ b/src/router/pages/DashboardPage.tsx @@ -154,7 +154,7 @@ export function DashboardPage() {

{status.kubernetesVersion}

- {status.ready ? 'Ready' : 'Not ready'} + {status.status === 'ready' ? 'Ready' : 'Not ready'}

) : ( diff --git a/src/services/api/types/cluster.ts b/src/services/api/types/cluster.ts index ab655d8..4be8c65 100644 --- a/src/services/api/types/cluster.ts +++ b/src/services/api/types/cluster.ts @@ -12,7 +12,7 @@ export interface NodeStatus { } export interface ClusterStatus { - ready: boolean; + status: string; // "ready", "pending", "error", "not_bootstrapped", "unreachable", "degraded" nodes: number; controlPlaneNodes: number; workerNodes: number;