Populated config screen with a basic editor

This commit is contained in:
2026-06-11 19:18:50 -05:00
parent 2eee7c96b9
commit 1da6f04219
4 changed files with 109 additions and 9 deletions

View File

@@ -12,6 +12,9 @@ class api {
} export default api;
// --- GET CALLS ---
export async function fetchInstanceList(): Promise<Instance[]> {
const res = await fetch(`${ADDRESS}/instances/list`);
if (!res.ok) {
@@ -30,13 +33,24 @@ export async function fetchInstanceLogs(name: string): Promise<string[]> {
return res.json();
}
export async function fetchInstanceConfig(name: string): Promise<string> {
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()}`);
}
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()}`);
}
return res.json;
return res.text();
}
export async function sendStopInstanceRequest(name: string) {
@@ -45,7 +59,7 @@ export async function sendStopInstanceRequest(name: string) {
const message = await res.text();
throw new Error(`Stop Instance Request failed: ${message.trim()}`);
}
return res.json;
return res.text();
}
export async function sendCreateInstanceRequest(name: string, version: string, port: number) {
@@ -54,7 +68,7 @@ export async function sendCreateInstanceRequest(name: string, version: string, p
const message = await res.text();
throw new Error(`Create instance request failed: ${message.trim()}`);
}
return res.json;
return res.text();
}
export async function sendDeleteInstanceRequest(name: string, purge: boolean) {
@@ -63,7 +77,7 @@ export async function sendDeleteInstanceRequest(name: string, purge: boolean) {
const message = await res.text();
throw new Error(`Delete instance request failed: ${message.trim()}`);
}
return res.json;
return res.text();
}
export async function sendModifyInstanceRequest(name: string, newName: string, newPort: number) {
@@ -72,7 +86,7 @@ export async function sendModifyInstanceRequest(name: string, newName: string, n
const message = await res.text();
throw new Error(`Modify instance request failed: ${message.trim()}`);
}
return res.json;
return res.text();
}
export async function sendCommandRequest(name: string, payload: string) {
@@ -85,5 +99,18 @@ export async function sendCommandRequest(name: string, payload: string) {
const message = await res.text();
throw new Error(`Send Instance Command failed: ${message.trim()}`);
}
return res.json;
return res.text();
}
export async function sendSetInstanceConfigRequest(name: string, config: string) {
const res = await fetch(`${ADDRESS}/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()}`);
}
return res.text();
}

View File

@@ -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>
);
}

View File

@@ -2,6 +2,7 @@ import { useState } from "react";
import type { Instance } from "../types";
import { sendDeleteInstanceRequest, sendModifyInstanceRequest } from "../api";
import InstanceConsole from "./InstanceConsole";
import InstanceConfig from "./InstanceConfig";
type Props = {
instance: Instance;
@@ -58,8 +59,8 @@ export default function InstanceDetail({ instance, onClose, onDeleted, onSaved }
<div className="tabBar">
<button onClick={() => setActiveTab("general")}>General</button>
<button onClick={() => setActiveTab("config")}>Configuration</button>
<button onClick={() => setActiveTab("console")}>Console</button>
<button onClick={() => setActiveTab("config")}>Configuration</button>
<button onClick={() => setActiveTab("danger")}>Danger Zone</button>
</div>
@@ -83,10 +84,10 @@ export default function InstanceDetail({ instance, onClose, onDeleted, onSaved }
</div>
}
{activeTab === "config" && <p>Config (coming soon)</p>}
{activeTab === "console" && <InstanceConsole instance={instance} />}
{activeTab === "config" && <InstanceConfig instance={instance}/>}
{activeTab === "danger" &&
<div className="dangerZone">
<h3>Danger Zone</h3>

View File

@@ -442,4 +442,26 @@ input[type="checkbox"] {
.consoleLine i {
color: #a0a0c0;
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;
}