@c2l2c/backstage-plugin-dora-metrics 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1311 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { useState, useEffect, useRef } from 'react';
3
+ import { useTheme } from '@material-ui/core/styles';
4
+ import { useApi } from '@backstage/core-plugin-api';
5
+ import { useEntity } from '@backstage/plugin-catalog-react';
6
+ import { InfoCard } from '@backstage/core-components';
7
+ import { doraMetricsApiRef } from '../api/types.esm.js';
8
+ import { RatingBadge } from './ui/badge.esm.js';
9
+ import { Card, CardLabel, CardDescription, CardFooter } from './ui/card.esm.js';
10
+ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from './ui/select.esm.js';
11
+ import { ExternalLink } from 'lucide-react';
12
+
13
+ const ANNOTATION_PROJECT_SLUG = "github.com/project-slug";
14
+ const DATE_RANGE_OPTIONS = [
15
+ { value: "7", label: "Last 7 days" },
16
+ { value: "14", label: "Last 14 days" },
17
+ { value: "30", label: "Last 30 days" },
18
+ { value: "60", label: "Last 60 days" },
19
+ { value: "90", label: "Last 90 days" }
20
+ ];
21
+ function formatDuration(hours) {
22
+ const totalMinutes = Math.round(hours * 60);
23
+ const d = Math.floor(totalMinutes / (24 * 60));
24
+ const h = Math.floor(totalMinutes % (24 * 60) / 60);
25
+ const m = totalMinutes % 60;
26
+ if (d > 0) return h > 0 ? `${d}d ${h}h` : `${d}d`;
27
+ if (h > 0) return m > 0 ? `${h}h ${m}m` : `${h}h`;
28
+ return `${m}m`;
29
+ }
30
+ const DURATION_GRADIENT = ["#f87171", "#fb923c", "#fbbf24", "#a3e635", "#4ade80"];
31
+ function durationColor(index, total) {
32
+ if (total <= 1) return DURATION_GRADIENT[0];
33
+ const step = (DURATION_GRADIENT.length - 1) / (total - 1);
34
+ return DURATION_GRADIENT[Math.round(index * step)];
35
+ }
36
+ function buildDeltaLabel(values, unit, lowerIsBetter) {
37
+ if (values.length < 2) return null;
38
+ const curr = values[values.length - 1];
39
+ const prev = values[values.length - 2];
40
+ const diff = curr - prev;
41
+ if (Math.abs(diff) < 1e-3) return null;
42
+ const good = lowerIsBetter ? diff < 0 : diff > 0;
43
+ const arrow = diff > 0 ? "\u2191" : "\u2193";
44
+ const fmt = (v) => unit === "hours" ? formatDuration(v) : unit === "%" ? `${Math.round(v * 10) / 10}%` : unit === "per week" ? `${Math.round(v * 10) / 10}/wk` : `${Math.round(v * 10) / 10} ${unit}`;
45
+ const direction = good ? "improving" : "worsening";
46
+ return {
47
+ text: `${arrow} ${fmt(Math.abs(diff))} ${direction}`,
48
+ tooltip: `Latest period: ${fmt(curr)} \xB7 Previous period: ${fmt(prev)}
49
+ Each period = one time bucket (~1/7th of your selected date range)`,
50
+ good
51
+ };
52
+ }
53
+ const LOADING_MESSAGES = [
54
+ "Establishing routes\u2026",
55
+ "Syncing catalog\u2026",
56
+ "Loading plugins\u2026",
57
+ "Fetching PR data\u2026",
58
+ "Reading entity graph\u2026"
59
+ ];
60
+ const STAGES = ["FETCH", "PARSE", "SCORE", "RENDER"];
61
+ const FONT_IMPACT = '"Impact","Haettenschweiler","Franklin Gothic Heavy","Arial Black",sans-serif';
62
+ const FONT_MONO = '"JetBrains Mono","Fira Code","Consolas",monospace';
63
+ function DojoLoadingOverlay() {
64
+ const theme = useTheme();
65
+ const isDark = theme.palette.type === "dark";
66
+ const [activeStage, setActiveStage] = useState(0);
67
+ const [msgKey, setMsgKey] = useState(0);
68
+ const [msgIdx, setMsgIdx] = useState(0);
69
+ useEffect(() => {
70
+ const st = setInterval(() => setActiveStage((s) => (s + 1) % STAGES.length), 800);
71
+ return () => clearInterval(st);
72
+ }, []);
73
+ useEffect(() => {
74
+ const mt = setInterval(() => {
75
+ setMsgIdx((i) => (i + 1) % LOADING_MESSAGES.length);
76
+ setMsgKey((k) => k + 1);
77
+ }, 2200);
78
+ return () => clearInterval(mt);
79
+ }, []);
80
+ const textShadow = isDark ? "3px 3px 0 #C24E00, 6px 6px 0 #902800, 9px 9px 0 #5A1800, 12px 12px 0 rgba(0,0,0,0.55), 0 0 60px rgba(250,100,0,0.45)" : "3px 3px 0 #C24E00, 6px 6px 0 #902800, 9px 9px 0 rgba(80,20,0,0.28), 0 0 32px rgba(250,100,0,0.22)";
81
+ const mutedColor = isDark ? "#3f3f46" : "#a1a1aa";
82
+ const subtitleColor = isDark ? "#52525b" : "#71717a";
83
+ const borderColor = isDark ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.08)";
84
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
85
+ /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: {
86
+ position: "fixed",
87
+ inset: 0,
88
+ zIndex: 900,
89
+ backdropFilter: "blur(12px)",
90
+ WebkitBackdropFilter: "blur(12px)",
91
+ background: isDark ? "rgba(6,6,9,0.65)" : "rgba(253,252,251,0.70)"
92
+ } }),
93
+ /* @__PURE__ */ jsxs("div", { style: {
94
+ position: "fixed",
95
+ zIndex: 901,
96
+ top: "50%",
97
+ left: "50%",
98
+ transform: "translateX(-50%) translateY(-50%)",
99
+ display: "flex",
100
+ flexDirection: "column",
101
+ alignItems: "center",
102
+ padding: "48px 56px 40px",
103
+ borderRadius: 20,
104
+ background: isDark ? "rgba(9,9,12,0.82)" : "rgba(255,255,255,0.88)",
105
+ border: `1px solid ${borderColor}`,
106
+ boxShadow: isDark ? "0 0 0 1px rgba(250,100,0,0.06), 0 32px 80px rgba(0,0,0,0.65)" : "0 32px 80px rgba(0,0,0,0.12)",
107
+ backdropFilter: "blur(20px)",
108
+ WebkitBackdropFilter: "blur(20px)",
109
+ animation: "dlsCardIn 0.35s cubic-bezier(0.34,1.2,0.64,1) both",
110
+ overflow: "visible"
111
+ }, children: [
112
+ /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: {
113
+ position: "absolute",
114
+ width: 500,
115
+ height: 360,
116
+ left: "50%",
117
+ top: "50%",
118
+ transform: "translateX(-50%) translateY(-50%)",
119
+ background: `radial-gradient(ellipse at center, ${isDark ? "rgba(250,100,0,0.08)" : "rgba(250,100,0,0.05)"} 0%, transparent 70%)`,
120
+ animation: "dlsBreath 3.2s ease-in-out infinite",
121
+ pointerEvents: "none",
122
+ borderRadius: 20
123
+ } }),
124
+ /* @__PURE__ */ jsx("span", { style: {
125
+ fontFamily: "Inter,system-ui,sans-serif",
126
+ fontSize: 11,
127
+ fontWeight: 700,
128
+ letterSpacing: "0.18em",
129
+ textTransform: "uppercase",
130
+ marginBottom: 6,
131
+ color: isDark ? "rgba(250,100,0,0.55)" : "rgba(180,70,0,0.55)",
132
+ animation: "dlsFadeUp 0.5s ease both"
133
+ }, children: "FleetPanda" }),
134
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", lineHeight: 1, marginBottom: 6 }, children: ["D", "O", "J", "O"].map((letter, i) => /* @__PURE__ */ jsx("span", { style: {
135
+ fontFamily: FONT_IMPACT,
136
+ fontWeight: 900,
137
+ fontSize: 110,
138
+ color: "#FA6400",
139
+ letterSpacing: "0.04em",
140
+ display: "inline-block",
141
+ transform: "skewX(-6deg)",
142
+ textShadow,
143
+ WebkitTextStroke: "0.5px #C24E00",
144
+ animation: `dlsLetterDrop 0.6s cubic-bezier(0.34,1.45,0.64,1) ${i * 0.1}s both`
145
+ }, children: letter }, i)) }),
146
+ /* @__PURE__ */ jsx("span", { style: {
147
+ fontFamily: "Inter,system-ui,sans-serif",
148
+ fontSize: 13,
149
+ fontWeight: 400,
150
+ letterSpacing: "0.06em",
151
+ color: subtitleColor,
152
+ marginBottom: 32,
153
+ animation: "dlsFadeUp 0.4s ease 0.55s both"
154
+ }, children: "Developer Portal" }),
155
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "center", gap: 0, marginBottom: 20, animation: "dlsFadeUp 0.4s ease 0.7s both" }, children: STAGES.map((stage, i) => {
156
+ const isActive = i === activeStage;
157
+ const isComplete = i < activeStage;
158
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 0 }, children: [
159
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 5, width: 64 }, children: [
160
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative", width: 12, height: 12 }, children: [
161
+ isActive && /* @__PURE__ */ jsx("div", { style: {
162
+ position: "absolute",
163
+ inset: -6,
164
+ borderRadius: "50%",
165
+ border: "1.5px solid rgba(250,100,0,0.5)",
166
+ animation: "dlsPulse 1s ease-out infinite"
167
+ } }),
168
+ /* @__PURE__ */ jsx("div", { style: {
169
+ width: 12,
170
+ height: 12,
171
+ borderRadius: "50%",
172
+ background: isActive || isComplete ? "#FA6400" : isDark ? "#27272a" : "#e4e4e7",
173
+ border: `2px solid ${isActive || isComplete ? "#C24E00" : isDark ? "#3f3f46" : "#d4d4d8"}`,
174
+ boxShadow: isActive ? "0 0 14px rgba(250,100,0,0.7)" : "none",
175
+ transition: "background 0.3s, box-shadow 0.3s"
176
+ } })
177
+ ] }),
178
+ /* @__PURE__ */ jsx("span", { style: {
179
+ fontFamily: "Inter,system-ui,sans-serif",
180
+ fontSize: 8,
181
+ fontWeight: 700,
182
+ letterSpacing: "0.12em",
183
+ textTransform: "uppercase",
184
+ color: isActive ? "#FA6400" : isComplete ? mutedColor : mutedColor,
185
+ transition: "color 0.3s"
186
+ }, children: stage })
187
+ ] }),
188
+ i < STAGES.length - 1 && /* @__PURE__ */ jsx("div", { style: {
189
+ width: 32,
190
+ height: 2,
191
+ marginBottom: 14,
192
+ position: "relative",
193
+ overflow: "hidden",
194
+ background: isDark ? "#27272a" : "#e4e4e7",
195
+ borderRadius: 2
196
+ }, children: /* @__PURE__ */ jsx("div", { style: {
197
+ position: "absolute",
198
+ inset: 0,
199
+ borderRadius: 2,
200
+ background: "#FA6400",
201
+ transformOrigin: "left center",
202
+ transform: `scaleX(${i < activeStage ? 1 : isActive ? 0.5 : 0})`,
203
+ transition: "transform 0.4s ease"
204
+ } }) })
205
+ ] }, stage);
206
+ }) }),
207
+ /* @__PURE__ */ jsx("span", { style: {
208
+ fontFamily: FONT_MONO,
209
+ fontSize: 11,
210
+ color: mutedColor,
211
+ letterSpacing: "0.04em",
212
+ animation: "dlsFadeUp 0.3s ease both"
213
+ }, children: LOADING_MESSAGES[msgIdx] }, msgKey)
214
+ ] }),
215
+ /* @__PURE__ */ jsx("style", { children: `
216
+ @keyframes dlsLetterDrop {
217
+ from { opacity: 0; transform: skewX(-6deg) translateY(-32px) scale(1.1); }
218
+ 65% { transform: skewX(-6deg) translateY(6px) scale(0.95); }
219
+ to { opacity: 1; transform: skewX(-6deg) translateY(0) scale(1); }
220
+ }
221
+ @keyframes dlsFadeUp {
222
+ from { opacity: 0; transform: translateY(10px); }
223
+ to { opacity: 1; transform: translateY(0); }
224
+ }
225
+ @keyframes dlsBreath {
226
+ 0%, 100% { transform: translateX(-50%) translateY(-50%) scale(0.85); opacity: 0.6; }
227
+ 50% { transform: translateX(-50%) translateY(-50%) scale(1.2); opacity: 1; }
228
+ }
229
+ @keyframes dlsPulse {
230
+ from { transform: scale(0.6); opacity: 0.9; }
231
+ to { transform: scale(2.2); opacity: 0; }
232
+ }
233
+ @keyframes dlsCardIn {
234
+ from { opacity: 0; transform: translateX(-50%) translateY(-46%); }
235
+ to { opacity: 1; transform: translateX(-50%) translateY(-50%); }
236
+ }
237
+ ` })
238
+ ] });
239
+ }
240
+ function CompactSparkline({
241
+ values,
242
+ weekLabels,
243
+ color,
244
+ type,
245
+ formatValue,
246
+ height = 36
247
+ }) {
248
+ const theme = useTheme();
249
+ const isDark = theme.palette.type === "dark";
250
+ const [hoverIdx, setHoverIdx] = useState(null);
251
+ const VW = 400;
252
+ const mutedBorder = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.06)";
253
+ const tooltipBg = isDark ? "#1c1c20" : "#ffffff";
254
+ const tooltipBdr = isDark ? "#3f3f46" : "#e4e4e7";
255
+ const tooltipText = isDark ? "#ededef" : "#111114";
256
+ const font = "Inter,system-ui,sans-serif";
257
+ const handleMouseMove = (e) => {
258
+ const rect = e.currentTarget.getBoundingClientRect();
259
+ const relX = (e.clientX - rect.left) / rect.width;
260
+ setHoverIdx(Math.max(0, Math.min(values.length - 1, Math.round(relX * (values.length - 1)))));
261
+ };
262
+ if (type === "bar") {
263
+ const max2 = Math.max(...values, 1);
264
+ const gap = 4;
265
+ const barW = (VW - gap * (values.length - 1)) / values.length;
266
+ const hoverX = hoverIdx !== null ? hoverIdx * (barW + gap) + barW / 2 : null;
267
+ const tooltipX = hoverX !== null ? Math.min(hoverX - 34, VW - 72) : 0;
268
+ return /* @__PURE__ */ jsxs("div", { children: [
269
+ /* @__PURE__ */ jsxs(
270
+ "svg",
271
+ {
272
+ viewBox: `0 0 ${VW} ${height}`,
273
+ width: "100%",
274
+ style: { display: "block", overflow: "visible" },
275
+ onMouseMove: handleMouseMove,
276
+ onMouseLeave: () => setHoverIdx(null),
277
+ children: [
278
+ values.map((v, i) => {
279
+ const h = Math.max(2, v / max2 * (height - 4));
280
+ return /* @__PURE__ */ jsx(
281
+ "rect",
282
+ {
283
+ x: i * (barW + gap),
284
+ y: height - h,
285
+ width: barW,
286
+ height: h,
287
+ rx: 1.5,
288
+ fill: color,
289
+ opacity: i === hoverIdx ? 1 : 0.3 + i / values.length * 0.55
290
+ },
291
+ i
292
+ );
293
+ }),
294
+ /* @__PURE__ */ jsx("line", { x1: 0, y1: height - 0.5, x2: VW, y2: height - 0.5, stroke: mutedBorder, strokeWidth: 1 }),
295
+ hoverIdx !== null && hoverX !== null && /* @__PURE__ */ jsxs(Fragment, { children: [
296
+ /* @__PURE__ */ jsx("rect", { x: tooltipX, y: 2, width: 72, height: 22, rx: 4, fill: tooltipBg, stroke: tooltipBdr, strokeWidth: 1 }),
297
+ /* @__PURE__ */ jsxs("text", { x: tooltipX + 36, y: 17, textAnchor: "middle", fill: tooltipText, fontSize: 9, fontWeight: 600, fontFamily: font, children: [
298
+ weekLabels[hoverIdx],
299
+ ": ",
300
+ formatValue(values[hoverIdx])
301
+ ] })
302
+ ] })
303
+ ]
304
+ }
305
+ ),
306
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", marginTop: 3 }, children: [
307
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: isDark ? "#3f3f46" : "#a1a1aa" }, children: weekLabels[0] }),
308
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: isDark ? "#3f3f46" : "#a1a1aa" }, children: weekLabels[weekLabels.length - 1] })
309
+ ] })
310
+ ] });
311
+ }
312
+ if (values.length < 2) return null;
313
+ const max = Math.max(...values, 0.01);
314
+ const min = Math.min(...values);
315
+ const range = max - min || max || 1;
316
+ const pad = 3;
317
+ const pts = values.map((v, i) => [
318
+ i / (values.length - 1) * (VW - pad * 2) + pad,
319
+ height - pad * 2 - (v - min) / range * (height - pad * 2 - 2) + pad
320
+ ]);
321
+ const d = pts.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`).join(" ");
322
+ const area = `${d} L${pts[pts.length - 1][0]},${height} L${pts[0][0]},${height} Z`;
323
+ const hoverPt = hoverIdx !== null ? pts[hoverIdx] : null;
324
+ const tooltipX2 = hoverPt ? Math.min(hoverPt[0] - 34, VW - 72) : 0;
325
+ return /* @__PURE__ */ jsxs("div", { children: [
326
+ /* @__PURE__ */ jsxs(
327
+ "svg",
328
+ {
329
+ viewBox: `0 0 ${VW} ${height}`,
330
+ width: "100%",
331
+ style: { display: "block", overflow: "visible" },
332
+ onMouseMove: handleMouseMove,
333
+ onMouseLeave: () => setHoverIdx(null),
334
+ children: [
335
+ /* @__PURE__ */ jsx("path", { d: area, fill: color, opacity: 0.07 }),
336
+ /* @__PURE__ */ jsx("path", { d, fill: "none", stroke: color, strokeWidth: 1.5, strokeLinejoin: "round", strokeLinecap: "round" }),
337
+ pts.map(([x, y], i) => /* @__PURE__ */ jsx("circle", { cx: x, cy: y, r: i === hoverIdx ? 3.5 : i === pts.length - 1 ? 2.5 : 0, fill: color }, i)),
338
+ /* @__PURE__ */ jsx("line", { x1: 0, y1: height - 0.5, x2: VW, y2: height - 0.5, stroke: mutedBorder, strokeWidth: 1 }),
339
+ hoverPt && hoverIdx !== null && /* @__PURE__ */ jsxs(Fragment, { children: [
340
+ /* @__PURE__ */ jsx("line", { x1: hoverPt[0], y1: 0, x2: hoverPt[0], y2: height, stroke: color, strokeWidth: 1, strokeDasharray: "2 2", opacity: 0.4 }),
341
+ /* @__PURE__ */ jsx("rect", { x: tooltipX2, y: 2, width: 72, height: 22, rx: 4, fill: tooltipBg, stroke: tooltipBdr, strokeWidth: 1 }),
342
+ /* @__PURE__ */ jsxs("text", { x: tooltipX2 + 36, y: 17, textAnchor: "middle", fill: tooltipText, fontSize: 9, fontWeight: 600, fontFamily: font, children: [
343
+ weekLabels[hoverIdx],
344
+ ": ",
345
+ formatValue(values[hoverIdx])
346
+ ] })
347
+ ] })
348
+ ]
349
+ }
350
+ ),
351
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", marginTop: 3 }, children: [
352
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: isDark ? "#3f3f46" : "#a1a1aa" }, children: weekLabels[0] }),
353
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: isDark ? "#3f3f46" : "#a1a1aa" }, children: weekLabels[weekLabels.length - 1] })
354
+ ] })
355
+ ] });
356
+ }
357
+ function DetailedChart({
358
+ values,
359
+ weekLabels,
360
+ bucketMidMs,
361
+ color,
362
+ type,
363
+ formatValue,
364
+ prs,
365
+ days,
366
+ hoveredPrNumber,
367
+ onPrHover
368
+ }) {
369
+ const theme = useTheme();
370
+ const isDark = theme.palette.type === "dark";
371
+ const [hoverIdx, setHoverIdx] = useState(null);
372
+ const [tooltip, setTooltip] = useState(null);
373
+ const containerRef = useRef(null);
374
+ const W = 800, H = 260;
375
+ const PAD = { top: 16, right: 16, bottom: 36, left: 56 };
376
+ const cW = W - PAD.left - PAD.right;
377
+ const cH = H - PAD.top - PAD.bottom;
378
+ const dataMax = Math.max(...values, 0.01);
379
+ const dataMin = type === "line" ? Math.min(...values, 0) : 0;
380
+ const dataRange = dataMax - dataMin || dataMax || 1;
381
+ const yScale = (v) => cH - (v - dataMin) / dataRange * cH;
382
+ const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1e3;
383
+ const rangeMs = days * 24 * 60 * 60 * 1e3;
384
+ const xTime = (ms) => Math.max(0, Math.min(cW, (ms - cutoffMs) / rangeMs * cW));
385
+ const xIdx = (i) => bucketMidMs.length === values.length ? xTime(bucketMidMs[i]) : values.length > 1 ? i / (values.length - 1) * cW : cW / 2;
386
+ const pts = values.map((v, i) => [PAD.left + xIdx(i), PAD.top + yScale(v)]);
387
+ const linePath = pts.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`).join(" ");
388
+ const areaPath = pts.length > 1 ? `${linePath} L${pts[pts.length - 1][0]},${PAD.top + cH} L${pts[0][0]},${PAD.top + cH} Z` : "";
389
+ const gap = 6;
390
+ const barW = values.length > 1 ? (cW - gap * (values.length - 1)) / values.length : cW;
391
+ const yTicks = [0, 0.25, 0.5, 0.75, 1].map((t) => dataMin + t * dataRange);
392
+ const gridColor = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)";
393
+ const axisColor = isDark ? "#3f3f46" : "#d4d4d8";
394
+ const labelColor = isDark ? "#52525b" : "#a1a1aa";
395
+ const font = "Inter,system-ui,sans-serif";
396
+ const gradId = `dg_${color.replace("#", "")}`;
397
+ const nearestBucket = (ms) => {
398
+ if (bucketMidMs.length === 0) return 0;
399
+ let best = 0, bestDist = Infinity;
400
+ bucketMidMs.forEach((mid, i) => {
401
+ const d = Math.abs(ms - mid);
402
+ if (d < bestDist) {
403
+ bestDist = d;
404
+ best = i;
405
+ }
406
+ });
407
+ return best;
408
+ };
409
+ const handleSvgMouseMove = (e) => {
410
+ const rect = e.currentTarget.getBoundingClientRect();
411
+ const relX = (e.clientX - rect.left - PAD.left * (rect.width / W)) / (rect.width * cW / W);
412
+ const idx = Math.max(0, Math.min(values.length - 1, Math.round(relX * (values.length - 1))));
413
+ setHoverIdx(idx);
414
+ const cRect = containerRef.current?.getBoundingClientRect();
415
+ const x = cRect ? e.clientX - cRect.left : 0;
416
+ const y = cRect ? e.clientY - cRect.top : 0;
417
+ setTooltip({ x, y, primary: formatValue(values[idx]), secondary: weekLabels[idx] });
418
+ };
419
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, style: { position: "relative" }, children: [
420
+ /* @__PURE__ */ jsxs(
421
+ "svg",
422
+ {
423
+ viewBox: `0 0 ${W} ${H}`,
424
+ width: "100%",
425
+ style: { display: "block" },
426
+ onMouseMove: handleSvgMouseMove,
427
+ onMouseLeave: () => {
428
+ setHoverIdx(null);
429
+ setTooltip(null);
430
+ },
431
+ children: [
432
+ /* @__PURE__ */ jsxs("defs", { children: [
433
+ /* @__PURE__ */ jsxs("linearGradient", { id: gradId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
434
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: 0.28 }),
435
+ /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: color, stopOpacity: 0 })
436
+ ] }),
437
+ /* @__PURE__ */ jsx("clipPath", { id: `prClip_${gradId}`, children: /* @__PURE__ */ jsx("rect", { x: PAD.left, y: PAD.top - 8, width: cW, height: cH + 16 }) })
438
+ ] }),
439
+ yTicks.map((v, i) => {
440
+ const y = PAD.top + yScale(v);
441
+ return /* @__PURE__ */ jsxs("g", { children: [
442
+ i > 0 && /* @__PURE__ */ jsx("line", { x1: PAD.left, y1: y, x2: PAD.left + cW, y2: y, stroke: gridColor, strokeWidth: 1, strokeDasharray: "3 4" }),
443
+ /* @__PURE__ */ jsx("text", { x: PAD.left - 6, y: y + 4, textAnchor: "end", fill: labelColor, fontSize: 10, fontFamily: font, children: formatValue(v) })
444
+ ] }, i);
445
+ }),
446
+ /* @__PURE__ */ jsx("line", { x1: PAD.left, y1: PAD.top + cH, x2: PAD.left + cW, y2: PAD.top + cH, stroke: axisColor, strokeWidth: 1 }),
447
+ type === "bar" ? values.map((v, i) => {
448
+ const bH = Math.max(2, (v - dataMin) / dataRange * cH);
449
+ const x = PAD.left + i * (barW + gap);
450
+ return /* @__PURE__ */ jsx(
451
+ "rect",
452
+ {
453
+ x,
454
+ y: PAD.top + cH - bH,
455
+ width: barW,
456
+ height: bH,
457
+ rx: 3,
458
+ fill: color,
459
+ opacity: i === hoverIdx ? 0.95 : 0.4 + i / Math.max(values.length - 1, 1) * 0.45
460
+ },
461
+ i
462
+ );
463
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
464
+ areaPath && /* @__PURE__ */ jsx("path", { d: areaPath, fill: `url(#${gradId})` }),
465
+ /* @__PURE__ */ jsx("path", { d: linePath, fill: "none", stroke: color, strokeWidth: 2.5, strokeLinejoin: "round", strokeLinecap: "round" }),
466
+ pts.map(([x, y], i) => /* @__PURE__ */ jsx(
467
+ "circle",
468
+ {
469
+ cx: x,
470
+ cy: y,
471
+ r: i === hoverIdx ? 5.5 : 4,
472
+ fill: isDark ? "#0f0f12" : "#ffffff",
473
+ stroke: color,
474
+ strokeWidth: i === hoverIdx ? 2.5 : 1.5
475
+ },
476
+ i
477
+ ))
478
+ ] }),
479
+ prs && type === "line" && /* @__PURE__ */ jsx("g", { clipPath: `url(#prClip_${gradId})`, children: prs.map((pr, dotIdx) => {
480
+ const mergedMs = new Date(pr.mergedAt).getTime();
481
+ const bIdx = nearestBucket(mergedMs);
482
+ const jitter = ((pr.number * 17 + dotIdx * 11) % 13 - 6) * 5;
483
+ const px = PAD.left + xIdx(bIdx) + jitter;
484
+ const rawPy = PAD.top + yScale(pr.durationHours);
485
+ const py = Math.max(PAD.top + 4, Math.min(PAD.top + cH - 4, rawPy));
486
+ const isHov = pr.number === hoveredPrNumber;
487
+ const trendPt = pts[bIdx];
488
+ return /* @__PURE__ */ jsxs(
489
+ "g",
490
+ {
491
+ style: { cursor: "pointer" },
492
+ onMouseEnter: (e) => {
493
+ e.stopPropagation();
494
+ const cRect = containerRef.current?.getBoundingClientRect();
495
+ const x = cRect ? e.clientX - cRect.left : 0;
496
+ const y = cRect ? e.clientY - cRect.top : 0;
497
+ setTooltip({
498
+ x,
499
+ y,
500
+ primary: `#${pr.number}: ${pr.title}`,
501
+ secondary: `${formatValue(pr.durationHours)} \xB7 ${new Date(pr.mergedAt).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`
502
+ });
503
+ onPrHover?.(pr.number);
504
+ },
505
+ onMouseLeave: () => {
506
+ setTooltip(null);
507
+ onPrHover?.(null);
508
+ },
509
+ children: [
510
+ trendPt && /* @__PURE__ */ jsx(
511
+ "line",
512
+ {
513
+ x1: px,
514
+ y1: py,
515
+ x2: trendPt[0],
516
+ y2: trendPt[1],
517
+ stroke: color,
518
+ strokeWidth: isHov ? 1 : 0.5,
519
+ strokeDasharray: "2 3",
520
+ opacity: isHov ? 0.5 : 0.2
521
+ }
522
+ ),
523
+ /* @__PURE__ */ jsx("circle", { cx: px, cy: py, r: isHov ? 11 : 7, fill: color, opacity: isHov ? 0.2 : 0.1 }),
524
+ /* @__PURE__ */ jsx(
525
+ "circle",
526
+ {
527
+ cx: px,
528
+ cy: py,
529
+ r: isHov ? 5.5 : 3.5,
530
+ fill: isHov ? color : isDark ? "#1a1a20" : "#fff",
531
+ stroke: color,
532
+ strokeWidth: isHov ? 0 : 1.5,
533
+ opacity: isHov ? 1 : 0.8
534
+ }
535
+ )
536
+ ]
537
+ },
538
+ pr.number
539
+ );
540
+ }) }),
541
+ hoverIdx !== null && type === "line" && pts[hoverIdx] && /* @__PURE__ */ jsxs(Fragment, { children: [
542
+ /* @__PURE__ */ jsx(
543
+ "line",
544
+ {
545
+ x1: pts[hoverIdx][0],
546
+ y1: PAD.top,
547
+ x2: pts[hoverIdx][0],
548
+ y2: PAD.top + cH,
549
+ stroke: color,
550
+ strokeWidth: 1,
551
+ strokeDasharray: "3 3",
552
+ opacity: 0.3
553
+ }
554
+ ),
555
+ /* @__PURE__ */ jsx(
556
+ "line",
557
+ {
558
+ x1: PAD.left,
559
+ y1: pts[hoverIdx][1],
560
+ x2: PAD.left + cW,
561
+ y2: pts[hoverIdx][1],
562
+ stroke: color,
563
+ strokeWidth: 1,
564
+ strokeDasharray: "3 3",
565
+ opacity: 0.2
566
+ }
567
+ )
568
+ ] }),
569
+ weekLabels.map((lbl, i) => {
570
+ const total = weekLabels.length;
571
+ const step = Math.max(1, Math.floor(total / 5));
572
+ if (i !== 0 && i !== total - 1 && i % step !== 0) return null;
573
+ const x = type === "bar" ? PAD.left + i * (barW + gap) + barW / 2 : PAD.left + xIdx(i);
574
+ return /* @__PURE__ */ jsx("text", { x, y: H - 8, textAnchor: "middle", fill: labelColor, fontSize: 10, fontFamily: font, children: lbl }, i);
575
+ })
576
+ ]
577
+ }
578
+ ),
579
+ tooltip && (() => {
580
+ const TW = 220;
581
+ const cW2 = containerRef.current?.offsetWidth ?? 800;
582
+ const nearRight = tooltip.x + TW + 12 > cW2;
583
+ return /* @__PURE__ */ jsxs("div", { style: {
584
+ position: "absolute",
585
+ left: nearRight ? tooltip.x - TW - 8 : tooltip.x + 8,
586
+ top: tooltip.y - 36,
587
+ zIndex: 9999,
588
+ pointerEvents: "none",
589
+ background: isDark ? "#18181b" : "#ffffff",
590
+ border: `1px solid ${isDark ? "#3f3f46" : "#e4e4e7"}`,
591
+ borderRadius: 7,
592
+ padding: "7px 10px",
593
+ boxShadow: isDark ? "0 6px 20px rgba(0,0,0,0.55)" : "0 6px 20px rgba(0,0,0,0.10)",
594
+ width: TW
595
+ }, children: [
596
+ /* @__PURE__ */ jsx("div", { style: {
597
+ fontSize: 12,
598
+ fontWeight: 600,
599
+ color: isDark ? "#ededef" : "#111114",
600
+ fontFamily: font,
601
+ lineHeight: 1.4
602
+ }, children: tooltip.primary }),
603
+ tooltip.secondary && /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: isDark ? "#71717a" : "#a1a1aa", fontFamily: font, marginTop: 2 }, children: tooltip.secondary })
604
+ ] });
605
+ })()
606
+ ] });
607
+ }
608
+ function DetailedOverlay({
609
+ data,
610
+ days,
611
+ onClose
612
+ }) {
613
+ const theme = useTheme();
614
+ const isDark = theme.palette.type === "dark";
615
+ const [hoveredPrNumber, setHoveredPrNumber] = useState(null);
616
+ const prRowRefs = useRef(/* @__PURE__ */ new Map());
617
+ useEffect(() => {
618
+ const onKey = (e) => {
619
+ if (e.key === "Escape") onClose();
620
+ };
621
+ window.addEventListener("keydown", onKey);
622
+ return () => window.removeEventListener("keydown", onKey);
623
+ }, [onClose]);
624
+ useEffect(() => {
625
+ if (hoveredPrNumber !== null) {
626
+ const el = prRowRefs.current.get(hoveredPrNumber);
627
+ if (el) el.scrollIntoView({ behavior: "smooth", block: "nearest" });
628
+ }
629
+ }, [hoveredPrNumber]);
630
+ const { title, metric, description, lowerIsBetter, sparklineValues, sparklineType, sparklineColor, weekLabels, bucketMidMs, prs } = data;
631
+ const isDeployFreq = sparklineType === "bar";
632
+ const displayValue = metric.unit === "hours" ? formatDuration(metric.value) : String(metric.value);
633
+ const displayUnit = metric.unit === "hours" ? "" : metric.unit;
634
+ const formatVal = (v) => metric.unit === "hours" ? formatDuration(v) : `${Math.round(v * 10) / 10}`;
635
+ const borderColor = isDark ? "#27272a" : "#e4e4e7";
636
+ const mutedColor = isDark ? "#52525b" : "#a1a1aa";
637
+ const targetLabel = lowerIsBetter ? `Target: < ${metric.unit === "hours" ? formatDuration(metric.target) : metric.target} ${displayUnit}` : `Target: \u2265 ${metric.target} ${displayUnit}`;
638
+ const prsByDate = isDeployFreq && prs ? prs.reduce((acc, pr) => {
639
+ const date = new Date(pr.mergedAt).toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
640
+ if (!acc[date]) acc[date] = [];
641
+ acc[date].push(pr);
642
+ return acc;
643
+ }, {}) : {};
644
+ const sortedDates = Object.keys(prsByDate).sort(
645
+ (a, b) => new Date(prsByDate[b][0].mergedAt).getTime() - new Date(prsByDate[a][0].mergedAt).getTime()
646
+ );
647
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
648
+ /* @__PURE__ */ jsx("div", { onClick: onClose, style: {
649
+ position: "fixed",
650
+ inset: 0,
651
+ zIndex: 499,
652
+ background: isDark ? "rgba(0,0,0,0.6)" : "rgba(0,0,0,0.3)",
653
+ backdropFilter: "blur(4px)"
654
+ } }),
655
+ /* @__PURE__ */ jsxs("div", { style: {
656
+ position: "fixed",
657
+ top: "50%",
658
+ left: "50%",
659
+ transform: "translateX(-50%) translateY(-50%)",
660
+ width: "min(92vw, 960px)",
661
+ maxHeight: "calc(100vh - 80px)",
662
+ zIndex: 500,
663
+ background: isDark ? "#09090c" : "#fafafa",
664
+ border: `1px solid ${borderColor}`,
665
+ borderRadius: 14,
666
+ display: "flex",
667
+ flexDirection: "column",
668
+ overflow: "hidden",
669
+ animation: "detailExpandIn 0.2s cubic-bezier(0.34,1.2,0.64,1)",
670
+ boxShadow: isDark ? "0 24px 64px rgba(0,0,0,0.7)" : "0 24px 64px rgba(0,0,0,0.18)"
671
+ }, children: [
672
+ /* @__PURE__ */ jsxs("div", { style: {
673
+ display: "flex",
674
+ justifyContent: "space-between",
675
+ alignItems: "flex-start",
676
+ padding: "20px 24px 16px",
677
+ borderBottom: `1px solid ${borderColor}`,
678
+ flexShrink: 0
679
+ }, children: [
680
+ /* @__PURE__ */ jsxs("div", { children: [
681
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 10, fontWeight: 700, color: mutedColor, textTransform: "uppercase", letterSpacing: "0.12em", marginBottom: 8 }, children: [
682
+ title,
683
+ " \xB7 ",
684
+ weekLabels[0],
685
+ " \u2013 ",
686
+ weekLabels[weekLabels.length - 1]
687
+ ] }),
688
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 8, flexWrap: "wrap" }, children: [
689
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 52, fontWeight: 700, color: theme.palette.text.primary, lineHeight: 1, letterSpacing: "-0.02em" }, children: displayValue }),
690
+ displayUnit && /* @__PURE__ */ jsx("span", { style: { fontSize: 18, color: mutedColor, fontWeight: 500 }, children: displayUnit }),
691
+ /* @__PURE__ */ jsx(RatingBadge, { rating: metric.rating })
692
+ ] }),
693
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 12, color: mutedColor, marginTop: 6 }, children: [
694
+ description,
695
+ " \xB7 ",
696
+ /* @__PURE__ */ jsx("span", { style: { fontStyle: "italic" }, children: targetLabel })
697
+ ] })
698
+ ] }),
699
+ /* @__PURE__ */ jsx(
700
+ "button",
701
+ {
702
+ type: "button",
703
+ onClick: onClose,
704
+ style: {
705
+ all: "unset",
706
+ cursor: "pointer",
707
+ width: 30,
708
+ height: 30,
709
+ display: "flex",
710
+ alignItems: "center",
711
+ justifyContent: "center",
712
+ borderRadius: "50%",
713
+ background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.06)",
714
+ color: mutedColor,
715
+ fontSize: 18,
716
+ fontWeight: 300,
717
+ transition: "all 0.12s",
718
+ flexShrink: 0
719
+ },
720
+ onMouseEnter: (e) => {
721
+ e.currentTarget.style.background = isDark ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.1)";
722
+ },
723
+ onMouseLeave: (e) => {
724
+ e.currentTarget.style.background = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.06)";
725
+ },
726
+ children: "\u2715"
727
+ }
728
+ )
729
+ ] }),
730
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1, overflow: "auto", padding: "20px 24px", display: "flex", flexDirection: "column", gap: 20 }, children: [
731
+ weekLabels.length >= 2 ? /* @__PURE__ */ jsx(
732
+ DetailedChart,
733
+ {
734
+ values: sparklineValues,
735
+ weekLabels,
736
+ bucketMidMs,
737
+ color: sparklineColor,
738
+ type: sparklineType,
739
+ formatValue: formatVal,
740
+ prs: isDeployFreq ? void 0 : prs,
741
+ days,
742
+ hoveredPrNumber,
743
+ onPrHover: setHoveredPrNumber
744
+ }
745
+ ) : /* @__PURE__ */ jsx("div", { style: { color: mutedColor, fontSize: 13, textAlign: "center", padding: "48px 0" }, children: "Not enough data \u2014 select a wider date range." }),
746
+ isDeployFreq && prs && prs.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
747
+ /* @__PURE__ */ jsxs("div", { style: {
748
+ fontSize: 10,
749
+ fontWeight: 700,
750
+ color: mutedColor,
751
+ textTransform: "uppercase",
752
+ letterSpacing: "0.12em",
753
+ marginBottom: 12,
754
+ borderTop: `1px solid ${borderColor}`,
755
+ paddingTop: 12
756
+ }, children: [
757
+ "All deployments \xB7 ",
758
+ prs.length,
759
+ " PRs merged"
760
+ ] }),
761
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: sortedDates.map((date) => /* @__PURE__ */ jsxs("div", { children: [
762
+ /* @__PURE__ */ jsxs("div", { style: {
763
+ fontSize: 11,
764
+ fontWeight: 700,
765
+ color: sparklineColor,
766
+ marginBottom: 6,
767
+ display: "flex",
768
+ alignItems: "center",
769
+ gap: 8
770
+ }, children: [
771
+ /* @__PURE__ */ jsx("span", { children: date }),
772
+ /* @__PURE__ */ jsxs("span", { style: { color: mutedColor, fontWeight: 500 }, children: [
773
+ "\xB7 ",
774
+ prsByDate[date].length,
775
+ " PR",
776
+ prsByDate[date].length > 1 ? "s" : ""
777
+ ] })
778
+ ] }),
779
+ /* @__PURE__ */ jsx("div", { style: {
780
+ display: "flex",
781
+ flexDirection: "column",
782
+ gap: 4,
783
+ paddingLeft: 12,
784
+ borderLeft: `2px solid ${sparklineColor}22`
785
+ }, children: prsByDate[date].map((pr) => /* @__PURE__ */ jsxs("div", { style: {
786
+ display: "flex",
787
+ flexDirection: "column",
788
+ gap: 5,
789
+ padding: "7px 10px",
790
+ borderRadius: 7,
791
+ background: isDark ? "#0d0d10" : "#f7f7f9",
792
+ border: `1px solid ${borderColor}`,
793
+ transition: "background 0.12s"
794
+ }, children: [
795
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
796
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 10, fontWeight: 700, color: mutedColor, flexShrink: 0, minWidth: 36 }, children: [
797
+ "#",
798
+ pr.number
799
+ ] }),
800
+ /* @__PURE__ */ jsx("span", { style: {
801
+ fontSize: 12,
802
+ color: theme.palette.text.primary,
803
+ flex: 1,
804
+ whiteSpace: "nowrap",
805
+ overflow: "hidden",
806
+ textOverflow: "ellipsis"
807
+ }, title: pr.title, children: pr.title }),
808
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 10, color: mutedColor, flexShrink: 0 }, children: [
809
+ formatDuration(pr.durationHours),
810
+ " lead"
811
+ ] }),
812
+ /* @__PURE__ */ jsx(
813
+ "a",
814
+ {
815
+ href: pr.url,
816
+ target: "_blank",
817
+ rel: "noopener noreferrer",
818
+ style: { color: mutedColor, flexShrink: 0 },
819
+ children: /* @__PURE__ */ jsx(ExternalLink, { size: 11 })
820
+ }
821
+ )
822
+ ] }),
823
+ pr.author && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 5 }, children: [
824
+ pr.authorAvatar ? /* @__PURE__ */ jsx(
825
+ "img",
826
+ {
827
+ src: pr.authorAvatar,
828
+ alt: pr.author,
829
+ style: {
830
+ width: 16,
831
+ height: 16,
832
+ borderRadius: "50%",
833
+ flexShrink: 0,
834
+ border: `1px solid ${isDark ? "#3f3f46" : "#e4e4e7"}`
835
+ }
836
+ }
837
+ ) : /* @__PURE__ */ jsx("div", { style: {
838
+ width: 16,
839
+ height: 16,
840
+ borderRadius: "50%",
841
+ flexShrink: 0,
842
+ background: isDark ? "#27272a" : "#e4e4e7",
843
+ display: "flex",
844
+ alignItems: "center",
845
+ justifyContent: "center",
846
+ fontSize: 8,
847
+ fontWeight: 700,
848
+ color: mutedColor
849
+ }, children: pr.author[0].toUpperCase() }),
850
+ /* @__PURE__ */ jsxs(
851
+ "a",
852
+ {
853
+ href: `https://github.com/${pr.author}`,
854
+ target: "_blank",
855
+ rel: "noopener noreferrer",
856
+ style: {
857
+ fontSize: 10,
858
+ fontWeight: 500,
859
+ color: mutedColor,
860
+ textDecoration: "none",
861
+ letterSpacing: "0.01em"
862
+ },
863
+ onMouseEnter: (e) => {
864
+ e.currentTarget.style.color = "#FA6400";
865
+ },
866
+ onMouseLeave: (e) => {
867
+ e.currentTarget.style.color = mutedColor;
868
+ },
869
+ children: [
870
+ "@",
871
+ pr.author
872
+ ]
873
+ }
874
+ )
875
+ ] })
876
+ ] }, pr.number)) })
877
+ ] }, date)) })
878
+ ] }),
879
+ !isDeployFreq && prs && prs.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
880
+ /* @__PURE__ */ jsxs("div", { style: {
881
+ fontSize: 10,
882
+ fontWeight: 700,
883
+ color: mutedColor,
884
+ textTransform: "uppercase",
885
+ letterSpacing: "0.12em",
886
+ marginBottom: 10,
887
+ borderTop: `1px solid ${borderColor}`,
888
+ paddingTop: 12
889
+ }, children: [
890
+ title.toLowerCase().includes("restore") ? "Slowest Hotfix PRs" : "Slowest PRs",
891
+ " ",
892
+ "\xB7 hover a row to highlight on chart"
893
+ ] }),
894
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: prs.map((pr, i) => {
895
+ const col = durationColor(i, prs.length);
896
+ const maxDur = prs[0].durationHours;
897
+ const barPct = maxDur > 0 ? pr.durationHours / maxDur * 100 : 0;
898
+ const isHov = pr.number === hoveredPrNumber;
899
+ return /* @__PURE__ */ jsxs(
900
+ "div",
901
+ {
902
+ ref: (el) => {
903
+ if (el) prRowRefs.current.set(pr.number, el);
904
+ else prRowRefs.current.delete(pr.number);
905
+ },
906
+ onMouseEnter: () => setHoveredPrNumber(pr.number),
907
+ onMouseLeave: () => setHoveredPrNumber(null),
908
+ style: {
909
+ padding: "8px 12px",
910
+ borderRadius: 8,
911
+ background: isHov ? isDark ? "#18181f" : "#f0f0ff" : isDark ? "#111114" : "#f4f4f6",
912
+ border: `1px solid ${isHov ? sparklineColor + "55" : borderColor}`,
913
+ display: "flex",
914
+ flexDirection: "column",
915
+ gap: 5,
916
+ transition: "background 0.12s, border-color 0.12s",
917
+ cursor: "default"
918
+ },
919
+ children: [
920
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, minWidth: 0 }, children: [
921
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 10, fontWeight: 700, color: col, flexShrink: 0 }, children: [
922
+ "#",
923
+ pr.number
924
+ ] }),
925
+ /* @__PURE__ */ jsx("span", { style: {
926
+ fontSize: 12,
927
+ color: theme.palette.text.primary,
928
+ flex: 1,
929
+ whiteSpace: "nowrap",
930
+ overflow: "hidden",
931
+ textOverflow: "ellipsis"
932
+ }, title: pr.title, children: pr.title }),
933
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: mutedColor, flexShrink: 0 }, children: new Date(pr.mergedAt).toLocaleDateString("en-US", { month: "short", day: "numeric" }) }),
934
+ /* @__PURE__ */ jsx(
935
+ "a",
936
+ {
937
+ href: pr.url,
938
+ target: "_blank",
939
+ rel: "noopener noreferrer",
940
+ style: { color: mutedColor, flexShrink: 0, transition: "color 0.12s" },
941
+ children: /* @__PURE__ */ jsx(ExternalLink, { size: 11 })
942
+ }
943
+ )
944
+ ] }),
945
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
946
+ /* @__PURE__ */ jsx(AnimatedBar, { widthPct: barPct, color: col }),
947
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 11, fontWeight: 700, color: col, minWidth: 52, textAlign: "right", flexShrink: 0 }, children: formatDuration(pr.durationHours) })
948
+ ] })
949
+ ]
950
+ },
951
+ pr.number
952
+ );
953
+ }) })
954
+ ] })
955
+ ] })
956
+ ] }),
957
+ /* @__PURE__ */ jsx("style", { children: `
958
+ @keyframes detailExpandIn {
959
+ from { opacity: 0; transform: translateX(-50%) translateY(-46%); }
960
+ to { opacity: 1; transform: translateX(-50%) translateY(-50%); }
961
+ }
962
+ ` })
963
+ ] });
964
+ }
965
+ function AnimatedBar({ widthPct, color }) {
966
+ const [width, setWidth] = useState(0);
967
+ const ref = useRef(null);
968
+ const theme = useTheme();
969
+ const isDark = theme.palette.type === "dark";
970
+ useEffect(() => {
971
+ const timer = setTimeout(() => setWidth(widthPct), 80);
972
+ return () => clearTimeout(timer);
973
+ }, [widthPct]);
974
+ return /* @__PURE__ */ jsx(
975
+ "div",
976
+ {
977
+ ref,
978
+ style: {
979
+ background: isDark ? "#1a1a1e" : "#f0f0f2",
980
+ borderRadius: 4,
981
+ height: 5,
982
+ overflow: "hidden",
983
+ flex: 1,
984
+ minWidth: 40
985
+ },
986
+ children: /* @__PURE__ */ jsx(
987
+ "div",
988
+ {
989
+ style: {
990
+ height: "100%",
991
+ width: `${width}%`,
992
+ background: color,
993
+ borderRadius: 4,
994
+ transition: "width 0.55s cubic-bezier(0.4, 0, 0.2, 1)",
995
+ boxShadow: `0 0 6px ${color}88`
996
+ }
997
+ }
998
+ )
999
+ }
1000
+ );
1001
+ }
1002
+ function MetricCard({
1003
+ title,
1004
+ metric,
1005
+ description,
1006
+ lowerIsBetter = false,
1007
+ sparklineData,
1008
+ sparklineType = "line",
1009
+ sparklineColor,
1010
+ weekLabels,
1011
+ onExpand
1012
+ }) {
1013
+ const theme = useTheme();
1014
+ const isDark = theme.palette.type === "dark";
1015
+ const [hovered, setHovered] = useState(false);
1016
+ const displayValue = metric.unit === "hours" ? formatDuration(metric.value) : metric.value;
1017
+ const displayUnit = metric.unit === "hours" ? "" : metric.unit;
1018
+ const targetValue = metric.unit === "hours" ? formatDuration(metric.target) : metric.target;
1019
+ const targetLabel = lowerIsBetter ? `Target: < ${targetValue} ${displayUnit}`.trim() : `Target: \u2265 ${targetValue} ${displayUnit}`.trim();
1020
+ const hasSparkline = sparklineData && sparklineData.length >= 2 && weekLabels && weekLabels.length >= 2;
1021
+ const delta = hasSparkline ? buildDeltaLabel(sparklineData, metric.unit, lowerIsBetter) : null;
1022
+ const formatVal = (v) => metric.unit === "hours" ? formatDuration(v) : `${Math.round(v * 10) / 10}`;
1023
+ return /* @__PURE__ */ jsxs(
1024
+ Card,
1025
+ {
1026
+ style: { cursor: onExpand ? "pointer" : void 0 },
1027
+ onClick: onExpand,
1028
+ onMouseEnter: () => setHovered(true),
1029
+ onMouseLeave: () => setHovered(false),
1030
+ children: [
1031
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 }, children: [
1032
+ /* @__PURE__ */ jsx(CardLabel, { children: title }),
1033
+ onExpand && /* @__PURE__ */ jsx("span", { style: {
1034
+ fontSize: 9,
1035
+ fontWeight: 600,
1036
+ color: isDark ? "#3f3f46" : "#d4d4d8",
1037
+ opacity: hovered ? 1 : 0,
1038
+ transition: "opacity 0.15s",
1039
+ letterSpacing: "0.05em",
1040
+ textTransform: "uppercase",
1041
+ flexShrink: 0
1042
+ }, children: "expand \u2197" })
1043
+ ] }),
1044
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4 }, children: [
1045
+ delta && /* @__PURE__ */ jsx("span", { title: delta.tooltip, style: {
1046
+ fontSize: 10,
1047
+ fontWeight: 600,
1048
+ cursor: "help",
1049
+ color: delta.good ? "#4ade80" : "#f87171",
1050
+ background: delta.good ? isDark ? "rgba(74,222,128,0.08)" : "rgba(74,222,128,0.12)" : isDark ? "rgba(248,113,113,0.08)" : "rgba(248,113,113,0.12)",
1051
+ padding: "2px 8px",
1052
+ borderRadius: 4,
1053
+ whiteSpace: "nowrap"
1054
+ }, children: delta.text }),
1055
+ /* @__PURE__ */ jsx("span", { style: {
1056
+ fontSize: 10,
1057
+ fontWeight: 600,
1058
+ color: isDark ? "#3f3f46" : "#d4d4d8",
1059
+ whiteSpace: "nowrap"
1060
+ }, children: lowerIsBetter ? "\u2193 lower = better" : "\u2191 higher = better" })
1061
+ ] }),
1062
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 6 }, children: [
1063
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 36, fontWeight: 700, color: isDark ? "#f4f4f5" : "#111114", lineHeight: 1 }, children: displayValue }),
1064
+ displayUnit && /* @__PURE__ */ jsx("span", { style: { fontSize: 14, color: isDark ? "#71717a" : "#71717a", fontWeight: 500 }, children: displayUnit })
1065
+ ] }),
1066
+ /* @__PURE__ */ jsx(RatingBadge, { rating: metric.rating }),
1067
+ hasSparkline && /* @__PURE__ */ jsx("div", { style: { marginTop: 2 }, children: /* @__PURE__ */ jsx(
1068
+ CompactSparkline,
1069
+ {
1070
+ values: sparklineData,
1071
+ weekLabels,
1072
+ color: sparklineColor ?? "#FA6400",
1073
+ type: sparklineType,
1074
+ formatValue: formatVal
1075
+ }
1076
+ ) }),
1077
+ /* @__PURE__ */ jsx(CardDescription, { children: description }),
1078
+ /* @__PURE__ */ jsx(CardFooter, { children: /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, color: isDark ? "#a1a1aa" : "#52525b" }, children: targetLabel }) })
1079
+ ]
1080
+ }
1081
+ );
1082
+ }
1083
+ function NaCard({ title, description }) {
1084
+ return /* @__PURE__ */ jsxs(Card, { children: [
1085
+ /* @__PURE__ */ jsx(CardLabel, { children: title }),
1086
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "baseline", gap: 6 }, children: /* @__PURE__ */ jsx("span", { style: { fontSize: 36, fontWeight: 700, color: "#52525b", lineHeight: 1 }, children: "N/A" }) }),
1087
+ /* @__PURE__ */ jsx(CardDescription, { muted: true, children: description })
1088
+ ] });
1089
+ }
1090
+ function FilterLabel({ children }) {
1091
+ const theme = useTheme();
1092
+ return /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 600, color: theme.palette.text.primary, marginBottom: 4, textTransform: "uppercase", letterSpacing: "0.06em" }, children });
1093
+ }
1094
+ function DoraMetricsContent() {
1095
+ const { entity } = useEntity();
1096
+ const doraApi = useApi(doraMetricsApiRef);
1097
+ const muiTheme = useTheme();
1098
+ const textPrimary = muiTheme.palette.text.primary;
1099
+ const textSecondary = muiTheme.palette.text.secondary;
1100
+ const projectSlug = entity.metadata.annotations?.[ANNOTATION_PROJECT_SLUG] ?? "";
1101
+ const environments = doraApi.getEnvironments();
1102
+ const defaultEnv = environments[0];
1103
+ const defaultDays = doraApi.getDefaultDays();
1104
+ const [selectedBranch, setSelectedBranch] = useState(defaultEnv?.branch ?? "");
1105
+ const [selectedDays, setSelectedDays] = useState(String(defaultDays));
1106
+ const selectedEnv = environments.find((e) => e.branch === selectedBranch) ?? defaultEnv;
1107
+ const days = parseInt(selectedDays, 10);
1108
+ const [metrics, setMetrics] = useState(null);
1109
+ const [loading, setLoading] = useState(true);
1110
+ const [error, setError] = useState(null);
1111
+ const [history, setHistory] = useState(null);
1112
+ const [expanded, setExpanded] = useState(null);
1113
+ useEffect(() => {
1114
+ if (!projectSlug) {
1115
+ setError(new Error(`Entity is missing the "${ANNOTATION_PROJECT_SLUG}" annotation.`));
1116
+ setLoading(false);
1117
+ return;
1118
+ }
1119
+ if (!selectedEnv) {
1120
+ setError(new Error("No environments configured for DORA metrics."));
1121
+ setLoading(false);
1122
+ return;
1123
+ }
1124
+ let cancelled = false;
1125
+ setLoading(true);
1126
+ setError(null);
1127
+ setHistory(null);
1128
+ doraApi.getMetrics(projectSlug, selectedEnv.branch, selectedEnv.isProduction, selectedEnv.label, days).then((data) => {
1129
+ if (!cancelled) {
1130
+ setMetrics(data);
1131
+ setLoading(false);
1132
+ }
1133
+ }).catch((err) => {
1134
+ if (!cancelled) {
1135
+ setError(err);
1136
+ setLoading(false);
1137
+ }
1138
+ });
1139
+ doraApi.getHistory(projectSlug, selectedEnv.branch, selectedEnv.isProduction, selectedEnv.label, days).then((data) => {
1140
+ if (!cancelled) setHistory(data);
1141
+ }).catch(() => {
1142
+ });
1143
+ return () => {
1144
+ cancelled = true;
1145
+ };
1146
+ }, [doraApi, projectSlug, selectedEnv?.branch, days]);
1147
+ if (loading) return /* @__PURE__ */ jsx(DojoLoadingOverlay, {});
1148
+ if (error) {
1149
+ return /* @__PURE__ */ jsx(InfoCard, { title: "DORA Metrics", children: /* @__PURE__ */ jsxs("div", { style: { padding: 24, color: "#f87171" }, children: [
1150
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 600 }, children: "Error: " }),
1151
+ error.message
1152
+ ] }) });
1153
+ }
1154
+ if (!metrics) return null;
1155
+ return /* @__PURE__ */ jsx(InfoCard, { title: " ", children: /* @__PURE__ */ jsxs("div", { style: { padding: "4px 0", position: "relative" }, children: [
1156
+ expanded && /* @__PURE__ */ jsx(DetailedOverlay, { data: expanded, days, onClose: () => setExpanded(null) }),
1157
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: 16 }, children: [
1158
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 22, fontWeight: 700, color: textPrimary, letterSpacing: "-0.02em" }, children: "DORA Metrics" }),
1159
+ selectedEnv && /* @__PURE__ */ jsxs("div", { style: { fontSize: 12, color: textSecondary, marginTop: 5 }, children: [
1160
+ "Showing metrics for branch",
1161
+ " ",
1162
+ /* @__PURE__ */ jsx("strong", { style: { color: textPrimary, fontFamily: "monospace" }, children: selectedEnv.branch })
1163
+ ] })
1164
+ ] }),
1165
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "flex-end", gap: 16, flexWrap: "wrap", marginBottom: 20 }, children: [
1166
+ environments.length > 1 && /* @__PURE__ */ jsxs("div", { children: [
1167
+ /* @__PURE__ */ jsx(FilterLabel, { children: "Branch" }),
1168
+ /* @__PURE__ */ jsxs(Select, { value: selectedBranch, onValueChange: setSelectedBranch, children: [
1169
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select branch" }) }),
1170
+ /* @__PURE__ */ jsx(SelectContent, { children: environments.map((env) => /* @__PURE__ */ jsxs(SelectItem, { value: env.branch, children: [
1171
+ env.branch,
1172
+ " (",
1173
+ env.name,
1174
+ ")"
1175
+ ] }, env.branch)) })
1176
+ ] })
1177
+ ] }),
1178
+ /* @__PURE__ */ jsxs("div", { children: [
1179
+ /* @__PURE__ */ jsx(FilterLabel, { children: "Date Range" }),
1180
+ /* @__PURE__ */ jsxs(Select, { value: selectedDays, onValueChange: setSelectedDays, children: [
1181
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select date range" }) }),
1182
+ /* @__PURE__ */ jsx(SelectContent, { children: DATE_RANGE_OPTIONS.map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })
1183
+ ] })
1184
+ ] })
1185
+ ] }),
1186
+ (() => {
1187
+ const wkLabels = history?.map((h) => h.weekLabel) ?? [];
1188
+ const deployData = history?.map((h) => h.deploymentCount) ?? [];
1189
+ const leadData = history?.map((h) => h.leadTimeHours) ?? [];
1190
+ const cfrData = history?.map((h) => h.changeFailureRate ?? 0) ?? [];
1191
+ const mttrData = history?.map((h) => h.mttrHours ?? 0) ?? [];
1192
+ const bmMs = history?.map((h) => h.bucketMidMs) ?? [];
1193
+ const mkExpand = (title, metric, description, lowerIsBetter, sparklineValues, sparklineType, sparklineColor, prs) => () => setExpanded({
1194
+ title,
1195
+ metric,
1196
+ description,
1197
+ lowerIsBetter,
1198
+ sparklineValues,
1199
+ sparklineType,
1200
+ sparklineColor,
1201
+ weekLabels: wkLabels,
1202
+ bucketMidMs: bmMs,
1203
+ prs
1204
+ });
1205
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: 12 }, children: [
1206
+ /* @__PURE__ */ jsx(
1207
+ MetricCard,
1208
+ {
1209
+ title: "Deployment Frequency",
1210
+ metric: metrics.deploymentFrequency,
1211
+ description: "How often code is deployed to this branch",
1212
+ sparklineData: deployData,
1213
+ sparklineType: "bar",
1214
+ sparklineColor: "#FA6400",
1215
+ weekLabels: wkLabels,
1216
+ onExpand: mkExpand(
1217
+ "Deployment Frequency",
1218
+ metrics.deploymentFrequency,
1219
+ "How often code is deployed to this branch",
1220
+ false,
1221
+ deployData,
1222
+ "bar",
1223
+ "#FA6400",
1224
+ metrics.deploymentFrequency.slowestPRs
1225
+ )
1226
+ }
1227
+ ),
1228
+ /* @__PURE__ */ jsx(
1229
+ MetricCard,
1230
+ {
1231
+ title: "Lead Time for Changes",
1232
+ metric: metrics.leadTime,
1233
+ description: "Average time from PR creation to merge",
1234
+ lowerIsBetter: true,
1235
+ sparklineData: leadData,
1236
+ sparklineType: "line",
1237
+ sparklineColor: "#818cf8",
1238
+ weekLabels: wkLabels,
1239
+ onExpand: mkExpand(
1240
+ "Lead Time for Changes",
1241
+ metrics.leadTime,
1242
+ "Average time from PR creation to merge",
1243
+ true,
1244
+ leadData,
1245
+ "line",
1246
+ "#818cf8",
1247
+ metrics.leadTime.slowestPRs
1248
+ )
1249
+ }
1250
+ ),
1251
+ metrics.changeFailureRate !== null ? /* @__PURE__ */ jsx(
1252
+ MetricCard,
1253
+ {
1254
+ title: "Change Failure Rate",
1255
+ metric: metrics.changeFailureRate,
1256
+ description: "% of all merged PRs that were hotfixes",
1257
+ lowerIsBetter: true,
1258
+ sparklineData: cfrData,
1259
+ sparklineType: "line",
1260
+ sparklineColor: "#f87171",
1261
+ weekLabels: wkLabels,
1262
+ onExpand: mkExpand(
1263
+ "Change Failure Rate",
1264
+ metrics.changeFailureRate,
1265
+ "% of all merged PRs that were hotfixes",
1266
+ true,
1267
+ cfrData,
1268
+ "line",
1269
+ "#f87171"
1270
+ )
1271
+ }
1272
+ ) : /* @__PURE__ */ jsx(NaCard, { title: "Change Failure Rate", description: "Only tracked on production branches" }),
1273
+ metrics.mttr !== null ? /* @__PURE__ */ jsx(
1274
+ MetricCard,
1275
+ {
1276
+ title: "Mean Time to Restore",
1277
+ metric: metrics.mttr,
1278
+ description: "Avg time from hotfix PR open to merge",
1279
+ lowerIsBetter: true,
1280
+ sparklineData: mttrData,
1281
+ sparklineType: "line",
1282
+ sparklineColor: "#fbbf24",
1283
+ weekLabels: wkLabels,
1284
+ onExpand: mkExpand(
1285
+ "Mean Time to Restore",
1286
+ metrics.mttr,
1287
+ "Avg time from hotfix PR open to merge",
1288
+ true,
1289
+ mttrData,
1290
+ "line",
1291
+ "#fbbf24",
1292
+ metrics.mttr.slowestPRs
1293
+ )
1294
+ }
1295
+ ) : /* @__PURE__ */ jsx(NaCard, { title: "Mean Time to Restore", description: "Only tracked on production branches" }),
1296
+ metrics.numberOfHotfixes !== null ? /* @__PURE__ */ jsx(
1297
+ MetricCard,
1298
+ {
1299
+ title: "Hotfixes to Production",
1300
+ metric: metrics.numberOfHotfixes,
1301
+ description: `PRs labeled '${selectedEnv?.label ?? "hotfix"}' merged to production`,
1302
+ lowerIsBetter: true
1303
+ }
1304
+ ) : /* @__PURE__ */ jsx(NaCard, { title: "Hotfixes to Production", description: "Only tracked on production branches" })
1305
+ ] });
1306
+ })()
1307
+ ] }) });
1308
+ }
1309
+
1310
+ export { DoraMetricsContent };
1311
+ //# sourceMappingURL=DoraMetricsContent.esm.js.map