multica/apps/web/features/runtimes/components/runtimes-page.tsx
Naiyuan Qing e40341ab73 feat(core): migrate workspace + runtimes to TanStack Query (Phase 3+4)
- Create core/workspace/ with queries (members, agents, skills, list) and mutations
- Create core/runtimes/ with queries
- Migrate 11 consumer files from useWorkspaceStore.members/agents/skills to useQuery
- Replace all WS refreshMap entries with qc.invalidateQueries
- Simplify workspace store: delete members/agents/skills fields + refresh methods,
  hydrateWorkspace becomes synchronous (TQ auto-fetches on component mount)
- Delete useRuntimeStore (no consumers left), runtimes-page uses local useState + TQ
- Remove workspace→runtime cross-store dependency
- Clean up dead test helper mocks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:19:52 +08:00

115 lines
3.8 KiB
TypeScript

"use client";
import { useState, useCallback } from "react";
import { Server } from "lucide-react";
import { useDefaultLayout } from "react-resizable-panels";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable";
import { Skeleton } from "@/components/ui/skeleton";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceId } from "@core/hooks";
import { runtimeListOptions, runtimeKeys } from "@core/runtimes/queries";
import { useWSEvent } from "@/features/realtime";
import { RuntimeList } from "./runtime-list";
import { RuntimeDetail } from "./runtime-detail";
export default function RuntimesPage() {
const isLoading = useAuthStore((s) => s.isLoading);
const wsId = useWorkspaceId();
const qc = useQueryClient();
const { data: runtimes = [], isLoading: fetching } = useQuery(runtimeListOptions(wsId));
const [selectedId, setSelectedId] = useState("");
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
id: "multica_runtimes_layout",
});
// Re-fetch on daemon register/deregister events.
const handleDaemonEvent = useCallback(() => {
qc.invalidateQueries({ queryKey: runtimeKeys.list(wsId) });
}, [qc, wsId]);
useWSEvent("daemon:register", handleDaemonEvent);
// Auto-select first runtime if nothing selected
const effectiveSelectedId = selectedId && runtimes.some((r) => r.id === selectedId)
? selectedId
: runtimes[0]?.id ?? "";
const selected = runtimes.find((r) => r.id === effectiveSelectedId) ?? null;
if (isLoading || fetching) {
return (
<div className="flex flex-1 min-h-0">
{/* List skeleton */}
<div className="w-72 border-r">
<div className="flex h-12 items-center justify-between border-b px-4">
<Skeleton className="h-4 w-20" />
</div>
<div className="divide-y">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="flex items-center gap-3 px-4 py-3">
<Skeleton className="h-5 w-5 rounded" />
<div className="flex-1 space-y-1.5">
<Skeleton className="h-4 w-28" />
<Skeleton className="h-3 w-20" />
</div>
</div>
))}
</div>
</div>
{/* Detail skeleton */}
<div className="flex-1 p-6 space-y-6">
<div className="flex items-center gap-3">
<Skeleton className="h-5 w-5 rounded" />
<Skeleton className="h-5 w-32" />
</div>
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-16 w-full rounded-lg" />
))}
</div>
</div>
</div>
);
}
return (
<ResizablePanelGroup
orientation="horizontal"
className="flex-1 min-h-0"
defaultLayout={defaultLayout}
onLayoutChanged={onLayoutChanged}
>
<ResizablePanel
id="list"
defaultSize={280}
minSize={240}
maxSize={400}
groupResizeBehavior="preserve-pixel-size"
>
<RuntimeList
runtimes={runtimes}
selectedId={effectiveSelectedId}
onSelect={setSelectedId}
/>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel id="detail" minSize="50%">
{selected ? (
<RuntimeDetail key={selected.id} runtime={selected} />
) : (
<div className="flex h-full flex-col items-center justify-center text-muted-foreground">
<Server className="h-10 w-10 text-muted-foreground/30" />
<p className="mt-3 text-sm">Select a runtime to view details</p>
</div>
)}
</ResizablePanel>
</ResizablePanelGroup>
);
}