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