@dunx/dashboard 1.2.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/LICENSE +21 -0
- package/README.md +242 -0
- package/dist/api/bounded.d.ts +14 -0
- package/dist/api/redis.d.ts +9 -0
- package/dist/api/runtime.d.ts +13 -0
- package/dist/api/snapshot.d.ts +13 -0
- package/dist/api/types.d.ts +110 -0
- package/dist/board.d.ts +42 -0
- package/dist/chunk-xg5554k6.js +107 -0
- package/dist/chunk-xg5554k6.js.map +10 -0
- package/dist/contracts.d.ts +93 -0
- package/dist/html.d.ts +4 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +460 -0
- package/dist/index.js.map +17 -0
- package/dist/middleware.d.ts +32 -0
- package/dist/module.d.ts +44 -0
- package/dist/options.d.ts +152 -0
- package/dist/router.d.ts +17 -0
- package/dist/ui-bundle.d.ts +4 -0
- package/dist/ui.d.ts +21 -0
- package/dist/ui.js +42 -0
- package/dist/ui.js.map +12 -0
- package/package.json +90 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the dashboard needs from the things it reports on, restated structurally.
|
|
3
|
+
*
|
|
4
|
+
* **This package depends on `@dunx/infra` not at all**, and on `bullmq` not at
|
|
5
|
+
* all - the same choice `@dunx/auth` makes with `DrizzleSource` and `RedisStore`.
|
|
6
|
+
* A dashboard that peer-depended on the queue library would oblige an app with no
|
|
7
|
+
* queues to install it to see its routes, and would put a build-order edge between
|
|
8
|
+
* two packages that never call each other.
|
|
9
|
+
*
|
|
10
|
+
* This list used to be twice as long. `DashboardQueue` and `DashboardJob` restated
|
|
11
|
+
* bullmq's `Queue` and `Job` in enough detail to drive a queue table - signatures
|
|
12
|
+
* shaped to satisfy bullmq's own variance, with a paragraph explaining why. All of
|
|
13
|
+
* it went when bull-board took the queue UI back: the queue object is now passed
|
|
14
|
+
* straight through to `BullMQAdapter`, so there is nothing left to describe.
|
|
15
|
+
*
|
|
16
|
+
* Everything here is satisfied by an object an app already has:
|
|
17
|
+
*
|
|
18
|
+
* | This | Satisfied by |
|
|
19
|
+
* | ---------------- | ---------------------------------------- |
|
|
20
|
+
* | `QueueSource` | `JobPublisher` from `@dunx/infra/queue` |
|
|
21
|
+
* | `RedisProbe` | `RedisConnection` from `@dunx/infra/redis` |
|
|
22
|
+
* | `ConfigValues` | `ConfigService` from `@dunx/core` |
|
|
23
|
+
*
|
|
24
|
+
* No adapter, no wrapper - `queues: publisher` in the options is the whole wiring,
|
|
25
|
+
* which is what makes the restatement worth its lines rather than a tax.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* The validated configuration. `ConfigService` satisfies it as written.
|
|
29
|
+
*
|
|
30
|
+
* Passed in rather than resolved from the container, and that is deliberate twice
|
|
31
|
+
* over. Mechanically, `inject()` only works inside a class the container builds and
|
|
32
|
+
* this middleware is built by a factory. But the better reason is that showing an
|
|
33
|
+
* app's configuration should be something the app **says yes to** - the same
|
|
34
|
+
* instinct behind `reveal` defaulting to revealing nothing.
|
|
35
|
+
*/
|
|
36
|
+
export interface ConfigValues {
|
|
37
|
+
/**
|
|
38
|
+
* `object`, not `Record<string, unknown>`: an app's `AppConfig` is an interface
|
|
39
|
+
* with no index signature, so the record type would reject the very
|
|
40
|
+
* `ConfigService` this exists to accept. It is enumerated, never read by a key
|
|
41
|
+
* this package knows.
|
|
42
|
+
*/
|
|
43
|
+
readonly values: object;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Where queues come from. `JobPublisher` satisfies it as written.
|
|
47
|
+
*
|
|
48
|
+
* `opened` is what the publisher has opened *so far*, which is deliberately not the
|
|
49
|
+
* same as "every queue this app has" - a queue is a key prefix opened on first use,
|
|
50
|
+
* so a web process that has published to none has opened none. That is why
|
|
51
|
+
* `DashboardOptions.queueNames` exists: a process that consumes a queue it never
|
|
52
|
+
* publishes to has to name it, and the panel says which of the two it is showing.
|
|
53
|
+
*/
|
|
54
|
+
export interface QueueSource {
|
|
55
|
+
readonly opened: readonly string[];
|
|
56
|
+
/**
|
|
57
|
+
* bullmq's `Queue`, handed to bull-board's `BullMQAdapter` untouched - which is
|
|
58
|
+
* why the return type is `unknown` rather than a restatement. dunx reads nothing
|
|
59
|
+
* off it and calls nothing on it; matching bullmq's own signatures here was a
|
|
60
|
+
* whole file of variance notes existing only to describe a UI dunx no longer
|
|
61
|
+
* renders.
|
|
62
|
+
*/
|
|
63
|
+
queue(name: string): unknown;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Enough Redis to answer "is it up and what is it doing". `RedisConnection`
|
|
67
|
+
* satisfies it, and so does `Bun.RedisClient` with a `send`.
|
|
68
|
+
*
|
|
69
|
+
* `send` rather than a typed `info()`: `INFO` is one command whose reply is a text
|
|
70
|
+
* blob, and adding a method per Redis command to a restatement is how a
|
|
71
|
+
* restatement becomes a client library.
|
|
72
|
+
*/
|
|
73
|
+
export interface RedisProbe {
|
|
74
|
+
readonly connected: boolean;
|
|
75
|
+
ping(message?: string): Promise<string>;
|
|
76
|
+
send(command: string, args?: readonly string[]): Promise<unknown>;
|
|
77
|
+
}
|
|
78
|
+
/** The state a probe reports. `unknown` is not `down`; see `StatusDot`. */
|
|
79
|
+
export type ProbeState = 'up' | 'down' | 'unknown';
|
|
80
|
+
export interface ProbeResult {
|
|
81
|
+
readonly state: ProbeState;
|
|
82
|
+
/** One line for the operator: a latency, a version, a failure message. */
|
|
83
|
+
readonly detail?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Anything else worth a light on the page - a third-party API, a disk, a leader
|
|
87
|
+
* election. The dashboard awaits it with a timeout and never lets it throw into a
|
|
88
|
+
* response, so a probe that hangs costs one panel rather than the page.
|
|
89
|
+
*/
|
|
90
|
+
export interface DashboardProbe {
|
|
91
|
+
readonly name: string;
|
|
92
|
+
check(): Promise<ProbeResult> | ProbeResult;
|
|
93
|
+
}
|
package/dist/html.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DashboardOptions } from './options.js';
|
|
2
|
+
/** The id the bundle reads its meta from. Shared with `internal/dashboard-ui`. */
|
|
3
|
+
export declare const META_ELEMENT_ID = "dunx-dashboard-meta";
|
|
4
|
+
export declare const renderShell: (options: DashboardOptions, ui: string, favicon: string) => string;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { DashboardModule } from './module.js';
|
|
2
|
+
export { DashboardMiddleware } from './middleware.js';
|
|
3
|
+
export { DashboardOptions, normalizeMount, type Authorize, type DashboardOptionsInit, type Reveal, } from './options.js';
|
|
4
|
+
export type { ConfigValues, DashboardProbe, ProbeResult, ProbeState, QueueSource, RedisProbe, } from './contracts.js';
|
|
5
|
+
export type { ConfigEntry, GatewayNode, MemoryReport, Meta, ModuleNode, ProbeReport, ProviderNode, QueuesReport, RedisAbsent, RedisReport, RouteNode, RuntimeReport, Snapshot, } from './api/types.js';
|
|
6
|
+
export { handleDashboard, type RouterDeps } from './router.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
__decorateElement,
|
|
4
|
+
__decoratorMetadata,
|
|
5
|
+
__decoratorStart,
|
|
6
|
+
__require,
|
|
7
|
+
__runInitializers,
|
|
8
|
+
snapshotOf
|
|
9
|
+
} from "./chunk-xg5554k6.js";
|
|
10
|
+
|
|
11
|
+
// src/module.ts
|
|
12
|
+
import {
|
|
13
|
+
Logger as Logger2,
|
|
14
|
+
Module,
|
|
15
|
+
provide,
|
|
16
|
+
ROOT_MODULE
|
|
17
|
+
} from "@dunx/core";
|
|
18
|
+
|
|
19
|
+
// src/middleware.ts
|
|
20
|
+
import { Logger } from "@dunx/core";
|
|
21
|
+
|
|
22
|
+
// src/board.ts
|
|
23
|
+
var load = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const [api, bullmq, bun] = await Promise.all([
|
|
26
|
+
import("@bull-board/api"),
|
|
27
|
+
import("@bull-board/api/bullMQAdapter"),
|
|
28
|
+
import("@bull-board/bun")
|
|
29
|
+
]);
|
|
30
|
+
return {
|
|
31
|
+
createBullBoard: api.createBullBoard,
|
|
32
|
+
BullMQAdapter: bullmq.BullMQAdapter,
|
|
33
|
+
BunAdapter: bun.BunAdapter
|
|
34
|
+
};
|
|
35
|
+
} catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var boardNames = (options) => {
|
|
40
|
+
if (options.queues === undefined) {
|
|
41
|
+
return {
|
|
42
|
+
names: [],
|
|
43
|
+
unavailable: "This app passed no `queues` to DashboardModule. `JobPublisher` from " + "@dunx/infra/queue satisfies it as written."
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const opened = options.queues.opened;
|
|
47
|
+
const names = [
|
|
48
|
+
...opened,
|
|
49
|
+
...options.queueNames.filter((name) => !opened.includes(name))
|
|
50
|
+
].sort();
|
|
51
|
+
if (names.length === 0) {
|
|
52
|
+
return {
|
|
53
|
+
names,
|
|
54
|
+
unavailable: "This process has opened no queues. A queue it only consumes has never " + "been opened by the publisher - name it in DashboardOptions.queueNames."
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { names };
|
|
58
|
+
};
|
|
59
|
+
var uiConfigFor = (options, favicon) => ({
|
|
60
|
+
readOnlyMode: !options.commands,
|
|
61
|
+
boardTitle: `${options.title} queues`,
|
|
62
|
+
boardLogo: { path: favicon, width: 26, height: 26 },
|
|
63
|
+
favIcon: { default: favicon, alternative: favicon }
|
|
64
|
+
});
|
|
65
|
+
var buildBoard = async (options, basePath, favicon) => {
|
|
66
|
+
const { names, unavailable } = boardNames(options);
|
|
67
|
+
if (unavailable !== undefined || options.queues === undefined) {
|
|
68
|
+
return { unavailable: unavailable ?? "no queue source", queues: names };
|
|
69
|
+
}
|
|
70
|
+
const modules = await load();
|
|
71
|
+
if (modules === undefined) {
|
|
72
|
+
return {
|
|
73
|
+
unavailable: "bull-board is not installed. It is an optional peer, so add it with " + "`bun add @bull-board/api @bull-board/ui @bull-board/bun`.",
|
|
74
|
+
queues: names
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const { createBullBoard, BullMQAdapter, BunAdapter } = modules;
|
|
78
|
+
const source = options.queues;
|
|
79
|
+
const serverAdapter = new BunAdapter;
|
|
80
|
+
serverAdapter.setBasePath(basePath);
|
|
81
|
+
createBullBoard({
|
|
82
|
+
queues: names.map((name) => new BullMQAdapter(source.queue(name))),
|
|
83
|
+
serverAdapter,
|
|
84
|
+
options: { uiConfig: uiConfigFor(options, favicon) }
|
|
85
|
+
});
|
|
86
|
+
const routes = serverAdapter.getRoutes();
|
|
87
|
+
const entry = routes[basePath]?.["GET"];
|
|
88
|
+
return {
|
|
89
|
+
routes,
|
|
90
|
+
queues: names,
|
|
91
|
+
...entry === undefined ? {} : { entry }
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
var compile = (routes) => Object.entries(routes).map(([pattern, handlers]) => {
|
|
95
|
+
const raw = pattern.split("/").filter(Boolean);
|
|
96
|
+
const wildcard = raw.at(-1) === "*";
|
|
97
|
+
const segments = wildcard ? raw.slice(0, -1) : raw;
|
|
98
|
+
return {
|
|
99
|
+
segments,
|
|
100
|
+
wildcard,
|
|
101
|
+
literals: segments.filter((s) => !s.startsWith(":")).length,
|
|
102
|
+
handlers
|
|
103
|
+
};
|
|
104
|
+
}).sort((a, b) => b.literals - a.literals || b.segments.length - a.segments.length || Number(a.wildcard) - Number(b.wildcard));
|
|
105
|
+
var matches = (pattern, path) => {
|
|
106
|
+
if (pattern.wildcard) {
|
|
107
|
+
if (path.length < pattern.segments.length)
|
|
108
|
+
return false;
|
|
109
|
+
} else if (path.length !== pattern.segments.length) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return pattern.segments.every((segment, index) => segment.startsWith(":") || segment === path[index]);
|
|
113
|
+
};
|
|
114
|
+
var matchBoard = (routes, method, pathname) => {
|
|
115
|
+
const exact = routes[pathname]?.[method];
|
|
116
|
+
if (exact)
|
|
117
|
+
return exact;
|
|
118
|
+
const path = pathname.split("/").filter(Boolean);
|
|
119
|
+
for (const pattern of compile(routes)) {
|
|
120
|
+
if (matches(pattern, path)) {
|
|
121
|
+
const handler = pattern.handlers[method];
|
|
122
|
+
if (handler)
|
|
123
|
+
return handler;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/options.ts
|
|
130
|
+
class DashboardOptions {
|
|
131
|
+
path;
|
|
132
|
+
authorize;
|
|
133
|
+
title;
|
|
134
|
+
queues;
|
|
135
|
+
queueNames;
|
|
136
|
+
redis;
|
|
137
|
+
probes;
|
|
138
|
+
config;
|
|
139
|
+
reveal;
|
|
140
|
+
openApiPath;
|
|
141
|
+
pollMs;
|
|
142
|
+
probeTimeoutMs;
|
|
143
|
+
commands;
|
|
144
|
+
constructor(init = {}) {
|
|
145
|
+
this.path = normalizeMount(init.path ?? "/_dunx");
|
|
146
|
+
this.authorize = init.authorize;
|
|
147
|
+
this.title = init.title ?? "dunx";
|
|
148
|
+
this.queues = init.queues;
|
|
149
|
+
this.queueNames = init.queueNames ?? [];
|
|
150
|
+
this.redis = init.redis;
|
|
151
|
+
this.probes = init.probes ?? [];
|
|
152
|
+
this.config = init.config;
|
|
153
|
+
this.reveal = init.reveal ?? (() => false);
|
|
154
|
+
this.openApiPath = init.openApiPath;
|
|
155
|
+
this.pollMs = init.pollMs ?? 5000;
|
|
156
|
+
this.probeTimeoutMs = init.probeTimeoutMs ?? 2000;
|
|
157
|
+
this.commands = init.commands ?? true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
Object.defineProperty(DashboardOptions, Symbol.for("dunx.deps"), {
|
|
161
|
+
value: () => [{ unresolved: "init: DashboardOptionsInit = {}" }]
|
|
162
|
+
});
|
|
163
|
+
var normalizeMount = (path) => {
|
|
164
|
+
const trimmed = `/${path.split("/").filter(Boolean).join("/")}`;
|
|
165
|
+
if (trimmed === "/") {
|
|
166
|
+
throw new Error('DashboardOptions.path cannot be "/": the dashboard is a middleware that ' + "claims every path under its mount, so mounting it at the root would " + 'answer every request in the app. Use "/_dunx" or another prefix.');
|
|
167
|
+
}
|
|
168
|
+
return trimmed;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/api/bounded.ts
|
|
172
|
+
var bounded = async (work, ms, onTimeout) => {
|
|
173
|
+
let timer;
|
|
174
|
+
try {
|
|
175
|
+
return await Promise.race([
|
|
176
|
+
work(),
|
|
177
|
+
new Promise((resolve) => {
|
|
178
|
+
timer = setTimeout(() => resolve(onTimeout()), ms);
|
|
179
|
+
})
|
|
180
|
+
]);
|
|
181
|
+
} finally {
|
|
182
|
+
if (timer !== undefined)
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/api/redis.ts
|
|
188
|
+
var message = (error) => error instanceof Error ? error.message : String(error);
|
|
189
|
+
var INFO_FIELDS = [
|
|
190
|
+
"redis_version",
|
|
191
|
+
"redis_mode",
|
|
192
|
+
"os",
|
|
193
|
+
"uptime_in_seconds",
|
|
194
|
+
"connected_clients",
|
|
195
|
+
"blocked_clients",
|
|
196
|
+
"used_memory_human",
|
|
197
|
+
"used_memory_peak_human",
|
|
198
|
+
"maxmemory_human",
|
|
199
|
+
"keyspace_hits",
|
|
200
|
+
"keyspace_misses",
|
|
201
|
+
"total_commands_processed",
|
|
202
|
+
"rejected_connections"
|
|
203
|
+
];
|
|
204
|
+
var parseInfo = (raw) => {
|
|
205
|
+
const wanted = new Set(INFO_FIELDS);
|
|
206
|
+
const out = {};
|
|
207
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
208
|
+
if (line === "" || line.startsWith("#"))
|
|
209
|
+
continue;
|
|
210
|
+
const colon = line.indexOf(":");
|
|
211
|
+
if (colon < 1)
|
|
212
|
+
continue;
|
|
213
|
+
const key = line.slice(0, colon);
|
|
214
|
+
if (wanted.has(key))
|
|
215
|
+
out[key] = line.slice(colon + 1);
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
};
|
|
219
|
+
var redisReport = async (redis, timeoutMs) => {
|
|
220
|
+
const started = performance.now();
|
|
221
|
+
const failed = (error) => ({
|
|
222
|
+
configured: true,
|
|
223
|
+
connected: redis.connected,
|
|
224
|
+
pingMs: undefined,
|
|
225
|
+
info: {},
|
|
226
|
+
error
|
|
227
|
+
});
|
|
228
|
+
return bounded(async () => {
|
|
229
|
+
try {
|
|
230
|
+
await redis.ping();
|
|
231
|
+
const pingMs = Math.round(performance.now() - started);
|
|
232
|
+
const raw = await redis.send("INFO", []);
|
|
233
|
+
return {
|
|
234
|
+
configured: true,
|
|
235
|
+
connected: redis.connected,
|
|
236
|
+
pingMs,
|
|
237
|
+
info: typeof raw === "string" ? parseInfo(raw) : {}
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return failed(message(error));
|
|
241
|
+
}
|
|
242
|
+
}, timeoutMs, () => failed(`no answer in ${timeoutMs}ms`));
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/api/runtime.ts
|
|
246
|
+
var withTimeout = (probe, ms) => bounded(async () => {
|
|
247
|
+
try {
|
|
248
|
+
return await probe.check();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return {
|
|
251
|
+
state: "down",
|
|
252
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}, ms, () => ({ state: "unknown", detail: `no answer in ${ms}ms` }));
|
|
256
|
+
var runProbe = async (probe, timeoutMs) => {
|
|
257
|
+
const started = performance.now();
|
|
258
|
+
const result = await withTimeout(probe, timeoutMs);
|
|
259
|
+
return {
|
|
260
|
+
name: probe.name,
|
|
261
|
+
state: result.state,
|
|
262
|
+
...result.detail === undefined ? {} : { detail: result.detail },
|
|
263
|
+
ms: Math.round(performance.now() - started)
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
var redisProbe = (redis) => ({
|
|
267
|
+
name: "redis",
|
|
268
|
+
check: async () => {
|
|
269
|
+
const started = performance.now();
|
|
270
|
+
await redis.ping();
|
|
271
|
+
return {
|
|
272
|
+
state: "up",
|
|
273
|
+
detail: `PING ${Math.round(performance.now() - started)}ms`
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
var memory = () => {
|
|
278
|
+
const usage = process.memoryUsage();
|
|
279
|
+
return {
|
|
280
|
+
rss: usage.rss,
|
|
281
|
+
heapUsed: usage.heapUsed,
|
|
282
|
+
heapTotal: usage.heapTotal,
|
|
283
|
+
external: usage.external
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
var runtimeReport = async (options, startedAt) => {
|
|
287
|
+
const probes = [
|
|
288
|
+
...options.redis ? [redisProbe(options.redis)] : [],
|
|
289
|
+
...options.probes
|
|
290
|
+
];
|
|
291
|
+
return {
|
|
292
|
+
pid: process.pid,
|
|
293
|
+
uptimeMs: Math.round(performance.now() - startedAt),
|
|
294
|
+
bun: Bun.version,
|
|
295
|
+
platform: process.platform,
|
|
296
|
+
arch: process.arch,
|
|
297
|
+
memory: memory(),
|
|
298
|
+
probes: await Promise.all(probes.map((probe) => runProbe(probe, options.probeTimeoutMs))),
|
|
299
|
+
now: Date.now()
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// src/router.ts
|
|
304
|
+
var json = (body, status = 200) => Response.json(body, {
|
|
305
|
+
status,
|
|
306
|
+
headers: { "cache-control": "no-store" }
|
|
307
|
+
});
|
|
308
|
+
var fail = (status, error) => json({ error }, status);
|
|
309
|
+
var handleApi = async (deps, method, segments) => {
|
|
310
|
+
if (method !== "GET")
|
|
311
|
+
return fail(405, `${method} is not allowed here`);
|
|
312
|
+
switch (segments[0]) {
|
|
313
|
+
case "snapshot":
|
|
314
|
+
return json(snapshotOf(deps.root, deps.options));
|
|
315
|
+
case "runtime":
|
|
316
|
+
return json(await runtimeReport(deps.options, deps.startedAt));
|
|
317
|
+
case "redis":
|
|
318
|
+
return json(deps.options.redis === undefined ? { configured: false } : await redisReport(deps.options.redis, deps.options.probeTimeoutMs));
|
|
319
|
+
case "queues": {
|
|
320
|
+
const { names, unavailable } = boardNames(deps.options);
|
|
321
|
+
return json({
|
|
322
|
+
queues: names,
|
|
323
|
+
...unavailable === undefined ? {} : { unavailable }
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
default:
|
|
327
|
+
return fail(404, "no such dashboard endpoint");
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
var handleDashboard = async (deps, request, rest) => {
|
|
331
|
+
const segments = rest.split("/").filter(Boolean);
|
|
332
|
+
const { method } = request;
|
|
333
|
+
if (segments[0] === "api") {
|
|
334
|
+
try {
|
|
335
|
+
return await handleApi(deps, method, segments.slice(1));
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return fail(500, error instanceof Error ? error.message : String(error));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (segments[0] === "queues") {
|
|
341
|
+
const board = await deps.board();
|
|
342
|
+
if (board.routes === undefined) {
|
|
343
|
+
return fail(503, board.unavailable ?? "no queue board");
|
|
344
|
+
}
|
|
345
|
+
const handler = matchBoard(board.routes, method, new URL(request.url).pathname);
|
|
346
|
+
if (handler !== undefined)
|
|
347
|
+
return handler(request);
|
|
348
|
+
if (method === "GET" && board.entry)
|
|
349
|
+
return board.entry(request);
|
|
350
|
+
return fail(404, "no such bull-board route");
|
|
351
|
+
}
|
|
352
|
+
if (method !== "GET")
|
|
353
|
+
return fail(405, `${method} is not allowed here`);
|
|
354
|
+
return new Response(await deps.page(), {
|
|
355
|
+
headers: {
|
|
356
|
+
"content-type": "text/html; charset=utf-8",
|
|
357
|
+
"cache-control": "no-store"
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/middleware.ts
|
|
363
|
+
class DashboardMiddleware {
|
|
364
|
+
#options;
|
|
365
|
+
#deps;
|
|
366
|
+
#prefix;
|
|
367
|
+
#page;
|
|
368
|
+
#board;
|
|
369
|
+
constructor(options, root, logger) {
|
|
370
|
+
this.#options = options;
|
|
371
|
+
this.#prefix = `${options.path}/`;
|
|
372
|
+
this.#deps = {
|
|
373
|
+
root,
|
|
374
|
+
options,
|
|
375
|
+
startedAt: performance.now(),
|
|
376
|
+
page: () => this.#renderPage(),
|
|
377
|
+
board: () => this.#buildBoard()
|
|
378
|
+
};
|
|
379
|
+
if (options.authorize === undefined) {
|
|
380
|
+
logger.warn(`The dashboard at ${options.path} has no authorize function, so it is ` + "served to anyone who can reach this port - including the route table, " + "the provider graph and the config keys. Pass " + "DashboardModule.forRoot({ authorize }) unless this port is private.");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async handle(req, _ctx, next) {
|
|
384
|
+
const url = new URL(req.url);
|
|
385
|
+
const { pathname } = url;
|
|
386
|
+
if (pathname !== this.#options.path && !pathname.startsWith(this.#prefix)) {
|
|
387
|
+
return next();
|
|
388
|
+
}
|
|
389
|
+
if (this.#options.authorize && !await this.#options.authorize(req)) {
|
|
390
|
+
return Response.json({ error: "NOT_FOUND", status: 404 }, { status: 404 });
|
|
391
|
+
}
|
|
392
|
+
const rest = pathname.slice(this.#options.path.length);
|
|
393
|
+
return handleDashboard(this.#deps, req, rest);
|
|
394
|
+
}
|
|
395
|
+
#buildBoard() {
|
|
396
|
+
this.#board ??= import("./ui.js").then(({ FAVICON }) => buildBoard(this.#options, `${this.#options.path}/queues`, FAVICON));
|
|
397
|
+
return this.#board;
|
|
398
|
+
}
|
|
399
|
+
#renderPage() {
|
|
400
|
+
this.#page ??= import("./ui.js").then(({ renderPage }) => renderPage(this.#options));
|
|
401
|
+
return this.#page;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
Object.defineProperty(DashboardMiddleware, Symbol.for("dunx.deps"), {
|
|
405
|
+
value: () => [DashboardOptions, { unresolved: "root: ModuleRef", typeOnly: "ModuleRef" }, Logger]
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// src/module.ts
|
|
409
|
+
var middleware = () => provide(DashboardMiddleware, {
|
|
410
|
+
useFactory: (options, root, logger) => new DashboardMiddleware(options, root, logger),
|
|
411
|
+
inject: [DashboardOptions, ROOT_MODULE, Logger2]
|
|
412
|
+
});
|
|
413
|
+
var _dec = [
|
|
414
|
+
Module({})
|
|
415
|
+
];
|
|
416
|
+
var _init = __decoratorStart(undefined);
|
|
417
|
+
|
|
418
|
+
class DashboardModule {
|
|
419
|
+
static forRoot(init = {}) {
|
|
420
|
+
return {
|
|
421
|
+
module: DashboardModule,
|
|
422
|
+
exports: [DashboardOptions, DashboardMiddleware],
|
|
423
|
+
providers: [
|
|
424
|
+
provide(DashboardOptions, { useValue: new DashboardOptions(init) }),
|
|
425
|
+
middleware()
|
|
426
|
+
]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
static forRootAsync(source) {
|
|
430
|
+
const load2 = typeof source === "function" ? source : source.useFactory;
|
|
431
|
+
const inject = typeof source === "function" ? [] : source.inject ?? [];
|
|
432
|
+
const imports = typeof source === "function" ? [] : source.imports ?? [];
|
|
433
|
+
return {
|
|
434
|
+
module: DashboardModule,
|
|
435
|
+
imports,
|
|
436
|
+
exports: [DashboardOptions, DashboardMiddleware],
|
|
437
|
+
providers: [
|
|
438
|
+
provide(DashboardOptions, {
|
|
439
|
+
useFactory: async (...deps) => new DashboardOptions(await load2(...deps)),
|
|
440
|
+
inject
|
|
441
|
+
}),
|
|
442
|
+
middleware()
|
|
443
|
+
]
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
DashboardModule = __decorateElement(_init, 0, "DashboardModule", _dec, DashboardModule);
|
|
448
|
+
__runInitializers(_init, 1, DashboardModule);
|
|
449
|
+
__decoratorMetadata(_init, DashboardModule);
|
|
450
|
+
let _DashboardModule = DashboardModule;
|
|
451
|
+
export {
|
|
452
|
+
normalizeMount,
|
|
453
|
+
handleDashboard,
|
|
454
|
+
DashboardOptions,
|
|
455
|
+
DashboardModule,
|
|
456
|
+
DashboardMiddleware
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
//# debugId=9D760F53991891E164756E2164756E21
|
|
460
|
+
//# sourceMappingURL=index.js.map
|