Added a create instance and more error handling and some style changes
This commit is contained in:
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1780749050,
|
||||||
|
"narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a799d3e3886da994fa307f817a6bc705ae538eeb",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -14,18 +14,36 @@ class api {
|
|||||||
|
|
||||||
export async function fetchInstanceList(): Promise<Instance[]> {
|
export async function fetchInstanceList(): Promise<Instance[]> {
|
||||||
const res = await fetch(`${ADDRESS}/instances/list`);
|
const res = await fetch(`${ADDRESS}/instances/list`);
|
||||||
if (!res.ok) throw new Error(`Could not fetch instance list: ${res.status}`);
|
if (!res.ok) {
|
||||||
|
const message = await res.text();
|
||||||
|
throw new Error(`Could not fetch instance list: ${message.trim()}`);
|
||||||
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendStartInstanceRequest(name: string) {
|
export async function sendStartInstanceRequest(name: string) {
|
||||||
const res = await fetch(`${ADDRESS}/instances/start?name=${name}`, {method: "POST"});
|
const res = await fetch(`${ADDRESS}/instances/start?name=${name}`, {method: "POST"});
|
||||||
if (!res.ok) throw new Error(`Start Server Request failed: ${res.status}`);
|
if (!res.ok) {
|
||||||
|
const message = await res.text();
|
||||||
|
throw new Error(`Start Instance Request failed: ${message.trim()}`);
|
||||||
|
}
|
||||||
return res.json;
|
return res.json;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendStopInstanceRequest(name: string) {
|
export async function sendStopInstanceRequest(name: string) {
|
||||||
const res = await fetch(`${ADDRESS}/instances/stop?name=${name}`, {method: "POST"});
|
const res = await fetch(`${ADDRESS}/instances/stop?name=${name}`, {method: "POST"});
|
||||||
if (!res.ok) throw new Error(`Stop Server Request failed: ${res.status}`);
|
if (!res.ok) {
|
||||||
|
const message = await res.text();
|
||||||
|
throw new Error(`Stop Instance Request failed: ${message.trim()}`);
|
||||||
|
}
|
||||||
|
return res.json;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendCreateInstanceRequest(name: string, version: string, port: number) {
|
||||||
|
const res = await fetch(`${ADDRESS}/instances/create?name=${name}&version=${version}&port=${port}`, {method: "POST"});
|
||||||
|
if (!res.ok) {
|
||||||
|
const message = await res.text();
|
||||||
|
throw new Error(`Create instance request failed: ${message.trim()}`);
|
||||||
|
}
|
||||||
return res.json;
|
return res.json;
|
||||||
}
|
}
|
||||||
BIN
vssm_web/vssm_web/src/assets/Goudament.ttf
Normal file
BIN
vssm_web/vssm_web/src/assets/Goudament.ttf
Normal file
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {fetchInstanceList, sendStartInstanceRequest, sendStopInstanceRequest} from "../api";
|
import {fetchInstanceList, sendStartInstanceRequest, sendStopInstanceRequest, sendCreateInstanceRequest} from "../api";
|
||||||
|
|
||||||
type Instance = {
|
type Instance = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -14,6 +14,11 @@ export default function InstancesTable() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [actionLoading, setActionLoading] = useState<Record<string, boolean>>({});
|
const [actionLoading, setActionLoading] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
|
const [createForm, setCreateForm] = useState({ name: "", version: "", port: "" });
|
||||||
|
const [createError, setCreateError] = useState<string | null>(null);
|
||||||
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
|
|
||||||
async function refreshInstanceList() {
|
async function refreshInstanceList() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -72,13 +77,37 @@ export default function InstancesTable() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCreateModal() {
|
||||||
|
setCreateForm({ name: "", version: "", port: "" });
|
||||||
|
setCreateError(null);
|
||||||
|
setCreateModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
setCreateForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
setCreateError(null);
|
||||||
|
setCreateLoading(true);
|
||||||
|
try {
|
||||||
|
await sendCreateInstanceRequest(createForm.name, createForm.version, parseInt(createForm.port));
|
||||||
|
setCreateModalOpen(false);
|
||||||
|
await refreshInstanceList();
|
||||||
|
} catch (e: any) {
|
||||||
|
setCreateError(e.message ?? String(e));
|
||||||
|
} finally {
|
||||||
|
setCreateLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshInstanceList();
|
refreshInstanceList();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="instancesBlock">
|
<div className="instancesBlock">
|
||||||
<h3>Instances</h3>
|
<h2>Instances</h2>
|
||||||
|
|
||||||
{error && <div className="error">Error: {error}</div>}
|
{error && <div className="error">Error: {error}</div>}
|
||||||
|
|
||||||
@@ -94,7 +123,7 @@ export default function InstancesTable() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{!instances && !loading && (
|
{!instances && !loading && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4}>No data</td>
|
<td colSpan={4}>No instances found, create one below!</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -108,19 +137,49 @@ export default function InstancesTable() {
|
|||||||
</td>
|
</td>
|
||||||
<td>{inst.port}</td>
|
<td>{inst.port}</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<div className="flexboxHorizontal">
|
||||||
|
<button
|
||||||
id="startStopButton"
|
id="startStopButton"
|
||||||
className={inst.status === "RUNNING" ? "stop" : "start"}
|
className={inst.status === "RUNNING" ? "stop" : "start"}
|
||||||
onClick={() => void handleToggle(inst)}
|
onClick={() => void handleToggle(inst)}
|
||||||
disabled={loading || actionLoading[inst.name]}
|
disabled={loading || actionLoading[inst.name]}
|
||||||
>
|
>
|
||||||
{actionLoading[inst.name] ? "..." : inst.status === "RUNNING" ? "\u25FC Stop" : "\u25B6 Start"}
|
{actionLoading[inst.name] ? "..." : inst.status === "RUNNING" ? "\u25FC Stop" : "\u25B6 Start"}
|
||||||
</button>
|
</button>
|
||||||
|
<button className="normalButton">✎ Edit</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div className="flexboxHorizontal">
|
||||||
|
<b>Create new instance</b>
|
||||||
|
<button onClick={() => openCreateModal()}>+</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{createModalOpen && (
|
||||||
|
<div className="modalBackdrop" onClick={() => setCreateModalOpen(false)}>
|
||||||
|
<div className="modalCard" onClick={e => e.stopPropagation()}>
|
||||||
|
<h3>Create New Instance</h3>
|
||||||
|
{createError && <div className="error">Error: {createError}</div>}
|
||||||
|
<input type="text" name="name" value={createForm.name} onChange={handleFormChange} placeholder="Instance Name" />
|
||||||
|
<input type="text" name="version" value={createForm.version} onChange={handleFormChange} placeholder="1.22.3" />
|
||||||
|
<input type="text" name="port" value={createForm.port} onChange={handleFormChange} placeholder="12345" />
|
||||||
|
<button onClick={handleCreate} disabled={createLoading}>
|
||||||
|
{createLoading ? "Creating..." : "Create"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||||
|
|
||||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
--heading: Goudament, system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||||
--mono: ui-monospace, Consolas, monospace;
|
--mono: ui-monospace, Consolas, monospace;
|
||||||
|
|
||||||
font: 18px/145% var(--sans);
|
font: 18px/145% var(--sans);
|
||||||
@@ -30,6 +30,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Goudament;
|
||||||
|
src: url(assets/Goudament.ttf);
|
||||||
|
}
|
||||||
|
|
||||||
/* @media (prefers-color-scheme: dark) {
|
/* @media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--text: #9ca3af;
|
--text: #9ca3af;
|
||||||
@@ -85,22 +90,27 @@ body {
|
|||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2 {
|
h2 {
|
||||||
font-family: var(--heading);
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-h);
|
color: #91a357;
|
||||||
|
text-shadow: 3px 3px 5px #2c0105 ;
|
||||||
|
font-family: var(--heading);
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 56px;
|
font-size: 4.5em;
|
||||||
letter-spacing: -1.68px;
|
letter-spacing: -1.68px;
|
||||||
margin: 32px 0;
|
margin: 32px 0;
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
font-family: var(--heading);
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 24px;
|
font-size: 4em;
|
||||||
line-height: 118%;
|
line-height: 118%;
|
||||||
letter-spacing: -0.24px;
|
letter-spacing: -0.24px;
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
@@ -108,6 +118,7 @@ h2 {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -128,33 +139,91 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contentBox {
|
.contentBox {
|
||||||
background-color: rgba(255,255,255,0.6);
|
background-color: var(--accent-bg);
|
||||||
width: 1200;
|
/* opacity: 0.8; */
|
||||||
max-width: 95%;
|
width: 1000;
|
||||||
|
max-width: 85%;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
width: 100%;
|
max-width: 100%;
|
||||||
background-color: rgba(103, 183, 7, 0.27);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
color: #333;
|
color: #333;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 5px 25px;
|
padding: 5px 20px;
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
display: flex;
|
|
||||||
}
|
}
|
||||||
header h1 {
|
header h1 {
|
||||||
font-size: xx-large;
|
font-size: 4em;
|
||||||
font-family: 'Times New Roman', Times, serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Layout stuff */
|
||||||
|
.flexboxHorizontal {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalBackdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.modalCard {
|
||||||
|
background: var(--accent-bg);
|
||||||
|
padding: 50px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Normal components styling */
|
||||||
|
button {
|
||||||
|
transition: transform 0.15s ease-in-out 0s;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #f4f3ec;
|
||||||
|
background-color: var(--accent);
|
||||||
|
border: transparent 0px solid;
|
||||||
|
min-height: 3px;
|
||||||
|
font-size: medium;
|
||||||
|
} button:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
transition: transform 0.15s ease-in-out 0s;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #f4f3ec;
|
||||||
|
background-color: #333;
|
||||||
|
border: transparent 0px solid;
|
||||||
|
min-height: 25px;
|
||||||
|
} input:focus {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.normalButton {
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #f4f3ec;
|
||||||
|
background-color: #455487;
|
||||||
|
border: transparent 0px solid;
|
||||||
|
}
|
||||||
|
|
||||||
/* -- INSTANCE TABLE */
|
/* -- INSTANCE TABLE */
|
||||||
|
|
||||||
.instancesBlock {
|
.instancesBlock {
|
||||||
background-color: var(--accent-bg);
|
background-color: transparent;
|
||||||
max-width: 95%;
|
max-width: 80%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
@@ -169,8 +238,8 @@ header h1 {
|
|||||||
border: var(--accent-border) 0px solid;
|
border: var(--accent-border) 0px solid;
|
||||||
}
|
}
|
||||||
.instancesBlock #startStopButton.stop{
|
.instancesBlock #startStopButton.stop{
|
||||||
background-color: red;
|
background-color: rgb(206, 62, 62);
|
||||||
border: #a40000 0px solid;
|
border: transparent 0px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.instancesBlock table {
|
.instancesBlock table {
|
||||||
@@ -179,14 +248,12 @@ header h1 {
|
|||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.instancesBlock th {
|
.instancesBlock th {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border-bottom: 2px solid var(--border);
|
border-bottom: 2px solid var(--border);
|
||||||
color: var(--text-h);
|
color: var(--text-h);
|
||||||
}
|
}
|
||||||
|
|
||||||
.instancesBlock td {
|
.instancesBlock td {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
@@ -195,9 +262,6 @@ header h1 {
|
|||||||
#statusIndicator {
|
#statusIndicator {
|
||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
/* background-color: var(--accent); */
|
|
||||||
/* border: var(--accent-border) 3px solid; */
|
|
||||||
}
|
}
|
||||||
#statusIndicator.running {
|
#statusIndicator.running {
|
||||||
color: var(--accent-border);
|
color: var(--accent-border);
|
||||||
@@ -205,3 +269,6 @@ header h1 {
|
|||||||
#statusIndicator.stopped {
|
#statusIndicator.stopped {
|
||||||
color: #a40000;
|
color: #a40000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user