From e00170b4e5ce0a2d60b9b505107aa204f3e6e1ea Mon Sep 17 00:00:00 2001 From: ParrotXray Date: Wed, 8 Jul 2026 16:32:40 +0800 Subject: [PATCH] feat: remove training page and related configurations --- src/components/Sidebar.tsx | 6 - src/config.ts | 25 +-- src/pages/training.tsx | 389 ------------------------------------- 3 files changed, 1 insertion(+), 419 deletions(-) delete mode 100644 src/pages/training.tsx diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 11bee98..1793f83 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -52,12 +52,6 @@ const NAV_GROUPS = [ { href: '/logs', icon: faScroll, label: 'Logs' }, ], }, - { - label: 'TRAINING', - items: [ - { href: '/training', icon: faServer, label: 'Training' }, - ], - }, ] const PAGE_META: Record = { diff --git a/src/config.ts b/src/config.ts index cd6f7d8..ee47d76 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,32 +30,9 @@ export const urls = { listType: ListType ) => `${httpProtocol}://${host}/ebpf/access_control/${nic}/${ipVersion}/${flow}/${listType}`, - training: { - // Node registry lives on the main Mantis backend (SQLCipher app.db). - nodes: `${httpProtocol}://${host}/training/nodes`, - node: (id: string) => `${httpProtocol}://${host}/training/nodes/${id}`, - }, + } as const; -// Trainer nodes are arbitrary user-registered host:port addresses, not the -// main Mantis backend, so these are built directly from the node's own -// host/port rather than the fixed `host` above. There is no auth between -// the frontend and a trainer node, so callers must NOT attach getAuthHeaders() -// (that token is for the main backend only) when hitting these URLs. -export const trainerNodeUrls = (nodeHost: string, nodePort: number) => { - const base = `${httpProtocol}://${nodeHost}:${nodePort}`; - return { - health: `${base}/health`, - datasets: `${base}/datasets`, - train: (datasetId: string) => `${base}/datasets/${datasetId}/train`, - jobs: `${base}/jobs`, - job: (jobId: string) => `${base}/jobs/${jobId}`, - } as const; -}; - -export const trainerNodeWsUrl = (nodeHost: string, nodePort: number, jobId: string) => - `${websocketProtocol}://${nodeHost}:${nodePort}/jobs/${jobId}/ws`; - export const websocketUrl = { ipv4FlowStats: (direction: string, flow_direction: string, timeType: string) => `${websocketProtocol}://${host}/ebpf/statistics/websocket/ipv4/${direction}/${flow_direction}/${timeType}`, diff --git a/src/pages/training.tsx b/src/pages/training.tsx deleted file mode 100644 index 8d68ffa..0000000 --- a/src/pages/training.tsx +++ /dev/null @@ -1,389 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { - faServer, - faPlus, - faTrash, - faCircle, - faPlay, - faDatabase, - faSpinner, -} from '@fortawesome/free-solid-svg-icons' -import { useTheme } from '../providers/ThemeProvider' -import { urls, trainerNodeUrls, trainerNodeWsUrl } from '../config' -import { fetchData, postData, deleteData } from '../utils/connectionUtils' -import Layout from '../components/Layout' -import { - TrainerNode, - TrainerNodeListResponse, - Dataset, - DatasetListResponse, - TrainingJob, - JobListResponse, - JobSocketMessage, -} from '../types/TrainingTypes' - -const STATUS_STYLES: Record = { - pending: 'text-slate-400', - running: 'text-blue-400', - succeeded: 'text-green-400', - failed: 'text-red-400', -} - -// Trainer nodes are arbitrary user-registered addresses with no auth of their -// own, so all calls here go straight to the node's host:port with a plain -// fetch -- never with the main backend's Authorization header. -function useTrainerNodeSocket( - node: TrainerNode | null, - jobId: string | null, - onMessage: (msg: JobSocketMessage) => void -) { - const onMessageRef = useRef(onMessage) - useEffect(() => { onMessageRef.current = onMessage }, [onMessage]) - - const [connected, setConnected] = useState(false) - - useEffect(() => { - if (!node || !jobId) { - setConnected(false) - return - } - const ws = new WebSocket(trainerNodeWsUrl(node.host, node.port, jobId)) - ws.onopen = () => setConnected(true) - ws.onclose = () => setConnected(false) - ws.onerror = () => ws.close() - ws.onmessage = (e) => { - try { - onMessageRef.current(JSON.parse(e.data)) - } catch {} - } - return () => ws.close() - }, [node?.host, node?.port, jobId]) - - return connected -} - -const TrainingPage: React.FC = () => { - const { actualTheme } = useTheme() - const isDark = actualTheme === 'dark' - - const [nodes, setNodes] = useState([]) - const [nodesLoading, setNodesLoading] = useState(true) - const [newNode, setNewNode] = useState({ name: '', host: '', port: '8000' }) - - const [selectedNode, setSelectedNode] = useState(null) - const [datasets, setDatasets] = useState([]) - const [jobs, setJobs] = useState([]) - const [kaggleId, setKaggleId] = useState('') - - const [activeJobId, setActiveJobId] = useState(null) - const [jobLogs, setJobLogs] = useState([]) - const [jobLive, setJobLive] = useState<{ status: string; current_stage: string | null } | null>(null) - - const logsBottomRef = useRef(null) - - const loadNodes = useCallback(() => { - setNodesLoading(true) - fetchData( - urls.training.nodes, - (raw) => { - const parsed: TrainerNodeListResponse = JSON.parse(raw) - setNodes(parsed.items) - setNodesLoading(false) - }, - () => setNodesLoading(false) - ) - }, []) - - useEffect(() => { loadNodes() }, [loadNodes]) - - const addNode = useCallback(() => { - const port = parseInt(newNode.port, 10) - if (!newNode.name.trim() || !newNode.host.trim() || !port) return - postData( - urls.training.nodes, - { name: newNode.name.trim(), host: newNode.host.trim(), port }, - () => { - setNewNode({ name: '', host: '', port: '8000' }) - loadNodes() - } - ) - }, [newNode, loadNodes]) - - const removeNode = useCallback((id: string) => { - deleteData(urls.training.node(id), {}, () => { - if (selectedNode?.id === id) setSelectedNode(null) - loadNodes() - }) - }, [selectedNode, loadNodes]) - - const loadNodeState = useCallback((node: TrainerNode) => { - const nodeUrls = trainerNodeUrls(node.host, node.port) - fetch(nodeUrls.datasets) - .then(r => r.json()) - .then((parsed: DatasetListResponse) => setDatasets(parsed.items)) - .catch(() => setDatasets([])) - fetch(nodeUrls.jobs) - .then(r => r.json()) - .then((parsed: JobListResponse) => setJobs(parsed.items)) - .catch(() => setJobs([])) - }, []) - - const selectNode = useCallback((node: TrainerNode) => { - setSelectedNode(node) - setActiveJobId(null) - setJobLogs([]) - setJobLive(null) - loadNodeState(node) - }, [loadNodeState]) - - const createDataset = useCallback(() => { - if (!selectedNode || !kaggleId.trim()) return - const form = new FormData() - form.append('name', kaggleId.trim().split('/').pop() || kaggleId.trim()) - form.append('kaggle_dataset_id', kaggleId.trim()) - fetch(trainerNodeUrls(selectedNode.host, selectedNode.port).datasets, { - method: 'POST', - body: form, - }) - .then(() => { - setKaggleId('') - loadNodeState(selectedNode) - }) - .catch(() => {}) - }, [selectedNode, kaggleId, loadNodeState]) - - const startTraining = useCallback((datasetId: string) => { - if (!selectedNode) return - fetch(trainerNodeUrls(selectedNode.host, selectedNode.port).train(datasetId), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({}), - }) - .then(r => r.json()) - .then((job: TrainingJob) => { - setActiveJobId(job.id) - setJobLogs([]) - setJobLive({ status: job.status, current_stage: job.current_stage }) - loadNodeState(selectedNode) - }) - .catch(() => {}) - }, [selectedNode, loadNodeState]) - - const handleSocketMessage = useCallback((msg: JobSocketMessage) => { - if (msg.type === 'status') { - setJobLive({ status: msg.status, current_stage: msg.current_stage }) - if (msg.status === 'succeeded' || msg.status === 'failed') { - if (selectedNode) loadNodeState(selectedNode) - } - } else if (msg.type === 'log') { - setJobLogs(prev => [...prev, msg.log_line]) - } - }, [selectedNode, loadNodeState]) - - const wsConnected = useTrainerNodeSocket(selectedNode, activeJobId, handleSocketMessage) - - useEffect(() => { - logsBottomRef.current?.scrollIntoView({ behavior: 'smooth' }) - }, [jobLogs]) - - const panelClass = `rounded-xl border ${isDark ? 'bg-[#0e1e2c] border-slate-700/40' : 'bg-white border-slate-200'}` - - return ( - -
- {/* Node registry */} -
-
- -

- Trainer Nodes -

-
- -
- {nodesLoading && ( -

Loading...

- )} - {!nodesLoading && nodes.length === 0 && ( -

No nodes registered yet.

- )} - {nodes.map(node => ( -
selectNode(node)} - className={`flex items-center justify-between px-2.5 py-2 rounded-lg cursor-pointer text-xs transition-colors ${ - selectedNode?.id === node.id - ? 'bg-[#4ab5cc]/15 text-[#4ab5cc]' - : isDark ? 'text-slate-300 hover:bg-slate-800/60' : 'text-slate-600 hover:bg-slate-100' - }`} - > -
-

{node.name}

-

{node.host}:{node.port}

-
- -
- ))} -
- -
- setNewNode(n => ({ ...n, name: e.target.value }))} - className={`px-2.5 py-1.5 rounded-lg text-xs border outline-none ${ - isDark ? 'bg-[#131929] border-slate-700/50 text-slate-200' : 'bg-slate-50 border-slate-200 text-slate-700' - }`} - /> -
- setNewNode(n => ({ ...n, host: e.target.value }))} - className={`flex-1 min-w-0 px-2.5 py-1.5 rounded-lg text-xs border outline-none ${ - isDark ? 'bg-[#131929] border-slate-700/50 text-slate-200' : 'bg-slate-50 border-slate-200 text-slate-700' - }`} - /> - setNewNode(n => ({ ...n, port: e.target.value }))} - className={`w-16 px-2.5 py-1.5 rounded-lg text-xs border outline-none ${ - isDark ? 'bg-[#131929] border-slate-700/50 text-slate-200' : 'bg-slate-50 border-slate-200 text-slate-700' - }`} - /> -
- -
-
- - {/* Node detail */} - {!selectedNode ? ( -
-

- Select a trainer node to view its datasets and jobs. -

-
- ) : ( -
-
-
- -

- Datasets on {selectedNode.name} -

-
- -
- setKaggleId(e.target.value)} - className={`flex-1 min-w-0 px-2.5 py-1.5 rounded-lg text-xs border outline-none ${ - isDark ? 'bg-[#131929] border-slate-700/50 text-slate-200' : 'bg-slate-50 border-slate-200 text-slate-700' - }`} - /> - -
- -
- {datasets.length === 0 && ( -

No datasets yet.

- )} - {datasets.map(ds => ( -
-
-

{ds.name}

-

{ds.row_count} rows ยท {ds.source}

-
- -
- ))} -
-
- -
-
-
- -

- Live Job -

-
- {activeJobId && ( -
- - - {jobLive?.status ?? 'connecting'}{jobLive?.current_stage ? ` (${jobLive.current_stage})` : ''} - -
- )} -
- - {!activeJobId ? ( -

- Start training on a dataset above to see live logs here. -

- ) : ( -
- {jobLogs.map((line, i) => ( -
{line}
- ))} -
-
- )} -
- - {jobs.length > 0 && ( -
-

Past Jobs

- {jobs.map(job => ( -
{ setActiveJobId(job.id); setJobLogs([]); setJobLive({ status: job.status, current_stage: job.current_stage }) }} - className={`flex items-center justify-between px-2.5 py-1.5 rounded-lg text-xs cursor-pointer ${ - isDark ? 'hover:bg-slate-800/60' : 'hover:bg-slate-100' - }`} - > - {job.id.slice(0, 8)} - {job.status} -
- ))} -
- )} -
- )} -
- - ) -} - -export default TrainingPage