Simplify detection UI.
This commit is contained in:
@@ -24,7 +24,6 @@ export function ClusterNodesComponent() {
|
|||||||
addNode,
|
addNode,
|
||||||
addError,
|
addError,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
isDeleting,
|
|
||||||
deleteError,
|
deleteError,
|
||||||
discover,
|
discover,
|
||||||
isDiscovering,
|
isDiscovering,
|
||||||
@@ -50,7 +49,6 @@ export function ClusterNodesComponent() {
|
|||||||
|
|
||||||
const { data: clusterStatusData } = useClusterStatus(currentInstance || '');
|
const { data: clusterStatusData } = useClusterStatus(currentInstance || '');
|
||||||
|
|
||||||
const [discoverSubnet, setDiscoverSubnet] = useState('');
|
|
||||||
const [addNodeIp, setAddNodeIp] = useState('');
|
const [addNodeIp, setAddNodeIp] = useState('');
|
||||||
const [discoverError, setDiscoverError] = useState<string | null>(null);
|
const [discoverError, setDiscoverError] = useState<string | null>(null);
|
||||||
const [detectError, setDetectError] = useState<string | null>(null);
|
const [detectError, setDetectError] = useState<string | null>(null);
|
||||||
@@ -182,7 +180,6 @@ export function ClusterNodesComponent() {
|
|||||||
role: data.role,
|
role: data.role,
|
||||||
disk: data.disk,
|
disk: data.disk,
|
||||||
target_ip: data.targetIp,
|
target_ip: data.targetIp,
|
||||||
current_ip: data.currentIp,
|
|
||||||
interface: data.interface,
|
interface: data.interface,
|
||||||
schematic_id: data.schematicId,
|
schematic_id: data.schematicId,
|
||||||
maintenance: data.maintenance,
|
maintenance: data.maintenance,
|
||||||
@@ -198,9 +195,7 @@ export function ClusterNodesComponent() {
|
|||||||
nodeName: drawerState.node.hostname,
|
nodeName: drawerState.node.hostname,
|
||||||
updates: {
|
updates: {
|
||||||
role: data.role,
|
role: data.role,
|
||||||
disk: data.disk,
|
|
||||||
target_ip: data.targetIp,
|
target_ip: data.targetIp,
|
||||||
current_ip: data.currentIp,
|
|
||||||
interface: data.interface,
|
interface: data.interface,
|
||||||
schematic_id: data.schematicId,
|
schematic_id: data.schematicId,
|
||||||
maintenance: data.maintenance,
|
maintenance: data.maintenance,
|
||||||
@@ -231,8 +226,8 @@ export function ClusterNodesComponent() {
|
|||||||
const handleDiscover = () => {
|
const handleDiscover = () => {
|
||||||
setDiscoverError(null);
|
setDiscoverError(null);
|
||||||
setDiscoverSuccess(null);
|
setDiscoverSuccess(null);
|
||||||
// Pass subnet only if it's not empty, otherwise auto-detect
|
// Always use auto-detect to scan all local networks
|
||||||
discover(discoverSubnet || undefined);
|
discover(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -268,7 +263,9 @@ export function ClusterNodesComponent() {
|
|||||||
|
|
||||||
// Check if cluster is already bootstrapped using cluster status
|
// Check if cluster is already bootstrapped using cluster status
|
||||||
// The backend checks for kubeconfig existence and cluster connectivity
|
// 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;
|
return hasReadyControlPlane && !hasBootstrapped;
|
||||||
}, [assignedNodes, clusterStatus]);
|
}, [assignedNodes, clusterStatus]);
|
||||||
@@ -433,26 +430,21 @@ export function ClusterNodesComponent() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* DISCOVERY SECTION - Scan subnet for nodes */}
|
{/* ADD NODES SECTION - Discovery and manual add combined */}
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
||||||
Discover Nodes on Network
|
Add Nodes to Cluster
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
Scan a specific subnet or leave empty to auto-detect all local networks
|
Discover nodes on the network or manually add by IP address
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex gap-3 mb-4">
|
{/* Discovery button */}
|
||||||
<Input
|
<div className="flex gap-2 mb-4">
|
||||||
type="text"
|
|
||||||
value={discoverSubnet}
|
|
||||||
onChange={(e) => setDiscoverSubnet(e.target.value)}
|
|
||||||
placeholder="192.168.1.0/24 (optional - leave empty to auto-detect)"
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDiscover}
|
onClick={handleDiscover}
|
||||||
disabled={isDiscovering || discoveryStatus?.active}
|
disabled={isDiscovering || discoveryStatus?.active}
|
||||||
|
className="flex-1"
|
||||||
>
|
>
|
||||||
{isDiscovering || discoveryStatus?.active ? (
|
{isDiscovering || discoveryStatus?.active ? (
|
||||||
<>
|
<>
|
||||||
@@ -460,7 +452,7 @@ export function ClusterNodesComponent() {
|
|||||||
Discovering...
|
Discovering...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Discover'
|
'Discover Nodes'
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
{(isDiscovering || discoveryStatus?.active) && (
|
{(isDiscovering || discoveryStatus?.active) && (
|
||||||
@@ -475,69 +467,60 @@ export function ClusterNodesComponent() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Discovered nodes display */}
|
||||||
{discoveryStatus?.nodes_found && discoveryStatus.nodes_found.length > 0 && (
|
{discoveryStatus?.nodes_found && discoveryStatus.nodes_found.length > 0 && (
|
||||||
<div className="mt-6">
|
<div className="space-y-3 mb-4">
|
||||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
{discoveryStatus.nodes_found.map((discovered) => (
|
||||||
Discovered {discoveryStatus.nodes_found.length} node(s)
|
<div key={discovered.ip} className="border border-gray-300 dark:border-gray-600 rounded-lg p-4">
|
||||||
</h4>
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-3">
|
<div>
|
||||||
{discoveryStatus.nodes_found.map((discovered) => (
|
<p className="font-medium font-mono text-gray-900 dark:text-gray-100">{discovered.ip}</p>
|
||||||
<div key={discovered.ip} className="border border-gray-300 dark:border-gray-600 rounded-lg p-4">
|
{discovered.version && discovered.version !== 'maintenance' && (
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium font-mono text-gray-900 dark:text-gray-100">{discovered.ip}</p>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
Maintenance mode{discovered.version ? ` • Talos ${discovered.version}` : ''}
|
{discovered.version}
|
||||||
</p>
|
</p>
|
||||||
{discovered.hostname && (
|
)}
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">{discovered.hostname}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() => handleAddFromDiscovery(discovered)}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Add to Cluster
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleAddFromDiscovery(discovered)}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Add to Cluster
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ADD NODE SECTION - Add single node by IP */}
|
{/* Manual add by IP - styled like a list item */}
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
|
<div className="border border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-4">
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
<div className="flex items-center gap-3">
|
||||||
Add Single Node
|
<Input
|
||||||
</h3>
|
type="text"
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
value={addNodeIp}
|
||||||
Add a node by IP address to detect hardware and configure
|
onChange={(e) => setAddNodeIp(e.target.value)}
|
||||||
</p>
|
placeholder="192.168.8.128"
|
||||||
|
className="flex-1 font-mono"
|
||||||
<div className="flex gap-3">
|
/>
|
||||||
<Input
|
<Button
|
||||||
type="text"
|
onClick={handleAddNode}
|
||||||
value={addNodeIp}
|
disabled={isGettingHardware}
|
||||||
onChange={(e) => setAddNodeIp(e.target.value)}
|
size="sm"
|
||||||
placeholder="192.168.8.128"
|
>
|
||||||
className="flex-1"
|
{isGettingHardware ? (
|
||||||
/>
|
<>
|
||||||
<Button
|
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||||
onClick={handleAddNode}
|
Detecting...
|
||||||
disabled={isGettingHardware}
|
</>
|
||||||
variant="secondary"
|
) : (
|
||||||
>
|
'Add to Cluster'
|
||||||
{isGettingHardware ? (
|
)}
|
||||||
<>
|
</Button>
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
</div>
|
||||||
Detecting...
|
<p className="text-xs text-gray-500 dark:text-gray-500 mt-2">
|
||||||
</>
|
Add a node by IP address if not discovered automatically
|
||||||
) : (
|
</p>
|
||||||
'Add Node'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export interface NodeFormData {
|
|||||||
role: 'controlplane' | 'worker';
|
role: 'controlplane' | 'worker';
|
||||||
disk: string;
|
disk: string;
|
||||||
targetIp: string;
|
targetIp: string;
|
||||||
currentIp?: string;
|
|
||||||
interface?: string;
|
interface?: string;
|
||||||
schematicId?: string;
|
schematicId?: string;
|
||||||
maintenance: boolean;
|
maintenance: boolean;
|
||||||
@@ -111,8 +110,7 @@ function getInitialValues(
|
|||||||
hostname: initial?.hostname || defaultHostname,
|
hostname: initial?.hostname || defaultHostname,
|
||||||
role,
|
role,
|
||||||
disk: defaultDisk,
|
disk: defaultDisk,
|
||||||
targetIp: initial?.targetIp || '', // Don't auto-fill targetIp from detection
|
targetIp: initial?.targetIp || detection?.ip || '', // Auto-fill from detection
|
||||||
currentIp: initial?.currentIp || detection?.ip || '', // Auto-fill from detection
|
|
||||||
interface: defaultInterface,
|
interface: defaultInterface,
|
||||||
schematicId: initial?.schematicId || '',
|
schematicId: initial?.schematicId || '',
|
||||||
maintenance: initial?.maintenance ?? true,
|
maintenance: initial?.maintenance ?? true,
|
||||||
@@ -152,15 +150,24 @@ export function NodeForm({
|
|||||||
const role = watch('role');
|
const role = watch('role');
|
||||||
const hostname = watch('hostname');
|
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
|
// 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 prevHostnameRef = useRef<string | undefined>(undefined);
|
||||||
|
const prevModeRef = useRef<'add' | 'configure' | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentHostname = initialValues?.hostname;
|
const currentHostname = initialValues?.hostname;
|
||||||
// Only reset if the hostname actually changed (switching between nodes)
|
const currentMode = initialValues?.hostname ? 'configure' : 'add';
|
||||||
if (currentHostname !== prevHostnameRef.current) {
|
|
||||||
|
// 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;
|
prevHostnameRef.current = currentHostname;
|
||||||
|
prevModeRef.current = currentMode;
|
||||||
const newValues = getInitialValues(initialValues, detection, nodes, hostnamePrefix);
|
const newValues = getInitialValues(initialValues, detection, nodes, hostnamePrefix);
|
||||||
reset(newValues);
|
reset(newValues);
|
||||||
}
|
}
|
||||||
@@ -291,6 +298,13 @@ export function NodeForm({
|
|||||||
// Skip if this is an existing node (configure mode)
|
// Skip if this is an existing node (configure mode)
|
||||||
if (initialValues?.targetIp) return;
|
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 clusterConfig = instanceConfig?.cluster as any;
|
||||||
const vip = clusterConfig?.nodes?.control?.vip as string | undefined;
|
const vip = clusterConfig?.nodes?.control?.vip as string | undefined;
|
||||||
|
|
||||||
@@ -342,8 +356,8 @@ export function NodeForm({
|
|||||||
|
|
||||||
// Set the calculated IP
|
// Set the calculated IP
|
||||||
setValue('targetIp', `${vipPrefix}.${nextOctet}`);
|
setValue('targetIp', `${vipPrefix}.${nextOctet}`);
|
||||||
} else if (role === 'worker') {
|
} else if (role === 'worker' && !detection?.ip) {
|
||||||
// For new worker nodes, clear target IP (let user set if needed)
|
// For worker nodes without detection, only clear if it looks like an auto-calculated control plane IP
|
||||||
const currentTargetIp = watch('targetIp');
|
const currentTargetIp = watch('targetIp');
|
||||||
// Only clear if it looks like an auto-calculated IP (matches VIP pattern)
|
// Only clear if it looks like an auto-calculated IP (matches VIP pattern)
|
||||||
if (currentTargetIp && vip) {
|
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
|
// Build disk options from both detection and initial values
|
||||||
const diskOptions = (() => {
|
const diskOptions = (() => {
|
||||||
@@ -433,21 +447,25 @@ export function NodeForm({
|
|||||||
name="disk"
|
name="disk"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: 'Disk is required' }}
|
rules={{ required: 'Disk is required' }}
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<Select value={field.value || ''} onValueChange={field.onChange}>
|
// Ensure we have a value - use the field value or fall back to first option
|
||||||
<SelectTrigger className="mt-1">
|
const value = field.value || (diskOptions.length > 0 ? diskOptions[0].path : '');
|
||||||
<SelectValue placeholder="Select a disk" />
|
return (
|
||||||
</SelectTrigger>
|
<Select value={value} onValueChange={field.onChange}>
|
||||||
<SelectContent>
|
<SelectTrigger className="mt-1">
|
||||||
{diskOptions.map((disk) => (
|
<SelectValue placeholder="Select a disk" />
|
||||||
<SelectItem key={disk.path} value={disk.path}>
|
</SelectTrigger>
|
||||||
{disk.path}
|
<SelectContent>
|
||||||
{disk.size > 0 && ` (${formatBytes(disk.size)})`}
|
{diskOptions.map((disk) => (
|
||||||
</SelectItem>
|
<SelectItem key={disk.path} value={disk.path}>
|
||||||
))}
|
{disk.path}
|
||||||
</SelectContent>
|
{disk.size > 0 && ` (${formatBytes(disk.size)})`}
|
||||||
</Select>
|
</SelectItem>
|
||||||
)}
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Controller
|
<Controller
|
||||||
@@ -487,45 +505,30 @@ export function NodeForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<Label htmlFor="interface">Network Interface</Label>
|
<Label htmlFor="interface">Network Interface</Label>
|
||||||
{interfaceOptions.length > 0 ? (
|
{interfaceOptions.length > 0 ? (
|
||||||
<Controller
|
<Controller
|
||||||
name="interface"
|
name="interface"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<Select value={field.value || ''} onValueChange={field.onChange}>
|
// Ensure we have a value - use the field value or fall back to first option
|
||||||
<SelectTrigger className="mt-1">
|
const value = field.value || (interfaceOptions.length > 0 ? interfaceOptions[0] : '');
|
||||||
<SelectValue placeholder="Select interface..." />
|
return (
|
||||||
</SelectTrigger>
|
<Select value={value} onValueChange={field.onChange}>
|
||||||
<SelectContent>
|
<SelectTrigger className="mt-1">
|
||||||
{interfaceOptions.map((iface) => (
|
<SelectValue placeholder="Select interface..." />
|
||||||
<SelectItem key={iface} value={iface}>
|
</SelectTrigger>
|
||||||
{iface}
|
<SelectContent>
|
||||||
</SelectItem>
|
{interfaceOptions.map((iface) => (
|
||||||
))}
|
<SelectItem key={iface} value={iface}>
|
||||||
</SelectContent>
|
{iface}
|
||||||
</Select>
|
</SelectItem>
|
||||||
)}
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Controller
|
<Controller
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export function NodeFormDrawer({
|
|||||||
role: node.role,
|
role: node.role,
|
||||||
disk: node.disk,
|
disk: node.disk,
|
||||||
targetIp: node.target_ip,
|
targetIp: node.target_ip,
|
||||||
currentIp: node.current_ip,
|
|
||||||
interface: node.interface,
|
interface: node.interface,
|
||||||
schematicId: node.schematic_id,
|
schematicId: node.schematic_id,
|
||||||
maintenance: node.maintenance ?? true,
|
maintenance: node.maintenance ?? true,
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ export function ClusterHealthPage() {
|
|||||||
<Skeleton className="h-8 w-24" />
|
<Skeleton className="h-8 w-24" />
|
||||||
) : status ? (
|
) : status ? (
|
||||||
<div>
|
<div>
|
||||||
<Badge variant={status.ready ? 'outline' : 'secondary'} className={status.ready ? 'border-green-500' : ''}>
|
<Badge variant={status.status === 'ready' ? 'outline' : 'secondary'} className={status.status === 'ready' ? 'border-green-500' : ''}>
|
||||||
{status.ready ? 'Ready' : 'Not Ready'}
|
{status.status === 'ready' ? 'Ready' : 'Not Ready'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
{status.nodes} nodes total
|
{status.nodes} nodes total
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export function DashboardPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-xl font-bold font-mono">{status.kubernetesVersion}</div>
|
<div className="text-xl font-bold font-mono">{status.kubernetesVersion}</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
{status.ready ? 'Ready' : 'Not ready'}
|
{status.status === 'ready' ? 'Ready' : 'Not ready'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export interface NodeStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ClusterStatus {
|
export interface ClusterStatus {
|
||||||
ready: boolean;
|
status: string; // "ready", "pending", "error", "not_bootstrapped", "unreachable", "degraded"
|
||||||
nodes: number;
|
nodes: number;
|
||||||
controlPlaneNodes: number;
|
controlPlaneNodes: number;
|
||||||
workerNodes: number;
|
workerNodes: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user