@c2l2c/backstage-plugin-dora-metrics 0.4.0 → 0.4.2

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