Merge pull request 'feat(dashboard): hash routing + health check polling' (#138) from fix/128-dashboard-enhancements into main
This commit is contained in:
@@ -5,12 +5,10 @@ import { StatusBar } from "./components/status-bar.tsx";
|
||||
import { ThreadDetail } from "./components/thread-detail.tsx";
|
||||
import { ThreadList } from "./components/thread-list.tsx";
|
||||
import { WorkflowList } from "./components/workflow-list.tsx";
|
||||
|
||||
type View = "threads" | "workflows";
|
||||
import { useHashRoute } from "./use-hash-route.ts";
|
||||
|
||||
export function App() {
|
||||
const [view, setView] = useState<View>("threads");
|
||||
const [selectedThread, setSelectedThread] = useState<string | null>(null);
|
||||
const { view, threadId, setView, setThreadId } = useHashRoute();
|
||||
const [showRun, setShowRun] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -19,9 +17,9 @@ export function App() {
|
||||
<main className="flex-1 overflow-hidden flex flex-col">
|
||||
<StatusBar onRun={() => setShowRun(true)} />
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
{view === "threads" && !selectedThread && <ThreadList onSelect={setSelectedThread} />}
|
||||
{view === "threads" && selectedThread && (
|
||||
<ThreadDetail threadId={selectedThread} onBack={() => setSelectedThread(null)} />
|
||||
{view === "threads" && threadId === null && <ThreadList onSelect={setThreadId} />}
|
||||
{view === "threads" && threadId !== null && (
|
||||
<ThreadDetail threadId={threadId} onBack={() => setThreadId(null)} />
|
||||
)}
|
||||
{view === "workflows" && <WorkflowList />}
|
||||
</div>
|
||||
@@ -31,8 +29,7 @@ export function App() {
|
||||
onClose={() => setShowRun(false)}
|
||||
onCreated={(id) => {
|
||||
setShowRun(false);
|
||||
setView("threads");
|
||||
setSelectedThread(id);
|
||||
setThreadId(id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,47 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { getHealth } from "../api.ts";
|
||||
import { useFetch } from "../hooks.ts";
|
||||
|
||||
type HealthStatus = "connected" | "disconnected" | "reconnecting";
|
||||
|
||||
type Props = {
|
||||
onRun: () => void;
|
||||
};
|
||||
|
||||
function statusLabel(status: HealthStatus): { text: string; color: string } {
|
||||
if (status === "connected") {
|
||||
return { text: "● Connected", color: "var(--color-success)" };
|
||||
}
|
||||
if (status === "reconnecting") {
|
||||
return { text: "● Reconnecting...", color: "var(--color-warning, #f59e0b)" };
|
||||
}
|
||||
return { text: "● Offline", color: "var(--color-error)" };
|
||||
}
|
||||
|
||||
export function StatusBar({ onRun }: Props) {
|
||||
const health = useFetch(() => getHealth(), []);
|
||||
const [status, setStatus] = useState<HealthStatus>("disconnected");
|
||||
const wasConnectedRef = useRef(false);
|
||||
|
||||
const checkHealth = useCallback(async () => {
|
||||
try {
|
||||
await getHealth();
|
||||
wasConnectedRef.current = true;
|
||||
setStatus("connected");
|
||||
} catch {
|
||||
if (wasConnectedRef.current) {
|
||||
setStatus("reconnecting");
|
||||
} else {
|
||||
setStatus("disconnected");
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkHealth();
|
||||
const interval = setInterval(checkHealth, 10_000);
|
||||
return () => clearInterval(interval);
|
||||
}, [checkHealth]);
|
||||
|
||||
const label = statusLabel(status);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -24,15 +59,7 @@ export function StatusBar({ onRun }: Props) {
|
||||
▶ Run Thread
|
||||
</button>
|
||||
</div>
|
||||
<span>
|
||||
{health.status === "loading" && "⏳ Connecting..."}
|
||||
{health.status === "ok" && (
|
||||
<span style={{ color: "var(--color-success)" }}>● Connected</span>
|
||||
)}
|
||||
{health.status === "error" && (
|
||||
<span style={{ color: "var(--color-error)" }}>● Offline</span>
|
||||
)}
|
||||
</span>
|
||||
<span style={{ color: label.color }}>{label.text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
type View = "threads" | "workflows";
|
||||
|
||||
type HashRoute = {
|
||||
view: View;
|
||||
threadId: string | null;
|
||||
};
|
||||
|
||||
function parseHash(hash: string): HashRoute {
|
||||
const raw = hash.replace(/^#\/?/, "");
|
||||
if (raw.startsWith("threads/")) {
|
||||
const id = raw.slice("threads/".length);
|
||||
if (id.length > 0) {
|
||||
return { view: "threads", threadId: id };
|
||||
}
|
||||
}
|
||||
if (raw === "workflows") {
|
||||
return { view: "workflows", threadId: null };
|
||||
}
|
||||
return { view: "threads", threadId: null };
|
||||
}
|
||||
|
||||
function buildHash(route: HashRoute): string {
|
||||
if (route.view === "workflows") {
|
||||
return "#workflows";
|
||||
}
|
||||
if (route.threadId !== null) {
|
||||
return `#threads/${route.threadId}`;
|
||||
}
|
||||
return "#threads";
|
||||
}
|
||||
|
||||
export function useHashRoute(): {
|
||||
view: View;
|
||||
threadId: string | null;
|
||||
setView: (v: View) => void;
|
||||
setThreadId: (id: string | null) => void;
|
||||
} {
|
||||
const [route, setRoute] = useState<HashRoute>(() => parseHash(window.location.hash));
|
||||
|
||||
useEffect(() => {
|
||||
function onHashChange(): void {
|
||||
setRoute(parseHash(window.location.hash));
|
||||
}
|
||||
window.addEventListener("hashchange", onHashChange);
|
||||
return () => window.removeEventListener("hashchange", onHashChange);
|
||||
}, []);
|
||||
|
||||
const navigate = useCallback((next: HashRoute) => {
|
||||
const hash = buildHash(next);
|
||||
window.location.hash = hash;
|
||||
setRoute(next);
|
||||
}, []);
|
||||
|
||||
const setView = useCallback((v: View) => navigate({ view: v, threadId: null }), [navigate]);
|
||||
|
||||
const setThreadId = useCallback(
|
||||
(id: string | null) => navigate({ view: "threads", threadId: id }),
|
||||
[navigate],
|
||||
);
|
||||
|
||||
return { view: route.view, threadId: route.threadId, setView, setThreadId };
|
||||
}
|
||||
Reference in New Issue
Block a user