Can now see a list of instances, their status, and start/stop them
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import InstancesTable from './components/InstanceBlock.tsx'
|
import InstancesTable from './components/InstanceTable.tsx'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [count, setCount] = useState(0)
|
const [count, setCount] = useState(0)
|
||||||
@@ -8,7 +8,9 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section id='main'>
|
<section id='main'>
|
||||||
<h1>VSSM - Dashboard</h1>
|
<header>
|
||||||
|
<h1>VSSM - Dashboard</h1>
|
||||||
|
</header>
|
||||||
<div className='contentBox'>
|
<div className='contentBox'>
|
||||||
<InstancesTable />
|
<InstancesTable />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
31
vssm_web/vssm_web/src/api.ts
Normal file
31
vssm_web/vssm_web/src/api.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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">
|
|
||||||
◦Stopped
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>12345</td>
|
|
||||||
<td>
|
|
||||||
<button id="startStopButton" className="start">
|
|
||||||
▶ Start
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>Server 2</td>
|
|
||||||
<td>
|
|
||||||
<span id="statusIndicator" className="running">
|
|
||||||
◦Running
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>54321</td>
|
|
||||||
<td>
|
|
||||||
<button id="startStopButton" className="stop">
|
|
||||||
◼ Stop
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} export default InstancesTable;
|
|
||||||
|
|
||||||
126
vssm_web/vssm_web/src/components/InstanceTable.tsx
Normal file
126
vssm_web/vssm_web/src/components/InstanceTable.tsx
Normal 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()}>
|
||||||
|
◦{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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -136,6 +136,20 @@ code {
|
|||||||
padding: 20px 0;
|
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 */
|
/* -- INSTANCE TABLE */
|
||||||
|
|
||||||
.instancesBlock {
|
.instancesBlock {
|
||||||
@@ -147,16 +161,16 @@ code {
|
|||||||
|
|
||||||
.instancesBlock #startStopButton {
|
.instancesBlock #startStopButton {
|
||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
border-radius: 10px;
|
border-radius: 6px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.instancesBlock #startStopButton.start{
|
.instancesBlock #startStopButton.start{
|
||||||
background-color: var(--accent);
|
background-color: var(--accent);
|
||||||
border: var(--accent-border) 3px solid;
|
border: var(--accent-border) 0px solid;
|
||||||
}
|
}
|
||||||
.instancesBlock #startStopButton.stop{
|
.instancesBlock #startStopButton.stop{
|
||||||
background-color: red;
|
background-color: red;
|
||||||
border: #a40000 3px solid;
|
border: #a40000 0px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.instancesBlock table {
|
.instancesBlock table {
|
||||||
|
|||||||
Reference in New Issue
Block a user