refactor(ui): clean up Dashboard from 9 chaotic sections to 4 focused ones
Remove HealthRing, DepartmentOverview, TaskPipeline, SprintProgress, GoalProgress, WorkforceGrid, ConnectorHealth. Move Needs Attention to top. Reduce 9 metric cards to 4. Add Active Work section. -50KB bundle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d5c9440ab4
commit
bb59eb0d45
3 files changed
+169
-187
No files matched your search
@@ -808,12 +808,7 @@ export function App() {
|
|||||||
projects={companyProjects}
|
projects={companyProjects}
|
||||||
activity={filteredActivity}
|
activity={filteredActivity}
|
||||||
costs={companyCosts}
|
costs={companyCosts}
|
||||||
connectors={snapshot.connectors}
|
|
||||||
meetings={companyMeetings}
|
|
||||||
sprints={companySprints}
|
|
||||||
messages={snapshot.agentMessages?.filter((message) => !currentCompany || message.companyId === currentCompany.id) ?? []}
|
messages={snapshot.agentMessages?.filter((message) => !currentCompany || message.companyId === currentCompany.id) ?? []}
|
||||||
browserActions={browserActions}
|
|
||||||
workflows={snapshot.workflows?.filter((workflow) => !currentCompany || workflow.companyId === currentCompany.id) ?? []}
|
|
||||||
onNavigate={navigateTo}
|
onNavigate={navigateTo}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
export { Dashboard } from "./dashboard/Dashboard";
|
export { Dashboard } from "./dashboard/Dashboard";
|
||||||
export type { DashboardProps } from "./dashboard/Dashboard";
|
export type { DashboardProps } from "./dashboard/Dashboard";
|
||||||
export { KanbanBoard } from "./dashboard/KanbanBoard";
|
export { KanbanBoard } from "./dashboard/KanbanBoard";
|
||||||
export { HealthRing, computeHealthScore } from "./dashboard/HealthCard";
|
|
||||||
export { MetricCard } from "./dashboard/MetricsGrid";
|
export { MetricCard } from "./dashboard/MetricsGrid";
|
||||||
export { ActivityFeed } from "./dashboard/ActivityFeed";
|
export { ActivityFeed } from "./dashboard/ActivityFeed";
|
||||||
export { WorkforceGrid } from "./dashboard/QuickActions";
|
|
||||||
export { DepartmentOverview } from "./dashboard/DepartmentOverview";
|
|
||||||
export { TaskPipeline } from "./dashboard/TaskPipeline";
|
|
||||||
export { SprintProgress } from "./dashboard/SprintProgress";
|
|
||||||
export { GoalProgress } from "./dashboard/GoalProgress";
|
|
||||||
export { NeedsAttention } from "./dashboard/NeedsAttention";
|
|
||||||
@@ -2,46 +2,28 @@ import { useMemo } from "react";
|
|||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Briefcase,
|
Briefcase,
|
||||||
Cpu,
|
|
||||||
CurrencyCircleDollar,
|
CurrencyCircleDollar,
|
||||||
Heartbeat,
|
Robot,
|
||||||
Lightning,
|
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Target,
|
|
||||||
TreeStructure,
|
|
||||||
Users,
|
|
||||||
Warning,
|
Warning,
|
||||||
|
XCircle,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import type {
|
import type {
|
||||||
ActivityRecord,
|
ActivityRecord,
|
||||||
AgentRecord,
|
AgentRecord,
|
||||||
AgentMessageRecord,
|
AgentMessageRecord,
|
||||||
ApprovalRecord,
|
ApprovalRecord,
|
||||||
BrowserActionRecord,
|
|
||||||
ConnectorRecord,
|
|
||||||
CostRecord,
|
CostRecord,
|
||||||
GoalRecord,
|
GoalRecord,
|
||||||
MeetingRecord,
|
|
||||||
ProjectRecord,
|
ProjectRecord,
|
||||||
RunRecord,
|
RunRecord,
|
||||||
SectionId,
|
SectionId,
|
||||||
SprintRecord,
|
|
||||||
TaskRecord,
|
TaskRecord,
|
||||||
WorkflowPipelineRecord,
|
|
||||||
} from "@shared/types";
|
} from "@shared/types";
|
||||||
import { isConnectorExecutionReady } from "@shared/connector-policy";
|
|
||||||
import { formatMoney } from "../../lib/formatters";
|
import { formatMoney } from "../../lib/formatters";
|
||||||
import { StatusPill } from "../ui";
|
|
||||||
import { useT } from "../../i18n";
|
import { useT } from "../../i18n";
|
||||||
import { computeHealthScore, HealthRing } from "./HealthCard";
|
|
||||||
import { MetricCard } from "./MetricsGrid";
|
import { MetricCard } from "./MetricsGrid";
|
||||||
import { ActivityFeed } from "./ActivityFeed";
|
import { ActivityFeed } from "./ActivityFeed";
|
||||||
import { WorkforceGrid } from "./QuickActions";
|
|
||||||
import { DepartmentOverview } from "./DepartmentOverview";
|
|
||||||
import { TaskPipeline } from "./TaskPipeline";
|
|
||||||
import { SprintProgress } from "./SprintProgress";
|
|
||||||
import { GoalProgress } from "./GoalProgress";
|
|
||||||
import { NeedsAttention } from "./NeedsAttention";
|
|
||||||
import { RecoveryBanner } from "./RecoveryBanner";
|
import { RecoveryBanner } from "./RecoveryBanner";
|
||||||
import { BudgetAlertBanner } from "./BudgetAlertBanner";
|
import { BudgetAlertBanner } from "./BudgetAlertBanner";
|
||||||
|
|
||||||
@@ -54,194 +36,206 @@ export interface DashboardProps {
|
|||||||
projects: ProjectRecord[];
|
projects: ProjectRecord[];
|
||||||
activity: ActivityRecord[];
|
activity: ActivityRecord[];
|
||||||
costs: CostRecord[];
|
costs: CostRecord[];
|
||||||
connectors: ConnectorRecord[];
|
|
||||||
messages?: AgentMessageRecord[];
|
messages?: AgentMessageRecord[];
|
||||||
browserActions?: BrowserActionRecord[];
|
|
||||||
workflows?: WorkflowPipelineRecord[];
|
|
||||||
meetings?: MeetingRecord[];
|
|
||||||
sprints?: SprintRecord[];
|
|
||||||
onNavigate: (section: SectionId, entityId?: string) => void;
|
onNavigate: (section: SectionId, entityId?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dashboard(props: DashboardProps) {
|
export function Dashboard(props: DashboardProps) {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { agents, tasks, runs, approvals, goals, projects, activity, costs, connectors, messages = [], browserActions = [], workflows = [], sprints = [], onNavigate } = props;
|
const { agents, tasks, runs, approvals, activity, costs, messages = [], onNavigate } = props;
|
||||||
|
|
||||||
const healthScore = useMemo(
|
|
||||||
() => computeHealthScore({ agents, tasks, runs, approvals, connectors, messages, browserActions, workflows }),
|
|
||||||
[agents, tasks, runs, approvals, connectors, messages, browserActions, workflows],
|
|
||||||
);
|
|
||||||
|
|
||||||
const liveRuns = useMemo(() => runs.filter((r) => r.status === "running" || r.status === "queued"), [runs]);
|
|
||||||
const failedRuns = useMemo(() => runs.filter((r) => r.status === "failed" || r.status === "timed_out" || r.status === "interrupted"), [runs]);
|
|
||||||
const pendingApprovals = useMemo(() => approvals.filter((a) => a.state === "pending"), [approvals]);
|
|
||||||
const activeTasks = useMemo(() => tasks.filter((t) => t.status === "in_progress" || t.status === "in_review"), [tasks]);
|
|
||||||
const activeGoals = useMemo(() => goals.filter((g) => g.status === "active"), [goals]);
|
|
||||||
const totalSpend = useMemo(() => costs.reduce((sum, c) => sum + (c.amountUsd ?? 0), 0), [costs]);
|
|
||||||
const activeAgents = useMemo(() => agents.filter((a) => a.status !== "terminated" && a.status !== "pending_approval"), [agents]);
|
const activeAgents = useMemo(() => agents.filter((a) => a.status !== "terminated" && a.status !== "pending_approval"), [agents]);
|
||||||
const readyConnectors = useMemo(() => connectors.filter((connector) => isConnectorExecutionReady(connector)), [connectors]);
|
const activeTasks = useMemo(() => tasks.filter((t) => t.status === "in_progress" || t.status === "in_review"), [tasks]);
|
||||||
const activeSprint = useMemo(() => sprints.find((s) => s.status === "active"), [sprints]);
|
const pendingApprovals = useMemo(() => approvals.filter((a) => a.state === "pending"), [approvals]);
|
||||||
const unreadMessages = useMemo(() => messages.filter((message) => !message.readAt), [messages]);
|
const totalSpend = useMemo(() => costs.reduce((sum, c) => sum + (c.amountUsd ?? 0), 0), [costs]);
|
||||||
const urgentMessages = useMemo(() => messages.filter((message) => !message.readAt && (message.priority === "urgent" || message.channel === "incident")), [messages]);
|
|
||||||
const blockedBrowserActions = useMemo(() => browserActions.filter((action) => action.status === "approval_required" || action.status === "failed"), [browserActions]);
|
// Attention items
|
||||||
const stalledWorkflows = useMemo(() => workflows.filter((workflow) => workflow.status === "paused" || workflow.status === "failed"), [workflows]);
|
const failedRuns = useMemo(() => runs.filter((r) => r.status === "failed" || r.status === "timed_out" || r.status === "interrupted"), [runs]);
|
||||||
|
const urgentMessages = useMemo(() => messages.filter((m) => !m.readAt && (m.priority === "urgent" || m.channel === "incident")), [messages]);
|
||||||
|
const hasAttentionItems = pendingApprovals.length > 0 || failedRuns.length > 0 || urgentMessages.length > 0;
|
||||||
|
|
||||||
|
// Active work — agents currently running tasks
|
||||||
|
const activeWork = useMemo(() => {
|
||||||
|
return runs
|
||||||
|
.filter((r) => r.status === "running" || r.status === "queued")
|
||||||
|
.map((run) => {
|
||||||
|
const agent = agents.find((a) => a.id === run.agentId);
|
||||||
|
const task = tasks.find((t) => t.id === run.taskId);
|
||||||
|
return { run, agent, task };
|
||||||
|
})
|
||||||
|
.slice(0, 8);
|
||||||
|
}, [runs, agents, tasks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Recovery banner for interrupted runs */}
|
{/* Conditional banners */}
|
||||||
<RecoveryBanner />
|
<RecoveryBanner />
|
||||||
|
|
||||||
{/* Budget alert banner for agents approaching budget limits */}
|
|
||||||
<BudgetAlertBanner agents={agents} onNavigate={onNavigate} />
|
<BudgetAlertBanner agents={agents} onNavigate={onNavigate} />
|
||||||
|
|
||||||
{/* Health + Metrics */}
|
{/* Key Metrics — 4 cards */}
|
||||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-start">
|
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||||
<div className="shrink-0 rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
|
<MetricCard
|
||||||
<HealthRing score={healthScore} />
|
label={t("dash.activeTasks")}
|
||||||
<div className="mt-3 flex gap-3">
|
value={activeTasks.length}
|
||||||
<div className="text-center">
|
icon={Briefcase}
|
||||||
<div className="text-[16px] font-bold text-[color:var(--text)]">{activeAgents.length}</div>
|
tone={activeTasks.length > 0 ? "accent" : "default"}
|
||||||
<div className="text-[10px] text-[color:var(--muted)]">Agents</div>
|
onClick={() => onNavigate("tasks")}
|
||||||
</div>
|
subtitle={`${tasks.filter((t) => t.status === "todo").length} ${t("dash.queued")}`}
|
||||||
<div className="h-8 w-px bg-[color:var(--line)]" />
|
/>
|
||||||
<div className="text-center">
|
<MetricCard
|
||||||
<div className="text-[16px] font-bold text-[color:var(--text)]">{readyConnectors.length}/{connectors.length}</div>
|
label={t("dash.approvals")}
|
||||||
<div className="text-[10px] text-[color:var(--muted)]">Connectors</div>
|
value={pendingApprovals.length}
|
||||||
</div>
|
icon={ShieldCheck}
|
||||||
<div className="h-8 w-px bg-[color:var(--line)]" />
|
tone={pendingApprovals.length > 0 ? "warn" : "default"}
|
||||||
<div className="text-center">
|
onClick={() => onNavigate("approvals")}
|
||||||
<div className="text-[16px] font-bold text-[color:var(--text)]">{activeGoals.length}</div>
|
subtitle={pendingApprovals.length > 0 ? t("dash.needsAttention") : t("dash.allClear")}
|
||||||
<div className="text-[10px] text-[color:var(--muted)]">Goals</div>
|
/>
|
||||||
</div>
|
<MetricCard
|
||||||
</div>
|
label="Agents"
|
||||||
</div>
|
value={activeAgents.length}
|
||||||
|
icon={Robot}
|
||||||
<div className="grid flex-1 grid-cols-2 gap-2 lg:grid-cols-3">
|
tone={activeAgents.filter((a) => a.status === "running").length > 0 ? "success" : "default"}
|
||||||
<MetricCard label={t("dash.liveRuns")} value={liveRuns.length} icon={Heartbeat} tone={liveRuns.length > 0 ? "success" : "default"} onClick={() => onNavigate("runs")} subtitle={liveRuns.length > 0 ? `${liveRuns.filter((r) => r.status === "running").length} ${t("dash.running")}` : t("dash.allQuiet")} />
|
onClick={() => onNavigate("agents")}
|
||||||
<MetricCard label={t("dash.activeTasks")} value={activeTasks.length} icon={Briefcase} tone={activeTasks.length > 0 ? "accent" : "default"} onClick={() => onNavigate("tasks")} subtitle={`${tasks.filter((t) => t.status === "todo").length} ${t("dash.queued")}`} />
|
subtitle={`${activeAgents.filter((a) => a.status === "running").length} running`}
|
||||||
<MetricCard label={t("dash.approvals")} value={pendingApprovals.length} icon={ShieldCheck} tone={pendingApprovals.length > 0 ? "warn" : "default"} onClick={() => onNavigate("approvals")} subtitle={pendingApprovals.length > 0 ? t("dash.needsAttention") : t("dash.allClear")} />
|
/>
|
||||||
<MetricCard label={t("dash.failedRuns")} value={failedRuns.length} icon={Warning} tone={failedRuns.length > 0 ? "danger" : "default"} onClick={() => onNavigate("runs")} />
|
<MetricCard
|
||||||
<MetricCard label={t("dash.totalSpend")} value={formatMoney(totalSpend)} icon={CurrencyCircleDollar} onClick={() => onNavigate("costs")} />
|
label={t("dash.totalSpend")}
|
||||||
<MetricCard label={t("dash.projects")} value={projects.filter((p) => p.status === "in_progress").length} icon={Target} onClick={() => onNavigate("projects")} subtitle={`${projects.length} ${t("dash.total")}`} />
|
value={formatMoney(totalSpend)}
|
||||||
<MetricCard label={t("dash.unreadMessages")} value={unreadMessages.length} icon={Users} tone={urgentMessages.length > 0 ? "warn" : unreadMessages.length > 0 ? "accent" : "default"} onClick={() => onNavigate("communication")} subtitle={urgentMessages.length > 0 ? `${urgentMessages.length} urgent` : unreadMessages.length > 0 ? "Needs triage" : "All read"} />
|
icon={CurrencyCircleDollar}
|
||||||
<MetricCard label={t("dash.browserOps")} value={blockedBrowserActions.length} icon={Cpu} tone={blockedBrowserActions.length > 0 ? "warn" : "default"} onClick={() => onNavigate("social")} subtitle={blockedBrowserActions.length > 0 ? "Blocked or failed" : "Nominal"} />
|
onClick={() => onNavigate("settings")}
|
||||||
<MetricCard label={t("dash.workflowStalls")} value={stalledWorkflows.length} icon={Lightning} tone={stalledWorkflows.length > 0 ? "warn" : "default"} onClick={() => onNavigate("automation")} subtitle={stalledWorkflows.length > 0 ? "Paused or failed" : "Healthy"} />
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Department Overview */}
|
{/* Needs Attention — moved to top, only shows when items exist */}
|
||||||
<section>
|
{hasAttentionItems ? (
|
||||||
<div className="mb-3 flex items-center justify-between">
|
|
||||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.departments")}</div>
|
|
||||||
<button type="button" onClick={() => onNavigate("orgchart")} className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline">
|
|
||||||
{t("dash.orgChart")} <TreeStructure size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<DepartmentOverview agents={agents} tasks={tasks} runs={runs} onNavigate={onNavigate} />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Sprint + Goals */}
|
|
||||||
<div className="grid gap-6 lg:grid-cols-2">
|
|
||||||
{activeSprint ? (
|
|
||||||
<section>
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
|
||||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.currentSprint")}</div>
|
|
||||||
<button type="button" onClick={() => onNavigate("sprints")} className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline">
|
|
||||||
Manage <ArrowRight size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<SprintProgress sprint={activeSprint} tasks={tasks} />
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
<section>
|
<section>
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">
|
||||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.goalProgress")}</div>
|
{t("dash.needsAttentionSection")}
|
||||||
<button type="button" onClick={() => onNavigate("goals")} className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline">
|
</div>
|
||||||
All goals <ArrowRight size={12} />
|
<div className="space-y-1.5">
|
||||||
</button>
|
{pendingApprovals.slice(0, 5).map((approval) => {
|
||||||
|
const requester = agents.find((a) => a.id === approval.requestedByAgentId);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={approval.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onNavigate("approvals", approval.id)}
|
||||||
|
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||||
|
>
|
||||||
|
<ShieldCheck size={18} className="shrink-0 text-[color:var(--warn)]" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
|
||||||
|
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
|
||||||
|
<span className="uppercase tracking-[0.1em]">{approval.type.replaceAll("_", " ")}</span>
|
||||||
|
{requester ? <span>by {requester.name}</span> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{failedRuns.slice(0, 3).map((run) => {
|
||||||
|
const agent = agents.find((a) => a.id === run.agentId);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={run.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onNavigate("runs", run.id)}
|
||||||
|
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--danger)] border-opacity-20 bg-[color:var(--danger-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||||
|
>
|
||||||
|
<XCircle size={18} className="shrink-0 text-[color:var(--danger)]" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{run.summary || "Run failed"}</div>
|
||||||
|
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
|
||||||
|
{agent?.name} — {run.errorMessage?.slice(0, 80) ?? "Unknown error"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{urgentMessages.slice(0, 3).map((msg) => {
|
||||||
|
const sender = agents.find((a) => a.id === msg.fromAgentId);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={msg.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onNavigate("communication", msg.id)}
|
||||||
|
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||||
|
>
|
||||||
|
<Warning size={18} className="shrink-0 text-[color:var(--warn)]" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{msg.subject}</div>
|
||||||
|
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
|
||||||
|
{sender?.name ?? "Unknown"} — {msg.channel}{msg.priority === "urgent" ? " · urgent" : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<GoalProgress goals={goals} tasks={tasks} onNavigate={onNavigate} />
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
) : null}
|
||||||
|
|
||||||
{/* Task Pipeline */}
|
{/* Active Work — what's running right now */}
|
||||||
<section>
|
<section>
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.taskPipeline")}</div>
|
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||||
<button type="button" onClick={() => onNavigate("tasks")} className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline">
|
Active Work
|
||||||
View all <ArrowRight size={12} />
|
</div>
|
||||||
|
<button type="button" onClick={() => onNavigate("runs")} className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline">
|
||||||
|
All runs <ArrowRight size={12} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
|
{activeWork.length > 0 ? (
|
||||||
<TaskPipeline tasks={tasks} />
|
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] divide-y divide-[color:var(--line)]">
|
||||||
</div>
|
{activeWork.map(({ run, agent, task }) => (
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Workforce + Activity */}
|
|
||||||
<div className="grid gap-6 lg:grid-cols-5">
|
|
||||||
<section className="lg:col-span-3">
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
|
||||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.workforce")}</div>
|
|
||||||
<button type="button" onClick={() => onNavigate("orgchart")} className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline">
|
|
||||||
{t("dash.orgChart")} <TreeStructure size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<WorkforceGrid agents={agents} onSelectAgent={(id) => onNavigate("agents", id)} />
|
|
||||||
</section>
|
|
||||||
<section className="lg:col-span-2">
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
|
||||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.recentActivity")}</div>
|
|
||||||
<button type="button" onClick={() => onNavigate("activity")} className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline">
|
|
||||||
View all <ArrowRight size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-[400px] overflow-y-auto rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2">
|
|
||||||
<ActivityFeed activity={activity} agents={agents} />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Connector Health */}
|
|
||||||
<section>
|
|
||||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("dash.infrastructure")}</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">
|
|
||||||
{connectors.map((connector) => {
|
|
||||||
const isReady = isConnectorExecutionReady(connector);
|
|
||||||
const isError = connector.status === "error" || connector.status === "auth_required";
|
|
||||||
const agentsUsing = agents.filter((a) => a.connectorId === connector.id).length;
|
|
||||||
return (
|
|
||||||
<button
|
<button
|
||||||
key={connector.id}
|
key={run.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onNavigate("connectors", connector.id)}
|
onClick={() => onNavigate("runs", run.id)}
|
||||||
className={`focus-ring flex items-center gap-2.5 rounded-[8px] border px-3 py-2.5 text-left transition duration-150 hover:bg-[color:var(--panel-soft)] ${
|
className="focus-ring flex w-full items-center gap-3 px-4 py-3 text-left transition hover:bg-[color:var(--panel-soft)]"
|
||||||
isError ? "border-[color:var(--danger)] border-opacity-30" : "border-[color:var(--line)]"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Cpu size={16} className={isReady ? "text-[color:var(--success)]" : isError ? "text-[color:var(--danger)]" : "text-[color:var(--muted)]"} />
|
<div className="relative shrink-0">
|
||||||
|
<Robot size={16} className="text-[color:var(--success)]" />
|
||||||
|
{run.status === "running" ? (
|
||||||
|
<span className="absolute -right-0.5 -top-0.5 h-2 w-2">
|
||||||
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" />
|
||||||
|
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[color:var(--success)]" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="truncate text-[12px] font-medium text-[color:var(--text)]">{connector.label}</div>
|
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">
|
||||||
<div className="flex items-center gap-2 text-[10px] text-[color:var(--muted)]">
|
{task?.title ?? run.summary ?? `Run ${run.id.slice(0, 8)}`}
|
||||||
<StatusPill status={connector.status} />
|
</div>
|
||||||
{agentsUsing > 0 ? <span>{agentsUsing} agent{agentsUsing > 1 ? "s" : ""}</span> : null}
|
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
|
||||||
|
{agent?.name ?? "Unknown agent"} · {run.status}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
))}
|
||||||
})}
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div className="rounded-[10px] border border-dashed border-[color:var(--line)] py-8 text-center">
|
||||||
|
<div className="text-[13px] text-[color:var(--muted)]">{t("dash.allQuiet")}</div>
|
||||||
|
<div className="mt-1 text-[11px] text-[color:var(--muted)]">No tasks currently running</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Needs Attention */}
|
{/* Recent Activity */}
|
||||||
<NeedsAttention
|
<section>
|
||||||
agents={agents}
|
<div className="mb-3 flex items-center justify-between">
|
||||||
approvals={approvals}
|
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||||
runs={runs}
|
{t("dash.recentActivity")}
|
||||||
messages={messages}
|
</div>
|
||||||
browserActions={browserActions}
|
</div>
|
||||||
workflows={workflows}
|
<div className="max-h-[360px] overflow-y-auto rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2">
|
||||||
onNavigate={onNavigate}
|
<ActivityFeed activity={activity} agents={agents} limit={10} />
|
||||||
/>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user