feat(ui): fold the Workflows page into Dashboard → Analytics → Workflow

The workflow analytics now render under the Analytics tab's Workflow
sub-tab; ?tab= and ?sub= deep-link the Dashboard, /workflows redirects there,
and the sidebar drops its Workflows item.
This commit is contained in:
2026-10-01 22:05:55 +07:00
parent b71024f244
commit 744e9c6cda
14 changed files with 121 additions and 98 deletions
+88 -71
View File
@@ -63,7 +63,8 @@ import {
useRef,
type ReactNode,
} from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate, useSearchParams } from "react-router-dom";
import { Workflows } from "./Workflows";
import { useTranslation } from "react-i18next";
import {
LayoutDashboard,
@@ -732,9 +733,14 @@ function AnalyticsTab() {
const [costData, setCostData] = useState<CostResult | null>(null);
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
const [loading, setLoading] = useState(true);
// `?sub=` deep-links a sub-tab (the old /workflows route lands on `workflow`).
const [params] = useSearchParams();
const [analyticsSubTab, setAnalyticsSubTab] = useState<
"tokens" | "cost" | "workflow" | "productivity"
>("cost");
>(() => {
const sub = params.get("sub");
return sub === "tokens" || sub === "workflow" || sub === "productivity" ? sub : "cost";
});
// Global data scope; a change re-runs `load` (api injects the `sources` param).
const [scope] = useDataScope();
@@ -1340,81 +1346,89 @@ function AnalyticsTab() {
)}
{analyticsSubTab === "workflow" && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Agent type distribution */}
<div className="card p-5">
<h3 className="text-sm font-medium text-fg-secondary mb-5">
{t("subagentTypes")}
</h3>
{(data?.agent_types ?? []).length === 0 ? (
<p className="text-sm text-fg-muted">{t("noSubagentData")}</p>
) : (
<div className="space-y-3">
{(data?.agent_types ?? []).slice(0, 10).map(({ subagent_type, count }) => (
<BarRow
key={subagent_type}
label={subagent_type}
count={count}
max={maxAgentTypeCount}
color="bg-violet-400"
/>
))}
</div>
)}
</div>
{/* Agent status donut */}
<div className="card p-5">
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("agentStatus")}</h3>
<DonutChart segments={agentStatusSegments} />
<div className="mt-4 pt-4 border-t border-border space-y-1.5">
<div className="flex justify-between text-xs text-fg-muted">
<span>{t("totalAgentsLabel")}</span>
<Tip raw={(data?.overview.total_agents ?? 0).toLocaleString()}>
<span className="text-fg-secondary font-mono">
{fmt(data?.overview.total_agents ?? 0)}
</span>
</Tip>
</div>
{agentStatusSegments.map((s) => (
<div
key={s.label}
className="flex items-center justify-between text-xs text-fg-muted"
>
<span className="flex items-center gap-1.5">
<span
className="w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: s.color }}
<div className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Agent type distribution */}
<div className="card p-5">
<h3 className="text-sm font-medium text-fg-secondary mb-5">
{t("subagentTypes")}
</h3>
{(data?.agent_types ?? []).length === 0 ? (
<p className="text-sm text-fg-muted">{t("noSubagentData")}</p>
) : (
<div className="space-y-3">
{(data?.agent_types ?? []).slice(0, 10).map(({ subagent_type, count }) => (
<BarRow
key={subagent_type}
label={subagent_type}
count={count}
max={maxAgentTypeCount}
color="bg-violet-400"
/>
{s.label}
</span>
<Tip raw={s.value.toLocaleString()}>
<span className="text-fg-secondary font-mono">{fmt(s.value)}</span>
))}
</div>
)}
</div>
{/* Agent status donut */}
<div className="card p-5">
<h3 className="text-sm font-medium text-fg-secondary mb-5">
{t("agentStatus")}
</h3>
<DonutChart segments={agentStatusSegments} />
<div className="mt-4 pt-4 border-t border-border space-y-1.5">
<div className="flex justify-between text-xs text-fg-muted">
<span>{t("totalAgentsLabel")}</span>
<Tip raw={(data?.overview.total_agents ?? 0).toLocaleString()}>
<span className="text-fg-secondary font-mono">
{fmt(data?.overview.total_agents ?? 0)}
</span>
</Tip>
</div>
))}
</div>
</div>
{/* Event type breakdown */}
<div className="card p-5">
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("eventTypes")}</h3>
{(data?.event_types ?? []).length === 0 ? (
<p className="text-sm text-fg-muted">{t("noEventData")}</p>
) : (
<div className="space-y-3">
{(data?.event_types ?? []).map(({ event_type, count }) => (
<BarRow
key={event_type}
label={event_type}
count={count}
max={maxEventTypeCount}
color={EVENT_TYPE_COLORS[event_type] ?? "bg-surface-4"}
/>
{agentStatusSegments.map((s) => (
<div
key={s.label}
className="flex items-center justify-between text-xs text-fg-muted"
>
<span className="flex items-center gap-1.5">
<span
className="w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: s.color }}
/>
{s.label}
</span>
<Tip raw={s.value.toLocaleString()}>
<span className="text-fg-secondary font-mono">{fmt(s.value)}</span>
</Tip>
</div>
))}
</div>
)}
</div>
{/* Event type breakdown */}
<div className="card p-5">
<h3 className="text-sm font-medium text-fg-secondary mb-5">
{t("eventTypes")}
</h3>
{(data?.event_types ?? []).length === 0 ? (
<p className="text-sm text-fg-muted">{t("noEventData")}</p>
) : (
<div className="space-y-3">
{(data?.event_types ?? []).map(({ event_type, count }) => (
<BarRow
key={event_type}
label={event_type}
count={count}
max={maxEventTypeCount}
color={EVENT_TYPE_COLORS[event_type] ?? "bg-surface-4"}
/>
))}
</div>
)}
</div>
</div>
{/* The former Workflows page, folded into this sub-tab. */}
<Workflows />
</div>
)}
@@ -2346,7 +2360,10 @@ export function Dashboard() {
const { t } = useTranslation("dashboard");
// Persistent Tab State
const [tabParams] = useSearchParams();
const [activeTab, setActiveTab] = useState<"monitor" | "analytics" | "health">(() => {
const tab = tabParams.get("tab");
if (tab === "monitor" || tab === "analytics" || tab === "health") return tab;
return (
(localStorage.getItem("dashboard_tab") as "monitor" | "analytics" | "health") || "analytics"
);