mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[w9-2] Add 90d window, time-window selector, and chart/table toggle (#528)
Backend: widen usage _PeriodType to 24h/7d/30d/90d and add a 90d branch to _parse_period (daily buckets already cover it). TestParsePeriod pins the contract per window. Frontend: UsagePeriod += 90d with a scaleFor helper (replacing 6 inline ternaries) and 90 daily mock points. One generic SegmentedControl primitive (reuses Radix Tabs) drives both the metrics time-window selector (24h/7d/30d/90d) and the per-chart Chart/Table view toggle — one file, two roles. The Token Usage & Costs tab drops 8 hardcoded '24h' hooks for a single period state + selector; the stale '(24h)' cost-card parenthetical goes too. The Performance landing tab gains a TaskStatusChart donut fed by the status counts already on the page (no new hook). Agent/team bar charts gain an inline table view. Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { useOrchestratorStatus } from "@/hooks/use-agents";
|
||||
import { useTasks } from "@/hooks/use-tasks";
|
||||
@@ -17,11 +17,13 @@ import {
|
||||
useSpawnWaste,
|
||||
useUsageSessions,
|
||||
} from "@/hooks/use-usage";
|
||||
import type { UsagePeriod } from "@/lib/api/usage";
|
||||
import { TaskStatus, Team } from "@/types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { SegmentedControl } from "@/components/ui/segmented-control";
|
||||
import { OfflineState } from "@/components/ui/offline-state";
|
||||
import {
|
||||
ResponsiveTable,
|
||||
@@ -36,6 +38,7 @@ import {
|
||||
ModelUsageDonut,
|
||||
AgentUsageChart,
|
||||
TeamUsageChart,
|
||||
TaskStatusChart,
|
||||
SessionsTable,
|
||||
} from "@/components/metrics";
|
||||
import {
|
||||
@@ -327,31 +330,42 @@ function PerformanceTabContent() {
|
||||
{/* Task Status */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-3">Task Status</h2>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 xl:grid-cols-5 2xl:grid-cols-5">
|
||||
<MetricCard
|
||||
title="Pending"
|
||||
value={pending}
|
||||
icon={<Clock className="h-4 w-4 text-gray-500" />}
|
||||
/>
|
||||
<MetricCard
|
||||
title="In Progress"
|
||||
value={inProgress}
|
||||
icon={<Activity className="h-4 w-4 text-blue-500" />}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Blocked"
|
||||
value={blocked}
|
||||
icon={<AlertTriangle className="h-4 w-4 text-red-500" />}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Awaiting QA"
|
||||
value={awaitingQa}
|
||||
icon={<Timer className="h-4 w-4 text-yellow-500" />}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Completed"
|
||||
value={completed}
|
||||
icon={<CheckCircle className="h-4 w-4 text-green-500" />}
|
||||
<div className="grid gap-4 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-3">
|
||||
<div className="lg:col-span-2 grid gap-4 sm:grid-cols-2 lg:grid-cols-5 xl:grid-cols-5 2xl:grid-cols-5">
|
||||
<MetricCard
|
||||
title="Pending"
|
||||
value={pending}
|
||||
icon={<Clock className="h-4 w-4 text-gray-500" />}
|
||||
/>
|
||||
<MetricCard
|
||||
title="In Progress"
|
||||
value={inProgress}
|
||||
icon={<Activity className="h-4 w-4 text-blue-500" />}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Blocked"
|
||||
value={blocked}
|
||||
icon={<AlertTriangle className="h-4 w-4 text-red-500" />}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Awaiting QA"
|
||||
value={awaitingQa}
|
||||
icon={<Timer className="h-4 w-4 text-yellow-500" />}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Completed"
|
||||
value={completed}
|
||||
icon={<CheckCircle className="h-4 w-4 text-green-500" />}
|
||||
/>
|
||||
</div>
|
||||
<TaskStatusChart
|
||||
slices={[
|
||||
{ name: "Pending", value: pending },
|
||||
{ name: "In Progress", value: inProgress },
|
||||
{ name: "Blocked", value: blocked },
|
||||
{ name: "Awaiting QA", value: awaitingQa },
|
||||
{ name: "Completed", value: completed },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -408,23 +422,41 @@ function PerformanceTabContent() {
|
||||
|
||||
// ─── Token Usage & Costs tab content ─────────────────────────────────────────
|
||||
|
||||
const TIME_WINDOW_OPTIONS: { value: UsagePeriod; label: string }[] = [
|
||||
{ value: "24h", label: "24h" },
|
||||
{ value: "7d", label: "7d" },
|
||||
{ value: "30d", label: "30d" },
|
||||
{ value: "90d", label: "90d" },
|
||||
];
|
||||
|
||||
function TokenUsageCostsSection() {
|
||||
const { data: summary, isLoading: loadingSnap } = useUsageSummary("24h");
|
||||
const { data: timeSeries, isLoading: loadingTS } = useUsageTimeSeries("24h");
|
||||
const { data: agentUsage, isLoading: loadingAgents } = useAgentUsage("24h");
|
||||
const { data: teamUsage, isLoading: loadingTeams } = useTeamUsage("24h");
|
||||
const [period, setPeriod] = useState<UsagePeriod>("24h");
|
||||
const { data: summary, isLoading: loadingSnap } = useUsageSummary(period);
|
||||
const { data: timeSeries, isLoading: loadingTS } = useUsageTimeSeries(period);
|
||||
const { data: agentUsage, isLoading: loadingAgents } = useAgentUsage(period);
|
||||
const { data: teamUsage, isLoading: loadingTeams } = useTeamUsage(period);
|
||||
const { data: sessions, isLoading: loadingSessions } = useUsageSessions(100);
|
||||
const { data: modelUsage, isLoading: loadingModels } = useModelUsage("24h");
|
||||
const { data: modelUsage, isLoading: loadingModels } = useModelUsage(period);
|
||||
const { data: projection, isLoading: loadingProj } = useUsageProjection();
|
||||
const { data: cacheStats, isLoading: loadingCache } =
|
||||
useCacheEfficiency("24h");
|
||||
const { data: roleUsage, isLoading: loadingRoles } = useRoleUsage("24h");
|
||||
const { data: waste, isLoading: loadingWaste } = useSpawnWaste("24h");
|
||||
useCacheEfficiency(period);
|
||||
const { data: roleUsage, isLoading: loadingRoles } = useRoleUsage(period);
|
||||
const { data: waste, isLoading: loadingWaste } = useSpawnWaste(period);
|
||||
|
||||
const trendUp = (summary?.trend_pct ?? 0) >= 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Time window selector — drives every period-scoped hook below */}
|
||||
<div className="flex justify-end">
|
||||
<SegmentedControl
|
||||
options={TIME_WINDOW_OPTIONS}
|
||||
value={period}
|
||||
onValueChange={(v) => setPeriod(v as UsagePeriod)}
|
||||
aria-label="Usage time window"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 1 — Summary cards */}
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 2xl:grid-cols-6">
|
||||
<SummaryCard
|
||||
@@ -440,7 +472,7 @@ function TokenUsageCostsSection() {
|
||||
isLoading={loadingSnap}
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Total Cost (24h)"
|
||||
title="Total Cost"
|
||||
value={summary ? "$" + summary.total_cost_usd.toFixed(4) : undefined}
|
||||
icon={<Coins className="h-4 w-4 text-green-500" />}
|
||||
isLoading={loadingSnap}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { TaskStatusChart } from "../task-status-chart";
|
||||
|
||||
describe("TaskStatusChart", () => {
|
||||
it("renders the card title", () => {
|
||||
render(
|
||||
<TaskStatusChart
|
||||
slices={[
|
||||
{ name: "Completed", value: 5 },
|
||||
{ name: "Pending", value: 0 },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByText("Task Status Distribution"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows an empty state when every slice is zero", () => {
|
||||
render(
|
||||
<TaskStatusChart
|
||||
slices={[
|
||||
{ name: "Pending", value: 0 },
|
||||
{ name: "Completed", value: 0 },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("No tasks")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show the empty state while loading", () => {
|
||||
render(
|
||||
<TaskStatusChart
|
||||
slices={[{ name: "Pending", value: 0 }]}
|
||||
isLoading
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText("No tasks")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
} from "recharts";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SegmentedControl } from "@/components/ui/segmented-control";
|
||||
import { useIsMobile } from "@/hooks/use-is-mobile";
|
||||
import type { AgentUsageRow } from "@/types";
|
||||
|
||||
@@ -24,8 +26,14 @@ function fmtK(n: number): string {
|
||||
return String(n);
|
||||
}
|
||||
|
||||
const VIEW_OPTIONS = [
|
||||
{ value: "chart", label: "Chart" },
|
||||
{ value: "table", label: "Table" },
|
||||
];
|
||||
|
||||
export function AgentUsageChart({ data, isLoading }: AgentUsageChartProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const [view, setView] = useState<"chart" | "table">("chart");
|
||||
// Fewer bars on a phone — 10 labels at ~30deg rotation still overlap below
|
||||
// ~400px, so cap the label density instead of shrinking text further.
|
||||
const chartData = [...(data ?? [])]
|
||||
@@ -35,15 +43,51 @@ export function AgentUsageChart({ data, isLoading }: AgentUsageChartProps) {
|
||||
name: row.agent_slug,
|
||||
Tokens: row.total_tokens,
|
||||
}));
|
||||
const tableRows = [...(data ?? [])].sort(
|
||||
(a, b) => b.total_tokens - a.total_tokens,
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Agent Tokens Today</CardTitle>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<CardTitle className="text-base">Agent Tokens</CardTitle>
|
||||
<SegmentedControl
|
||||
options={VIEW_OPTIONS}
|
||||
value={view}
|
||||
onValueChange={(v) => setView(v as "chart" | "table")}
|
||||
aria-label="Agent tokens view"
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-52 w-full" />
|
||||
) : view === "table" ? (
|
||||
<div className="max-h-52 overflow-y-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-muted-foreground text-xs">
|
||||
<tr>
|
||||
<th className="text-left font-medium py-1">Agent</th>
|
||||
<th className="text-right font-medium py-1">Tokens</th>
|
||||
<th className="text-right font-medium py-1">%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableRows.map((row) => (
|
||||
<tr key={row.agent_slug} className="border-t">
|
||||
<td className="py-1 truncate">{row.agent_slug}</td>
|
||||
<td className="py-1 text-right tabular-nums">
|
||||
{row.total_tokens.toLocaleString()}
|
||||
</td>
|
||||
<td className="py-1 text-right tabular-nums">
|
||||
{row.pct_of_total.toFixed(1)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={208}>
|
||||
<BarChart
|
||||
|
||||
@@ -2,4 +2,5 @@ export { UsageTimeSeriesChart } from "./usage-time-series-chart";
|
||||
export { ModelUsageDonut } from "./model-usage-donut";
|
||||
export { AgentUsageChart } from "./agent-usage-chart";
|
||||
export { TeamUsageChart } from "./team-usage-chart";
|
||||
export { TaskStatusChart } from "./task-status-chart";
|
||||
export { SessionsTable } from "./sessions-table";
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Legend,
|
||||
} from "recharts";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
interface TaskStatusSlice {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface TaskStatusChartProps {
|
||||
slices: TaskStatusSlice[];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const CHART_COLORS = [
|
||||
"var(--chart-1)",
|
||||
"var(--chart-2)",
|
||||
"var(--chart-3)",
|
||||
"var(--chart-4)",
|
||||
"var(--chart-5)",
|
||||
];
|
||||
|
||||
/**
|
||||
* Task-status distribution donut for the Performance landing tab — the one
|
||||
* chart that tab was missing (W9-2). Fed by the status counts already
|
||||
* computed on the page; no extra hook.
|
||||
*/
|
||||
export function TaskStatusChart({ slices, isLoading }: TaskStatusChartProps) {
|
||||
const chartData = slices.filter((s) => s.value > 0);
|
||||
const total = chartData.reduce((sum, s) => sum + s.value, 0);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Task Status Distribution</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-52 w-full" />
|
||||
) : total === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-12">
|
||||
No tasks
|
||||
</p>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={208}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={chartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={52}
|
||||
outerRadius={80}
|
||||
dataKey="value"
|
||||
paddingAngle={3}
|
||||
>
|
||||
{chartData.map((_, idx) => (
|
||||
<Cell
|
||||
key={idx}
|
||||
fill={CHART_COLORS[idx % CHART_COLORS.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={(value, name) => [
|
||||
`${typeof value === "number" ? value : 0} tasks`,
|
||||
name,
|
||||
]}
|
||||
contentStyle={{ fontSize: 12 }}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
} from "recharts";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SegmentedControl } from "@/components/ui/segmented-control";
|
||||
import { useIsMobile } from "@/hooks/use-is-mobile";
|
||||
import type { TeamUsageRow } from "@/types";
|
||||
|
||||
@@ -24,23 +26,67 @@ function fmtK(n: number): string {
|
||||
return String(n);
|
||||
}
|
||||
|
||||
const VIEW_OPTIONS = [
|
||||
{ value: "chart", label: "Chart" },
|
||||
{ value: "table", label: "Table" },
|
||||
];
|
||||
|
||||
export function TeamUsageChart({ data, isLoading }: TeamUsageChartProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const [view, setView] = useState<"chart" | "table">("chart");
|
||||
const chartData = [...(data ?? [])]
|
||||
.sort((a, b) => b.total_tokens - a.total_tokens)
|
||||
.map((row) => ({
|
||||
name: row.team.replace(/_/g, " "),
|
||||
Tokens: row.total_tokens,
|
||||
}));
|
||||
const tableRows = [...(data ?? [])].sort(
|
||||
(a, b) => b.total_tokens - a.total_tokens,
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Team Tokens</CardTitle>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<CardTitle className="text-base">Team Tokens</CardTitle>
|
||||
<SegmentedControl
|
||||
options={VIEW_OPTIONS}
|
||||
value={view}
|
||||
onValueChange={(v) => setView(v as "chart" | "table")}
|
||||
aria-label="Team tokens view"
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-52 w-full" />
|
||||
) : view === "table" ? (
|
||||
<div className="max-h-52 overflow-y-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-muted-foreground text-xs">
|
||||
<tr>
|
||||
<th className="text-left font-medium py-1">Team</th>
|
||||
<th className="text-right font-medium py-1">Tokens</th>
|
||||
<th className="text-right font-medium py-1">%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableRows.map((row) => (
|
||||
<tr key={row.team} className="border-t">
|
||||
<td className="py-1 capitalize">
|
||||
{row.team.replace(/_/g, " ")}
|
||||
</td>
|
||||
<td className="py-1 text-right tabular-nums">
|
||||
{row.total_tokens.toLocaleString()}
|
||||
</td>
|
||||
<td className="py-1 text-right tabular-nums">
|
||||
{row.pct_of_total.toFixed(1)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={208}>
|
||||
<BarChart
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { SegmentedControl } from "../segmented-control";
|
||||
|
||||
const WINDOWS = [
|
||||
{ value: "24h", label: "24h" },
|
||||
{ value: "7d", label: "7d" },
|
||||
{ value: "30d", label: "30d" },
|
||||
{ value: "90d", label: "90d" },
|
||||
];
|
||||
|
||||
describe("SegmentedControl", () => {
|
||||
it("renders every option as a tab", () => {
|
||||
render(
|
||||
<SegmentedControl
|
||||
options={WINDOWS}
|
||||
value="24h"
|
||||
onValueChange={() => {}}
|
||||
aria-label="Time window"
|
||||
/>,
|
||||
);
|
||||
for (const opt of WINDOWS) {
|
||||
expect(screen.getByRole("tab", { name: opt.label })).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it("marks only the selected option active", () => {
|
||||
render(
|
||||
<SegmentedControl
|
||||
options={WINDOWS}
|
||||
value="7d"
|
||||
onValueChange={() => {}}
|
||||
aria-label="Time window"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("tab", { name: "7d" })).toHaveAttribute(
|
||||
"data-state",
|
||||
"active",
|
||||
);
|
||||
expect(screen.getByRole("tab", { name: "24h" })).toHaveAttribute(
|
||||
"data-state",
|
||||
"inactive",
|
||||
);
|
||||
});
|
||||
|
||||
it("calls onValueChange with the clicked option's value", async () => {
|
||||
const onChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<SegmentedControl
|
||||
options={WINDOWS}
|
||||
value="24h"
|
||||
onValueChange={onChange}
|
||||
aria-label="Time window"
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole("tab", { name: "90d" }));
|
||||
expect(onChange).toHaveBeenCalledWith("90d");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
export interface SegmentedOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SegmentedControlProps {
|
||||
options: SegmentedOption[];
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
"aria-label"?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic segmented control — a value + N mutually-exclusive options.
|
||||
* Built on the Radix Tabs primitive (value/onValueChange, no panels).
|
||||
* Used for the metrics time-window selector (24h/7d/30d/90d) and the
|
||||
* chart/table view toggle — one primitive, two roles.
|
||||
*/
|
||||
export function SegmentedControl({
|
||||
options,
|
||||
value,
|
||||
onValueChange,
|
||||
className,
|
||||
...rest
|
||||
}: SegmentedControlProps) {
|
||||
return (
|
||||
<Tabs
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
className={className}
|
||||
>
|
||||
<TabsList {...rest}>
|
||||
{options.map((opt) => (
|
||||
<TabsTrigger key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -13,14 +13,19 @@ import type {
|
||||
UsageSession,
|
||||
} from "@/types";
|
||||
|
||||
export type UsagePeriod = "24h" | "7d" | "30d";
|
||||
export type UsagePeriod = "24h" | "7d" | "30d" | "90d";
|
||||
|
||||
// =============================================================================
|
||||
// MOCK DATA — shapes must exactly match the real backend response schemas
|
||||
// =============================================================================
|
||||
|
||||
/** Linear scale factor for mock aggregates so a longer window shows more volume. */
|
||||
function scaleFor(period: UsagePeriod): number {
|
||||
return period === "90d" ? 90 : period === "30d" ? 30 : period === "7d" ? 7 : 1;
|
||||
}
|
||||
|
||||
function mockSummary(period: UsagePeriod = "24h"): UsageSummary {
|
||||
const scale = period === "30d" ? 30 : period === "7d" ? 7 : 1;
|
||||
const scale = scaleFor(period);
|
||||
const base = 124_800 * scale;
|
||||
return {
|
||||
tokens_input: Math.round(base * 0.55),
|
||||
@@ -34,7 +39,7 @@ function mockSummary(period: UsagePeriod = "24h"): UsageSummary {
|
||||
|
||||
function mockTimeSeries(period: UsagePeriod = "24h"): UsageTimePoint[] {
|
||||
const now = new Date();
|
||||
const points = period === "24h" ? 24 : period === "7d" ? 7 : 30;
|
||||
const points = period === "24h" ? 24 : period === "7d" ? 7 : period === "90d" ? 90 : 30;
|
||||
const step = period === "24h" ? "hour" : "day";
|
||||
return Array.from({ length: points }, (_, i) => {
|
||||
const ts = new Date(now);
|
||||
@@ -59,7 +64,7 @@ function mockTimeSeries(period: UsagePeriod = "24h"): UsageTimePoint[] {
|
||||
}
|
||||
|
||||
function mockAgentUsage(period: UsagePeriod = "24h"): AgentUsageRow[] {
|
||||
const scale = period === "30d" ? 30 : period === "7d" ? 7 : 1;
|
||||
const scale = scaleFor(period);
|
||||
const agents = [
|
||||
{ agent_slug: "be-dev-1" },
|
||||
{ agent_slug: "be-dev-2" },
|
||||
@@ -87,7 +92,7 @@ function mockAgentUsage(period: UsagePeriod = "24h"): AgentUsageRow[] {
|
||||
}
|
||||
|
||||
function mockTeamUsage(period: UsagePeriod = "24h"): TeamUsageRow[] {
|
||||
const scale = period === "30d" ? 30 : period === "7d" ? 7 : 1;
|
||||
const scale = scaleFor(period);
|
||||
const teams = ["backend", "frontend", "ux_ui", "main_pm"];
|
||||
const grand = teams.length * 50_000 * scale;
|
||||
return teams.map((team) => {
|
||||
@@ -106,7 +111,7 @@ function mockTeamUsage(period: UsagePeriod = "24h"): TeamUsageRow[] {
|
||||
}
|
||||
|
||||
function mockModelUsage(period: UsagePeriod = "24h"): ModelUsageSlice[] {
|
||||
const scale = period === "30d" ? 30 : period === "7d" ? 7 : 1;
|
||||
const scale = scaleFor(period);
|
||||
const models = [
|
||||
{ model: "claude-opus-4", share: 0.548 },
|
||||
{ model: "claude-sonnet-4", share: 0.346 },
|
||||
@@ -156,7 +161,7 @@ function mockCacheEfficiency(
|
||||
}
|
||||
|
||||
function mockRoleUsage(period: UsagePeriod = "24h"): RoleUsageRow[] {
|
||||
const scale = period === "30d" ? 30 : period === "7d" ? 7 : 1;
|
||||
const scale = scaleFor(period);
|
||||
const roles = [
|
||||
{ role: "developer", share: 0.375 },
|
||||
{ role: "main_pm", share: 0.301 },
|
||||
@@ -187,7 +192,7 @@ function mockRoleUsage(period: UsagePeriod = "24h"): RoleUsageRow[] {
|
||||
}
|
||||
|
||||
function mockSpawnWaste(period: UsagePeriod = "24h"): SpawnWasteResponse {
|
||||
const scale = period === "30d" ? 30 : period === "7d" ? 7 : 1;
|
||||
const scale = scaleFor(period);
|
||||
const by_role = [
|
||||
{ role: "developer", spawns: 51 * scale, unproductive: 42 * scale },
|
||||
{ role: "cell_pm", spawns: 34 * scale, unproductive: 18 * scale },
|
||||
|
||||
Reference in New Issue
Block a user