several servers + groups + url
This commit is contained in:
+68
@@ -359,3 +359,71 @@ pre {
|
||||
.main { grid-template-columns: 1fr; }
|
||||
.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 axios from "axios";
|
||||
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" }];
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────────────
|
||||
function StatusBadge({ status, ok }) {
|
||||
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 [running, setRunning] = useState(false);
|
||||
const [log, setLog] = useState([]);
|
||||
const [collapsedGroups, setCollapsedGroups] = useState(new Set());
|
||||
|
||||
const pushLog = useCallback((msg) => {
|
||||
setLog((l) => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...l].slice(0, 50));
|
||||
@@ -178,7 +192,7 @@ export default function App() {
|
||||
|
||||
// ── Connect socket once ──────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
const socket = io(SERVER_URL);
|
||||
const socket = io({ path: SOCKET_IO_PATH });
|
||||
socketRef.current = socket;
|
||||
|
||||
socket.on("connect", () => {
|
||||
@@ -274,7 +288,7 @@ export default function App() {
|
||||
console.log("sending:", JSON.stringify(payload));
|
||||
|
||||
try {
|
||||
await axios.post("http://localhost:4000/api/parallel-fetch", payload);
|
||||
await axios.post(`${API_BASE}/parallel-fetch`, payload);
|
||||
} catch (err) {
|
||||
pushLog(`POST error: ${err.message}`);
|
||||
setRunning(false);
|
||||
@@ -282,6 +296,22 @@ export default function App() {
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="app">
|
||||
@@ -354,9 +384,49 @@ export default function App() {
|
||||
)}
|
||||
|
||||
<div className="results-list">
|
||||
{results.map((r, i) => (
|
||||
<ResultCard key={i} result={r} index={i} />
|
||||
))}
|
||||
{groupedResults.map((group) => {
|
||||
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>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -4,6 +4,7 @@ import babel from '@rolldown/plugin-babel'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: '/tool-gui/',
|
||||
plugins: [
|
||||
react(),
|
||||
babel({ presets: [reactCompilerPreset()] })
|
||||
|
||||
Reference in New Issue
Block a user