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`); + }, };