From 854a6023cdf5bd35f94ad2ecef509fc518e2159f Mon Sep 17 00:00:00 2001
From: Paul Payne
Date: Sat, 8 Nov 2025 22:56:48 +0000
Subject: [PATCH] Reset a node to maintenance mode.
---
src/components/ClusterNodesComponent.tsx | 57 +++++++++++++++----
src/components/apps/AppDetailModal.tsx | 4 +-
src/components/nodes/NodeForm.tsx | 46 ++++++++-------
src/components/nodes/NodeFormDrawer.tsx | 1 +
.../services/ServiceConfigEditor.tsx | 4 +-
src/hooks/useNodes.ts | 10 ++++
src/services/api/nodes.ts | 4 ++
7 files changed, 89 insertions(+), 37 deletions(-)
diff --git a/src/components/ClusterNodesComponent.tsx b/src/components/ClusterNodesComponent.tsx
index 032d04a..789551e 100644
--- a/src/components/ClusterNodesComponent.tsx
+++ b/src/components/ClusterNodesComponent.tsx
@@ -4,7 +4,7 @@ import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { Alert } from './ui/alert';
import { Input } from './ui/input';
-import { Cpu, HardDrive, Network, Monitor, CheckCircle, AlertCircle, BookOpen, ExternalLink, Loader2 } from 'lucide-react';
+import { Cpu, HardDrive, Network, Monitor, CheckCircle, AlertCircle, BookOpen, ExternalLink, Loader2, RotateCcw } from 'lucide-react';
import { useInstanceContext } from '../hooks/useInstanceContext';
import { useNodes, useDiscoveryStatus } from '../hooks/useNodes';
import { useCluster } from '../hooks/useCluster';
@@ -36,6 +36,8 @@ export function ClusterNodesComponent() {
updateNode,
applyNode,
isApplying,
+ resetNode,
+ isResetting,
refetch
} = useNodes(currentInstance);
@@ -194,14 +196,12 @@ export function ClusterNodesComponent() {
nodeName: drawerState.node.hostname,
updates: {
role: data.role,
- config: {
- disk: data.disk,
- target_ip: data.targetIp,
- current_ip: data.currentIp,
- interface: data.interface,
- schematic_id: data.schematicId,
- maintenance: data.maintenance,
- },
+ disk: data.disk,
+ target_ip: data.targetIp,
+ current_ip: data.currentIp,
+ interface: data.interface,
+ schematic_id: data.schematicId,
+ maintenance: data.maintenance,
},
});
closeDrawer();
@@ -214,6 +214,16 @@ export function ClusterNodesComponent() {
await applyNode(drawerState.node.hostname);
};
+ const handleResetNode = (node: Node) => {
+ if (
+ confirm(
+ `Reset node ${node.hostname}?\n\nThis will wipe the node and return it to maintenance mode. The node will need to be reconfigured.`
+ )
+ ) {
+ resetNode(node.hostname);
+ }
+ };
+
const handleDeleteNode = (hostname: string) => {
if (!currentInstance) return;
if (confirm(`Are you sure you want to remove node ${hostname}?`)) {
@@ -576,10 +586,21 @@ export function ClusterNodesComponent() {
)}
)}
- {node.talosVersion && (
+ {(node.version || node.schematic_id) && (
- Talos: {node.talosVersion}
- {node.kubernetesVersion && ` • K8s: ${node.kubernetesVersion}`}
+ {node.version && Talos: {node.version}}
+ {node.version && node.schematic_id && • }
+ {node.schematic_id && (
+ {
+ navigator.clipboard.writeText(node.schematic_id!);
+ }}
+ className="cursor-pointer hover:text-primary hover:underline"
+ >
+ Schema: {node.schematic_id.substring(0, 8)}...
+
+ )}
)}
@@ -600,6 +621,18 @@ export function ClusterNodesComponent() {
{isApplying ? : 'Apply'}
)}
+ {!node.maintenance && (node.configured || node.applied) && (
+
+ )}
-
-
-
-
-
-
-
- {showApplyButton && onApply && (
+ {onCancel && (
+
+ )}
+ {showApplyButton && onApply ? (
+ ) : (
+
)}
diff --git a/src/components/nodes/NodeFormDrawer.tsx b/src/components/nodes/NodeFormDrawer.tsx
index cdd9ce9..1b5f14c 100644
--- a/src/components/nodes/NodeFormDrawer.tsx
+++ b/src/components/nodes/NodeFormDrawer.tsx
@@ -58,6 +58,7 @@ export function NodeFormDrawer({
detection={detection}
onSubmit={onSubmit}
onApply={onApply}
+ onCancel={onClose}
submitLabel={mode === 'add' ? 'Add Node' : 'Save'}
showApplyButton={mode === 'configure'}
instanceName={instanceName}
diff --git a/src/components/services/ServiceConfigEditor.tsx b/src/components/services/ServiceConfigEditor.tsx
index 1a72bef..660e923 100644
--- a/src/components/services/ServiceConfigEditor.tsx
+++ b/src/components/services/ServiceConfigEditor.tsx
@@ -18,10 +18,12 @@ interface ServiceConfigEditorProps {
export function ServiceConfigEditor({
instanceName,
serviceName,
- manifest: _manifestProp, // Ignore the prop, fetch from status instead
+ manifest: _manifest, // Ignore the prop, fetch from status instead
onClose,
onSuccess,
}: ServiceConfigEditorProps) {
+ // Suppress unused variable warning - kept for API compatibility
+ void _manifest;
const { config, isLoading: configLoading, updateConfig, isUpdating } = useServiceConfig(instanceName, serviceName);
const { data: statusData, isLoading: statusLoading } = useServiceStatus(instanceName, serviceName);
diff --git a/src/hooks/useNodes.ts b/src/hooks/useNodes.ts
index cc6e684..b0a0964 100644
--- a/src/hooks/useNodes.ts
+++ b/src/hooks/useNodes.ts
@@ -71,6 +71,13 @@ export function useNodes(instanceName: string | null | undefined) {
mutationFn: (ip: string) => nodesApi.getHardware(instanceName!, ip),
});
+ const resetMutation = useMutation({
+ mutationFn: (nodeName: string) => nodesApi.reset(instanceName!, nodeName),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['instances', instanceName, 'nodes'] });
+ },
+ });
+
return {
nodes: nodesQuery.data?.nodes || [],
isLoading: nodesQuery.isLoading,
@@ -101,6 +108,9 @@ export function useNodes(instanceName: string | null | undefined) {
isFetchingTemplates: fetchTemplatesMutation.isPending,
cancelDiscovery: cancelDiscoveryMutation.mutate,
isCancellingDiscovery: cancelDiscoveryMutation.isPending,
+ resetNode: resetMutation.mutate,
+ isResetting: resetMutation.isPending,
+ resetError: resetMutation.error,
};
}
diff --git a/src/services/api/nodes.ts b/src/services/api/nodes.ts
index 006b6e3..3ae9f90 100644
--- a/src/services/api/nodes.ts
+++ b/src/services/api/nodes.ts
@@ -59,4 +59,8 @@ export const nodesApi = {
async fetchTemplates(instanceName: string): Promise {
return apiClient.post(`/api/v1/instances/${instanceName}/nodes/fetch-templates`);
},
+
+ async reset(instanceName: string, nodeName: string): Promise {
+ return apiClient.post(`/api/v1/instances/${instanceName}/nodes/${nodeName}/reset`);
+ },
};