diff --git a/vssm_web/vssm_web/src/App.tsx b/vssm_web/vssm_web/src/App.tsx index 154dee7..079b4b7 100644 --- a/vssm_web/vssm_web/src/App.tsx +++ b/vssm_web/vssm_web/src/App.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import './App.css' -import InstancesTable from './components/InstanceBlock.tsx' +import InstancesTable from './components/InstanceTable.tsx' function App() { const [count, setCount] = useState(0) @@ -8,7 +8,9 @@ function App() { return ( <>
-

VSSM - Dashboard

+
+

VSSM - Dashboard

+
diff --git a/vssm_web/vssm_web/src/api.ts b/vssm_web/vssm_web/src/api.ts new file mode 100644 index 0000000..d302643 --- /dev/null +++ b/vssm_web/vssm_web/src/api.ts @@ -0,0 +1,31 @@ +const ADDRESS = "http://127.0.0.1:65000"; + +type Instance = { + name: string; + version: string; + port: number; + status: string; +}; + +class api { + + +} export default api; + +export async function fetchInstanceList(): Promise { + const res = await fetch(`${ADDRESS}/instances/list`); + if (!res.ok) throw new Error(`Could not fetch instance list: ${res.status}`); + return res.json(); +} + +export async function sendStartInstanceRequest(name: string) { + const res = await fetch(`${ADDRESS}/instances/start?name=${name}`, {method: "POST"}); + if (!res.ok) throw new Error(`Start Server Request failed: ${res.status}`); + return res.json; +} + +export async function sendStopInstanceRequest(name: string) { + const res = await fetch(`${ADDRESS}/instances/stop?name=${name}`, {method: "POST"}); + if (!res.ok) throw new Error(`Stop Server Request failed: ${res.status}`); + return res.json; +} \ No newline at end of file diff --git a/vssm_web/vssm_web/src/components/InstanceBlock.tsx b/vssm_web/vssm_web/src/components/InstanceBlock.tsx deleted file mode 100644 index 28c6308..0000000 --- a/vssm_web/vssm_web/src/components/InstanceBlock.tsx +++ /dev/null @@ -1,46 +0,0 @@ -function InstancesTable() { - return ( -
-

Instances

- - - - - - - - - - - - - - - - - - - - - -
NameStatusPortAction
Server 1 - - ◦Stopped - - 12345 - -
Server 2 - - ◦Running - - 54321 - -
-
- ); -} export default InstancesTable; - diff --git a/vssm_web/vssm_web/src/components/InstanceTable.tsx b/vssm_web/vssm_web/src/components/InstanceTable.tsx new file mode 100644 index 0000000..72d457c --- /dev/null +++ b/vssm_web/vssm_web/src/components/InstanceTable.tsx @@ -0,0 +1,126 @@ +import React, { useEffect, useState } from "react"; +import {fetchInstanceList, sendStartInstanceRequest, sendStopInstanceRequest} from "../api"; + +type Instance = { + name: string; + version: string; + port: number; + status: string; +}; + +export default function InstancesTable() { + const [instances, setInstances] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [actionLoading, setActionLoading] = useState>({}); + + async function refreshInstanceList() { + setLoading(true); + setError(null); + try { + const data = await fetchInstanceList(); + setInstances(data); + } catch (e: any) { + setError(e.message ?? String(e)); + setInstances([]); + } finally { + setLoading(false); + } + } + + async function waitForStatus(name: string, expectedStatus: string, timeoutMs = 10000, intervalMs = 700) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + const list = await fetchInstanceList(); + const inst = list.find(i => i.name === name); + if (inst?.status === expectedStatus) { + setInstances(list); + return inst; + } + setInstances(list); + } catch (e) { + } + await new Promise(r => setTimeout(r, intervalMs)); + } + throw new Error("Timeout waiting for status change"); + } + + async function handleToggle(inst: Instance) { + setActionLoading(prev => ({ ...prev, [inst.name]: true })); + try { + if (inst.status === "STOPPED") { + await sendStartInstanceRequest(inst.name); + try { + await waitForStatus(inst.name, "RUNNING", 15000, 700); + } catch { + await refreshInstanceList(); + } + } else { + await sendStopInstanceRequest(inst.name); + try { + await waitForStatus(inst.name, "STOPPED", 15000, 700); + } catch { + await refreshInstanceList(); + } + } + } catch (e) { + console.error(e); + await refreshInstanceList(); + } finally { + setActionLoading(prev => ({ ...prev, [inst.name]: false })); + } + } + + useEffect(() => { + refreshInstanceList(); + }, []); + + return ( +
+

Instances

+ + {error &&
Error: {error}
} + + + + + + + + + + + + {!instances && !loading && ( + + + + )} + + {instances?.map((inst) => ( + + + + + + + ))} + +
NameStatusPortAction
No data
{inst.name} + + ◦{inst.status} + + {inst.port} + +
+
+ ); +} diff --git a/vssm_web/vssm_web/src/index.css b/vssm_web/vssm_web/src/index.css index f99fc1c..a227e35 100644 --- a/vssm_web/vssm_web/src/index.css +++ b/vssm_web/vssm_web/src/index.css @@ -136,6 +136,20 @@ code { padding: 20px 0; } +header { + width: 100%; + background-color: rgba(103, 183, 7, 0.27); + color: #333; + height: auto; + padding: 5px 25px; + margin-bottom: 25px; + display: flex; +} +header h1 { + font-size: xx-large; + font-family: 'Times New Roman', Times, serif; +} + /* -- INSTANCE TABLE */ .instancesBlock { @@ -147,16 +161,16 @@ code { .instancesBlock #startStopButton { padding: 5px 15px; - border-radius: 10px; + border-radius: 6px; color: var(--text); } .instancesBlock #startStopButton.start{ background-color: var(--accent); - border: var(--accent-border) 3px solid; + border: var(--accent-border) 0px solid; } .instancesBlock #startStopButton.stop{ background-color: red; - border: #a40000 3px solid; + border: #a40000 0px solid; } .instancesBlock table {