Compare commits

..
5 Commits
33 changed files with 5825 additions and 3276 deletions

No files matched your search

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2396 -1
View File
File diff suppressed because it is too large. Load diff
+3141 -1
View File
File diff suppressed because it is too large. Load diff
+34 -1
View File
@@ -1 +1,34 @@
{} {
"name": "vssm_web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@eslint/js": "^10.0.1",
"@rolldown/plugin-babel": "^0.2.3",
"@types/babel__core": "^7.20.5",
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12"
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,19 +1,22 @@
import { useState } from 'react' import { useState } from 'react'
import './App.css' import './App.css'
import InstancesTable from './components/InstanceTable.tsx' import InstancesTable from './components/InstanceTable.tsx'
import Settings from './components/Settings.tsx'
function App() { function App() {
const [count, setCount] = useState(0) const [settingsOpen, setSettingsOpen] = useState(false)
return ( return (
<> <>
<section id='main'> <section id='main'>
<header> <header>
<h1>VSSM - Dashboard</h1> <h1>VSSM - Dashboard</h1>
<button onClick={() => setSettingsOpen(true)}>⚙</button>
</header> </header>
<div className='contentBox'> <div className='contentBox'>
<InstancesTable /> <InstancesTable />
</div> </div>
{settingsOpen && <Settings onClose={() => setSettingsOpen(false)} />}
</section> </section>
</> </>
) )
+90
View File
@@ -0,0 +1,90 @@
import { getSetting, SettingKey } from "./settings";
type Instance = {
name: string;
version: string;
port: number;
status: string;
};
class api {
} export default api;
async function apiFetch(path: string, options: RequestInit = {}, errorContext: string): Promise<Response> {
const token = getSetting(SettingKey.Token);
const headers = {
...options.headers,
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
const res = await fetch(`${getSetting(SettingKey.Address)}${path}`, { ...options, headers });
if (!res.ok) {
const message = await res.text();
throw new Error(`${errorContext}: ${message.trim()}`);
}
return res;
}
// --- GET CALLS ---
export async function fetchInstanceList(): Promise<Instance[]> {
const res = await apiFetch("/instances/list", {}, "Could not fetch instance list");
return res.json();
}
export async function fetchInstanceLogs(name: string): Promise<string[]> {
const res = await apiFetch(`/instances/logs?name=${name}`, {}, "Could not fetch instance logs");
return res.json();
}
export async function fetchInstanceConfig(name: string): Promise<string> {
const res = await apiFetch(`/instances/getConfig?name=${name}`, {}, "Could not fetch instance config");
return res.text();
}
// --- POST CALLS ---
export async function sendStartInstanceRequest(name: string) {
const res = await apiFetch(`/instances/start?name=${name}`, { method: "POST" }, "Start Instance Request failed");
return res.text();
}
export async function sendStopInstanceRequest(name: string) {
const res = await apiFetch(`/instances/stop?name=${name}`, { method: "POST" }, "Stop Instance Request failed");
return res.text();
}
export async function sendCreateInstanceRequest(name: string, version: string, port: number) {
const res = await apiFetch(`/instances/create?name=${name}&version=${version}&port=${port}`, { method: "POST" }, "Create instance request failed");
return res.text();
}
export async function sendDeleteInstanceRequest(name: string, purge: boolean) {
const res = await apiFetch(`/instances/delete?name=${name}&purge=${purge}`, { method: "POST" }, "Delete instance request failed");
return res.text();
}
export async function sendModifyInstanceRequest(name: string, newName: string, newPort: number) {
const res = await apiFetch(`/instances/modify?name=${name}&newName=${newName}&newPort=${newPort}`, { method: "POST" }, "Modify instance request failed");
return res.text();
}
export async function sendCommandRequest(name: string, payload: string) {
const res = await apiFetch(`/instances/command?name=${name}`, {
method: "POST",
body: JSON.stringify({ command: payload }),
headers: { "Content-Type": "application/json" },
}, "Send Instance Command failed");
return res.text();
}
export async function sendSetInstanceConfigRequest(name: string, config: string) {
const res = await apiFetch(`/instances/setConfig?name=${name}`, {
method: "POST",
body: config,
headers: { "Content-Type": "application/json" },
}, "Set instance config request failed");
return res.text();
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,50 @@
import { useState, useEffect } from "react";
import type { Instance } from "../types";
import { fetchInstanceConfig, sendSetInstanceConfigRequest } from "../api";
type Props = {
instance: Instance;
};
export default function InstanceConfig({ instance }: Props) {
const [config, setConfig] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchInstanceConfig(instance.name)
.then(setConfig)
.catch(e => setError(e.message ?? String(e)))
.finally(() => setLoading(false));
}, [instance.name]);
async function handleSave() {
setSaving(true);
setError(null);
try {
await sendSetInstanceConfigRequest(instance.name, config);
} catch (e: any) {
setError(e.message ?? String(e));
} finally {
setSaving(false);
}
}
if (loading) return <p>Loading config...</p>;
return (
<div className="configEditor">
{error && <div className="error">Error: {error}</div>}
<textarea
value={config}
onChange={e => setConfig(e.target.value)}
rows={25}
spellCheck={false}
/>
<button disabled={saving} onClick={handleSave}>
{saving ? "Saving..." : "Save"}
</button>
</div>
);
}
@@ -2,6 +2,7 @@ import { useState } from "react";
import type { Instance } from "../types"; import type { Instance } from "../types";
import { sendDeleteInstanceRequest, sendModifyInstanceRequest } from "../api"; import { sendDeleteInstanceRequest, sendModifyInstanceRequest } from "../api";
import InstanceConsole from "./InstanceConsole"; import InstanceConsole from "./InstanceConsole";
import InstanceConfig from "./InstanceConfig";
type Props = { type Props = {
instance: Instance; instance: Instance;
@@ -58,8 +59,8 @@ export default function InstanceDetail({ instance, onClose, onDeleted, onSaved }
<div className="tabBar"> <div className="tabBar">
<button onClick={() => setActiveTab("general")}>General</button> <button onClick={() => setActiveTab("general")}>General</button>
<button onClick={() => setActiveTab("config")}>Configuration</button>
<button onClick={() => setActiveTab("console")}>Console</button> <button onClick={() => setActiveTab("console")}>Console</button>
<button onClick={() => setActiveTab("config")}>Configuration</button>
<button onClick={() => setActiveTab("danger")}>Danger Zone</button> <button onClick={() => setActiveTab("danger")}>Danger Zone</button>
</div> </div>
@@ -83,10 +84,10 @@ export default function InstanceDetail({ instance, onClose, onDeleted, onSaved }
</div> </div>
} }
{activeTab === "config" && <p>Config (coming soon)</p>}
{activeTab === "console" && <InstanceConsole instance={instance} />} {activeTab === "console" && <InstanceConsole instance={instance} />}
{activeTab === "config" && <InstanceConfig instance={instance}/>}
{activeTab === "danger" && {activeTab === "danger" &&
<div className="dangerZone"> <div className="dangerZone">
<h3>Danger Zone</h3> <h3>Danger Zone</h3>
+45
View File
@@ -0,0 +1,45 @@
import { useState } from "react";
import { getSetting, setSetting, SettingKey } from "../settings";
type Props = {
onClose: () => void;
};
export default function Settings({ onClose }: Props) {
const [address, setAddress] = useState(getSetting(SettingKey.Address));
const [token, setToken] = useState(getSetting(SettingKey.Token));
const [saved, setSaved] = useState(false);
function handleSave() {
setSetting(SettingKey.Address, address);
setSetting(SettingKey.Token, token);
setSaved(true);
}
return (
<div className="modalBackdrop" onClick={onClose}>
<div className="modalCard" onClick={e => e.stopPropagation()}>
<h3>Settings</h3>
<label>
API Address
<input type="text" value={address} onChange={e => { setAddress(e.target.value); setSaved(false); }} />
</label>
<label>
Auth Token
<input type="text" value={token} onChange={e => { setToken(e.target.value); setSaved(false); }} />
</label>
<p className="warningText">Stored locally in this browser only.</p>
{saved && <p>Saved.</p>}
<div className="flexboxHorizontal">
<button onClick={handleSave}>Save</button>
<button className="normalButton" onClick={onClose}>Close</button>
</div>
</div>
</div>
);
}
@@ -154,6 +154,9 @@ header {
height: auto; height: auto;
padding: 5px 20px; padding: 5px 20px;
margin-bottom: 25px; margin-bottom: 25px;
display: flex;
align-items: center;
justify-content: space-between;
} }
header h1 { header h1 {
font-size: 4em; font-size: 4em;
@@ -184,6 +187,15 @@ header h1 {
min-width: 300px; min-width: 300px;
} }
.modalCard label {
display: flex;
flex-direction: column;
gap: 6px;
font-weight: 600;
font-size: 0.9em;
text-align: left;
}
/* Normal components styling */ /* Normal components styling */
button { button {
transition: transform 0.15s ease-in-out 0s; transition: transform 0.15s ease-in-out 0s;
@@ -442,4 +454,26 @@ input[type="checkbox"] {
.consoleLine i { .consoleLine i {
color: #a0a0c0; color: #a0a0c0;
font-style: italic; font-style: italic;
}
/* Config editor styles */
.configEditor {
display: flex;
flex-direction: column;
gap: 8px;
}
.configEditor textarea {
font-family: var(--mono);
font-size: 0.85em;
background-color: #1a1a1a;
color: #d4d4d4;
border-radius: 6px;
padding: 12px;
border: transparent 0px solid;
resize: vertical;
}
.configEditor button {
max-width: fit-content;
} }
File renamed without changes.
+27
View File
@@ -0,0 +1,27 @@
export const SettingKey = {
Address: "vssm_api_address",
Token: "vssm_auth_token",
} as const;
export type SettingKey = typeof SettingKey[keyof typeof SettingKey];
const SettingDefaults: Record<SettingKey, string> = {
[SettingKey.Address]: "",
[SettingKey.Token]: "",
};
export function getSetting(key: SettingKey): string {
const value = localStorage.getItem(key);
if (!value) {
return SettingDefaults[key];
}
return value;
}
export function setSetting(key: SettingKey, value: string) {
localStorage.setItem(key, value);
}
export function clearSetting(key: SettingKey) {
localStorage.removeItem(key);
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
-3146
View File
File diff suppressed because it is too large. Load diff
-34
View File
@@ -1,34 +0,0 @@
{
"name": "vssm_web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@eslint/js": "^10.0.1",
"@rolldown/plugin-babel": "^0.2.3",
"@types/babel__core": "^7.20.5",
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12"
}
}
-89
View File
@@ -1,89 +0,0 @@
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) {
const message = await res.text();
throw new Error(`Could not fetch instance list: ${message.trim()}`);
}
return res.json();
}
export async function fetchInstanceLogs(name: string): Promise<string[]> {
const res = await fetch(`${ADDRESS}/instances/logs?name=${name}`);
if (!res.ok) {
const message = await res.text();
throw new Error(`Could not fetch instance logs: ${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) {
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) {
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;
}
export async function sendDeleteInstanceRequest(name: string, purge: boolean) {
const res = await fetch(`${ADDRESS}/instances/delete?name=${name}&purge=${purge}`, { method: "POST" });
if (!res.ok) {
const message = await res.text();
throw new Error(`Delete instance request failed: ${message.trim()}`);
}
return res.json;
}
export async function sendModifyInstanceRequest(name: string, newName: string, newPort: number) {
const res = await fetch(`${ADDRESS}/instances/modify?name=${name}&newName=${newName}&newPort=${newPort}`, { method: "POST" });
if (!res.ok) {
const message = await res.text();
throw new Error(`Modify instance request failed: ${message.trim()}`);
}
return res.json;
}
export async function sendCommandRequest(name: string, payload: string) {
const res = await fetch(`${ADDRESS}/instances/command?name=${name}`, {
method: "POST",
body: JSON.stringify({ command: payload }),
headers: { "Content-Type": "application/json" },
});
if (!res.ok) {
const message = await res.text();
throw new Error(`Send Instance Command failed: ${message.trim()}`);
}
return res.json;
}