diff --git a/vssm_web/vssm_web/src/App.tsx b/vssm_web/vssm_web/src/App.tsx index 079b4b7..e4627b0 100644 --- a/vssm_web/vssm_web/src/App.tsx +++ b/vssm_web/vssm_web/src/App.tsx @@ -1,19 +1,22 @@ import { useState } from 'react' import './App.css' import InstancesTable from './components/InstanceTable.tsx' +import Settings from './components/Settings.tsx' function App() { - const [count, setCount] = useState(0) + const [settingsOpen, setSettingsOpen] = useState(false) return ( <> VSSM - Dashboard + setSettingsOpen(true)}>⚙ + {settingsOpen && setSettingsOpen(false)} />} > ) diff --git a/vssm_web/vssm_web/src/api.ts b/vssm_web/vssm_web/src/api.ts index 9810361..3f207a6 100644 --- a/vssm_web/vssm_web/src/api.ts +++ b/vssm_web/vssm_web/src/api.ts @@ -1,4 +1,4 @@ -const ADDRESS = "http://127.0.0.1:65000"; +import { getSetting, SettingKey } from "./settings"; type Instance = { name: string; @@ -12,105 +12,79 @@ class api { } export default api; +async function apiFetch(path: string, options: RequestInit = {}, errorContext: string): Promise { + 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 { - 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()}`); - } + const res = await apiFetch("/instances/list", {}, "Could not fetch instance list"); return res.json(); } export async function fetchInstanceLogs(name: string): Promise { - 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()}`); - } + const res = await apiFetch(`/instances/logs?name=${name}`, {}, "Could not fetch instance logs"); return res.json(); } export async function fetchInstanceConfig(name: string): Promise { - const res = await fetch(`${ADDRESS}/instances/getConfig?name=${name}`); - if (!res.ok) { - const message = await res.text(); - throw new Error(`Could not fetch instance config: ${message.trim()}`); - } + 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 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()}`); - } + 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 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()}`); - } + 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 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()}`); - } + 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 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()}`); - } + 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 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()}`); - } + 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 fetch(`${ADDRESS}/instances/command?name=${name}`, { + const res = await apiFetch(`/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()}`); - } + headers: { "Content-Type": "application/json" }, + }, "Send Instance Command failed"); return res.text(); } export async function sendSetInstanceConfigRequest(name: string, config: string) { - const res = await fetch(`${ADDRESS}/instances/setConfig?name=${name}`, { + const res = await apiFetch(`/instances/setConfig?name=${name}`, { method: "POST", body: config, headers: { "Content-Type": "application/json" }, - }); - if (!res.ok) { - const message = await res.text(); - throw new Error(`Set instance config request failed: ${message.trim()}`); - } + }, "Set instance config request failed"); return res.text(); } \ No newline at end of file diff --git a/vssm_web/vssm_web/src/components/Settings.tsx b/vssm_web/vssm_web/src/components/Settings.tsx new file mode 100644 index 0000000..55db27f --- /dev/null +++ b/vssm_web/vssm_web/src/components/Settings.tsx @@ -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 ( + + e.stopPropagation()}> + Settings + + + API Address + { setAddress(e.target.value); setSaved(false); }} /> + + + + Auth Token + { setToken(e.target.value); setSaved(false); }} /> + + + Stored locally in this browser only. + + {saved && Saved.} + + + Save + Close + + + + ); +} \ 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 b66f64f..66e98e5 100644 --- a/vssm_web/vssm_web/src/index.css +++ b/vssm_web/vssm_web/src/index.css @@ -154,6 +154,9 @@ header { height: auto; padding: 5px 20px; margin-bottom: 25px; + display: flex; + align-items: center; + justify-content: space-between; } header h1 { font-size: 4em; @@ -184,6 +187,15 @@ header h1 { 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 */ button { transition: transform 0.15s ease-in-out 0s; diff --git a/vssm_web/vssm_web/src/settings.ts b/vssm_web/vssm_web/src/settings.ts new file mode 100644 index 0000000..218517d --- /dev/null +++ b/vssm_web/vssm_web/src/settings.ts @@ -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.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); +} \ No newline at end of file
Stored locally in this browser only.
Saved.