diff --git a/platform/src/app/[tenant]/dashboard/sections/CycleTime.tsx b/platform/src/app/[tenant]/dashboard/sections/CycleTime.tsx index f58596d..a651c95 100644 --- a/platform/src/app/[tenant]/dashboard/sections/CycleTime.tsx +++ b/platform/src/app/[tenant]/dashboard/sections/CycleTime.tsx @@ -1,6 +1,16 @@ "use client"; import { Zap } from "lucide-react"; +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + CartesianGrid, + Cell, +} from "recharts"; import { MetricCard } from "@/components/charts/MetricCard"; import { @@ -11,7 +21,6 @@ import { CardTitle, } from "@/components/ui/card"; import { useTranslation } from "@/hooks/useTranslation"; -import { cn } from "@/lib/utils"; import type { CycleTimeData } from "@/types/org-summary"; // Insight banner is only shown once cycle-time data is dense enough @@ -19,13 +28,46 @@ import type { CycleTimeData } from "@/types/org-summary"; // render the section but hide the headline. const INSIGHT_MIN_MERGED = 50; -// Cutoffs for the "% merged within 24h" horizontal bar color ramp. -// Tuned so a repo that ships in a day most of the time reads green, -// "mixed" reads yellow, and slow repos read orange/red. +// Cutoffs for the "% merged within 24h" bar color ramp. Tuned so a repo that +// ships in a day most of the time reads green, "mixed" reads yellow, and slow +// repos read orange. const FAST_GREEN_PCT = 0.8; const MID_YELLOW_PCT = 0.65; const SLOW_ORANGE_PCT = 0.5; +// Match the Stabilization Distribution chart for a consistent dashboard look. +const CHART_HEIGHT = 200; +const Y_AXIS_WIDTH = 170; +const AXIS_TICK = { fontSize: 10, fill: "var(--color-muted-foreground)" }; + +// Cycle-time buckets, in fastest→slowest order, shared by the stacked bars, +// the tooltip, and the legend. +const BUCKETS = [ + { + key: "same_day", + labelKey: "sameDay", + color: "var(--color-bucket-same-day)", + }, + { key: "one_day", labelKey: "oneDay", color: "var(--color-bucket-one-day)" }, + { + key: "two_to_three_days", + labelKey: "twoThree", + color: "var(--color-bucket-two-three)", + }, + { + key: "four_to_seven_days", + labelKey: "fourSeven", + color: "var(--color-bucket-four-seven)", + }, + { + key: "seven_plus_days", + labelKey: "sevenPlus", + color: "var(--color-bucket-seven-plus)", + }, +] as const; + +type RepoRow = CycleTimeData["perRepo"][number]; + interface CycleTimeProps { data: CycleTimeData; } @@ -114,136 +156,173 @@ export function CycleTime({ data }: CycleTimeProps) { ); } -interface Row { - name: string; - merged: number; - pctWithin24h: number; - buckets: CycleTimeData["perRepo"][number]["buckets"]; +function truncateRepo(value: string): string { + return value.length > 26 ? `…${value.slice(-25)}` : value; +} + +const tickPct = (v: number) => `${(v * 100).toFixed(0)}%`; + +function rampColor(pct: number): string { + if (pct >= FAST_GREEN_PCT) return "var(--color-signal-green)"; + if (pct >= MID_YELLOW_PCT) return "var(--color-bucket-one-day)"; + if (pct >= SLOW_ORANGE_PCT) return "var(--color-signal-yellow)"; + return "var(--color-bucket-four-seven)"; } -function RankingChart({ rows }: { rows: Row[] }) { +/** Single bar per repo — share of PRs merged within a day. Mirrors the + * Stabilization Distribution chart (horizontal Recharts bars + % axis). */ +function RankingChart({ rows }: { rows: RepoRow[] }) { const { t } = useTranslation(); + const data = [...rows].sort((a, b) => b.pctWithin24h - a.pctWithin24h); + return ( -
{p.name}
++ {formatPct(p.pctWithin24h)} ·{" "} + {t("dashboard.cycleTime.tooltips.ranking", { + merged: p.merged, + })} +
+{p.name as string}
+ {BUCKETS.map((b) => { + const frac = (p[b.key] as number) ?? 0; + if (frac <= 0) return null; + return ( ++ + {t(`dashboard.cycleTime.buckets.${b.labelKey}`)}:{" "} + {(frac * 100).toFixed(0)}% +
+ ); + })} +