@sparkelf/dsh-artifact-app 0.0.0-stage → 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/lib/client.js ADDED
@@ -0,0 +1,824 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@sparkelf/dsh-artifact-app",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/CockpitViewer.tsx
10
+ /**
11
+ * Sandboxed cockpit viewer.
12
+ *
13
+ * The iframe is served by the plugin's render route and runs with
14
+ * `sandbox="allow-scripts allow-same-origin"` (opaque origin). The workspace token reaches it only
15
+ * through postMessage, and only after this parent verified the message source.
16
+ *
17
+ * @module client/CockpitViewer
18
+ */
19
+ const frameStyle = {
20
+ width: "100%",
21
+ height: "100%",
22
+ minHeight: "520px",
23
+ border: "0",
24
+ display: "block",
25
+ background: "#060a14"
26
+ };
27
+ /**
28
+ * Render one cockpit in a sandboxed iframe and keep its bootstrap config fed.
29
+ * @param props.artifactId - cockpit (or preview draft) id.
30
+ * @param props.refreshKey - changing it reloads the iframe, used by "updated" events.
31
+ * @param props.reloadToken - bump from outside to force a reload.
32
+ */
33
+ function CockpitViewer({ artifactId, reloadToken }) {
34
+ const frameRef = (0, react.useRef)(null);
35
+ /** iframe 加载完成后重发一次引导配置:`hello` 可能在监听器挂上之前就发出去了。 */
36
+ const resendRef = (0, react.useRef)(null);
37
+ const [config, setConfig] = (0, react.useState)(null);
38
+ const [token, setToken] = (0, react.useState)(null);
39
+ const [error, setError] = (0, react.useState)(null);
40
+ (0, react.useEffect)(() => {
41
+ let alive = true;
42
+ (async () => {
43
+ try {
44
+ const [configResponse, tokenResponse] = await Promise.all([fetch("/dsh-artifact-app/config", { headers: { accept: "application/json" } }), fetch("/dsh-artifact-app/token", { headers: { accept: "application/json" } })]);
45
+ const configPayload = await configResponse.json();
46
+ const tokenPayload = await tokenResponse.json();
47
+ if (!configResponse.ok) throw new Error(configPayload.error ?? "读取驾驶舱配置失败");
48
+ if (!tokenResponse.ok || typeof tokenPayload.token !== "string") throw new Error(tokenPayload.error ?? "读取 DataOps 登录凭证失败");
49
+ if (alive) {
50
+ setConfig(configPayload);
51
+ setToken(tokenPayload.token);
52
+ }
53
+ } catch (cause) {
54
+ if (alive) setError(cause instanceof Error ? cause.message : String(cause));
55
+ }
56
+ })();
57
+ return () => {
58
+ alive = false;
59
+ };
60
+ }, []);
61
+ (0, react.useEffect)(() => {
62
+ if (config === null || token === null) return void 0;
63
+ /** 把引导配置推给 iframe;`hello` 与 iframe 的 load 事件都会触发它。 */
64
+ const sendConfig = () => {
65
+ frameRef.current?.contentWindow?.postMessage({
66
+ source: "dataops-artifact-host",
67
+ type: "config",
68
+ payload: {
69
+ artifactId,
70
+ token,
71
+ gatewayBaseUrl: config.gatewayBaseUrl,
72
+ refreshIntervalSeconds: 300,
73
+ keepConnection: false,
74
+ locale: "zh-CN",
75
+ theme: "dark"
76
+ }
77
+ }, "*");
78
+ };
79
+ const onMessage = (event) => {
80
+ const frame = frameRef.current;
81
+ if (frame === null || event.source !== frame.contentWindow) return;
82
+ const data = event.data;
83
+ if (data === null || typeof data !== "object" || data.source !== "dataops-artifact-app") return;
84
+ if (data.type === "request-access") {
85
+ const target = config.dataopsWebBaseUrl.replace(/\/+$/u, "");
86
+ window.open(target, "_blank", "noopener,noreferrer");
87
+ return;
88
+ }
89
+ if (data.type !== "hello" && data.type !== "ready" && data.type !== "token:expired") return;
90
+ sendConfig();
91
+ };
92
+ resendRef.current = sendConfig;
93
+ window.addEventListener("message", onMessage);
94
+ return () => {
95
+ resendRef.current = null;
96
+ window.removeEventListener("message", onMessage);
97
+ };
98
+ }, [
99
+ artifactId,
100
+ config,
101
+ token
102
+ ]);
103
+ if (error !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
104
+ role: "alert",
105
+ style: {
106
+ padding: 18,
107
+ color: "#b91c1c",
108
+ fontSize: 12
109
+ },
110
+ children: error
111
+ });
112
+ if (config === null || token === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
113
+ style: {
114
+ padding: 18,
115
+ color: "var(--dsw-alias-label-secondary, #6b7280)",
116
+ fontSize: 12
117
+ },
118
+ children: "正在准备驾驶舱…"
119
+ });
120
+ const src = config.renderBaseUrl.replace(/\/+$/u, "") + "/" + encodeURIComponent(artifactId) + (reloadToken === void 0 ? "" : "?r=" + String(reloadToken));
121
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
122
+ ref: frameRef,
123
+ title: "驾驶舱",
124
+ src,
125
+ sandbox: "allow-scripts allow-same-origin",
126
+ allow: "fullscreen",
127
+ onLoad: () => resendRef.current?.(),
128
+ style: frameStyle,
129
+ "data-artifact-viewer": artifactId
130
+ });
131
+ }
132
+ //#endregion
133
+ //#region src/client/CockpitTab.tsx
134
+ /**
135
+ * The `dataops-artifact-apps` right-sidebar tab.
136
+ *
137
+ * One pane holds the cockpit list and, once a card is opened, the sandboxed
138
+ * viewer. The list is proxied by the host so the browser holds no DataOps token
139
+ * for reads.
140
+ *
141
+ * @module client/CockpitTab
142
+ */
143
+ const readMeta = (value) => {
144
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
145
+ const record = value;
146
+ return {
147
+ cockpitId: typeof record.cockpitId === "string" ? record.cockpitId : void 0,
148
+ cockpitName: typeof record.cockpitName === "string" ? record.cockpitName : void 0,
149
+ nonce: typeof record.nonce === "string" ? record.nonce : void 0
150
+ };
151
+ };
152
+ const cardStyle = {
153
+ display: "grid",
154
+ gap: 4,
155
+ width: "100%",
156
+ minWidth: 0,
157
+ textAlign: "left",
158
+ padding: "10px 12px",
159
+ borderRadius: 10,
160
+ border: "1px solid var(--dsw-alias-border-l1, #e5e7eb)",
161
+ background: "var(--dsw-alias-bg-layer-1, #ffffff)",
162
+ color: "inherit",
163
+ cursor: "pointer"
164
+ };
165
+ const muted = {
166
+ fontSize: 10,
167
+ color: "var(--dsw-alias-label-secondary, #6b7280)"
168
+ };
169
+ /** Cockpit list plus viewer. */
170
+ function CockpitTab({ tab }) {
171
+ const meta = readMeta(tab.meta);
172
+ const [apps, setApps] = (0, react.useState)([]);
173
+ const [loading, setLoading] = (0, react.useState)(true);
174
+ const [error, setError] = (0, react.useState)(null);
175
+ const [selected, setSelected] = (0, react.useState)(meta.cockpitId === void 0 ? null : {
176
+ id: meta.cockpitId,
177
+ name: meta.cockpitName ?? "驾驶舱"
178
+ });
179
+ const [reloadToken, setReloadToken] = (0, react.useState)(0);
180
+ const load = (0, react.useCallback)(async () => {
181
+ setLoading(true);
182
+ setError(null);
183
+ try {
184
+ const response = await fetch("/dsh-artifact-app/apps?pageSize=50", { headers: { accept: "application/json" } });
185
+ const payload = await response.json();
186
+ if (!response.ok) throw new Error(payload.error ?? "HTTP " + String(response.status));
187
+ setApps(payload.items ?? []);
188
+ } catch (cause) {
189
+ setError(cause instanceof Error ? cause.message : String(cause));
190
+ } finally {
191
+ setLoading(false);
192
+ }
193
+ }, []);
194
+ (0, react.useEffect)(() => {
195
+ load();
196
+ }, [load]);
197
+ (0, react.useEffect)(() => {
198
+ if (meta.cockpitId === void 0) return;
199
+ setSelected({
200
+ id: meta.cockpitId,
201
+ name: meta.cockpitName ?? "驾驶舱"
202
+ });
203
+ setReloadToken((value) => value + 1);
204
+ }, [
205
+ meta.cockpitId,
206
+ meta.cockpitName,
207
+ meta.nonce
208
+ ]);
209
+ const catalogs = (0, react.useMemo)(() => [...new Set(apps.map((app) => app.catalog).filter((value) => typeof value === "string" && value !== ""))], [apps]);
210
+ /**
211
+ * 导出一份可脱离 DSH 单独分发的 HTML:服务端返回的 HTML 已烧死 appId 与网关地址,
212
+ * 这里再把运行时与 ECharts 内联进去,产物双击即可打开(地址栏加 #token=… 取数)。
213
+ */
214
+ const [exporting, setExporting] = (0, react.useState)(false);
215
+ const exportHtml = async () => {
216
+ if (selected === null || exporting) return;
217
+ setExporting(true);
218
+ try {
219
+ const base = "/dsh-artifact-app/gateway/api/artifact-apps";
220
+ const [htmlResponse, runtimeResponse, echartsResponse] = await Promise.all([
221
+ fetch(base.replace(/\/+$/u, "") + "/" + encodeURIComponent(selected.id) + "/html", { headers: { accept: "application/json" } }),
222
+ fetch(base.replace(/\/+$/u, "") + "/runtime.js"),
223
+ fetch(base.replace(/\/+$/u, "") + "/echarts.js")
224
+ ]);
225
+ if (!htmlResponse.ok) throw new Error("读取驾驶舱 HTML 失败");
226
+ const payload = await htmlResponse.json();
227
+ const runtime = runtimeResponse.ok ? await runtimeResponse.text() : "";
228
+ const echarts = echartsResponse.ok ? await echartsResponse.text() : "";
229
+ const inline = (runtime === "" ? "" : "<script>" + runtime + "<\/script>") + (echarts === "" ? "" : "<script>" + echarts + "<\/script>");
230
+ const html = (payload.html ?? "").replace("</body>", inline + "</body>");
231
+ const blob = new Blob([html], { type: "text/html;charset=utf-8" });
232
+ const url = URL.createObjectURL(blob);
233
+ const link = document.createElement("a");
234
+ link.href = url;
235
+ link.download = (selected.name || "cockpit") + ".html";
236
+ document.body.appendChild(link);
237
+ link.click();
238
+ link.remove();
239
+ URL.revokeObjectURL(url);
240
+ } catch (cause) {
241
+ setError(cause instanceof Error ? cause.message : String(cause));
242
+ } finally {
243
+ setExporting(false);
244
+ }
245
+ };
246
+ if (selected !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
247
+ style: {
248
+ display: "flex",
249
+ flexDirection: "column",
250
+ height: "100%",
251
+ minHeight: 0
252
+ },
253
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
254
+ style: {
255
+ display: "flex",
256
+ alignItems: "center",
257
+ gap: 8,
258
+ padding: "8px 10px",
259
+ borderBottom: "1px solid var(--dsw-alias-border-l1, #e5e7eb)"
260
+ },
261
+ children: [
262
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
263
+ type: "button",
264
+ onClick: () => setSelected(null),
265
+ style: {
266
+ border: "none",
267
+ background: "transparent",
268
+ color: "inherit",
269
+ cursor: "pointer",
270
+ fontSize: 12,
271
+ padding: 0
272
+ },
273
+ children: "← 列表"
274
+ }),
275
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
276
+ style: {
277
+ flex: 1,
278
+ minWidth: 0,
279
+ fontSize: 12,
280
+ fontWeight: 600,
281
+ overflow: "hidden",
282
+ textOverflow: "ellipsis",
283
+ whiteSpace: "nowrap"
284
+ },
285
+ children: selected.name
286
+ }),
287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
288
+ type: "button",
289
+ title: "刷新",
290
+ "aria-label": "刷新",
291
+ onClick: () => setReloadToken((value) => value + 1),
292
+ style: {
293
+ border: "none",
294
+ background: "transparent",
295
+ color: "inherit",
296
+ cursor: "pointer",
297
+ display: "grid",
298
+ placeItems: "center",
299
+ width: 26,
300
+ height: 26,
301
+ borderRadius: 6
302
+ },
303
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
304
+ width: "14",
305
+ height: "14",
306
+ viewBox: "0 0 24 24",
307
+ fill: "none",
308
+ stroke: "currentColor",
309
+ strokeWidth: "1.9",
310
+ strokeLinecap: "round",
311
+ strokeLinejoin: "round",
312
+ "aria-hidden": "true",
313
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 12a9 9 0 1 1-3-6.7" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 4v5h-5" })]
314
+ })
315
+ }),
316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
317
+ type: "button",
318
+ title: "全屏查看",
319
+ "aria-label": "全屏查看",
320
+ onClick: () => {
321
+ const target = document.querySelector("[data-artifact-fullscreen-host]") ?? document.querySelector("iframe[data-artifact-viewer]")?.parentElement ?? null;
322
+ if (target === null) return;
323
+ if (document.fullscreenElement !== null) document.exitFullscreen();
324
+ else target.requestFullscreen?.().catch(() => void 0);
325
+ },
326
+ style: {
327
+ border: "none",
328
+ background: "transparent",
329
+ color: "inherit",
330
+ cursor: "pointer",
331
+ display: "grid",
332
+ placeItems: "center",
333
+ width: 26,
334
+ height: 26,
335
+ borderRadius: 6
336
+ },
337
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
338
+ width: "14",
339
+ height: "14",
340
+ viewBox: "0 0 24 24",
341
+ fill: "none",
342
+ stroke: "currentColor",
343
+ strokeWidth: "1.9",
344
+ strokeLinecap: "round",
345
+ strokeLinejoin: "round",
346
+ "aria-hidden": "true",
347
+ children: [
348
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 9V4h5" }),
349
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M20 9V4h-5" }),
350
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 15v5h5" }),
351
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M20 15v5h-5" })
352
+ ]
353
+ })
354
+ }),
355
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
356
+ type: "button",
357
+ title: "导出 HTML",
358
+ "aria-label": "导出 HTML",
359
+ disabled: exporting,
360
+ onClick: () => void exportHtml(),
361
+ style: {
362
+ border: "none",
363
+ background: "transparent",
364
+ color: "inherit",
365
+ cursor: exporting ? "progress" : "pointer",
366
+ display: "grid",
367
+ placeItems: "center",
368
+ width: 26,
369
+ height: 26,
370
+ borderRadius: 6,
371
+ opacity: exporting ? .5 : 1
372
+ },
373
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
374
+ width: "14",
375
+ height: "14",
376
+ viewBox: "0 0 24 24",
377
+ fill: "none",
378
+ stroke: "currentColor",
379
+ strokeWidth: "1.9",
380
+ strokeLinecap: "round",
381
+ strokeLinejoin: "round",
382
+ "aria-hidden": "true",
383
+ children: [
384
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 3v12" }),
385
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7 10l5 5 5-5" }),
386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 21h16" })
387
+ ]
388
+ })
389
+ })
390
+ ]
391
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
392
+ "data-artifact-fullscreen-host": true,
393
+ style: {
394
+ flex: 1,
395
+ minHeight: 0
396
+ },
397
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CockpitViewer, {
398
+ artifactId: selected.id,
399
+ reloadToken
400
+ })
401
+ })]
402
+ });
403
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
404
+ style: {
405
+ display: "flex",
406
+ flexDirection: "column",
407
+ height: "100%",
408
+ minHeight: 0
409
+ },
410
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
411
+ style: {
412
+ display: "flex",
413
+ alignItems: "center",
414
+ gap: 8,
415
+ padding: "10px 12px",
416
+ borderBottom: "1px solid var(--dsw-alias-border-l1, #e5e7eb)"
417
+ },
418
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
419
+ style: {
420
+ flex: 1,
421
+ minWidth: 0
422
+ },
423
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
424
+ style: {
425
+ fontSize: 12,
426
+ fontWeight: 600
427
+ },
428
+ children: "驾驶舱"
429
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
430
+ style: muted,
431
+ children: [
432
+ "DataOps 驾驶舱资产 · ",
433
+ apps.length,
434
+ " 个",
435
+ catalogs.length > 0 ? " · " + catalogs.join(" / ") : ""
436
+ ]
437
+ })]
438
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
439
+ type: "button",
440
+ onClick: () => void load(),
441
+ style: {
442
+ border: "none",
443
+ background: "transparent",
444
+ color: "inherit",
445
+ cursor: "pointer",
446
+ fontSize: 12
447
+ },
448
+ children: "刷新"
449
+ })]
450
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
451
+ style: {
452
+ flex: 1,
453
+ minHeight: 0,
454
+ overflowY: "auto",
455
+ padding: 10,
456
+ display: "grid",
457
+ gap: 8,
458
+ alignContent: "start"
459
+ },
460
+ children: [
461
+ loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
462
+ style: {
463
+ ...muted,
464
+ textAlign: "center",
465
+ padding: 18
466
+ },
467
+ children: "正在加载驾驶舱…"
468
+ }) : null,
469
+ error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
470
+ role: "alert",
471
+ style: {
472
+ padding: "8px 10px",
473
+ borderRadius: 8,
474
+ background: "#fef2f2",
475
+ color: "#b91c1c",
476
+ fontSize: 11
477
+ },
478
+ children: error
479
+ }) : null,
480
+ !loading && error === null && apps.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
481
+ style: {
482
+ ...muted,
483
+ textAlign: "center",
484
+ padding: 18
485
+ },
486
+ children: "暂无驾驶舱"
487
+ }) : null,
488
+ apps.map((app) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
489
+ type: "button",
490
+ style: cardStyle,
491
+ onClick: () => setSelected({
492
+ id: app.id,
493
+ name: app.name
494
+ }),
495
+ "data-cockpit-id": app.id,
496
+ children: [
497
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
498
+ style: {
499
+ display: "flex",
500
+ alignItems: "center",
501
+ gap: 6,
502
+ minWidth: 0
503
+ },
504
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
505
+ style: {
506
+ flex: 1,
507
+ minWidth: 0,
508
+ fontSize: 12,
509
+ fontWeight: 600,
510
+ overflow: "hidden",
511
+ textOverflow: "ellipsis",
512
+ whiteSpace: "nowrap"
513
+ },
514
+ children: app.name
515
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
516
+ style: {
517
+ ...muted,
518
+ flex: "none"
519
+ },
520
+ children: app.status === "PUBLISHED" ? "已发布" : "草稿"
521
+ })]
522
+ }),
523
+ app.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
524
+ style: {
525
+ ...muted,
526
+ overflow: "hidden",
527
+ textOverflow: "ellipsis",
528
+ whiteSpace: "nowrap"
529
+ },
530
+ children: app.description
531
+ }) : null,
532
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
533
+ style: muted,
534
+ children: [
535
+ app.catalog ?? "未分类",
536
+ " · v",
537
+ app.publishedVersion || app.currentVersion
538
+ ]
539
+ })
540
+ ]
541
+ }, app.id))
542
+ ]
543
+ })]
544
+ });
545
+ }
546
+ //#endregion
547
+ //#region src/client/ArtifactFileViewer.tsx
548
+ /**
549
+ * HTML file viewer for cockpit files.
550
+ *
551
+ * The descriptor only matches `/artifact-apps/**` HTML carrying the
552
+ * `artifact-app` meta marker. Opening such a file creates or updates one draft
553
+ * through the host and renders it exactly like a published cockpit, so the file
554
+ * on disk is the draft.
555
+ *
556
+ * @module client/ArtifactFileViewer
557
+ */
558
+ /** Marker the file must carry for this viewer to claim it. */
559
+ const ARTIFACT_FILE_MARKER = /<meta[^>]+name=["']artifact-app["'][^>]*>/iu;
560
+ /**
561
+ * Whether a file is a cockpit document.
562
+ * @param path - workspace path.
563
+ * @param head - the first bytes the viewer sniffed.
564
+ */
565
+ const isArtifactFile = (path, head) => {
566
+ if (!/\/artifact-apps\/[^/]+\.html?$/iu.test(path)) return false;
567
+ const text = typeof head === "string" ? head : new TextDecoder().decode(head);
568
+ return ARTIFACT_FILE_MARKER.test(text);
569
+ };
570
+ /** Create/update the preview draft, then render it. */
571
+ function ArtifactFileViewer({ path, content, largeFile }) {
572
+ const [artifactId, setArtifactId] = (0, react.useState)(null);
573
+ const [error, setError] = (0, react.useState)(null);
574
+ const [busy, setBusy] = (0, react.useState)(false);
575
+ const [attempt, setAttempt] = (0, react.useState)(0);
576
+ (0, react.useEffect)(() => {
577
+ if (content === void 0) return void 0;
578
+ let alive = true;
579
+ setBusy(true);
580
+ setError(null);
581
+ (async () => {
582
+ try {
583
+ const response = await fetch("/dsh-artifact-app/preview", {
584
+ method: "POST",
585
+ headers: {
586
+ "content-type": "application/json",
587
+ accept: "application/json"
588
+ },
589
+ body: JSON.stringify({
590
+ path,
591
+ html: content
592
+ })
593
+ });
594
+ const payload = await response.json();
595
+ if (!response.ok || typeof payload.artifactId !== "string") throw new Error(payload.error ?? "HTTP " + String(response.status));
596
+ if (alive) setArtifactId(payload.artifactId);
597
+ } catch (cause) {
598
+ if (alive) setError(cause instanceof Error ? cause.message : String(cause));
599
+ } finally {
600
+ if (alive) setBusy(false);
601
+ }
602
+ })();
603
+ return () => {
604
+ alive = false;
605
+ };
606
+ }, [
607
+ path,
608
+ content,
609
+ attempt
610
+ ]);
611
+ if (largeFile !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
612
+ style: {
613
+ padding: 18,
614
+ fontSize: 12,
615
+ color: "var(--dsw-alias-label-secondary, #6b7280)"
616
+ },
617
+ children: [
618
+ "文件过大(",
619
+ Math.round(largeFile.size / 1024),
620
+ " KB),请缩小后再预览驾驶舱。"
621
+ ]
622
+ });
623
+ if (error !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
624
+ role: "alert",
625
+ style: {
626
+ padding: 12,
627
+ margin: 12,
628
+ borderRadius: 8,
629
+ background: "#fef2f2",
630
+ color: "#b91c1c",
631
+ fontSize: 12,
632
+ display: "grid",
633
+ gap: 8
634
+ },
635
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
636
+ style: { whiteSpace: "pre-wrap" },
637
+ children: error
638
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
639
+ type: "button",
640
+ onClick: () => setAttempt((value) => value + 1),
641
+ style: {
642
+ justifySelf: "start",
643
+ border: "1px solid #fca5a5",
644
+ background: "transparent",
645
+ color: "#b91c1c",
646
+ borderRadius: 6,
647
+ padding: "3px 10px",
648
+ cursor: "pointer",
649
+ fontSize: 11
650
+ },
651
+ children: "重试"
652
+ })]
653
+ });
654
+ if (busy || artifactId === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
655
+ style: {
656
+ padding: 18,
657
+ fontSize: 12,
658
+ color: "var(--dsw-alias-label-secondary, #6b7280)"
659
+ },
660
+ children: "正在生成驾驶舱预览…"
661
+ });
662
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CockpitViewer, { artifactId });
663
+ }
664
+ //#endregion
665
+ //#region src/client/index.tsx
666
+ /**
667
+ * Client half of the artifact-app plugin.
668
+ *
669
+ * Registers the `dataops-artifact-apps` right-sidebar tab, the cockpit HTML file
670
+ * viewer, and the SSE bridge that lets `cockpit_open` drive the sidebar. All three
671
+ * are optional when a profile has no Better Sidebar: the host routes and tools keep
672
+ * working without a browser surface.
673
+ *
674
+ * @module @sparkelf/dsh-artifact-app/client
675
+ */
676
+ /** Client services this plugin reads; `slots` owns the always-mounted bridge seat. */
677
+ const inject = ["slots"];
678
+ /** Tab id shared by registration, the bridge and the tools. */
679
+ const ARTIFACT_TAB_TYPE = "dataops-artifact-apps";
680
+ /** File viewer id. */
681
+ const ARTIFACT_VIEWER_ID = "artifact-app:cockpit";
682
+ /** Invisible bridge: SSE navigation events become Better Sidebar calls. */
683
+ function OpenCockpitBridge({ service, scope }) {
684
+ (0, react.useEffect)(() => {
685
+ if (typeof EventSource !== "function") return void 0;
686
+ const source = new EventSource("/dsh-artifact-app/open/events");
687
+ const open = (event) => {
688
+ let parsed;
689
+ try {
690
+ parsed = JSON.parse(String(event.data));
691
+ } catch {
692
+ return;
693
+ }
694
+ if (parsed.type !== "open_cockpit" || typeof parsed.cockpitId !== "string") return;
695
+ const meta = {
696
+ cockpitId: parsed.cockpitId,
697
+ cockpitName: parsed.cockpitName,
698
+ nonce: parsed.nonce,
699
+ mode: parsed.mode
700
+ };
701
+ service.openTab?.({
702
+ type: ARTIFACT_TAB_TYPE,
703
+ id: ARTIFACT_TAB_TYPE,
704
+ title: "驾驶舱",
705
+ meta
706
+ }, scope);
707
+ service.updateTab?.(ARTIFACT_TAB_TYPE, {
708
+ title: "驾驶舱",
709
+ meta
710
+ }, scope?.sessionId);
711
+ };
712
+ const updated = (event) => {
713
+ let parsed;
714
+ try {
715
+ parsed = JSON.parse(String(event.data));
716
+ } catch {
717
+ return;
718
+ }
719
+ if (parsed.type !== "cockpit_updated" || typeof parsed.cockpitId !== "string") return;
720
+ service.updateTab?.(ARTIFACT_TAB_TYPE, {
721
+ title: "驾驶舱",
722
+ meta: {
723
+ cockpitId: parsed.cockpitId,
724
+ nonce: parsed.nonce
725
+ }
726
+ }, scope?.sessionId);
727
+ };
728
+ source.addEventListener("dataops-open-cockpit", open);
729
+ source.addEventListener("dataops-cockpit-updated", updated);
730
+ return () => {
731
+ source.removeEventListener("dataops-open-cockpit", open);
732
+ source.removeEventListener("dataops-cockpit-updated", updated);
733
+ source.close();
734
+ };
735
+ }, [service, scope]);
736
+ return null;
737
+ }
738
+ /**
739
+ * Register the tab, the file viewer and the bridge.
740
+ * @param ctx - DSH client context.
741
+ */
742
+ function apply(ctx) {
743
+ try {
744
+ ctx.effect(() => {
745
+ let disposeTab;
746
+ let disposeViewer;
747
+ let disposeBridge;
748
+ let betterSidebarService;
749
+ const inject = ctx.inject;
750
+ inject?.(["betterSidebar"], (injected) => {
751
+ const service = injected.betterSidebar;
752
+ if (service?.registerTab === void 0) return;
753
+ betterSidebarService = service;
754
+ disposeTab = service.registerTab({
755
+ id: ARTIFACT_TAB_TYPE,
756
+ title: "驾驶舱",
757
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
758
+ width: "14",
759
+ height: "14",
760
+ viewBox: "0 0 24 24",
761
+ fill: "none",
762
+ stroke: "currentColor",
763
+ strokeWidth: "1.8",
764
+ strokeLinecap: "round",
765
+ strokeLinejoin: "round",
766
+ "aria-hidden": "true",
767
+ children: [
768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 18a8 8 0 1 1 16 0" }),
769
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 18l4.2-5" }),
770
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
771
+ cx: "12",
772
+ cy: "18",
773
+ r: "1.4"
774
+ })
775
+ ]
776
+ }),
777
+ description: "DataOps 驾驶舱资产、预览与发布",
778
+ order: 62,
779
+ single: true,
780
+ component: ((props) => CockpitTab(props))
781
+ });
782
+ disposeViewer = service.registerFileViewer?.({
783
+ id: ARTIFACT_VIEWER_ID,
784
+ title: "驾驶舱预览",
785
+ exts: [],
786
+ priority: 60,
787
+ fetchStrategy: "fsRead",
788
+ detect: (path, head) => isArtifactFile(path, head),
789
+ component: ((props) => ArtifactFileViewer(props))
790
+ });
791
+ });
792
+ const slots = ctx.slots;
793
+ disposeBridge = slots.inject("sidebar.footer.action", () => slots.register({
794
+ name: "sidebar.footer.action",
795
+ id: "artifact-app-bridge",
796
+ order: 96
797
+ }, ((props) => betterSidebarService === void 0 ? null : OpenCockpitBridge({
798
+ service: betterSidebarService,
799
+ scope: props?.scope
800
+ }))));
801
+ return () => {
802
+ disposeBridge?.();
803
+ disposeBridge = void 0;
804
+ disposeViewer?.();
805
+ disposeViewer = void 0;
806
+ disposeTab?.();
807
+ disposeTab = void 0;
808
+ };
809
+ }, "artifact-app: sidebar tab, file viewer and open bridge");
810
+ } catch (error) {
811
+ console.error("[artifact-app] apply failed", error);
812
+ throw error;
813
+ }
814
+ }
815
+ //#endregion
816
+ exports.ARTIFACT_TAB_TYPE = ARTIFACT_TAB_TYPE;
817
+ exports.ARTIFACT_VIEWER_ID = ARTIFACT_VIEWER_ID;
818
+ exports.apply = apply;
819
+ exports.inject = inject;
820
+ return module.exports;
821
+ }
822
+ });
823
+
824
+ //# sourceMappingURL=client.js.map