mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-06 17:08:10 +00:00
eb0c0263c7
Add Node modal — type selector & state reset:
- Restored a Local/Remote <Select> dropdown in renderFormFields so users can
explicitly choose the node type instead of it defaulting silently to 'remote'.
- Switching type clears api_url and api_token so no stale remote credentials
carry over if a user switches from Remote to Local mid-form.
- Replaced the static "Add Remote Node" title with a dynamic one that reflects
the currently selected type ("Add Local Node" / "Add Remote Node").
- onOpenChange now resets formData to defaultFormData whenever the dialog
opens, preventing stale values from a previous session leaking in.
Remote connection details — real metrics:
- testRemoteConnection previously returned hard-coded '-' for containers,
images, and cpus after a successful auth/check ping.
- Now fires three parallel requests (Promise.allSettled) after auth passes:
/api/stats → containers total + running count
/api/system/stats → cpu.cores
/api/system/images → image list length
- Each field falls back to '-' gracefully if an endpoint is unavailable,
so a slow or older remote instance never breaks the connection test.
DEP0060 util._extend suppression:
- http-proxy@1.18.1 calls util._extend when createProxyServer() is first
invoked at runtime (NOT at import time). A process.emitWarning override
placed before the proxy instantiations intercepts only DEP0060 without
suppressing any other warnings. No package version changes needed.
Also includes linter/formatter normalisation across multiple files.
103 lines
3.1 KiB
TypeScript
103 lines
3.1 KiB
TypeScript
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
|
|
import { apiFetch } from '@/lib/api';
|
|
|
|
export interface Node {
|
|
id: number;
|
|
name: string;
|
|
type: 'local' | 'remote';
|
|
compose_dir: string;
|
|
is_default: boolean;
|
|
status: 'online' | 'offline' | 'unknown';
|
|
created_at: number;
|
|
api_url?: string;
|
|
api_token?: string;
|
|
}
|
|
|
|
interface NodeContextType {
|
|
nodes: Node[];
|
|
activeNode: Node | null;
|
|
setActiveNode: (node: Node) => void;
|
|
refreshNodes: () => Promise<void>;
|
|
isLoading: boolean;
|
|
}
|
|
|
|
const NodeContext = createContext<NodeContextType | undefined>(undefined);
|
|
|
|
export function NodeProvider({ children }: { children: React.ReactNode }) {
|
|
const [nodes, setNodes] = useState<Node[]>([]);
|
|
const [activeNode, setActiveNodeState] = useState<Node | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
// Ref lets refreshNodes read current activeNode without being a dep (breaks infinite loop)
|
|
const activeNodeRef = useRef<Node | null>(null);
|
|
activeNodeRef.current = activeNode;
|
|
|
|
const refreshNodes = useCallback(async () => {
|
|
try {
|
|
const res = await apiFetch('/nodes');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setNodes(data);
|
|
|
|
const currentActive = activeNodeRef.current;
|
|
if (!currentActive) {
|
|
const defaultNode = data.find((n: Node) => n.is_default);
|
|
if (defaultNode) {
|
|
setActiveNodeState(defaultNode);
|
|
} else if (data.length > 0) {
|
|
setActiveNodeState(data[0]);
|
|
}
|
|
} else {
|
|
const updatedActive = data.find((n: Node) => n.id === currentActive.id);
|
|
if (updatedActive) {
|
|
setActiveNodeState(updatedActive);
|
|
} else {
|
|
const defaultNode = data.find((n: Node) => n.is_default);
|
|
if (defaultNode) {
|
|
setActiveNodeState(defaultNode);
|
|
} else if (data.length > 0) {
|
|
setActiveNodeState(data[0]);
|
|
} else {
|
|
setActiveNodeState(null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch nodes:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []); // stable - reads activeNode via ref, not closure capture
|
|
|
|
const setActiveNode = useCallback((node: Node) => {
|
|
setActiveNodeState(node);
|
|
localStorage.setItem('sencho-active-node', String(node.id));
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
refreshNodes();
|
|
|
|
const handleNodeNotFound = () => {
|
|
console.warn('[NodeContext] Active node is unreachable or deleted. Forcing sync...');
|
|
refreshNodes();
|
|
};
|
|
|
|
window.addEventListener('node-not-found', handleNodeNotFound);
|
|
return () => window.removeEventListener('node-not-found', handleNodeNotFound);
|
|
}, [refreshNodes]);
|
|
|
|
return (
|
|
<NodeContext.Provider value={{ nodes, activeNode, setActiveNode, refreshNodes, isLoading }}>
|
|
{children}
|
|
</NodeContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useNodes() {
|
|
const context = useContext(NodeContext);
|
|
if (!context) {
|
|
throw new Error('useNodes must be used within a NodeProvider');
|
|
}
|
|
return context;
|
|
}
|