Can now see a list of instances, their status, and start/stop them

This commit is contained in:
2026-06-07 23:51:36 -05:00
parent 0442642f8a
commit 9729a00ca6
5 changed files with 178 additions and 51 deletions

View File

@@ -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 (
<>
<section id='main'>
<h1>VSSM - Dashboard</h1>
<header>
<h1>VSSM - Dashboard</h1>
</header>
<div className='contentBox'>
<InstancesTable />
</div>

View File

@@ -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<Instance[]> {
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;
}

View File

@@ -1,46 +0,0 @@
function InstancesTable() {
return (
<div className="instancesBlock">
<h3>Instances</h3>
<table>
<tr>
<th>Name</th>
<th>Status</th>
<th>Port</th>
<th>Action</th>
</tr>
<tr>
<td>Server 1</td>
<td>
<span id="statusIndicator" className="stopped">
&#x25E6;Stopped
</span>
</td>
<td>12345</td>
<td>
<button id="startStopButton" className="start">
&#x25B6; Start
</button>
</td>
</tr>
<tr>
<td>Server 2</td>
<td>
<span id="statusIndicator" className="running">
&#x25E6;Running
</span>
</td>
<td>54321</td>
<td>
<button id="startStopButton" className="stop">
&#x25FC; Stop
</button>
</td>
</tr>
</table>
</div>
);
} export default InstancesTable;

View File

@@ -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<Instance[] | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [actionLoading, setActionLoading] = useState<Record<string, boolean>>({});
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 (
<div className="instancesBlock">
<h3>Instances</h3>
{error && <div className="error">Error: {error}</div>}
<table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Port</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{!instances && !loading && (
<tr>
<td colSpan={4}>No data</td>
</tr>
)}
{instances?.map((inst) => (
<tr key={`${inst.name}-${inst.port}`}>
<td>{inst.name}</td>
<td>
<span id="statusIndicator" className={inst.status.toLowerCase()}>
&#x25E6;{inst.status}
</span>
</td>
<td>{inst.port}</td>
<td>
<button
id="startStopButton"
className={inst.status === "RUNNING" ? "stop" : "start"}
onClick={() => void handleToggle(inst)}
disabled={loading || actionLoading[inst.name]}
>
{actionLoading[inst.name] ? "..." : inst.status === "RUNNING" ? "\u25FC Stop" : "\u25B6 Start"}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@@ -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 {