feat(workflows): show lane pipelines, drop the deep analytics charts
The Workflows page rendered eleven charts of Claude Code harness internals (collaboration network, subagent effectiveness, patterns, model delegation, error propagation, concurrency, complexity, compaction). Too specialized to read and nothing to act on, so they are gone along with their i18n keys. What is left is what a reader can act on: headline stats, Workflow-tool runs, the spawning DAG, the tool flow and the per-session drill-in — plus a new first section for this dashboard's OWN pipelines. LanePipelinePanel renders a horizontally scrolling lane strip and the selected lane's PipelineMap, reusing the Workspace components so a stage looks identical on both pages. The panel is read-only: the console-never-writes-a-stage boundary applies here too, so every lane action stays on the Workspace page. The workflows API response shape is untouched. Closes #6
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @file LanePipelinePanel.tsx
|
||||
* @description Read-only view of this dashboard's OWN pipelines on the Workflows
|
||||
* page: a horizontally scrolling strip of lane tiles, and the selected lane's
|
||||
* pipeline map underneath. It reuses the Workspace's `LaneStripCard` and
|
||||
* `PipelineMap` so a stage reads identically on both pages. Read-only on
|
||||
* purpose — the console-never-writes-a-stage boundary applies here too, so this
|
||||
* panel only observes; every lane action stays on the Workspace page.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { api } from "../../lib/api";
|
||||
import { eventBus } from "../../lib/eventBus";
|
||||
import type { Lane } from "../../lib/types";
|
||||
import LaneStripCard from "../lanes/LaneStripCard";
|
||||
import PipelineMap from "../lanes/PipelineMap";
|
||||
|
||||
/**
|
||||
* Lane strip plus the selected lane's pipeline map.
|
||||
* @returns The panel element; an empty-state hint when no lane exists yet.
|
||||
*/
|
||||
export function LanePipelinePanel() {
|
||||
const { t } = useTranslation(["workflows", "lanes"]);
|
||||
const [lanes, setLanes] = useState<Lane[]>([]);
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const r = await api.lanes.list();
|
||||
setLanes(r.lanes);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("workflows:failedLoad"));
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
// Lanes move as hooks land; debounce so a burst of events costs one fetch.
|
||||
useEffect(() => {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
const unsub = eventBus.subscribe(() => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => void refresh(), 3000);
|
||||
});
|
||||
return () => {
|
||||
unsub();
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [refresh]);
|
||||
|
||||
// Follow the list rather than pinning an id: a lane can be removed elsewhere.
|
||||
const selected = lanes.find((l) => l.id === selectedId) ?? lanes[0] ?? null;
|
||||
|
||||
if (error) return <p className="text-xs text-status-danger">{error}</p>;
|
||||
|
||||
if (!lanes.length) {
|
||||
return (
|
||||
<p className="text-sm text-fg-muted">
|
||||
{t("lanes:emptyState")} <code>ccam lanes add --cwd $(pwd)</code>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div data-testid="workflows-lane-strip" className="flex flex-row gap-2 overflow-x-auto pb-1">
|
||||
{lanes.map((l) => (
|
||||
<LaneStripCard
|
||||
key={l.id}
|
||||
lane={l}
|
||||
selected={selected?.id === l.id}
|
||||
onSelect={() => setSelectedId(l.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selected && (
|
||||
<div data-testid="workflows-lane-pipeline" className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="truncate text-sm font-semibold text-fg-primary">
|
||||
{t("lanes:laneHeader", {
|
||||
id: selected.id,
|
||||
title: selected.title || selected.cwd,
|
||||
pipeline: selected.pipeline_name,
|
||||
})}
|
||||
</span>
|
||||
<Link
|
||||
to="/run"
|
||||
className="ml-auto inline-flex items-center gap-1 text-xs text-fg-muted hover:text-accent"
|
||||
>
|
||||
{t("workflows:lanes.openWorkspace")}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</Link>
|
||||
</div>
|
||||
<PipelineMap nodes={selected.pipeline_nodes} detectedSignal={selected.detected_signal} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user