diff --git a/vssm_web/vssm_web/src/api.ts b/vssm_web/vssm_web/src/api.ts index 86e5aa7..eb9d94a 100644 --- a/vssm_web/vssm_web/src/api.ts +++ b/vssm_web/vssm_web/src/api.ts @@ -8,42 +8,60 @@ type Instance = { }; class api { - - + + } export default api; 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()}`); - } - return res.json(); + 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 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; + 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; + 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; + 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; } \ No newline at end of file diff --git a/vssm_web/vssm_web/src/components/InstanceDetail.tsx b/vssm_web/vssm_web/src/components/InstanceDetail.tsx new file mode 100644 index 0000000..a6e8392 --- /dev/null +++ b/vssm_web/vssm_web/src/components/InstanceDetail.tsx @@ -0,0 +1,124 @@ +import { useState } from "react"; +import type { Instance } from "../types"; +import { sendDeleteInstanceRequest, sendModifyInstanceRequest } from "../api"; + +type Props = { + instance: Instance; + onClose: () => void; + onDeleted: () => void; + onSaved: () => void; +}; + + +export default function InstanceDetail({ instance, onClose, onDeleted, onSaved }: Props) { + const [activeTab, setActiveTab] = useState<"general" | "config" | "console" | "danger">("general"); + + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [purgeFiles, setPurgeFiles] = useState(false); + const [deleteLoading, setDeleteLoading] = useState(false); + const [deleteError, setDeleteError] = useState(null); + + const [form, setForm] = useState({ name: instance.name, port: String(instance.port) }); + const [modifyLoading, setModifyLoading] = useState(false); + const [modifyError, setModifyError] = useState(null); + + const isRunning = instance.status === "RUNNING"; + + async function handleDelete() { + setDeleteLoading(true); + setDeleteError(null); + try { + await sendDeleteInstanceRequest(instance.name, purgeFiles); + onDeleted(); + } catch (e: any) { + setDeleteError(e.message ?? String(e)); + } finally { + setDeleteLoading(false); + } + } + + async function handleModify() { + setModifyLoading(true); + setModifyError(null); + try { + await sendModifyInstanceRequest(instance.name, form.name, parseInt(form.port)); + onSaved(); + } catch (e: any) { + setModifyError(e.message ?? String(e)); + } finally { + setModifyLoading(false); + } + } + + return ( +
+ +

{instance.name}

+ +
+ + + + +
+ +
+ {activeTab === "general" && +
+

Version: {instance.version}

+ + + {isRunning &&

Stop the server to edit these fields.

} + + {modifyError &&
Error: {modifyError}
} + + +
+ } + + {activeTab === "config" &&

Config (coming soon)

} + + {activeTab === "console" &&

Console (coming soon)

} + + {activeTab === "danger" && +
+

Danger Zone

+ + + {!showDeleteConfirm && ( + + )} + {instance.status === "RUNNING" &&

Stop the server before deleting.

} + + {showDeleteConfirm && ( +
+

Are you sure you want to delete {instance.name}?

+

THIS ACTION CANNOT BE UNDONE

+ + {deleteError &&
Error: {deleteError}
} +
+ + +
+
+ )} +
+ } +
+
+ ); +} \ No newline at end of file diff --git a/vssm_web/vssm_web/src/components/InstanceTable.tsx b/vssm_web/vssm_web/src/components/InstanceTable.tsx index c18ca08..4f69eb1 100644 --- a/vssm_web/vssm_web/src/components/InstanceTable.tsx +++ b/vssm_web/vssm_web/src/components/InstanceTable.tsx @@ -1,12 +1,8 @@ import React, { useEffect, useState } from "react"; -import {fetchInstanceList, sendStartInstanceRequest, sendStopInstanceRequest, sendCreateInstanceRequest} from "../api"; +import { fetchInstanceList, sendStartInstanceRequest, sendStopInstanceRequest, sendCreateInstanceRequest } from "../api"; +import type { Instance } from "../types"; +import InstanceDetail from "./InstanceDetail"; -type Instance = { - name: string; - version: string; - port: number; - status: string; -}; export default function InstancesTable() { const [instances, setInstances] = useState(null); @@ -18,6 +14,7 @@ export default function InstancesTable() { const [createForm, setCreateForm] = useState({ name: "", version: "", port: "" }); const [createError, setCreateError] = useState(null); const [createLoading, setCreateLoading] = useState(false); + const [selectedInstance, setSelectedInstance] = useState(null); async function refreshInstanceList() { setLoading(true); @@ -102,9 +99,14 @@ export default function InstancesTable() { } useEffect(() => { - refreshInstanceList(); + const interval = setInterval(refreshInstanceList, 3000); + return () => clearInterval(interval); }, []); + if (selectedInstance) { + return setSelectedInstance(null)} onDeleted={() => { setSelectedInstance(null); refreshInstanceList(); }} onSaved={() => {setSelectedInstance(null); refreshInstanceList(); }}/>; + } + return (

Instances

@@ -139,14 +141,14 @@ export default function InstancesTable() {
- +
diff --git a/vssm_web/vssm_web/src/index.css b/vssm_web/vssm_web/src/index.css index 705549d..4f5fb79 100644 --- a/vssm_web/vssm_web/src/index.css +++ b/vssm_web/vssm_web/src/index.css @@ -93,7 +93,6 @@ h2 { font-weight: 500; color: #91a357; text-shadow: 3px 3px 5px #2c0105 ; - font-family: var(--heading); } h3 { color: var(--text); @@ -110,7 +109,7 @@ h1 { font-family: var(--heading); } h2 { - font-size: 4em; + font-size: 3.5em; line-height: 118%; letter-spacing: -0.24px; margin: 0 0 8px; @@ -211,6 +210,13 @@ input { transform: scale(1.05); } +input[type="checkbox"] { + min-height: unset; + padding: 0; + width: 16px; + background: none; +} + .normalButton { padding: 5px 15px; border-radius: 6px; @@ -272,3 +278,115 @@ input { +/* -- INSTANCE DETAIL */ + +.instanceDetail { + max-width: 80%; + margin: auto; + padding: 15px; + text-align: left; +} + +.instanceDetail .backButton { + background: none; + color: var(--text); + padding: 0; + font-size: medium; + opacity: 0.7; +} +.instanceDetail .backButton:hover { + transform: none; + opacity: 1; +} + +.tabBar { + display: flex; + gap: 4px; + border-bottom: 2px solid var(--border); + margin-bottom: 24px; +} + +.tabBar button { + background: none; + color: var(--text); + border-radius: 6px 6px 0 0; + padding: 8px 20px; + opacity: 0.6; +} +.tabBar button:hover { + transform: none; + opacity: 0.85; +} +.tabBar button.activeTab { + background-color: var(--accent-bg); + color: var(--text-h); + opacity: 1; + border-bottom: 2px solid var(--accent); + margin-bottom: -2px; +} + +.tabContent { + background-color: var(--accent-bg); + padding: 24px; + border-radius: 0 8px 8px 8px; + display: flex; + flex-direction: column; + gap: 16px; + text-align: left; +} + +.tabContent label { + display: flex; + flex-direction: column; + gap: 6px; + font-weight: 600; + font-size: 0.9em; + max-width: 400px; +} + +.warningText { + color: #a06000; + font-size: 0.9em; +} + +/* Danger zone */ + +.dangerZone { + border: 2px solid #a40000; + border-radius: 8px; + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.dangerZone h3 { + color: #a40000; + margin: 0; +} + +.dangerConfirm { + display: flex; + flex-direction: column; + gap: 12px; + padding-top: 8px; + border-top: 1px solid #a40000; +} + +.dangerConfirm label { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + font-size: 0.95em; + font-weight: normal; +} + +.deleteButton { + background-color: #a40000; + max-width: fit-content; +} + +.deleteButton:hover { + background-color: #cc0000; +} \ No newline at end of file diff --git a/vssm_web/vssm_web/src/types.ts b/vssm_web/vssm_web/src/types.ts new file mode 100644 index 0000000..4547505 --- /dev/null +++ b/vssm_web/vssm_web/src/types.ts @@ -0,0 +1,6 @@ +export type Instance = { + name: string; + version: string; + port: number; + status: string; +}; \ No newline at end of file