@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/README.md +56 -2
- package/cordis.patch.yml +3 -0
- package/lib/client.js +824 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +638 -0
- package/lib/index.js +2154 -0
- package/package.json +82 -3
- package/skills/artifact-app-authoring/SKILL.md +157 -0
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
|