@fix-portal/ci-frontend 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/board.css +1079 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +889 -0
- package/dist/index.js.map +1 -0
- package/dist/tokens.css +55 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
// src/CiAdminContext.tsx
|
|
2
|
+
import { createContext, use } from "react";
|
|
3
|
+
var CiAdminContext = createContext(false);
|
|
4
|
+
CiAdminContext.displayName = "CiAdminContext";
|
|
5
|
+
var CiAdminProvider = CiAdminContext.Provider;
|
|
6
|
+
function useCiAdmin() {
|
|
7
|
+
return use(CiAdminContext);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/CiConfigContext.tsx
|
|
11
|
+
import { createContext as createContext2, use as use2 } from "react";
|
|
12
|
+
var DEFAULT_CI_API_BASE = "https://ci.fixportal.org";
|
|
13
|
+
var CiConfigContext = createContext2({ apiBase: DEFAULT_CI_API_BASE });
|
|
14
|
+
CiConfigContext.displayName = "CiConfigContext";
|
|
15
|
+
var CiConfigProvider = CiConfigContext.Provider;
|
|
16
|
+
function useCiConfig() {
|
|
17
|
+
return use2(CiConfigContext);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/pages/CiBoardContent.tsx
|
|
21
|
+
import { useState as useState5 } from "react";
|
|
22
|
+
|
|
23
|
+
// src/hooks/useDashboardSnapshot.ts
|
|
24
|
+
import { useQuery } from "@tanstack/react-query";
|
|
25
|
+
|
|
26
|
+
// src/api/getDashboardSnapshot.ts
|
|
27
|
+
async function getDashboardSnapshot(apiBase) {
|
|
28
|
+
const base = apiBase.replace(/\/$/, "");
|
|
29
|
+
const response = await fetch(`${base}/api/dashboard/snapshot`);
|
|
30
|
+
if (response.status === 204) return null;
|
|
31
|
+
if (!response.ok) throw new Error(`Dashboard snapshot failed: ${response.status}`);
|
|
32
|
+
return response.json();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/hooks/useDashboardSnapshot.ts
|
|
36
|
+
function useDashboardSnapshot() {
|
|
37
|
+
const { apiBase } = useCiConfig();
|
|
38
|
+
return useQuery({
|
|
39
|
+
queryKey: ["dashboard-snapshot", apiBase],
|
|
40
|
+
queryFn: () => getDashboardSnapshot(apiBase),
|
|
41
|
+
refetchInterval: 6e4,
|
|
42
|
+
// The 60s poll already drives freshness; without these, an incidental tab
|
|
43
|
+
// focus refetches and re-renders the whole board between ticks. Set per-query
|
|
44
|
+
// (not on the shared app QueryClient) so the host app is unaffected;
|
|
45
|
+
// structural sharing then lets the memoised boards skip a no-change tick.
|
|
46
|
+
staleTime: 3e4,
|
|
47
|
+
refetchOnWindowFocus: false
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/hooks/useCollapseState.ts
|
|
52
|
+
import { useCallback, useState } from "react";
|
|
53
|
+
var KEY = "ci-dashboard:collapsed";
|
|
54
|
+
function load() {
|
|
55
|
+
try {
|
|
56
|
+
const raw = localStorage.getItem(KEY);
|
|
57
|
+
return new Set(raw ? JSON.parse(raw) : []);
|
|
58
|
+
} catch {
|
|
59
|
+
return /* @__PURE__ */ new Set();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function save(set) {
|
|
63
|
+
try {
|
|
64
|
+
localStorage.setItem(KEY, JSON.stringify([...set]));
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function useCollapseState() {
|
|
69
|
+
const [collapsed, setCollapsed] = useState(load);
|
|
70
|
+
const mutate = useCallback((fn) => {
|
|
71
|
+
setCollapsed((prev) => {
|
|
72
|
+
const next = new Set(prev);
|
|
73
|
+
fn(next);
|
|
74
|
+
save(next);
|
|
75
|
+
return next;
|
|
76
|
+
});
|
|
77
|
+
}, []);
|
|
78
|
+
return {
|
|
79
|
+
isCollapsed: useCallback((name) => collapsed.has(name), [collapsed]),
|
|
80
|
+
allCollapsed: useCallback(
|
|
81
|
+
(names) => names.length > 0 && names.every((n) => collapsed.has(n)),
|
|
82
|
+
[collapsed]
|
|
83
|
+
),
|
|
84
|
+
toggle: useCallback((name) => mutate((s) => s.has(name) ? s.delete(name) : s.add(name)), [mutate]),
|
|
85
|
+
collapseAll: useCallback((names) => mutate((s) => names.forEach((n) => s.add(n))), [mutate]),
|
|
86
|
+
expandAll: useCallback(() => mutate((s) => s.clear()), [mutate])
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/hooks/useHideNoCi.ts
|
|
91
|
+
import { useCallback as useCallback2, useState as useState2 } from "react";
|
|
92
|
+
var KEY2 = "ci-dashboard:hide-no-ci";
|
|
93
|
+
function load2() {
|
|
94
|
+
try {
|
|
95
|
+
return localStorage.getItem(KEY2) === "true";
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function useHideNoCi() {
|
|
101
|
+
const [hidden, setHidden] = useState2(load2);
|
|
102
|
+
const toggle = useCallback2(() => {
|
|
103
|
+
setHidden((prev) => {
|
|
104
|
+
const next = !prev;
|
|
105
|
+
try {
|
|
106
|
+
localStorage.setItem(KEY2, String(next));
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
return next;
|
|
110
|
+
});
|
|
111
|
+
}, []);
|
|
112
|
+
return { hidden, toggle };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/lib/isNoCi.ts
|
|
116
|
+
function isNoCi(repository) {
|
|
117
|
+
return (repository.workflows?.length ?? 0) === 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/lib/computeSummary.ts
|
|
121
|
+
var ALWAYS_VISIBLE_KEYS = /* @__PURE__ */ new Set([
|
|
122
|
+
"repos",
|
|
123
|
+
"workflows",
|
|
124
|
+
"nloc",
|
|
125
|
+
// inventory
|
|
126
|
+
"open-prs",
|
|
127
|
+
// Review panel (carries next-in-queue / last-merged)
|
|
128
|
+
"running",
|
|
129
|
+
"failing",
|
|
130
|
+
"no-ci",
|
|
131
|
+
// core CI status
|
|
132
|
+
"deploys-running",
|
|
133
|
+
"deploys-failing",
|
|
134
|
+
// deploy lane (zero = nothing deploying / all clean)
|
|
135
|
+
"packages-failing"
|
|
136
|
+
// package lane
|
|
137
|
+
]);
|
|
138
|
+
function computeSummary(repos) {
|
|
139
|
+
const workflows = repos.flatMap((r) => r.workflows);
|
|
140
|
+
const deploys = repos.flatMap((r) => r.deploys ?? []);
|
|
141
|
+
const packages = repos.flatMap((r) => r.packages ?? []);
|
|
142
|
+
const openPrs = repos.flatMap((r) => r.pullRequests ?? []);
|
|
143
|
+
const nloc = repos.reduce((acc, r) => acc + (r.metrics?.nloc ?? 0), 0);
|
|
144
|
+
const all = [
|
|
145
|
+
{ key: "repos", count: repos.length },
|
|
146
|
+
{ key: "workflows", count: workflows.length },
|
|
147
|
+
{ key: "failing", count: workflows.filter((w) => w.state === "failure").length },
|
|
148
|
+
{ key: "running", count: workflows.filter((w) => w.state === "running").length },
|
|
149
|
+
{ key: "no-ci", count: repos.filter(isNoCi).length },
|
|
150
|
+
{ key: "open-prs", count: openPrs.length },
|
|
151
|
+
{ key: "nloc", count: nloc },
|
|
152
|
+
{ key: "deploys-failing", count: deploys.filter((d) => d.state === "failure").length },
|
|
153
|
+
{ key: "deploys-running", count: deploys.filter((d) => d.state === "running").length },
|
|
154
|
+
{ key: "packages-failing", count: packages.filter((p) => p.state === "failure").length }
|
|
155
|
+
];
|
|
156
|
+
return all.filter((c) => ALWAYS_VISIBLE_KEYS.has(c.key) || c.count > 0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/components/SummaryStrip.tsx
|
|
160
|
+
import { useEffect, useRef, useState as useState3 } from "react";
|
|
161
|
+
|
|
162
|
+
// src/lib/formatCompactNumber.ts
|
|
163
|
+
function formatCompactNumber(value) {
|
|
164
|
+
if (value < 1e3) return String(value);
|
|
165
|
+
if (value < 1e6) return `${(value / 1e3).toFixed(1)}k`;
|
|
166
|
+
return `${(value / 1e6).toFixed(1)}M`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/lib/relativeTime.ts
|
|
170
|
+
function formatRelativeTime(iso) {
|
|
171
|
+
const then = new Date(iso).getTime();
|
|
172
|
+
if (Number.isNaN(then)) return "";
|
|
173
|
+
const minutes = Math.round((Date.now() - then) / 6e4);
|
|
174
|
+
if (minutes < 1) return "just now";
|
|
175
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
176
|
+
const hours = Math.round(minutes / 60);
|
|
177
|
+
if (hours < 24) return `${hours}h ago`;
|
|
178
|
+
const days = Math.round(hours / 24);
|
|
179
|
+
return `${days}d ago`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/components/CiWeatherBar.tsx
|
|
183
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
184
|
+
var BLOCK_WORD = {
|
|
185
|
+
passing: "healthy",
|
|
186
|
+
failing: "failing",
|
|
187
|
+
noData: "no data"
|
|
188
|
+
};
|
|
189
|
+
function CiWeatherBar({ trend }) {
|
|
190
|
+
if (trend.length === 0) return null;
|
|
191
|
+
const failing = trend.filter((b) => b.state === "failing").length;
|
|
192
|
+
const healthy = trend.filter((b) => b.state === "passing").length;
|
|
193
|
+
const label = `CI health, last 24h: ${failing} failing, ${healthy} healthy`;
|
|
194
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
195
|
+
/* @__PURE__ */ jsx("div", { className: "ci-weather", role: "img", "aria-label": label, children: trend.map((b, i) => (
|
|
196
|
+
// Per-block hover reveals which hour a block is and its state — the
|
|
197
|
+
// data was previously exposed only to screen readers via aria-label.
|
|
198
|
+
/* @__PURE__ */ jsx(
|
|
199
|
+
"span",
|
|
200
|
+
{
|
|
201
|
+
className: "ci-weather__block",
|
|
202
|
+
"data-state": b.state,
|
|
203
|
+
title: `${formatRelativeTime(b.bucketStart)} \xB7 ${BLOCK_WORD[b.state]}`
|
|
204
|
+
},
|
|
205
|
+
i
|
|
206
|
+
)
|
|
207
|
+
)) }),
|
|
208
|
+
/* @__PURE__ */ jsxs("span", { className: "ci-weather__readout", "aria-hidden": "true", children: [
|
|
209
|
+
failing,
|
|
210
|
+
" failing \xB7 ",
|
|
211
|
+
healthy,
|
|
212
|
+
" healthy"
|
|
213
|
+
] })
|
|
214
|
+
] });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/components/SummaryStrip.tsx
|
|
218
|
+
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
219
|
+
var SUMMARY_LABELS = {
|
|
220
|
+
repos: "Repositories",
|
|
221
|
+
workflows: "Workflows",
|
|
222
|
+
failing: "Failing",
|
|
223
|
+
running: "Running",
|
|
224
|
+
"no-ci": "No CI",
|
|
225
|
+
"open-prs": "Open PRs",
|
|
226
|
+
nloc: "Lines of code",
|
|
227
|
+
"deploys-failing": "Deploys failing",
|
|
228
|
+
"deploys-running": "Deploys running",
|
|
229
|
+
"packages-failing": "Packages failing"
|
|
230
|
+
};
|
|
231
|
+
var PANELS = [
|
|
232
|
+
{ title: "Review", keys: ["open-prs"] },
|
|
233
|
+
{ title: "CI status", keys: ["running", "failing", "packages-failing", "deploys-running", "deploys-failing", "no-ci"] },
|
|
234
|
+
{ title: "Inventory", keys: ["repos", "workflows", "nloc"] }
|
|
235
|
+
];
|
|
236
|
+
var NEUTRAL_KEYS = /* @__PURE__ */ new Set(["repos", "workflows", "nloc"]);
|
|
237
|
+
var EMPTY_TREND = [];
|
|
238
|
+
function labelFor(key, count) {
|
|
239
|
+
if (key === "open-prs") return count === 1 ? "Open PR" : "Open PRs";
|
|
240
|
+
return SUMMARY_LABELS[key] ?? key.replaceAll("-", " ");
|
|
241
|
+
}
|
|
242
|
+
function formatCount(key, count) {
|
|
243
|
+
return key === "nloc" ? formatCompactNumber(count) : count;
|
|
244
|
+
}
|
|
245
|
+
function toneFor(key, count) {
|
|
246
|
+
if (count === 0 || NEUTRAL_KEYS.has(key)) return "ok";
|
|
247
|
+
if (key === "open-prs") return "review";
|
|
248
|
+
if (key === "failing" || key === "deploys-failing" || key === "packages-failing") return "fail";
|
|
249
|
+
if (key === "running" || key === "deploys-running") return "run";
|
|
250
|
+
if (key === "no-ci") return "no-ci";
|
|
251
|
+
return "alert";
|
|
252
|
+
}
|
|
253
|
+
function SummaryStrip({ summary, onOpenPrs, lastMerged, nextPr = null, ciTrend = EMPTY_TREND }) {
|
|
254
|
+
const byKey = new Map(summary.map((s) => [s.key, s.count]));
|
|
255
|
+
const [trendInfoOpen, setTrendInfoOpen] = useState3(false);
|
|
256
|
+
const trendLabelRef = useRef(null);
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (!trendInfoOpen) return;
|
|
259
|
+
function handleOutside(e) {
|
|
260
|
+
if (!trendLabelRef.current?.contains(e.target)) {
|
|
261
|
+
setTrendInfoOpen(false);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function handleEsc(e) {
|
|
265
|
+
if (e.key === "Escape") setTrendInfoOpen(false);
|
|
266
|
+
}
|
|
267
|
+
document.addEventListener("mousedown", handleOutside);
|
|
268
|
+
document.addEventListener("keydown", handleEsc);
|
|
269
|
+
return () => {
|
|
270
|
+
document.removeEventListener("mousedown", handleOutside);
|
|
271
|
+
document.removeEventListener("keydown", handleEsc);
|
|
272
|
+
};
|
|
273
|
+
}, [trendInfoOpen]);
|
|
274
|
+
return /* @__PURE__ */ jsx2("section", { className: "summary-panels", children: PANELS.map((panel) => {
|
|
275
|
+
const items = [];
|
|
276
|
+
for (const k of panel.keys) {
|
|
277
|
+
const count = byKey.get(k);
|
|
278
|
+
if (count !== void 0) items.push({ key: k, count });
|
|
279
|
+
}
|
|
280
|
+
if (items.length === 0) return null;
|
|
281
|
+
const isReview = panel.title === "Review";
|
|
282
|
+
const isCiStatus = panel.title === "CI status";
|
|
283
|
+
return /* @__PURE__ */ jsxs2("div", { className: `summary-panel${isReview ? " summary-panel--review" : ""}${isCiStatus ? " summary-panel--ci" : ""}`, children: [
|
|
284
|
+
/* @__PURE__ */ jsx2("span", { className: "summary-panel__title", children: panel.title }),
|
|
285
|
+
/* @__PURE__ */ jsx2("div", { className: "summary-panel__items", children: items.map((item) => {
|
|
286
|
+
const body = /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
287
|
+
/* @__PURE__ */ jsx2("span", { className: "summary__count", children: formatCount(item.key, item.count) }),
|
|
288
|
+
/* @__PURE__ */ jsx2("span", { className: "summary__label", children: labelFor(item.key, item.count) })
|
|
289
|
+
] });
|
|
290
|
+
if (item.key === "open-prs" && onOpenPrs) {
|
|
291
|
+
return /* @__PURE__ */ jsx2(
|
|
292
|
+
"button",
|
|
293
|
+
{
|
|
294
|
+
type: "button",
|
|
295
|
+
className: "summary__item summary__item--btn",
|
|
296
|
+
"data-key": item.key,
|
|
297
|
+
"data-tone": toneFor(item.key, item.count),
|
|
298
|
+
onClick: onOpenPrs,
|
|
299
|
+
disabled: item.count === 0,
|
|
300
|
+
children: body
|
|
301
|
+
},
|
|
302
|
+
item.key
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return /* @__PURE__ */ jsx2("div", { className: "summary__item", "data-key": item.key, "data-tone": toneFor(item.key, item.count), children: body }, item.key);
|
|
306
|
+
}) }),
|
|
307
|
+
isReview && nextPr && /* @__PURE__ */ jsxs2("div", { className: "summary-panel__next", children: [
|
|
308
|
+
/* @__PURE__ */ jsx2("span", { className: "summary-panel__q-lab", children: "next in queue" }),
|
|
309
|
+
/* @__PURE__ */ jsxs2("span", { className: "summary-panel__q-body", children: [
|
|
310
|
+
nextPr.repo,
|
|
311
|
+
" #",
|
|
312
|
+
nextPr.number
|
|
313
|
+
] }),
|
|
314
|
+
/* @__PURE__ */ jsx2("span", { className: "summary-panel__q-title", children: nextPr.title })
|
|
315
|
+
] }),
|
|
316
|
+
isReview && lastMerged && /* @__PURE__ */ jsxs2("a", { className: "summary-panel__merged", href: lastMerged.htmlUrl, target: "_blank", rel: "noopener noreferrer", children: [
|
|
317
|
+
/* @__PURE__ */ jsxs2("span", { className: "summary-panel__q-lab", children: [
|
|
318
|
+
"last merged ",
|
|
319
|
+
/* @__PURE__ */ jsxs2("span", { className: "summary-panel__q-age", children: [
|
|
320
|
+
"(",
|
|
321
|
+
formatRelativeTime(lastMerged.mergedAt),
|
|
322
|
+
")"
|
|
323
|
+
] })
|
|
324
|
+
] }),
|
|
325
|
+
/* @__PURE__ */ jsxs2("span", { className: "summary-panel__q-body", children: [
|
|
326
|
+
lastMerged.repo,
|
|
327
|
+
" #",
|
|
328
|
+
lastMerged.number
|
|
329
|
+
] }),
|
|
330
|
+
/* @__PURE__ */ jsx2("span", { className: "summary-panel__q-title", children: lastMerged.title })
|
|
331
|
+
] }),
|
|
332
|
+
panel.title === "CI status" && ciTrend.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "summary-panel__trend", children: [
|
|
333
|
+
/* @__PURE__ */ jsx2(CiWeatherBar, { trend: ciTrend }),
|
|
334
|
+
/* @__PURE__ */ jsxs2("div", { ref: trendLabelRef, className: "summary-panel__trend-label-row", children: [
|
|
335
|
+
/* @__PURE__ */ jsx2("span", { className: "summary-panel__trend-lab", children: "CI health \xB7 24h" }),
|
|
336
|
+
/* @__PURE__ */ jsx2(
|
|
337
|
+
"button",
|
|
338
|
+
{
|
|
339
|
+
type: "button",
|
|
340
|
+
className: "ci-trend-info-btn",
|
|
341
|
+
"aria-label": "CI health information",
|
|
342
|
+
"aria-expanded": trendInfoOpen,
|
|
343
|
+
"aria-controls": "ci-trend-popover",
|
|
344
|
+
onClick: () => setTrendInfoOpen((o) => !o),
|
|
345
|
+
children: "i"
|
|
346
|
+
}
|
|
347
|
+
),
|
|
348
|
+
trendInfoOpen && /* @__PURE__ */ jsxs2(
|
|
349
|
+
"section",
|
|
350
|
+
{
|
|
351
|
+
id: "ci-trend-popover",
|
|
352
|
+
"aria-label": "CI health explanation",
|
|
353
|
+
className: "ci-trend-popover",
|
|
354
|
+
children: [
|
|
355
|
+
/* @__PURE__ */ jsx2("div", { className: "ci-trend-popover__title", children: "CI health \xB7 24h" }),
|
|
356
|
+
/* @__PURE__ */ jsx2("p", { children: "Each bar is a 1-hour bucket of CI activity across the whole org." }),
|
|
357
|
+
/* @__PURE__ */ jsxs2("p", { children: [
|
|
358
|
+
/* @__PURE__ */ jsx2("span", { "aria-hidden": "true", className: "ci-trend-popover__swatch ci-trend-popover__swatch--fail", children: "\u25A0" }),
|
|
359
|
+
" Red \u2014 any run failed that hour (on any branch)."
|
|
360
|
+
] }),
|
|
361
|
+
/* @__PURE__ */ jsxs2("p", { children: [
|
|
362
|
+
/* @__PURE__ */ jsx2("span", { "aria-hidden": "true", className: "ci-trend-popover__swatch ci-trend-popover__swatch--pass", children: "\u25A0" }),
|
|
363
|
+
" Green \u2014 runs present, none failed. Quiet hours inherit the previous state."
|
|
364
|
+
] }),
|
|
365
|
+
/* @__PURE__ */ jsx2("p", { children: "Oldest bar on the left, newest on the right." }),
|
|
366
|
+
/* @__PURE__ */ jsx2("div", { className: "ci-trend-popover__caret", "aria-hidden": "true" })
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
] })
|
|
371
|
+
] })
|
|
372
|
+
] }, panel.title);
|
|
373
|
+
}) });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/components/RepoBoard.tsx
|
|
377
|
+
import { memo as memo3 } from "react";
|
|
378
|
+
|
|
379
|
+
// src/components/SignalChip.tsx
|
|
380
|
+
import { memo } from "react";
|
|
381
|
+
|
|
382
|
+
// src/lib/stateLabel.ts
|
|
383
|
+
var STATE_LABELS = {
|
|
384
|
+
success: "passing",
|
|
385
|
+
failure: "failing",
|
|
386
|
+
running: "running",
|
|
387
|
+
unknown: "status unknown"
|
|
388
|
+
};
|
|
389
|
+
function stateLabel(state) {
|
|
390
|
+
return STATE_LABELS[state];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/components/SignalChip.tsx
|
|
394
|
+
import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
395
|
+
function meta(wf) {
|
|
396
|
+
if (wf.state === "unknown") return wf.lastRun ? "no status" : "no runs";
|
|
397
|
+
return formatRelativeTime(wf.lastRun.updatedAt);
|
|
398
|
+
}
|
|
399
|
+
var SignalChip = memo(function SignalChip2({ workflow }) {
|
|
400
|
+
const url = workflow.lastRun?.htmlUrl;
|
|
401
|
+
const linkable = Boolean(url);
|
|
402
|
+
const className = `chip chip--${workflow.state}${linkable ? "" : " chip--static"}`;
|
|
403
|
+
const body = /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
404
|
+
/* @__PURE__ */ jsx3("span", { className: "chip__dot", "aria-hidden": "true" }),
|
|
405
|
+
/* @__PURE__ */ jsx3("span", { className: "chip__label", children: workflow.name }),
|
|
406
|
+
/* @__PURE__ */ jsx3("span", { className: "sr-only", children: stateLabel(workflow.state) }),
|
|
407
|
+
/* @__PURE__ */ jsx3("span", { className: "chip__meta", children: meta(workflow) })
|
|
408
|
+
] });
|
|
409
|
+
return linkable ? /* @__PURE__ */ jsx3("a", { className, href: url, title: stateLabel(workflow.state), target: "_blank", rel: "noopener noreferrer", children: body }) : /* @__PURE__ */ jsx3("span", { className, title: stateLabel(workflow.state), children: body });
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// src/components/RepoMetricsLine.tsx
|
|
413
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
414
|
+
function RepoMetricsLine({ metrics }) {
|
|
415
|
+
if (!metrics || metrics.nloc === 0) return null;
|
|
416
|
+
return /* @__PURE__ */ jsxs4("dl", { className: "repo-metrics", "aria-label": "code metrics", children: [
|
|
417
|
+
/* @__PURE__ */ jsxs4("div", { title: "non-comment lines of code", children: [
|
|
418
|
+
/* @__PURE__ */ jsx4("dt", { children: "NLOC" }),
|
|
419
|
+
/* @__PURE__ */ jsx4("dd", { children: formatCompactNumber(metrics.nloc) })
|
|
420
|
+
] }),
|
|
421
|
+
/* @__PURE__ */ jsxs4("div", { title: "average cyclomatic complexity (branch paths per function)", children: [
|
|
422
|
+
/* @__PURE__ */ jsx4("dt", { children: "avg CCN" }),
|
|
423
|
+
/* @__PURE__ */ jsx4("dd", { children: metrics.avgComplexity.toFixed(1) })
|
|
424
|
+
] }),
|
|
425
|
+
/* @__PURE__ */ jsxs4("div", { title: "number of functions", children: [
|
|
426
|
+
/* @__PURE__ */ jsx4("dt", { children: "functions" }),
|
|
427
|
+
/* @__PURE__ */ jsx4("dd", { children: formatCompactNumber(metrics.functionCount) })
|
|
428
|
+
] }),
|
|
429
|
+
metrics.highComplexityCount > 0 && /* @__PURE__ */ jsxs4("div", { className: "repo-metrics__complex", title: "functions over CCN 15 \u2014 refactor candidates", children: [
|
|
430
|
+
/* @__PURE__ */ jsx4("dt", { children: "complex" }),
|
|
431
|
+
/* @__PURE__ */ jsx4("dd", { children: metrics.highComplexityCount })
|
|
432
|
+
] })
|
|
433
|
+
] });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/components/PullRequestList.tsx
|
|
437
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
438
|
+
function PullRequestList({ pullRequests }) {
|
|
439
|
+
if (pullRequests.length === 0) return null;
|
|
440
|
+
return /* @__PURE__ */ jsxs5("div", { className: "repo-prs", children: [
|
|
441
|
+
/* @__PURE__ */ jsxs5("span", { className: "repo-prs__count", children: [
|
|
442
|
+
pullRequests.length,
|
|
443
|
+
" open PR",
|
|
444
|
+
pullRequests.length === 1 ? "" : "s"
|
|
445
|
+
] }),
|
|
446
|
+
/* @__PURE__ */ jsx5("ul", { children: pullRequests.map((pr) => /* @__PURE__ */ jsxs5("li", { className: pr.isDraft ? "repo-prs__item repo-prs__item--draft" : "repo-prs__item", children: [
|
|
447
|
+
/* @__PURE__ */ jsxs5("a", { href: pr.htmlUrl, target: "_blank", rel: "noopener noreferrer", children: [
|
|
448
|
+
/* @__PURE__ */ jsxs5("span", { className: "repo-prs__num", children: [
|
|
449
|
+
"#",
|
|
450
|
+
pr.number
|
|
451
|
+
] }),
|
|
452
|
+
/* @__PURE__ */ jsx5("span", { className: "repo-prs__title", children: pr.title })
|
|
453
|
+
] }),
|
|
454
|
+
/* @__PURE__ */ jsxs5("span", { className: "repo-prs__meta", children: [
|
|
455
|
+
pr.author,
|
|
456
|
+
" \xB7 ",
|
|
457
|
+
formatRelativeTime(pr.createdAt),
|
|
458
|
+
pr.isDraft ? " \xB7 draft" : ""
|
|
459
|
+
] })
|
|
460
|
+
] }, pr.number)) })
|
|
461
|
+
] });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/components/JobLaneRow.tsx
|
|
465
|
+
import { memo as memo2 } from "react";
|
|
466
|
+
|
|
467
|
+
// src/lib/dedupeJobLabel.ts
|
|
468
|
+
function dedupeJobLabel(name) {
|
|
469
|
+
const parts = name.split(" / ");
|
|
470
|
+
const deduped = parts.filter((part, i) => i === 0 || part !== parts[i - 1]);
|
|
471
|
+
return deduped.join(" / ");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/components/JobLaneRow.tsx
|
|
475
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
476
|
+
var JobLaneRow = memo2(function JobLaneRow2({
|
|
477
|
+
kind,
|
|
478
|
+
glyph,
|
|
479
|
+
label,
|
|
480
|
+
signals
|
|
481
|
+
}) {
|
|
482
|
+
if (signals.length === 0) return null;
|
|
483
|
+
return /* @__PURE__ */ jsxs6("div", { className: `repo-joblane repo-joblane--${kind}`, children: [
|
|
484
|
+
/* @__PURE__ */ jsxs6("span", { className: "repo-joblane__label", children: [
|
|
485
|
+
glyph,
|
|
486
|
+
" ",
|
|
487
|
+
label
|
|
488
|
+
] }),
|
|
489
|
+
/* @__PURE__ */ jsx6("div", { className: "repo-joblane__chips", children: signals.map((s, i) => /* @__PURE__ */ jsxs6(
|
|
490
|
+
"a",
|
|
491
|
+
{
|
|
492
|
+
className: `chip chip--${s.state} chip--joblane`,
|
|
493
|
+
href: s.htmlUrl,
|
|
494
|
+
title: `${s.workflow} \xB7 ${s.state}`,
|
|
495
|
+
target: "_blank",
|
|
496
|
+
rel: "noopener noreferrer",
|
|
497
|
+
children: [
|
|
498
|
+
/* @__PURE__ */ jsx6("span", { className: "chip__dot", "aria-hidden": "true" }),
|
|
499
|
+
/* @__PURE__ */ jsx6("span", { className: "chip__label", children: dedupeJobLabel(s.name) }),
|
|
500
|
+
/* @__PURE__ */ jsx6("span", { className: "sr-only", children: stateLabel(s.state) }),
|
|
501
|
+
/* @__PURE__ */ jsx6("span", { className: "chip__meta", children: formatRelativeTime(s.updatedAt) })
|
|
502
|
+
]
|
|
503
|
+
},
|
|
504
|
+
`${s.workflow}/${s.name}/${i}`
|
|
505
|
+
)) })
|
|
506
|
+
] });
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// src/lib/worstState.ts
|
|
510
|
+
function worstState(states) {
|
|
511
|
+
if (states.includes("failure")) return "failure";
|
|
512
|
+
if (states.includes("running")) return "running";
|
|
513
|
+
if (states.includes("success")) return "success";
|
|
514
|
+
return "none";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/components/RepoActivityIndicator.tsx
|
|
518
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
519
|
+
function RepoActivityIndicator({ repository }) {
|
|
520
|
+
const prCount = (repository.pullRequests ?? []).length;
|
|
521
|
+
const ci = worstState((repository.workflows ?? []).map((w) => w.state));
|
|
522
|
+
const cd = worstState([...repository.deploys ?? [], ...repository.packages ?? []].map((s) => s.state));
|
|
523
|
+
return /* @__PURE__ */ jsxs7("span", { className: "repo-activity", children: [
|
|
524
|
+
prCount > 0 && /* @__PURE__ */ jsxs7("span", { className: "repo-activity__pr", children: [
|
|
525
|
+
prCount,
|
|
526
|
+
" PR"
|
|
527
|
+
] }),
|
|
528
|
+
/* @__PURE__ */ jsxs7("span", { className: "repo-activity__sig", children: [
|
|
529
|
+
"CI",
|
|
530
|
+
/* @__PURE__ */ jsx7("span", { className: "repo-activity__dot", "data-activity": ci, "aria-label": `CI ${ci}` })
|
|
531
|
+
] }),
|
|
532
|
+
/* @__PURE__ */ jsxs7("span", { className: "repo-activity__sig", children: [
|
|
533
|
+
"CD",
|
|
534
|
+
/* @__PURE__ */ jsx7("span", { className: "repo-activity__dot", "data-activity": cd, "aria-label": `CD ${cd}` })
|
|
535
|
+
] })
|
|
536
|
+
] });
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/components/RepoBoard.tsx
|
|
540
|
+
import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
541
|
+
var RepoBoard = memo3(function RepoBoard2({
|
|
542
|
+
repository,
|
|
543
|
+
collapsed,
|
|
544
|
+
onToggle
|
|
545
|
+
}) {
|
|
546
|
+
const pullRequests = repository.pullRequests ?? [];
|
|
547
|
+
const noCi = isNoCi(repository);
|
|
548
|
+
return /* @__PURE__ */ jsxs8("section", { className: `repo-board${collapsed ? " repo-board--collapsed" : ""}${noCi ? " repo-board--no-ci" : ""}`, children: [
|
|
549
|
+
/* @__PURE__ */ jsxs8("header", { children: [
|
|
550
|
+
/* @__PURE__ */ jsxs8("button", { type: "button", className: "repo-board__toggle", onClick: () => onToggle(repository.name), "aria-expanded": !collapsed, children: [
|
|
551
|
+
/* @__PURE__ */ jsx8("span", { className: "repo-board__chev", "aria-hidden": "true", children: "\u25B8" }),
|
|
552
|
+
repository.name
|
|
553
|
+
] }),
|
|
554
|
+
noCi && /* @__PURE__ */ jsx8("span", { className: "repo-board__noci-tag", children: "No CI" }),
|
|
555
|
+
/* @__PURE__ */ jsx8(RepoMetricsLine, { metrics: repository.metrics }),
|
|
556
|
+
/* @__PURE__ */ jsx8(RepoActivityIndicator, { repository }),
|
|
557
|
+
/* @__PURE__ */ jsx8("a", { className: "repo-board__gh-link", href: repository.htmlUrl, target: "_blank", rel: "noopener noreferrer", "aria-label": `Open ${repository.name} on GitHub`, children: "GitHub \u2197" })
|
|
558
|
+
] }),
|
|
559
|
+
!collapsed && /* @__PURE__ */ jsxs8(Fragment4, { children: [
|
|
560
|
+
repository.workflows.length === 0 ? /* @__PURE__ */ jsx8("div", { className: "repo-board__empty", children: "no workflows" }) : /* @__PURE__ */ jsxs8("div", { className: "repo-workflows", children: [
|
|
561
|
+
/* @__PURE__ */ jsxs8("span", { className: "repo-workflows__label", children: [
|
|
562
|
+
"Workflows \xB7 ",
|
|
563
|
+
repository.workflows.length
|
|
564
|
+
] }),
|
|
565
|
+
/* @__PURE__ */ jsx8("div", { className: "repo-top-signals", children: repository.workflows.map((wf) => /* @__PURE__ */ jsx8(SignalChip, { workflow: wf }, wf.file)) })
|
|
566
|
+
] }),
|
|
567
|
+
/* @__PURE__ */ jsx8(JobLaneRow, { kind: "deploys", glyph: "\u25B2", label: "Deploys", signals: repository.deploys ?? [] }),
|
|
568
|
+
/* @__PURE__ */ jsx8(JobLaneRow, { kind: "packages", glyph: "\u25A3", label: "Packages", signals: repository.packages ?? [] }),
|
|
569
|
+
/* @__PURE__ */ jsx8(PullRequestList, { pullRequests })
|
|
570
|
+
] })
|
|
571
|
+
] });
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// src/components/RepoSection.tsx
|
|
575
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
576
|
+
function RepoSection({ label, count, collapsed, onToggle }) {
|
|
577
|
+
return /* @__PURE__ */ jsxs9(
|
|
578
|
+
"button",
|
|
579
|
+
{
|
|
580
|
+
type: "button",
|
|
581
|
+
className: "repo-section",
|
|
582
|
+
"aria-expanded": !collapsed,
|
|
583
|
+
onClick: onToggle,
|
|
584
|
+
children: [
|
|
585
|
+
/* @__PURE__ */ jsx9("span", { className: "repo-section__chevron", "aria-hidden": "true", children: collapsed ? "\u25B8" : "\u25BE" }),
|
|
586
|
+
/* @__PURE__ */ jsx9("span", { className: "repo-section__label", children: label }),
|
|
587
|
+
/* @__PURE__ */ jsxs9("span", { className: "repo-section__count", "aria-hidden": "true", children: [
|
|
588
|
+
"\xB7 ",
|
|
589
|
+
count
|
|
590
|
+
] })
|
|
591
|
+
]
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/components/MetricsLegend.tsx
|
|
597
|
+
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
598
|
+
var ITEMS = [
|
|
599
|
+
["NLOC", "non-comment lines of code"],
|
|
600
|
+
["avg CCN", "average cyclomatic complexity (branch paths per function)"],
|
|
601
|
+
["functions", "number of functions"],
|
|
602
|
+
["complex", "functions over CCN 15 \u2014 refactor candidates"]
|
|
603
|
+
];
|
|
604
|
+
function MetricsLegend() {
|
|
605
|
+
return /* @__PURE__ */ jsxs10("footer", { className: "metrics-legend", "aria-label": "Lizard metrics legend", children: [
|
|
606
|
+
/* @__PURE__ */ jsx10("span", { className: "metrics-legend__title", children: "Lizard metrics" }),
|
|
607
|
+
ITEMS.map(([term, meaning]) => /* @__PURE__ */ jsxs10("span", { className: "metrics-legend__item", children: [
|
|
608
|
+
/* @__PURE__ */ jsx10("b", { children: term }),
|
|
609
|
+
" ",
|
|
610
|
+
meaning
|
|
611
|
+
] }, term))
|
|
612
|
+
] });
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/components/StatusLegend.tsx
|
|
616
|
+
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
617
|
+
var STATUS_ITEMS = [
|
|
618
|
+
["success", "passing"],
|
|
619
|
+
["failure", "failing"],
|
|
620
|
+
["running", "running"],
|
|
621
|
+
["unknown", "unknown / no runs"]
|
|
622
|
+
];
|
|
623
|
+
function StatusLegend() {
|
|
624
|
+
return /* @__PURE__ */ jsxs11("footer", { className: "status-legend", "aria-label": "Status colour key", children: [
|
|
625
|
+
/* @__PURE__ */ jsx11("span", { className: "status-legend__title", children: "Status" }),
|
|
626
|
+
STATUS_ITEMS.map(([state, label]) => /* @__PURE__ */ jsxs11("span", { className: "status-legend__item", children: [
|
|
627
|
+
/* @__PURE__ */ jsx11("span", { className: "status-legend__dot", "data-state": state, "aria-hidden": "true" }),
|
|
628
|
+
label
|
|
629
|
+
] }, state)),
|
|
630
|
+
/* @__PURE__ */ jsxs11("span", { className: "status-legend__item", children: [
|
|
631
|
+
/* @__PURE__ */ jsx11("span", { className: "status-legend__noci", "aria-hidden": "true" }),
|
|
632
|
+
"No-CI repo"
|
|
633
|
+
] }),
|
|
634
|
+
/* @__PURE__ */ jsxs11("span", { className: "status-legend__gloss", children: [
|
|
635
|
+
/* @__PURE__ */ jsx11("b", { children: "CI" }),
|
|
636
|
+
" workflow runs \xB7 ",
|
|
637
|
+
/* @__PURE__ */ jsx11("b", { children: "CD" }),
|
|
638
|
+
" deploys & packages"
|
|
639
|
+
] })
|
|
640
|
+
] });
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/components/PullRequestStepper.tsx
|
|
644
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState4 } from "react";
|
|
645
|
+
|
|
646
|
+
// src/lib/prAgeTone.ts
|
|
647
|
+
function prAgeTone(createdAtIso, now = Date.now()) {
|
|
648
|
+
const days = (now - new Date(createdAtIso).getTime()) / 864e5;
|
|
649
|
+
if (days > 14) return "red";
|
|
650
|
+
if (days > 7) return "amber";
|
|
651
|
+
return "quiet";
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/components/PullRequestStepper.tsx
|
|
655
|
+
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
656
|
+
function PullRequestStepper({ prs, onClose }) {
|
|
657
|
+
const [i, setI] = useState4(0);
|
|
658
|
+
const pr = prs[i];
|
|
659
|
+
const dialogRef = useRef2(null);
|
|
660
|
+
useEffect2(() => {
|
|
661
|
+
const previouslyFocused = document.activeElement;
|
|
662
|
+
const dlg = dialogRef.current;
|
|
663
|
+
dlg?.showModal();
|
|
664
|
+
dlg?.focus();
|
|
665
|
+
return () => previouslyFocused?.focus?.();
|
|
666
|
+
}, []);
|
|
667
|
+
if (!pr) return null;
|
|
668
|
+
return /* @__PURE__ */ jsxs12(
|
|
669
|
+
"dialog",
|
|
670
|
+
{
|
|
671
|
+
ref: dialogRef,
|
|
672
|
+
className: "pr-modal",
|
|
673
|
+
"aria-label": "Open pull requests",
|
|
674
|
+
tabIndex: -1,
|
|
675
|
+
onCancel: (e) => {
|
|
676
|
+
e.preventDefault();
|
|
677
|
+
onClose();
|
|
678
|
+
},
|
|
679
|
+
onClick: (e) => {
|
|
680
|
+
if (e.target === e.currentTarget) onClose();
|
|
681
|
+
},
|
|
682
|
+
onKeyDown: (e) => {
|
|
683
|
+
if (e.key === "ArrowRight") setI((p) => Math.min(p + 1, prs.length - 1));
|
|
684
|
+
if (e.key === "ArrowLeft") setI((p) => Math.max(p - 1, 0));
|
|
685
|
+
},
|
|
686
|
+
children: [
|
|
687
|
+
/* @__PURE__ */ jsxs12("div", { className: "pr-modal__top", children: [
|
|
688
|
+
/* @__PURE__ */ jsx12("span", { className: "pr-modal__title", children: "Open pull requests" }),
|
|
689
|
+
/* @__PURE__ */ jsxs12("span", { className: "pr-modal__counter", children: [
|
|
690
|
+
i + 1,
|
|
691
|
+
" / ",
|
|
692
|
+
prs.length
|
|
693
|
+
] }),
|
|
694
|
+
/* @__PURE__ */ jsx12("button", { type: "button", className: "pr-modal__x", onClick: onClose, "aria-label": "Close", children: "\u2715" })
|
|
695
|
+
] }),
|
|
696
|
+
/* @__PURE__ */ jsxs12("div", { className: `pr-card pr-card--${prAgeTone(pr.createdAt)}${pr.isDraft ? " pr-card--draft" : ""}`, children: [
|
|
697
|
+
/* @__PURE__ */ jsxs12("div", { className: "pr-card__head", children: [
|
|
698
|
+
/* @__PURE__ */ jsx12("span", { className: "pr-card__repo", children: pr.repo }),
|
|
699
|
+
/* @__PURE__ */ jsxs12("span", { className: "pr-card__num", children: [
|
|
700
|
+
"#",
|
|
701
|
+
pr.number
|
|
702
|
+
] }),
|
|
703
|
+
/* @__PURE__ */ jsxs12("span", { className: "pr-card__meta", children: [
|
|
704
|
+
formatRelativeTime(pr.createdAt),
|
|
705
|
+
" \xB7 ",
|
|
706
|
+
pr.isDraft ? "draft" : "ready"
|
|
707
|
+
] })
|
|
708
|
+
] }),
|
|
709
|
+
/* @__PURE__ */ jsx12("div", { className: "pr-card__title", children: pr.title }),
|
|
710
|
+
/* @__PURE__ */ jsxs12("div", { className: "pr-card__foot", children: [
|
|
711
|
+
/* @__PURE__ */ jsxs12("span", { className: "pr-card__author", children: [
|
|
712
|
+
"@",
|
|
713
|
+
pr.author
|
|
714
|
+
] }),
|
|
715
|
+
/* @__PURE__ */ jsx12("a", { className: "pr-card__gh", href: pr.htmlUrl, target: "_blank", rel: "noopener noreferrer", children: "Open on GitHub \u2197" })
|
|
716
|
+
] })
|
|
717
|
+
] }),
|
|
718
|
+
prs.length > 1 && /* @__PURE__ */ jsxs12("div", { className: "pr-modal__nav", children: [
|
|
719
|
+
/* @__PURE__ */ jsx12("button", { type: "button", onClick: () => setI((p) => Math.max(p - 1, 0)), disabled: i === 0, children: "\u2039 Prev" }),
|
|
720
|
+
/* @__PURE__ */ jsx12("button", { type: "button", onClick: () => setI((p) => Math.min(p + 1, prs.length - 1)), disabled: i === prs.length - 1, children: "Next \u203A" })
|
|
721
|
+
] })
|
|
722
|
+
]
|
|
723
|
+
}
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/lib/flattenOpenPrs.ts
|
|
728
|
+
function flattenOpenPrs(repositories) {
|
|
729
|
+
return repositories.flatMap((r) => (r.pullRequests ?? []).map((pr) => ({ ...pr, repo: r.name }))).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/pages/CiBoardContent.tsx
|
|
733
|
+
import { Fragment as Fragment5, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
734
|
+
function CiBoardContent() {
|
|
735
|
+
const snapshot = useDashboardSnapshot();
|
|
736
|
+
const collapse = useCollapseState();
|
|
737
|
+
const hideNoCi = useHideNoCi();
|
|
738
|
+
const isAdmin = useCiAdmin();
|
|
739
|
+
const [stepperOpen, setStepperOpen] = useState5(false);
|
|
740
|
+
if (snapshot.isPending) {
|
|
741
|
+
return /* @__PURE__ */ jsx13("main", { className: "dashboard-page", children: /* @__PURE__ */ jsx13("div", { className: "state-msg", children: "Loading dashboard\u2026" }) });
|
|
742
|
+
}
|
|
743
|
+
if (snapshot.isError) {
|
|
744
|
+
return /* @__PURE__ */ jsx13("main", { className: "dashboard-page", children: /* @__PURE__ */ jsx13("div", { className: "state-msg state-msg--error", children: "Dashboard unavailable." }) });
|
|
745
|
+
}
|
|
746
|
+
if (!snapshot.data) {
|
|
747
|
+
return /* @__PURE__ */ jsx13("main", { className: "dashboard-page", children: /* @__PURE__ */ jsx13("div", { className: "state-msg", children: "Waiting for the first refresh\u2026" }) });
|
|
748
|
+
}
|
|
749
|
+
const { refreshedAt, repositories: allRepositories, lastMergedPr: rawLastMerged } = snapshot.data;
|
|
750
|
+
const repositories = isAdmin ? allRepositories : allRepositories.filter((r) => !r.private);
|
|
751
|
+
const summary = isAdmin ? snapshot.data.summary : computeSummary(repositories);
|
|
752
|
+
const lastMergedPr = rawLastMerged && repositories.some((r) => r.name === rawLastMerged.repo) ? rawLastMerged : null;
|
|
753
|
+
const repoNames = repositories.map((r) => r.name);
|
|
754
|
+
const noCiCount = repositories.filter(isNoCi).length;
|
|
755
|
+
const visibleRepos = hideNoCi.hidden ? repositories.filter((r) => !isNoCi(r)) : repositories;
|
|
756
|
+
const hiddenCount = repositories.length - visibleRepos.length;
|
|
757
|
+
const publicRepos = visibleRepos.filter((r) => !r.private);
|
|
758
|
+
const privateRepos = visibleRepos.filter((r) => r.private);
|
|
759
|
+
const showGroups = publicRepos.length > 0 && privateRepos.length > 0;
|
|
760
|
+
const KEY_PUBLIC = "section:public";
|
|
761
|
+
const KEY_PRIVATE = "section:private";
|
|
762
|
+
const sectionKeys = showGroups ? [KEY_PUBLIC, KEY_PRIVATE] : [];
|
|
763
|
+
const allCollapsed = collapse.allCollapsed([...repoNames, ...sectionKeys]);
|
|
764
|
+
const openPrs = flattenOpenPrs(repositories);
|
|
765
|
+
const nextPr = openPrs[0] ?? null;
|
|
766
|
+
let repoListContent;
|
|
767
|
+
if (visibleRepos.length === 0 && hideNoCi.hidden) {
|
|
768
|
+
repoListContent = /* @__PURE__ */ jsx13("div", { className: "state-msg", children: "All repositories are No-CI \u2014 hidden." });
|
|
769
|
+
} else if (showGroups) {
|
|
770
|
+
repoListContent = /* @__PURE__ */ jsxs13(Fragment5, { children: [
|
|
771
|
+
/* @__PURE__ */ jsx13(
|
|
772
|
+
RepoSection,
|
|
773
|
+
{
|
|
774
|
+
label: "Public",
|
|
775
|
+
count: publicRepos.length,
|
|
776
|
+
collapsed: collapse.isCollapsed(KEY_PUBLIC),
|
|
777
|
+
onToggle: () => collapse.toggle(KEY_PUBLIC)
|
|
778
|
+
}
|
|
779
|
+
),
|
|
780
|
+
!collapse.isCollapsed(KEY_PUBLIC) && publicRepos.map((repository) => /* @__PURE__ */ jsx13(
|
|
781
|
+
RepoBoard,
|
|
782
|
+
{
|
|
783
|
+
repository,
|
|
784
|
+
collapsed: collapse.isCollapsed(repository.name),
|
|
785
|
+
onToggle: collapse.toggle
|
|
786
|
+
},
|
|
787
|
+
repository.name
|
|
788
|
+
)),
|
|
789
|
+
/* @__PURE__ */ jsx13(
|
|
790
|
+
RepoSection,
|
|
791
|
+
{
|
|
792
|
+
label: "Private",
|
|
793
|
+
count: privateRepos.length,
|
|
794
|
+
collapsed: collapse.isCollapsed(KEY_PRIVATE),
|
|
795
|
+
onToggle: () => collapse.toggle(KEY_PRIVATE)
|
|
796
|
+
}
|
|
797
|
+
),
|
|
798
|
+
!collapse.isCollapsed(KEY_PRIVATE) && privateRepos.map((repository) => /* @__PURE__ */ jsx13(
|
|
799
|
+
RepoBoard,
|
|
800
|
+
{
|
|
801
|
+
repository,
|
|
802
|
+
collapsed: collapse.isCollapsed(repository.name),
|
|
803
|
+
onToggle: collapse.toggle
|
|
804
|
+
},
|
|
805
|
+
repository.name
|
|
806
|
+
))
|
|
807
|
+
] });
|
|
808
|
+
} else {
|
|
809
|
+
repoListContent = visibleRepos.map((repository) => /* @__PURE__ */ jsx13(
|
|
810
|
+
RepoBoard,
|
|
811
|
+
{
|
|
812
|
+
repository,
|
|
813
|
+
collapsed: collapse.isCollapsed(repository.name),
|
|
814
|
+
onToggle: collapse.toggle
|
|
815
|
+
},
|
|
816
|
+
repository.name
|
|
817
|
+
));
|
|
818
|
+
}
|
|
819
|
+
return /* @__PURE__ */ jsxs13("main", { className: "dashboard-page", children: [
|
|
820
|
+
/* @__PURE__ */ jsxs13("div", { className: "dashboard__toolbar", children: [
|
|
821
|
+
/* @__PURE__ */ jsxs13("span", { className: "dashboard__scope", children: [
|
|
822
|
+
snapshot.data.org,
|
|
823
|
+
" \xB7 ",
|
|
824
|
+
isAdmin ? "all repositories" : "public repositories"
|
|
825
|
+
] }),
|
|
826
|
+
/* @__PURE__ */ jsxs13("span", { className: "dashboard__toolbar-right", children: [
|
|
827
|
+
noCiCount > 0 && /* @__PURE__ */ jsx13(
|
|
828
|
+
"button",
|
|
829
|
+
{
|
|
830
|
+
type: "button",
|
|
831
|
+
className: `dashboard__hide-noci${hideNoCi.hidden ? " dashboard__hide-noci--on" : ""}`,
|
|
832
|
+
onClick: hideNoCi.toggle,
|
|
833
|
+
"aria-pressed": hideNoCi.hidden,
|
|
834
|
+
children: hideNoCi.hidden ? `Show No-CI \xB7 ${hiddenCount} hidden` : "Hide No-CI"
|
|
835
|
+
}
|
|
836
|
+
),
|
|
837
|
+
/* @__PURE__ */ jsx13(
|
|
838
|
+
"button",
|
|
839
|
+
{
|
|
840
|
+
type: "button",
|
|
841
|
+
className: "dashboard__collapse-all",
|
|
842
|
+
onClick: () => allCollapsed ? collapse.expandAll() : collapse.collapseAll([...repoNames, ...sectionKeys]),
|
|
843
|
+
children: allCollapsed ? "\u229E Expand all" : "\u229F Collapse all"
|
|
844
|
+
}
|
|
845
|
+
),
|
|
846
|
+
/* @__PURE__ */ jsxs13("span", { className: "dashboard__refreshed", children: [
|
|
847
|
+
/* @__PURE__ */ jsx13("span", { className: "live-dot", "aria-hidden": "true" }),
|
|
848
|
+
"updated ",
|
|
849
|
+
formatRelativeTime(refreshedAt)
|
|
850
|
+
] })
|
|
851
|
+
] })
|
|
852
|
+
] }),
|
|
853
|
+
/* @__PURE__ */ jsx13(SummaryStrip, { summary, onOpenPrs: isAdmin ? () => setStepperOpen(true) : void 0, lastMerged: lastMergedPr, nextPr, ciTrend: snapshot.data.ciTrend ?? [] }),
|
|
854
|
+
/* @__PURE__ */ jsx13("div", { className: "repo-list", children: repoListContent }),
|
|
855
|
+
/* @__PURE__ */ jsx13(StatusLegend, {}),
|
|
856
|
+
/* @__PURE__ */ jsx13(MetricsLegend, {}),
|
|
857
|
+
stepperOpen && /* @__PURE__ */ jsx13(PullRequestStepper, { prs: openPrs, onClose: () => setStepperOpen(false) })
|
|
858
|
+
] });
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/DefaultFooter.tsx
|
|
862
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
863
|
+
function DefaultFooter() {
|
|
864
|
+
return /* @__PURE__ */ jsx14("footer", { className: "site-footer", children: /* @__PURE__ */ jsx14("div", { className: "site-footer__band", "aria-hidden": "true", children: /* @__PURE__ */ jsx14("span", { className: "site-footer__tagline", children: "Continuous-integration overview" }) }) });
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/CiBoard.tsx
|
|
868
|
+
import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
869
|
+
function CiBoard({ adminSignal, apiBase = DEFAULT_CI_API_BASE, logo, footerSlot }) {
|
|
870
|
+
return /* @__PURE__ */ jsx15(CiConfigProvider, { value: { apiBase }, children: /* @__PURE__ */ jsxs14("div", { className: "ci-page", children: [
|
|
871
|
+
/* @__PURE__ */ jsxs14("div", { className: "ci-embed", children: [
|
|
872
|
+
/* @__PURE__ */ jsx15("header", { className: "ci-embed__header", children: /* @__PURE__ */ jsxs14("span", { className: "ci-embed__lockup", children: [
|
|
873
|
+
logo ?? /* @__PURE__ */ jsx15("span", { className: "ci-embed__wordmark-text", children: "CI Dashboard" }),
|
|
874
|
+
/* @__PURE__ */ jsxs14("span", { className: "ci-embed__descriptor", children: [
|
|
875
|
+
"CI Dashboard ",
|
|
876
|
+
adminSignal ? "[Admin]" : "[Guest]"
|
|
877
|
+
] })
|
|
878
|
+
] }) }),
|
|
879
|
+
/* @__PURE__ */ jsx15(CiAdminProvider, { value: adminSignal, children: /* @__PURE__ */ jsx15(CiBoardContent, {}) })
|
|
880
|
+
] }),
|
|
881
|
+
footerSlot ?? /* @__PURE__ */ jsx15(DefaultFooter, {})
|
|
882
|
+
] }) });
|
|
883
|
+
}
|
|
884
|
+
export {
|
|
885
|
+
CiBoard,
|
|
886
|
+
DEFAULT_CI_API_BASE,
|
|
887
|
+
DefaultFooter
|
|
888
|
+
};
|
|
889
|
+
//# sourceMappingURL=index.js.map
|