diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9140dbc --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/vssm_web/vssm_web/src/api.ts b/vssm_web/vssm_web/src/api.ts index d302643..86e5aa7 100644 --- a/vssm_web/vssm_web/src/api.ts +++ b/vssm_web/vssm_web/src/api.ts @@ -14,18 +14,36 @@ class 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}`); + if (!res.ok) { + const message = await res.text(); + throw new Error(`Could not fetch instance list: ${message.trim()}`); + } 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}`); + if (!res.ok) { + const message = await res.text(); + throw new Error(`Start Instance Request failed: ${message.trim()}`); + } 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}`); + 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; } \ No newline at end of file diff --git a/vssm_web/vssm_web/src/assets/Goudament.ttf b/vssm_web/vssm_web/src/assets/Goudament.ttf new file mode 100644 index 0000000..f504270 Binary files /dev/null and b/vssm_web/vssm_web/src/assets/Goudament.ttf differ diff --git a/vssm_web/vssm_web/src/components/InstanceTable.tsx b/vssm_web/vssm_web/src/components/InstanceTable.tsx index 72d457c..c18ca08 100644 --- a/vssm_web/vssm_web/src/components/InstanceTable.tsx +++ b/vssm_web/vssm_web/src/components/InstanceTable.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import {fetchInstanceList, sendStartInstanceRequest, sendStopInstanceRequest} from "../api"; +import {fetchInstanceList, sendStartInstanceRequest, sendStopInstanceRequest, sendCreateInstanceRequest} from "../api"; type Instance = { name: string; @@ -14,6 +14,11 @@ export default function InstancesTable() { const [error, setError] = useState(null); const [actionLoading, setActionLoading] = useState>({}); + const [createModalOpen, setCreateModalOpen] = useState(false); + const [createForm, setCreateForm] = useState({ name: "", version: "", port: "" }); + const [createError, setCreateError] = useState(null); + const [createLoading, setCreateLoading] = useState(false); + async function refreshInstanceList() { setLoading(true); 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) { + 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(() => { refreshInstanceList(); }, []); return (
-

Instances

+

Instances

{error &&
Error: {error}
} @@ -94,7 +123,7 @@ export default function InstancesTable() { {!instances && !loading && ( - No data + No instances found, create one below! )} @@ -108,19 +137,49 @@ export default function InstancesTable() { {inst.port} - + > + {actionLoading[inst.name] ? "..." : inst.status === "RUNNING" ? "\u25FC Stop" : "\u25B6 Start"} + + +
))} + + + +
+ Create new instance + +
+ + + + + + + {createModalOpen && ( +
setCreateModalOpen(false)}> +
e.stopPropagation()}> +

Create New Instance

+ {createError &&
Error: {createError}
} + + + + +
+
+ )} ); -} +} \ No newline at end of file diff --git a/vssm_web/vssm_web/src/index.css b/vssm_web/vssm_web/src/index.css index a227e35..705549d 100644 --- a/vssm_web/vssm_web/src/index.css +++ b/vssm_web/vssm_web/src/index.css @@ -12,7 +12,7 @@ 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; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; + --heading: Goudament, system-ui, 'Segoe UI', Roboto, sans-serif; --mono: ui-monospace, Consolas, monospace; font: 18px/145% var(--sans); @@ -30,6 +30,11 @@ } } +@font-face { + font-family: Goudament; + src: url(assets/Goudament.ttf); +} + /* @media (prefers-color-scheme: dark) { :root { --text: #9ca3af; @@ -85,22 +90,27 @@ body { h1, h2 { - font-family: var(--heading); font-weight: 500; - color: var(--text-h); + color: #91a357; + text-shadow: 3px 3px 5px #2c0105 ; + font-family: var(--heading); +} +h3 { + color: var(--text); } h1 { - font-size: 56px; + font-size: 4.5em; letter-spacing: -1.68px; margin: 32px 0; @media (max-width: 1024px) { font-size: 36px; margin: 20px 0; } + font-family: var(--heading); } h2 { - font-size: 24px; + font-size: 4em; line-height: 118%; letter-spacing: -0.24px; margin: 0 0 8px; @@ -108,6 +118,7 @@ h2 { font-size: 20px; } } + p { margin: 0; } @@ -128,33 +139,91 @@ code { } .contentBox { - background-color: rgba(255,255,255,0.6); - width: 1200; - max-width: 95%; + background-color: var(--accent-bg); + /* opacity: 0.8; */ + width: 1000; + max-width: 85%; min-height: 200px; margin: auto; padding: 20px 0; } header { - width: 100%; - background-color: rgba(103, 183, 7, 0.27); + max-width: 100%; + background-color: rgba(0, 0, 0, 0.2); color: #333; height: auto; - padding: 5px 25px; + padding: 5px 20px; margin-bottom: 25px; - display: flex; } header h1 { - font-size: xx-large; - font-family: 'Times New Roman', Times, serif; + font-size: 4em; } +/* 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 */ .instancesBlock { - background-color: var(--accent-bg); - max-width: 95%; + background-color: transparent; + max-width: 80%; margin: auto; padding: 15px; } @@ -169,8 +238,8 @@ header h1 { border: var(--accent-border) 0px solid; } .instancesBlock #startStopButton.stop{ - background-color: red; - border: #a40000 0px solid; + background-color: rgb(206, 62, 62); + border: transparent 0px solid; } .instancesBlock table { @@ -179,14 +248,12 @@ header h1 { border-collapse: collapse; text-align: left; } - .instancesBlock th { padding: 10px 12px; font-weight: 600; border-bottom: 2px solid var(--border); color: var(--text-h); } - .instancesBlock td { padding: 10px 12px; border-bottom: 1px solid var(--border); @@ -195,9 +262,6 @@ header h1 { #statusIndicator { padding: 5px 15px; border-radius: 10px; - - /* background-color: var(--accent); */ - /* border: var(--accent-border) 3px solid; */ } #statusIndicator.running { color: var(--accent-border); @@ -205,3 +269,6 @@ header h1 { #statusIndicator.stopped { color: #a40000; } + + +