feat: upgrade Dashboard with server status, repo summary, and quick actions
This commit is contained in:
@@ -1,13 +1,237 @@
|
||||
export function Dashboard() {
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { api } from "../services/api";
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const color =
|
||||
status === "running"
|
||||
? "#4ade80"
|
||||
: status === "stopped"
|
||||
? "#f87171"
|
||||
: "#fbbf24";
|
||||
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="font-serif text-4xl font-bold text-forge-accent">
|
||||
Layonara Forge
|
||||
</h1>
|
||||
<p className="mt-2 text-forge-text-secondary">
|
||||
NWN Development Environment
|
||||
</p>
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold"
|
||||
style={{ backgroundColor: `${color}20`, color }}
|
||||
>
|
||||
<span className="inline-block h-2 w-2 rounded-full" style={{ backgroundColor: color }} />
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ServerCard() {
|
||||
const [status, setStatus] = useState<{ nwserver: string; mariadb: string }>({
|
||||
nwserver: "unknown",
|
||||
mariadb: "unknown",
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const poll = () => {
|
||||
api.server
|
||||
.status()
|
||||
.then(setStatus)
|
||||
.catch(() => setStatus({ nwserver: "error", mariadb: "error" }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
poll();
|
||||
const id = setInterval(poll, 10000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const toggle = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (status.nwserver === "running") {
|
||||
await api.server.stop();
|
||||
} else {
|
||||
await api.server.start();
|
||||
}
|
||||
poll();
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg p-6"
|
||||
style={{
|
||||
backgroundColor: "var(--forge-surface)",
|
||||
border: "1px solid var(--forge-border)",
|
||||
}}
|
||||
>
|
||||
<h3 className="text-sm font-semibold" style={{ color: "var(--forge-text-secondary)" }}>
|
||||
Server Status
|
||||
</h3>
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm" style={{ color: "var(--forge-text)" }}>
|
||||
NWServer
|
||||
</span>
|
||||
<StatusBadge status={status.nwserver} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm" style={{ color: "var(--forge-text)" }}>
|
||||
MariaDB
|
||||
</span>
|
||||
<StatusBadge status={status.mariadb} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={toggle}
|
||||
disabled={loading}
|
||||
className="rounded px-4 py-2 text-sm font-semibold disabled:opacity-40"
|
||||
style={{
|
||||
backgroundColor:
|
||||
status.nwserver === "running" ? "#7f1d1d" : "var(--forge-accent)",
|
||||
color: status.nwserver === "running" ? "#fca5a5" : "#000",
|
||||
}}
|
||||
>
|
||||
{loading
|
||||
? "..."
|
||||
: status.nwserver === "running"
|
||||
? "Stop Server"
|
||||
: "Start Server"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReposSummary() {
|
||||
const repos = ["nwn-module", "nwn-haks", "unified"];
|
||||
const [repoStatus, setRepoStatus] = useState<Record<string, Record<string, unknown>>>({});
|
||||
|
||||
useEffect(() => {
|
||||
for (const repo of repos) {
|
||||
api.repos
|
||||
.status(repo)
|
||||
.then((s) => setRepoStatus((prev) => ({ ...prev, [repo]: s })))
|
||||
.catch(() => {});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg p-6"
|
||||
style={{
|
||||
backgroundColor: "var(--forge-surface)",
|
||||
border: "1px solid var(--forge-border)",
|
||||
}}
|
||||
>
|
||||
<h3 className="text-sm font-semibold" style={{ color: "var(--forge-text-secondary)" }}>
|
||||
Repositories
|
||||
</h3>
|
||||
<div className="mt-4 space-y-2">
|
||||
{repos.map((repo) => {
|
||||
const s = repoStatus[repo];
|
||||
const branch = (s?.branch as string) || "\u2014";
|
||||
const clean = s?.clean !== false;
|
||||
return (
|
||||
<div
|
||||
key={repo}
|
||||
className="flex items-center justify-between rounded p-3"
|
||||
style={{ backgroundColor: "var(--forge-bg)" }}
|
||||
>
|
||||
<span className="font-mono text-sm" style={{ color: "var(--forge-text)" }}>
|
||||
{repo}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs" style={{ color: "var(--forge-text-secondary)" }}>
|
||||
{branch}
|
||||
</span>
|
||||
<span
|
||||
className="inline-block h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: clean ? "#4ade80" : "#fbbf24" }}
|
||||
title={clean ? "Clean" : "Uncommitted changes"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickActions() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const actions = [
|
||||
{ label: "Build Module", onClick: () => navigate("/build") },
|
||||
{ label: "Build Haks", onClick: () => navigate("/build") },
|
||||
{ label: "Open Editor", onClick: () => navigate("/editor") },
|
||||
{
|
||||
label: "Open Terminal",
|
||||
onClick: () => {
|
||||
/* terminal is toggled from IDELayout via Ctrl+` */
|
||||
navigate("/editor");
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg p-6"
|
||||
style={{
|
||||
backgroundColor: "var(--forge-surface)",
|
||||
border: "1px solid var(--forge-border)",
|
||||
}}
|
||||
>
|
||||
<h3 className="text-sm font-semibold" style={{ color: "var(--forge-text-secondary)" }}>
|
||||
Quick Actions
|
||||
</h3>
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
{actions.map((a) => (
|
||||
<button
|
||||
key={a.label}
|
||||
onClick={a.onClick}
|
||||
className="rounded p-3 text-sm font-medium transition-colors hover:bg-white/5"
|
||||
style={{
|
||||
backgroundColor: "var(--forge-bg)",
|
||||
color: "var(--forge-text)",
|
||||
border: "1px solid var(--forge-border)",
|
||||
}}
|
||||
>
|
||||
{a.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<div className="h-full overflow-y-auto p-6">
|
||||
<div className="mb-8 text-center">
|
||||
<h1
|
||||
className="text-3xl font-bold"
|
||||
style={{
|
||||
fontFamily: "'Baskerville', 'Georgia', 'Palatino', serif",
|
||||
color: "var(--forge-accent)",
|
||||
}}
|
||||
>
|
||||
Layonara Forge
|
||||
</h1>
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--forge-text-secondary)" }}>
|
||||
NWN Development Environment
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto max-w-3xl space-y-4">
|
||||
<ServerCard />
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<ReposSummary />
|
||||
<QuickActions />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user