Simplify detection UI.

This commit is contained in:
2025-11-09 00:42:38 +00:00
parent a63519968e
commit 35bc44bc32
6 changed files with 123 additions and 138 deletions

View File

@@ -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<string | undefined>(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 }) => (
<Select value={field.value || ''} onValueChange={field.onChange}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select a disk" />
</SelectTrigger>
<SelectContent>
{diskOptions.map((disk) => (
<SelectItem key={disk.path} value={disk.path}>
{disk.path}
{disk.size > 0 && ` (${formatBytes(disk.size)})`}
</SelectItem>
))}
</SelectContent>
</Select>
)}
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 (
<Select value={value} onValueChange={field.onChange}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select a disk" />
</SelectTrigger>
<SelectContent>
{diskOptions.map((disk) => (
<SelectItem key={disk.path} value={disk.path}>
{disk.path}
{disk.size > 0 && ` (${formatBytes(disk.size)})`}
</SelectItem>
))}
</SelectContent>
</Select>
);
}}
/>
) : (
<Controller
@@ -487,45 +505,30 @@ export function NodeForm({
)}
</div>
<div>
<Label htmlFor="currentIp">Current IP Address</Label>
<Input
id="currentIp"
type="text"
{...register('currentIp')}
className="mt-1"
disabled={!!detection?.ip}
/>
{errors.currentIp && (
<p className="text-sm text-red-600 mt-1">{errors.currentIp.message}</p>
)}
{detection?.ip && (
<p className="mt-1 text-xs text-muted-foreground">
Auto-detected from hardware (read-only)
</p>
)}
</div>
<div>
<Label htmlFor="interface">Network Interface</Label>
{interfaceOptions.length > 0 ? (
<Controller
name="interface"
control={control}
render={({ field }) => (
<Select value={field.value || ''} onValueChange={field.onChange}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select interface..." />
</SelectTrigger>
<SelectContent>
{interfaceOptions.map((iface) => (
<SelectItem key={iface} value={iface}>
{iface}
</SelectItem>
))}
</SelectContent>
</Select>
)}
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 (
<Select value={value} onValueChange={field.onChange}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select interface..." />
</SelectTrigger>
<SelectContent>
{interfaceOptions.map((iface) => (
<SelectItem key={iface} value={iface}>
{iface}
</SelectItem>
))}
</SelectContent>
</Select>
);
}}
/>
) : (
<Controller