@hansenexus/hud 0.2.0 → 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.
- package/README.md +253 -9
- package/dist/access-CNvJOwKM.d.ts +54 -0
- package/dist/capabilities-CjulXFQX.js +189 -0
- package/dist/define-hud-BBzsSl8b.d.ts +36 -0
- package/dist/env-DfxpPfs9.js +28 -0
- package/dist/env-vEjyJfPW.d.ts +17 -0
- package/dist/force-state-BaqWsmrf.js +153 -0
- package/dist/force-state-CCqoTfVB.d.ts +23 -0
- package/dist/handler-WmE4Lu7l.d.ts +39 -0
- package/dist/index-BYsdS-7d.d.ts +55 -0
- package/dist/index.d.ts +6 -39
- package/dist/index.js +980 -264
- package/dist/marker-C66LxOhJ.d.ts +9 -0
- package/dist/marker-CMrvxPmS.d.ts +12 -0
- package/dist/marker-DjeGvL-F.js +9 -0
- package/dist/marker-uZYPOJ4M.js +12 -0
- package/dist/markers.d.ts +3 -0
- package/dist/markers.js +3 -0
- package/dist/next.d.ts +33 -0
- package/dist/next.js +52 -0
- package/dist/payload-CLKDVZWl.d.ts +68 -0
- package/dist/plugins/access/server.d.ts +122 -0
- package/dist/plugins/access/server.js +339 -0
- package/dist/plugins/access.d.ts +41 -0
- package/dist/plugins/access.js +250 -0
- package/dist/plugins/agent/server.d.ts +129 -0
- package/dist/plugins/agent/server.js +429 -0
- package/dist/plugins/agent.d.ts +37 -0
- package/dist/plugins/agent.js +411 -0
- package/dist/plugins/design.d.ts +125 -0
- package/dist/plugins/design.js +619 -0
- package/dist/plugins/observe.d.ts +64 -0
- package/dist/plugins/observe.js +617 -0
- package/dist/plugins/ops/server.d.ts +82 -0
- package/dist/plugins/ops/server.js +380 -0
- package/dist/plugins/ops.d.ts +26 -0
- package/dist/plugins/ops.js +362 -0
- package/dist/plugins/vitals.d.ts +25 -0
- package/dist/plugins/vitals.js +26 -0
- package/dist/plugins-CQ_0w1ot.d.ts +114 -0
- package/dist/plugins-DVy_vaYr.js +70 -0
- package/dist/review.d.ts +181 -0
- package/dist/review.js +1017 -0
- package/dist/selector-NSYSe7AZ.js +28 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +106 -0
- package/dist/shortcut-U6ftVY06.js +222 -0
- package/dist/state-boundary.d.ts +26 -0
- package/dist/state-boundary.js +61 -0
- package/dist/styles--slk86ZQ.js +332 -0
- package/dist/types-DADBRiGk.d.ts +50 -0
- package/dist/types-QJUajwWq.js +35 -0
- package/dist/types-f3jlNxzw.d.ts +92 -0
- package/dist/vitals-panel-Dvc5dnR-.js +87 -0
- package/package.json +36 -3
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { i as definePlugin, n as HudApiError } from "../plugins-DVy_vaYr.js";
|
|
2
|
+
import { i as OPS_CAPABILITY, r as stateRating, t as TELEMETRY_RANGES } from "../types-QJUajwWq.js";
|
|
3
|
+
import { useCallback, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/plugins/ops/poll.ts
|
|
6
|
+
/** Entries per plugin context, so the pill and the panel share one poller per path. */
|
|
7
|
+
const entries = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
function entryFor(ctx, path) {
|
|
9
|
+
let byPath = entries.get(ctx);
|
|
10
|
+
if (!byPath) {
|
|
11
|
+
byPath = /* @__PURE__ */ new Map();
|
|
12
|
+
entries.set(ctx, byPath);
|
|
13
|
+
}
|
|
14
|
+
let entry = byPath.get(path);
|
|
15
|
+
if (!entry) {
|
|
16
|
+
entry = {
|
|
17
|
+
state: { loading: true },
|
|
18
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
19
|
+
inflight: false
|
|
20
|
+
};
|
|
21
|
+
byPath.set(path, entry);
|
|
22
|
+
}
|
|
23
|
+
return entry;
|
|
24
|
+
}
|
|
25
|
+
/** A readable message for a failed poll; 502 bodies carry the provider's own. */
|
|
26
|
+
function describeError(error) {
|
|
27
|
+
if (error instanceof HudApiError) {
|
|
28
|
+
if (error.status === 404) return "route not mounted: add ops() to createHudHandler";
|
|
29
|
+
const body = error.body;
|
|
30
|
+
if (typeof body?.error === "string") return body.error;
|
|
31
|
+
return `HTTP ${error.status}`;
|
|
32
|
+
}
|
|
33
|
+
return error instanceof Error ? error.message : "request failed";
|
|
34
|
+
}
|
|
35
|
+
function set(entry, state) {
|
|
36
|
+
entry.state = state;
|
|
37
|
+
for (const listener of entry.listeners) listener();
|
|
38
|
+
}
|
|
39
|
+
async function tick(ctx, path, entry, force = false) {
|
|
40
|
+
if (entry.inflight || !force && typeof document !== "undefined" && document.hidden) return;
|
|
41
|
+
entry.inflight = true;
|
|
42
|
+
try {
|
|
43
|
+
set(entry, {
|
|
44
|
+
data: await ctx.api(path),
|
|
45
|
+
loading: false
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
set(entry, {
|
|
49
|
+
...entry.state,
|
|
50
|
+
error: describeError(error),
|
|
51
|
+
loading: false
|
|
52
|
+
});
|
|
53
|
+
} finally {
|
|
54
|
+
entry.inflight = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Polls one of the plugin's routes while anything renders it. Pauses while
|
|
59
|
+
* the tab is hidden; subscribers of the same path share one request.
|
|
60
|
+
*/
|
|
61
|
+
function usePoll(ctx, path, intervalMs) {
|
|
62
|
+
const entry = entryFor(ctx, path);
|
|
63
|
+
const subscribe = useCallback((listener) => {
|
|
64
|
+
entry.listeners.add(listener);
|
|
65
|
+
if (entry.listeners.size === 1) {
|
|
66
|
+
tick(ctx, path, entry);
|
|
67
|
+
entry.timer = setInterval(() => void tick(ctx, path, entry), intervalMs);
|
|
68
|
+
}
|
|
69
|
+
return () => {
|
|
70
|
+
entry.listeners.delete(listener);
|
|
71
|
+
if (entry.listeners.size === 0) clearInterval(entry.timer);
|
|
72
|
+
};
|
|
73
|
+
}, [
|
|
74
|
+
ctx,
|
|
75
|
+
path,
|
|
76
|
+
entry,
|
|
77
|
+
intervalMs
|
|
78
|
+
]);
|
|
79
|
+
const state = useSyncExternalStore(subscribe, () => entry.state, () => entry.state);
|
|
80
|
+
const refresh = useCallback(() => void tick(ctx, path, entry, true), [
|
|
81
|
+
ctx,
|
|
82
|
+
path,
|
|
83
|
+
entry
|
|
84
|
+
]);
|
|
85
|
+
return {
|
|
86
|
+
...state,
|
|
87
|
+
refresh
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/plugins/ops/styles.ts
|
|
92
|
+
/** Panel styles, rendered as a `<style>` inside the panel, so inside the shadow root. */
|
|
93
|
+
const OPS_CSS = `
|
|
94
|
+
.ops { display: grid; gap: 12px; }
|
|
95
|
+
.ops section { display: grid; gap: 6px; }
|
|
96
|
+
.ops h3 {
|
|
97
|
+
margin: 0;
|
|
98
|
+
font-size: 10px;
|
|
99
|
+
font-weight: 600;
|
|
100
|
+
letter-spacing: 0.06em;
|
|
101
|
+
text-transform: uppercase;
|
|
102
|
+
color: var(--hud-fg-muted);
|
|
103
|
+
}
|
|
104
|
+
.ops .head { display: flex; align-items: center; gap: 6px; font-family: var(--hud-mono); }
|
|
105
|
+
.ops .head .branch { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
106
|
+
.ops .warn { color: var(--hud-warn); margin: 0; }
|
|
107
|
+
.ops .muted { color: var(--hud-fg-muted); }
|
|
108
|
+
.ops ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
|
|
109
|
+
.ops li a, .ops li > span {
|
|
110
|
+
display: grid;
|
|
111
|
+
grid-template-columns: 6px minmax(0, 1fr) auto auto;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
padding: 3px 4px;
|
|
115
|
+
border-radius: 4px;
|
|
116
|
+
color: inherit;
|
|
117
|
+
text-decoration: none;
|
|
118
|
+
}
|
|
119
|
+
.ops li a:hover { background: var(--hud-bg-subtle); }
|
|
120
|
+
.ops .title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
121
|
+
.ops .title small { color: var(--hud-fg-muted); margin-left: 6px; }
|
|
122
|
+
.ops .mono { font-family: var(--hud-mono); color: var(--hud-fg-muted); }
|
|
123
|
+
.ops .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
|
|
124
|
+
.ops .stat { padding: 6px 8px; border: 1px solid var(--hud-border); border-radius: 6px; }
|
|
125
|
+
.ops .stat b { display: block; font-family: var(--hud-mono); font-size: 13px; }
|
|
126
|
+
.ops .stat span { font-size: 10px; color: var(--hud-fg-muted); }
|
|
127
|
+
.ops .row { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
|
|
128
|
+
.ops .ranges { display: flex; gap: 2px; }
|
|
129
|
+
.ops button.chip { padding: 1px 6px; border-radius: 999px; font-family: var(--hud-mono); }
|
|
130
|
+
.ops button.chip:hover, .ops button.chip[aria-pressed="true"] { background: var(--hud-bg-subtle); }
|
|
131
|
+
`;
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/plugins/ops/ops-panel.tsx
|
|
134
|
+
const CI_POLL_MS = 6e4;
|
|
135
|
+
const TELEMETRY_POLL_MS = 6e4;
|
|
136
|
+
const STATE_LABEL = {
|
|
137
|
+
success: "passing",
|
|
138
|
+
failure: "failing",
|
|
139
|
+
cancelled: "cancelled",
|
|
140
|
+
running: "running",
|
|
141
|
+
queued: "queued",
|
|
142
|
+
neutral: "neutral"
|
|
143
|
+
};
|
|
144
|
+
function stateLabel(state) {
|
|
145
|
+
return state ? STATE_LABEL[state] : "no runs";
|
|
146
|
+
}
|
|
147
|
+
/** `3m`, `5h`, `2d` since `iso`. */
|
|
148
|
+
function age(iso, now = Date.now()) {
|
|
149
|
+
if (!iso) return "";
|
|
150
|
+
const seconds = Math.max(0, Math.round((now - Date.parse(iso)) / 1e3));
|
|
151
|
+
if (seconds < 60) return `${seconds}s`;
|
|
152
|
+
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
|
|
153
|
+
if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;
|
|
154
|
+
return `${Math.round(seconds / 86400)}d`;
|
|
155
|
+
}
|
|
156
|
+
function Missing({ name }) {
|
|
157
|
+
return /* @__PURE__ */ jsxs("p", {
|
|
158
|
+
className: "warn",
|
|
159
|
+
children: [
|
|
160
|
+
"Not configured: set ",
|
|
161
|
+
/* @__PURE__ */ jsx("code", { children: name ?? "its env" }),
|
|
162
|
+
" in the app's server env."
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function RunRow({ run }) {
|
|
167
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
168
|
+
/* @__PURE__ */ jsx("span", {
|
|
169
|
+
className: "dot",
|
|
170
|
+
"data-rating": stateRating(run.state),
|
|
171
|
+
title: STATE_LABEL[run.state]
|
|
172
|
+
}),
|
|
173
|
+
/* @__PURE__ */ jsxs("span", {
|
|
174
|
+
className: "title",
|
|
175
|
+
title: run.title,
|
|
176
|
+
children: [run.name, /* @__PURE__ */ jsx("small", { children: run.title })]
|
|
177
|
+
}),
|
|
178
|
+
/* @__PURE__ */ jsx("span", {
|
|
179
|
+
className: "mono",
|
|
180
|
+
children: run.sha
|
|
181
|
+
}),
|
|
182
|
+
/* @__PURE__ */ jsx("span", {
|
|
183
|
+
className: "mono",
|
|
184
|
+
children: age(run.startedAt)
|
|
185
|
+
})
|
|
186
|
+
] });
|
|
187
|
+
return /* @__PURE__ */ jsx("li", { children: run.url ? /* @__PURE__ */ jsx("a", {
|
|
188
|
+
href: run.url,
|
|
189
|
+
target: "_blank",
|
|
190
|
+
rel: "noreferrer",
|
|
191
|
+
children: content
|
|
192
|
+
}) : /* @__PURE__ */ jsx("span", { children: content }) });
|
|
193
|
+
}
|
|
194
|
+
function Deployment({ deployment }) {
|
|
195
|
+
if (!deployment) return /* @__PURE__ */ jsx("p", {
|
|
196
|
+
className: "muted",
|
|
197
|
+
children: "Nothing deployed."
|
|
198
|
+
});
|
|
199
|
+
const rows = [
|
|
200
|
+
["Environment", deployment.environment],
|
|
201
|
+
["Image", deployment.image],
|
|
202
|
+
["Sha", deployment.sha],
|
|
203
|
+
["Ready", deployment.ready],
|
|
204
|
+
["Since", deployment.at && `${age(deployment.at)} ago`]
|
|
205
|
+
];
|
|
206
|
+
return /* @__PURE__ */ jsx("table", {
|
|
207
|
+
className: "vitals",
|
|
208
|
+
children: /* @__PURE__ */ jsx("tbody", { children: rows.flatMap(([label, value]) => value ? /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", {
|
|
209
|
+
className: "label",
|
|
210
|
+
children: label
|
|
211
|
+
}), /* @__PURE__ */ jsx("td", { children: value })] }, label) : []) })
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function CiSection({ ctx }) {
|
|
215
|
+
const { data, error, loading, refresh } = usePoll(ctx, "/ci", CI_POLL_MS);
|
|
216
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("section", {
|
|
217
|
+
"aria-label": "CI",
|
|
218
|
+
children: [
|
|
219
|
+
/* @__PURE__ */ jsxs("div", {
|
|
220
|
+
className: "row",
|
|
221
|
+
children: [/* @__PURE__ */ jsxs("h3", { children: ["CI", data ? ` · ${data.repo}` : ""] }), /* @__PURE__ */ jsx("button", {
|
|
222
|
+
type: "button",
|
|
223
|
+
className: "chip",
|
|
224
|
+
onClick: refresh,
|
|
225
|
+
children: "refresh"
|
|
226
|
+
})]
|
|
227
|
+
}),
|
|
228
|
+
loading && /* @__PURE__ */ jsx("p", {
|
|
229
|
+
className: "muted",
|
|
230
|
+
children: "Loading…"
|
|
231
|
+
}),
|
|
232
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
233
|
+
className: "warn",
|
|
234
|
+
children: error
|
|
235
|
+
}),
|
|
236
|
+
data && !data.configured && /* @__PURE__ */ jsx(Missing, { name: data.missing }),
|
|
237
|
+
data?.configured && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
238
|
+
className: "head",
|
|
239
|
+
children: [
|
|
240
|
+
/* @__PURE__ */ jsx("span", {
|
|
241
|
+
className: "dot",
|
|
242
|
+
"data-rating": stateRating(data.state)
|
|
243
|
+
}),
|
|
244
|
+
/* @__PURE__ */ jsx("span", {
|
|
245
|
+
className: "branch",
|
|
246
|
+
children: data.branch ?? "no branch"
|
|
247
|
+
}),
|
|
248
|
+
/* @__PURE__ */ jsxs("span", {
|
|
249
|
+
className: "muted",
|
|
250
|
+
children: [data.branch ? stateLabel(data.state) : "latest runs, all branches", data.sha ? ` @ ${data.sha}` : ""]
|
|
251
|
+
})
|
|
252
|
+
]
|
|
253
|
+
}), data.runs.length > 0 ? /* @__PURE__ */ jsx("ul", { children: data.runs.map((run) => /* @__PURE__ */ jsx(RunRow, { run }, run.id)) }) : /* @__PURE__ */ jsxs("p", {
|
|
254
|
+
className: "muted",
|
|
255
|
+
children: [
|
|
256
|
+
"No runs",
|
|
257
|
+
data.branch ? ` on ${data.branch}` : "",
|
|
258
|
+
"."
|
|
259
|
+
]
|
|
260
|
+
})] })
|
|
261
|
+
]
|
|
262
|
+
}), data && data.deployment !== void 0 && /* @__PURE__ */ jsxs("section", {
|
|
263
|
+
"aria-label": "Deployment",
|
|
264
|
+
children: [/* @__PURE__ */ jsx("h3", { children: "Deployment" }), /* @__PURE__ */ jsx(Deployment, { deployment: data.deployment })]
|
|
265
|
+
})] });
|
|
266
|
+
}
|
|
267
|
+
function formatStat(value, unit) {
|
|
268
|
+
if (value === null) return "–";
|
|
269
|
+
return `${value.toFixed(value < 10 ? 2 : 0)}${unit}`;
|
|
270
|
+
}
|
|
271
|
+
function TelemetrySection({ ctx }) {
|
|
272
|
+
const [range, setRange] = useState("1h");
|
|
273
|
+
const { data, error, loading } = usePoll(ctx, `/telemetry?range=${range}`, TELEMETRY_POLL_MS);
|
|
274
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
275
|
+
"aria-label": "Telemetry",
|
|
276
|
+
children: [
|
|
277
|
+
/* @__PURE__ */ jsxs("div", {
|
|
278
|
+
className: "row",
|
|
279
|
+
children: [/* @__PURE__ */ jsxs("h3", { children: ["Telemetry", data ? ` · ${data.service}` : ""] }), /* @__PURE__ */ jsx("div", {
|
|
280
|
+
className: "ranges",
|
|
281
|
+
children: TELEMETRY_RANGES.map((r) => /* @__PURE__ */ jsx("button", {
|
|
282
|
+
type: "button",
|
|
283
|
+
className: "chip",
|
|
284
|
+
"aria-pressed": r === range,
|
|
285
|
+
onClick: () => setRange(r),
|
|
286
|
+
children: r
|
|
287
|
+
}, r))
|
|
288
|
+
})]
|
|
289
|
+
}),
|
|
290
|
+
loading && !data && /* @__PURE__ */ jsx("p", {
|
|
291
|
+
className: "muted",
|
|
292
|
+
children: "Loading…"
|
|
293
|
+
}),
|
|
294
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
295
|
+
className: "warn",
|
|
296
|
+
children: error
|
|
297
|
+
}),
|
|
298
|
+
data && !data.configured && /* @__PURE__ */ jsx(Missing, { name: data.missing }),
|
|
299
|
+
data?.configured && /* @__PURE__ */ jsxs("div", {
|
|
300
|
+
className: "stats",
|
|
301
|
+
children: [
|
|
302
|
+
/* @__PURE__ */ jsxs("div", {
|
|
303
|
+
className: "stat",
|
|
304
|
+
children: [/* @__PURE__ */ jsx("b", { children: formatStat(data.errorRate, "/s") }), /* @__PURE__ */ jsx("span", { children: "errors" })]
|
|
305
|
+
}),
|
|
306
|
+
/* @__PURE__ */ jsxs("div", {
|
|
307
|
+
className: "stat",
|
|
308
|
+
children: [/* @__PURE__ */ jsx("b", { children: formatStat(data.p95Ms, " ms") }), /* @__PURE__ */ jsx("span", { children: "p95" })]
|
|
309
|
+
}),
|
|
310
|
+
/* @__PURE__ */ jsxs("div", {
|
|
311
|
+
className: "stat",
|
|
312
|
+
children: [/* @__PURE__ */ jsx("b", { children: formatStat(data.requestRate, "/s") }), /* @__PURE__ */ jsx("span", { children: "requests" })]
|
|
313
|
+
})
|
|
314
|
+
]
|
|
315
|
+
})
|
|
316
|
+
]
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function createOpsPanel({ ci, telemetry }) {
|
|
320
|
+
function OpsPanel({ ctx }) {
|
|
321
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
322
|
+
className: "ops",
|
|
323
|
+
children: [
|
|
324
|
+
/* @__PURE__ */ jsx("style", { children: OPS_CSS }),
|
|
325
|
+
ci && /* @__PURE__ */ jsx(CiSection, { ctx }),
|
|
326
|
+
telemetry && /* @__PURE__ */ jsx(TelemetrySection, { ctx })
|
|
327
|
+
]
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return OpsPanel;
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/plugins/ops/index.tsx
|
|
334
|
+
function OpsPill({ ctx }) {
|
|
335
|
+
const { data, error } = usePoll(ctx, "/ci", CI_POLL_MS);
|
|
336
|
+
const label = !data && error ? "error" : !data ? "–" : !data.configured ? "off" : stateLabel(data.state);
|
|
337
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
338
|
+
className: "dot",
|
|
339
|
+
"data-rating": data ? stateRating(data.state) : void 0
|
|
340
|
+
}), `CI ${label}`] });
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* CI and telemetry: the pill shows the CI state of the branch the server runs
|
|
344
|
+
* on, the panel lists recent runs, the deployment (when the CI provider knows
|
|
345
|
+
* it) and error rate, p95 and request rate. Client half; the providers live in
|
|
346
|
+
* the server half, `@hansenexus/hud/plugins/ops/server`, which reads tokens
|
|
347
|
+
* from env and never sends them to the browser.
|
|
348
|
+
*/
|
|
349
|
+
function ops({ ci = true, telemetry = true } = {}) {
|
|
350
|
+
return definePlugin({
|
|
351
|
+
id: "ops",
|
|
352
|
+
title: "Ops",
|
|
353
|
+
capability: OPS_CAPABILITY,
|
|
354
|
+
panel: createOpsPanel({
|
|
355
|
+
ci,
|
|
356
|
+
telemetry
|
|
357
|
+
}),
|
|
358
|
+
...ci ? { pill: OpsPill } : {}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
//#endregion
|
|
362
|
+
export { ops };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Metric } from "web-vitals";
|
|
2
|
+
//#region src/plugins/vitals/vitals-panel.d.ts
|
|
3
|
+
declare function VitalsPanel(): import("react").JSX.Element;
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/plugins/vitals/store.d.ts
|
|
6
|
+
type VitalName = Metric["name"];
|
|
7
|
+
type VitalRating = Metric["rating"];
|
|
8
|
+
interface Vital {
|
|
9
|
+
name: VitalName;
|
|
10
|
+
value: number;
|
|
11
|
+
rating: VitalRating;
|
|
12
|
+
}
|
|
13
|
+
type VitalsSnapshot = Partial<Record<VitalName, Vital>>;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/plugins/vitals/index.d.ts
|
|
16
|
+
declare function VitalsPill(): import("react").JSX.Element;
|
|
17
|
+
/** Core Web Vitals: LCP in the pill, all five metrics in the panel. Client only. */
|
|
18
|
+
export declare function vitals(): {
|
|
19
|
+
readonly id: "vitals";
|
|
20
|
+
readonly title: "Vitals";
|
|
21
|
+
readonly pill: typeof VitalsPill;
|
|
22
|
+
readonly panel: typeof VitalsPanel;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export type { Vital, VitalName, VitalRating, VitalsSnapshot };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { i as definePlugin } from "../plugins-DVy_vaYr.js";
|
|
2
|
+
import { i as useVitals, n as formatVital, r as startVitals, t as VitalsPanel } from "../vitals-panel-Dvc5dnR-.js";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/plugins/vitals/index.tsx
|
|
6
|
+
function VitalsPill() {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
startVitals();
|
|
9
|
+
}, []);
|
|
10
|
+
const lcp = useVitals().LCP;
|
|
11
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
12
|
+
className: "dot",
|
|
13
|
+
"data-rating": lcp?.rating
|
|
14
|
+
}), lcp ? `LCP ${formatVital("LCP", lcp.value)}` : "LCP –"] });
|
|
15
|
+
}
|
|
16
|
+
/** Core Web Vitals: LCP in the pill, all five metrics in the panel. Client only. */
|
|
17
|
+
function vitals() {
|
|
18
|
+
return definePlugin({
|
|
19
|
+
id: "vitals",
|
|
20
|
+
title: "Vitals",
|
|
21
|
+
pill: VitalsPill,
|
|
22
|
+
panel: VitalsPanel
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { vitals };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { r as GrabPayload } from "./payload-CLKDVZWl.js";
|
|
2
|
+
import { ComponentType } from "react";
|
|
3
|
+
//#region src/core/grabber/store.d.ts
|
|
4
|
+
interface HudGrabState {
|
|
5
|
+
/** The picker is active: the next click on the page completes the grab. */
|
|
6
|
+
grabbing: boolean;
|
|
7
|
+
/** The last completed grab, until a plugin clears it. */
|
|
8
|
+
payload: GrabPayload | null;
|
|
9
|
+
}
|
|
10
|
+
/** Grab mode for one HUD, shared by the shell and every plugin's `ctx.grab`. */
|
|
11
|
+
interface HudGrab {
|
|
12
|
+
start: () => void;
|
|
13
|
+
cancel: () => void;
|
|
14
|
+
/** Forgets the last payload, e.g. once it has been dispatched. */
|
|
15
|
+
clear: () => void;
|
|
16
|
+
getState: () => HudGrabState;
|
|
17
|
+
subscribe: (listener: () => void) => () => void;
|
|
18
|
+
}
|
|
19
|
+
/** Subscribes a component to a HUD's grab state: `useGrab(ctx.grab)`. */
|
|
20
|
+
declare function useGrab(grab: HudGrab): HudGrabState;
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/core/plugins.d.ts
|
|
23
|
+
/**
|
|
24
|
+
* A capability a plugin needs, such as `"cicd.read"`. Core assumes development
|
|
25
|
+
* and grants everything; the `access` plugin brings a matrix for deployed tiers.
|
|
26
|
+
*/
|
|
27
|
+
type HudCapability = string;
|
|
28
|
+
type PinEdge = "left" | "right" | "bottom";
|
|
29
|
+
/** What a plugin's panel and pill receive from the shell. */
|
|
30
|
+
interface HudPluginContext {
|
|
31
|
+
/** App slug from `defineHud({ app })`. */
|
|
32
|
+
app: string;
|
|
33
|
+
/** Whether the current tier grants `capability`. */
|
|
34
|
+
can: (capability: HudCapability) => boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Fetches JSON from this plugin's server routes: `api("/runs")` requests
|
|
37
|
+
* `<apiBase>/<plugin id>/runs`. Rejects with `HudApiError` on a non-2xx status.
|
|
38
|
+
*/
|
|
39
|
+
api: <T = unknown>(path: string, init?: RequestInit) => Promise<T>;
|
|
40
|
+
/** Pins this plugin's panel to an edge. Below 768 px the pin is kept but not applied. */
|
|
41
|
+
pin: (edge: PinEdge) => void;
|
|
42
|
+
/** Closes this plugin's panel. */
|
|
43
|
+
close: () => void;
|
|
44
|
+
/** Opens this plugin's panel. */
|
|
45
|
+
open: () => void;
|
|
46
|
+
/** The HUD's element grabber: start a grab, read or clear the last payload. See `useGrab`. */
|
|
47
|
+
grab: HudGrab;
|
|
48
|
+
}
|
|
49
|
+
interface HudPluginProps {
|
|
50
|
+
ctx: HudPluginContext;
|
|
51
|
+
}
|
|
52
|
+
interface HudCommand {
|
|
53
|
+
/** Unique within the plugin. */
|
|
54
|
+
id: string;
|
|
55
|
+
title: string;
|
|
56
|
+
/** Shortcut in `mod+shift+k` form; `mod` is ⌘ on macOS and Ctrl elsewhere. */
|
|
57
|
+
shortcut?: string;
|
|
58
|
+
run: (ctx: HudPluginContext) => void;
|
|
59
|
+
}
|
|
60
|
+
type HudHttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
61
|
+
interface HudRouteContext {
|
|
62
|
+
/** Values of `:name` segments in the matched path. */
|
|
63
|
+
params: Record<string, string>;
|
|
64
|
+
}
|
|
65
|
+
type HudRouteHandler = (request: Request, ctx: HudRouteContext) => Response | Promise<Response>;
|
|
66
|
+
/**
|
|
67
|
+
* Server routes keyed by path below the plugin's prefix, then by method:
|
|
68
|
+
* `{ "/runs/:id": { GET } }` serves `GET <apiBase>/<plugin id>/runs/42`.
|
|
69
|
+
*/
|
|
70
|
+
type HudRoutes = Record<string, Partial<Record<HudHttpMethod, HudRouteHandler>>>;
|
|
71
|
+
/**
|
|
72
|
+
* The plugin contract. A plugin ships as two subpath exports that share this
|
|
73
|
+
* type: `@hansenexus/hud/plugins/<id>` carries the client half (`panel`,
|
|
74
|
+
* `pill`, `commands`) and `@hansenexus/hud/plugins/<id>/server` the server half
|
|
75
|
+
* (`routes`), so route code never reaches a client bundle and React never
|
|
76
|
+
* reaches the route handler. Both halves carry the same `id` and `capability`.
|
|
77
|
+
*/
|
|
78
|
+
interface HudPlugin {
|
|
79
|
+
/** Unique per HUD; also the URL segment of the plugin's routes. */
|
|
80
|
+
id: string;
|
|
81
|
+
/** Panel title and pill label when the plugin has no `pill`. Defaults to `id`. */
|
|
82
|
+
title?: string;
|
|
83
|
+
/** Without it the plugin is dropped, client and server side. */
|
|
84
|
+
capability?: HudCapability;
|
|
85
|
+
/** Floating panel content, opened from the plugin's pill item. */
|
|
86
|
+
panel?: ComponentType<HudPluginProps>;
|
|
87
|
+
/** Live content inside the plugin's pill item. Keep it inline and non-interactive. */
|
|
88
|
+
pill?: ComponentType<HudPluginProps>;
|
|
89
|
+
commands?: readonly HudCommand[];
|
|
90
|
+
/** Called when a grab (⌘⇧G or the pill's crosshair) completes. */
|
|
91
|
+
onGrab?: (payload: GrabPayload, ctx: HudPluginContext) => void;
|
|
92
|
+
routes?: HudRoutes;
|
|
93
|
+
/** Opens deployed tiers (the `access` plugin). Without one the HUD renders only in development. */
|
|
94
|
+
access?: HudAccessControl;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* What the `access` plugin hands `defineHud`. `env` is the loader's tier
|
|
98
|
+
* (`development`, `preview`, `staging`, `production`, `disabled`).
|
|
99
|
+
*/
|
|
100
|
+
interface HudAccessControl {
|
|
101
|
+
/** Capability check for the tier; outside development it narrows `can`. */
|
|
102
|
+
can: (env: string, capability: HudCapability) => boolean;
|
|
103
|
+
/** Hook: whether the operator has unlocked this tier. Until then only the access plugin shows. */
|
|
104
|
+
useUnlocked: (env: string, apiBase: string) => boolean;
|
|
105
|
+
}
|
|
106
|
+
/** Identity helper that types a plugin object without widening it. */
|
|
107
|
+
declare function definePlugin<const P extends HudPlugin>(plugin: P): P;
|
|
108
|
+
declare class HudApiError extends Error {
|
|
109
|
+
readonly status: number;
|
|
110
|
+
readonly body: unknown;
|
|
111
|
+
constructor(status: number, body: unknown, url: string);
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
export { HudHttpMethod as a, HudPluginProps as c, HudRoutes as d, PinEdge as f, useGrab as g, HudGrabState as h, HudCommand as i, HudRouteContext as l, HudGrab as m, HudApiError as n, HudPlugin as o, definePlugin as p, HudCapability as r, HudPluginContext as s, HudAccessControl as t, HudRouteHandler as u };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//#region src/core/plugins.ts
|
|
2
|
+
const HUD_HTTP_METHODS = [
|
|
3
|
+
"GET",
|
|
4
|
+
"POST",
|
|
5
|
+
"PUT",
|
|
6
|
+
"PATCH",
|
|
7
|
+
"DELETE"
|
|
8
|
+
];
|
|
9
|
+
/** Identity helper that types a plugin object without widening it. */
|
|
10
|
+
function definePlugin(plugin) {
|
|
11
|
+
return plugin;
|
|
12
|
+
}
|
|
13
|
+
const PLUGIN_ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
14
|
+
/**
|
|
15
|
+
* Validates plugin ids and drops plugins whose capability `can` denies.
|
|
16
|
+
* Ids must be unique and URL-safe because they become route segments.
|
|
17
|
+
*/
|
|
18
|
+
function resolvePlugins(plugins, can) {
|
|
19
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20
|
+
for (const plugin of plugins) {
|
|
21
|
+
if (!PLUGIN_ID.test(plugin.id)) throw new Error(`hud: plugin id "${plugin.id}" must match ${PLUGIN_ID}`);
|
|
22
|
+
if (seen.has(plugin.id)) throw new Error(`hud: duplicate plugin id "${plugin.id}"`);
|
|
23
|
+
seen.add(plugin.id);
|
|
24
|
+
}
|
|
25
|
+
return plugins.filter((plugin) => plugin.capability === void 0 || can(plugin.capability));
|
|
26
|
+
}
|
|
27
|
+
var HudApiError = class extends Error {
|
|
28
|
+
status;
|
|
29
|
+
body;
|
|
30
|
+
constructor(status, body, url) {
|
|
31
|
+
super(`hud: ${url} answered ${status}`);
|
|
32
|
+
this.status = status;
|
|
33
|
+
this.body = body;
|
|
34
|
+
this.name = "HudApiError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function createPluginContext(plugin, { app, apiBase, can, pin, close, open, grab }) {
|
|
38
|
+
const prefix = `${apiBase.replace(/\/+$/, "")}/${plugin.id}`;
|
|
39
|
+
return {
|
|
40
|
+
app,
|
|
41
|
+
can,
|
|
42
|
+
pin,
|
|
43
|
+
close,
|
|
44
|
+
open,
|
|
45
|
+
grab,
|
|
46
|
+
api: async (path, init) => {
|
|
47
|
+
const url = `${prefix}/${path.replace(/^\/+/, "")}`;
|
|
48
|
+
const headers = new Headers(init?.headers);
|
|
49
|
+
if (!headers.has("accept")) headers.set("accept", "application/json");
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
...init,
|
|
52
|
+
headers
|
|
53
|
+
});
|
|
54
|
+
const body = parseBody(await res.text());
|
|
55
|
+
if (!res.ok) throw new HudApiError(res.status, body, url);
|
|
56
|
+
return body;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** JSON when it parses, the raw text otherwise (an HTML error page, say). */
|
|
61
|
+
function parseBody(text) {
|
|
62
|
+
if (!text) return void 0;
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(text);
|
|
65
|
+
} catch {
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { resolvePlugins as a, definePlugin as i, HudApiError as n, createPluginContext as r, HUD_HTTP_METHODS as t };
|