several servers + groups + url
This commit is contained in:
+68
@@ -359,3 +359,71 @@ pre {
|
|||||||
.main { grid-template-columns: 1fr; }
|
.main { grid-template-columns: 1fr; }
|
||||||
.panel-left { border-right: none; border-bottom: 1px solid var(--border); }
|
.panel-left { border-right: none; border-bottom: 1px solid var(--border); }
|
||||||
}
|
}
|
||||||
|
/* ─── Server Group ─────────────────────────────────────────── */
|
||||||
|
.server-group {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-group-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-group-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-host-sub {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-status-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.dot-ok { background: #22c55e; }
|
||||||
|
.dot-err { background: #ef4444; }
|
||||||
|
.dot-pending { background: #f59e0b; }
|
||||||
|
|
||||||
|
.group-ok { border-color: #22c55e44; }
|
||||||
|
.group-err { border-color: #ef444444; }
|
||||||
|
|
||||||
|
.server-group-results {
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-group-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-chevron {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-errors {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-err .server-errors {
|
||||||
|
color: var(--err);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|||||||
+77
-7
@@ -1,9 +1,11 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
const SERVER_URL = "http://localhost:4000";
|
const BASE = (import.meta.env.BASE_URL ?? "/").replace(/\/$/, "");
|
||||||
|
const API_BASE = `${BASE}/api`;
|
||||||
|
const SOCKET_IO_PATH = `${BASE}/socket.io`;
|
||||||
|
|
||||||
const DEFAULT_HOSTS = [{ url: "", method: "GET" }];
|
const DEFAULT_HOSTS = [{ url: "", method: "GET" }];
|
||||||
|
|
||||||
@@ -27,6 +29,17 @@ function isToolResult(result) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isResultError(result) {
|
||||||
|
if (!result) return false;
|
||||||
|
if (result.type === "error") return true;
|
||||||
|
const body = getToolBody(result);
|
||||||
|
if (body && typeof body.returncode === "number" && body.returncode !== 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (result.ok === false) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── StatusBadge ──────────────────────────────────────────────────────────────
|
// ─── StatusBadge ──────────────────────────────────────────────────────────────
|
||||||
function StatusBadge({ status, ok }) {
|
function StatusBadge({ status, ok }) {
|
||||||
if (status === undefined) return <span className="badge pending">PENDING</span>;
|
if (status === undefined) return <span className="badge pending">PENDING</span>;
|
||||||
@@ -171,6 +184,7 @@ export default function App() {
|
|||||||
const [progress, setProgress] = useState(null); // { done, total }
|
const [progress, setProgress] = useState(null); // { done, total }
|
||||||
const [running, setRunning] = useState(false);
|
const [running, setRunning] = useState(false);
|
||||||
const [log, setLog] = useState([]);
|
const [log, setLog] = useState([]);
|
||||||
|
const [collapsedGroups, setCollapsedGroups] = useState(new Set());
|
||||||
|
|
||||||
const pushLog = useCallback((msg) => {
|
const pushLog = useCallback((msg) => {
|
||||||
setLog((l) => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...l].slice(0, 50));
|
setLog((l) => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...l].slice(0, 50));
|
||||||
@@ -178,7 +192,7 @@ export default function App() {
|
|||||||
|
|
||||||
// ── Connect socket once ──────────────────────────────────────────────────
|
// ── Connect socket once ──────────────────────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const socket = io(SERVER_URL);
|
const socket = io({ path: SOCKET_IO_PATH });
|
||||||
socketRef.current = socket;
|
socketRef.current = socket;
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
@@ -274,7 +288,7 @@ export default function App() {
|
|||||||
console.log("sending:", JSON.stringify(payload));
|
console.log("sending:", JSON.stringify(payload));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post("http://localhost:4000/api/parallel-fetch", payload);
|
await axios.post(`${API_BASE}/parallel-fetch`, payload);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
pushLog(`POST error: ${err.message}`);
|
pushLog(`POST error: ${err.message}`);
|
||||||
setRunning(false);
|
setRunning(false);
|
||||||
@@ -282,6 +296,22 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pct = progress ? Math.round((progress.done / progress.total) * 100) : 0;
|
const pct = progress ? Math.round((progress.done / progress.total) * 100) : 0;
|
||||||
|
const groupedResults = useMemo(() => {
|
||||||
|
const groups = new Map();
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
if (!result) return;
|
||||||
|
const key = result.serverName || result.host || "Unknown server";
|
||||||
|
const entry = groups.get(key) || { items: [], errors: 0 };
|
||||||
|
entry.items.push({ result, index });
|
||||||
|
if (isResultError(result)) entry.errors += 1;
|
||||||
|
groups.set(key, entry);
|
||||||
|
});
|
||||||
|
return Array.from(groups.entries()).map(([serverName, data]) => ({
|
||||||
|
serverName,
|
||||||
|
items: data.items,
|
||||||
|
errors: data.errors,
|
||||||
|
}));
|
||||||
|
}, [results]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
@@ -354,9 +384,49 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="results-list">
|
<div className="results-list">
|
||||||
{results.map((r, i) => (
|
{groupedResults.map((group) => {
|
||||||
<ResultCard key={i} result={r} index={i} />
|
const { serverName, items, errors } = group;
|
||||||
))}
|
const isCollapsed = collapsedGroups.has(serverName);
|
||||||
|
const hasErrors = errors > 0;
|
||||||
|
const groupClass = `server-group ${
|
||||||
|
hasErrors ? "group-err" : "group-ok"
|
||||||
|
}`;
|
||||||
|
return (
|
||||||
|
<div className={groupClass} key={serverName}>
|
||||||
|
<div
|
||||||
|
className="server-group-header"
|
||||||
|
onClick={() =>
|
||||||
|
setCollapsedGroups((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(serverName)) next.delete(serverName);
|
||||||
|
else next.add(serverName);
|
||||||
|
return next;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="server-group-title">
|
||||||
|
<span className="group-chevron">
|
||||||
|
{isCollapsed ? "▶" : "▼"}
|
||||||
|
</span>
|
||||||
|
<strong>{serverName}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="server-group-meta">
|
||||||
|
<span className="server-errors">
|
||||||
|
{errors} error{errors === 1 ? "" : "s"}/
|
||||||
|
{items.length} total
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="server-group-results">
|
||||||
|
{items.map(({ result, index }) => (
|
||||||
|
<ResultCard key={index} result={result} index={index} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import babel from '@rolldown/plugin-babel'
|
|||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: '/tool-gui/',
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
babel({ presets: [reactCompilerPreset()] })
|
babel({ presets: [reactCompilerPreset()] })
|
||||||
|
|||||||
Reference in New Issue
Block a user