@burdenoff/microfe-bigconsole 2026.803.1 → 2026.803.2
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.
|
@@ -2,7 +2,27 @@ import e from "../../../hooks/useDashboardEmbedPolicy.js";
|
|
|
2
2
|
import { useEffect as t, useMemo as n, useState as r } from "react";
|
|
3
3
|
import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
4
4
|
//#region src/bigconsole/components/dashboard/ShareDialog/EmbedTab.tsx
|
|
5
|
-
|
|
5
|
+
var s = [
|
|
6
|
+
{
|
|
7
|
+
value: "light",
|
|
8
|
+
label: "Light"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
value: "dark",
|
|
12
|
+
label: "Dark"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
value: "auto",
|
|
16
|
+
label: "Auto (match viewer)"
|
|
17
|
+
}
|
|
18
|
+
];
|
|
19
|
+
function c(e, t) {
|
|
20
|
+
return t === "light" ? e : e.replace(/(?<=\s)src="([^"]*)"/, (e, n) => {
|
|
21
|
+
let r = n.indexOf("#"), i = r === -1 ? n : n.slice(0, r), a = r === -1 ? "" : n.slice(r);
|
|
22
|
+
return `src="${i}${i.includes("?") ? "&" : "?"}theme=${t}${a}"`;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function l(e) {
|
|
6
26
|
let t = e.trim();
|
|
7
27
|
if (!t) return null;
|
|
8
28
|
try {
|
|
@@ -12,65 +32,65 @@ function s(e) {
|
|
|
12
32
|
return null;
|
|
13
33
|
}
|
|
14
34
|
}
|
|
15
|
-
function
|
|
16
|
-
let { policy:
|
|
35
|
+
function u({ dashboardId: u, colors: d }) {
|
|
36
|
+
let { policy: f, secret: p, loading: m, error: h, fetchPolicy: g, publishToWeb: _, setAllowedOrigins: v, rotateKey: y, unpublish: b } = e(), [x, S] = r(""), [C, w] = r([]), [T, E] = r(!1), [D, O] = r(!1), [k, A] = r("light"), [j, M] = r(null);
|
|
17
37
|
t(() => {
|
|
18
|
-
|
|
19
|
-
}, [
|
|
20
|
-
let
|
|
21
|
-
|
|
22
|
-
let e =
|
|
38
|
+
u && g(u);
|
|
39
|
+
}, [u, g]);
|
|
40
|
+
let N = f?.status === "ACTIVE", P = N ? f?.allowedParentOrigins ?? [] : C, F = n(() => p?.iframeSnippet ? c(p.iframeSnippet, k) : null, [p, k]), I = () => {
|
|
41
|
+
M(null);
|
|
42
|
+
let e = l(x);
|
|
23
43
|
if (!e) {
|
|
24
|
-
|
|
44
|
+
M("Enter a valid https origin, e.g. https://example.com");
|
|
25
45
|
return;
|
|
26
46
|
}
|
|
27
|
-
if (
|
|
28
|
-
|
|
47
|
+
if (P.includes(e)) {
|
|
48
|
+
S("");
|
|
29
49
|
return;
|
|
30
50
|
}
|
|
31
|
-
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
if (
|
|
36
|
-
|
|
51
|
+
N && f ? v(f.id, [...P, e]) : w((t) => [...t, e]), S("");
|
|
52
|
+
}, L = (e) => {
|
|
53
|
+
N && f ? v(f.id, P.filter((t) => t !== e)) : w((t) => t.filter((t) => t !== e));
|
|
54
|
+
}, R = async () => {
|
|
55
|
+
if (M(null), C.length === 0) {
|
|
56
|
+
M("Add at least one allowed website domain before publishing.");
|
|
37
57
|
return;
|
|
38
58
|
}
|
|
39
|
-
if (!
|
|
40
|
-
|
|
59
|
+
if (!T) {
|
|
60
|
+
M("Please confirm this dashboard is safe to be public.");
|
|
41
61
|
return;
|
|
42
62
|
}
|
|
43
|
-
await
|
|
44
|
-
},
|
|
63
|
+
await _(u, C, !0);
|
|
64
|
+
}, z = async (e) => {
|
|
45
65
|
try {
|
|
46
|
-
await navigator.clipboard.writeText(e),
|
|
66
|
+
await navigator.clipboard.writeText(e), O(!0), setTimeout(() => O(!1), 1500);
|
|
47
67
|
} catch {
|
|
48
|
-
|
|
68
|
+
M("Could not copy — select and copy manually.");
|
|
49
69
|
}
|
|
50
|
-
},
|
|
70
|
+
}, B = {
|
|
51
71
|
padding: "8px 14px",
|
|
52
72
|
borderRadius: "8px",
|
|
53
73
|
border: "none",
|
|
54
|
-
background:
|
|
74
|
+
background: d.actionPrimaryBg,
|
|
55
75
|
color: "#fff",
|
|
56
76
|
fontSize: "13px",
|
|
57
77
|
fontWeight: 600,
|
|
58
78
|
cursor: "pointer"
|
|
59
|
-
},
|
|
79
|
+
}, V = {
|
|
60
80
|
padding: "8px 14px",
|
|
61
81
|
borderRadius: "8px",
|
|
62
|
-
border: `1px solid ${
|
|
82
|
+
border: `1px solid ${d.borderDefault}`,
|
|
63
83
|
background: "transparent",
|
|
64
|
-
color:
|
|
84
|
+
color: d.textPrimary,
|
|
65
85
|
fontSize: "13px",
|
|
66
86
|
cursor: "pointer"
|
|
67
|
-
},
|
|
87
|
+
}, H = {
|
|
68
88
|
flex: 1,
|
|
69
89
|
padding: "8px 10px",
|
|
70
90
|
borderRadius: "8px",
|
|
71
|
-
border: `1px solid ${
|
|
72
|
-
background:
|
|
73
|
-
color:
|
|
91
|
+
border: `1px solid ${d.borderDefault}`,
|
|
92
|
+
background: d.bgSecondary,
|
|
93
|
+
color: d.textPrimary,
|
|
74
94
|
fontSize: "13px"
|
|
75
95
|
};
|
|
76
96
|
return /* @__PURE__ */ o("div", {
|
|
@@ -83,23 +103,23 @@ function c({ dashboardId: c, colors: l }) {
|
|
|
83
103
|
justifyContent: "space-between",
|
|
84
104
|
padding: "12px 14px",
|
|
85
105
|
borderRadius: "10px",
|
|
86
|
-
background:
|
|
106
|
+
background: d.bgSecondary,
|
|
87
107
|
marginBottom: "16px"
|
|
88
108
|
},
|
|
89
109
|
children: [/* @__PURE__ */ o("div", { children: [/* @__PURE__ */ a("div", {
|
|
90
110
|
style: {
|
|
91
111
|
fontSize: "14px",
|
|
92
112
|
fontWeight: 600,
|
|
93
|
-
color:
|
|
113
|
+
color: d.textPrimary
|
|
94
114
|
},
|
|
95
115
|
children: "Publish to the web"
|
|
96
116
|
}), /* @__PURE__ */ a("div", {
|
|
97
117
|
style: {
|
|
98
118
|
fontSize: "12px",
|
|
99
|
-
color:
|
|
119
|
+
color: d.textSecondary,
|
|
100
120
|
marginTop: "2px"
|
|
101
121
|
},
|
|
102
|
-
children:
|
|
122
|
+
children: N ? "This dashboard is live. Anyone with the link on an allowed website sees its live data." : "Embed this dashboard on your website. Viewers see live data — no login required."
|
|
103
123
|
})] }), /* @__PURE__ */ a("span", {
|
|
104
124
|
style: {
|
|
105
125
|
fontSize: "11px",
|
|
@@ -107,9 +127,9 @@ function c({ dashboardId: c, colors: l }) {
|
|
|
107
127
|
padding: "3px 8px",
|
|
108
128
|
borderRadius: "999px",
|
|
109
129
|
color: "#fff",
|
|
110
|
-
background:
|
|
130
|
+
background: N ? d.statusSuccessBg : d.textTertiary
|
|
111
131
|
},
|
|
112
|
-
children:
|
|
132
|
+
children: N ? "PUBLISHED" : "PRIVATE"
|
|
113
133
|
})]
|
|
114
134
|
}),
|
|
115
135
|
/* @__PURE__ */ o("div", {
|
|
@@ -119,7 +139,7 @@ function c({ dashboardId: c, colors: l }) {
|
|
|
119
139
|
style: {
|
|
120
140
|
fontSize: "12px",
|
|
121
141
|
fontWeight: 600,
|
|
122
|
-
color:
|
|
142
|
+
color: d.textSecondary,
|
|
123
143
|
marginBottom: "6px"
|
|
124
144
|
},
|
|
125
145
|
children: "Allowed website domains"
|
|
@@ -131,24 +151,24 @@ function c({ dashboardId: c, colors: l }) {
|
|
|
131
151
|
marginBottom: "8px"
|
|
132
152
|
},
|
|
133
153
|
children: [/* @__PURE__ */ a("input", {
|
|
134
|
-
style:
|
|
154
|
+
style: H,
|
|
135
155
|
placeholder: "https://yoursite.com",
|
|
136
|
-
value:
|
|
137
|
-
onChange: (e) =>
|
|
156
|
+
value: x,
|
|
157
|
+
onChange: (e) => S(e.target.value),
|
|
138
158
|
onKeyDown: (e) => {
|
|
139
|
-
e.key === "Enter" &&
|
|
159
|
+
e.key === "Enter" && I();
|
|
140
160
|
}
|
|
141
161
|
}), /* @__PURE__ */ a("button", {
|
|
142
|
-
style:
|
|
143
|
-
onClick:
|
|
162
|
+
style: V,
|
|
163
|
+
onClick: I,
|
|
144
164
|
type: "button",
|
|
145
165
|
children: "Add"
|
|
146
166
|
})]
|
|
147
167
|
}),
|
|
148
|
-
|
|
168
|
+
P.length === 0 ? /* @__PURE__ */ a("div", {
|
|
149
169
|
style: {
|
|
150
170
|
fontSize: "12px",
|
|
151
|
-
color:
|
|
171
|
+
color: d.textTertiary
|
|
152
172
|
},
|
|
153
173
|
children: "No domains yet. Only listed https domains may frame this dashboard."
|
|
154
174
|
}) : /* @__PURE__ */ a("div", {
|
|
@@ -157,28 +177,28 @@ function c({ dashboardId: c, colors: l }) {
|
|
|
157
177
|
flexDirection: "column",
|
|
158
178
|
gap: "6px"
|
|
159
179
|
},
|
|
160
|
-
children:
|
|
180
|
+
children: P.map((e) => /* @__PURE__ */ o("div", {
|
|
161
181
|
style: {
|
|
162
182
|
display: "flex",
|
|
163
183
|
alignItems: "center",
|
|
164
184
|
justifyContent: "space-between",
|
|
165
185
|
padding: "6px 10px",
|
|
166
186
|
borderRadius: "8px",
|
|
167
|
-
background:
|
|
187
|
+
background: d.bgTertiary
|
|
168
188
|
},
|
|
169
189
|
children: [/* @__PURE__ */ a("span", {
|
|
170
190
|
style: {
|
|
171
191
|
fontSize: "13px",
|
|
172
|
-
color:
|
|
192
|
+
color: d.textPrimary
|
|
173
193
|
},
|
|
174
194
|
children: e
|
|
175
195
|
}), /* @__PURE__ */ a("button", {
|
|
176
196
|
style: {
|
|
177
|
-
...
|
|
197
|
+
...V,
|
|
178
198
|
padding: "2px 8px",
|
|
179
|
-
color:
|
|
199
|
+
color: d.statusErrorText
|
|
180
200
|
},
|
|
181
|
-
onClick: () =>
|
|
201
|
+
onClick: () => L(e),
|
|
182
202
|
type: "button",
|
|
183
203
|
children: "Remove"
|
|
184
204
|
})]
|
|
@@ -186,7 +206,7 @@ function c({ dashboardId: c, colors: l }) {
|
|
|
186
206
|
})
|
|
187
207
|
]
|
|
188
208
|
}),
|
|
189
|
-
!
|
|
209
|
+
!N && /* @__PURE__ */ o("div", {
|
|
190
210
|
style: { marginBottom: "8px" },
|
|
191
211
|
children: [/* @__PURE__ */ o("label", {
|
|
192
212
|
style: {
|
|
@@ -197,46 +217,85 @@ function c({ dashboardId: c, colors: l }) {
|
|
|
197
217
|
},
|
|
198
218
|
children: [/* @__PURE__ */ a("input", {
|
|
199
219
|
type: "checkbox",
|
|
200
|
-
checked:
|
|
201
|
-
onChange: (e) =>
|
|
220
|
+
checked: T,
|
|
221
|
+
onChange: (e) => E(e.target.checked)
|
|
202
222
|
}), /* @__PURE__ */ a("span", {
|
|
203
223
|
style: {
|
|
204
224
|
fontSize: "12px",
|
|
205
|
-
color:
|
|
225
|
+
color: d.textSecondary
|
|
206
226
|
},
|
|
207
227
|
children: "I understand this makes the dashboard's live data visible to anyone with the link, and I confirm it is safe to be public."
|
|
208
228
|
})]
|
|
209
229
|
}), /* @__PURE__ */ a("button", {
|
|
210
|
-
style:
|
|
211
|
-
onClick:
|
|
212
|
-
disabled:
|
|
230
|
+
style: B,
|
|
231
|
+
onClick: R,
|
|
232
|
+
disabled: m,
|
|
213
233
|
type: "button",
|
|
214
|
-
children:
|
|
234
|
+
children: m ? "Publishing…" : "Publish to web"
|
|
215
235
|
})]
|
|
216
236
|
}),
|
|
217
|
-
|
|
237
|
+
N && /* @__PURE__ */ o(i, { children: [F ? /* @__PURE__ */ o("div", {
|
|
218
238
|
style: { marginBottom: "16px" },
|
|
219
239
|
children: [
|
|
220
|
-
/* @__PURE__ */
|
|
240
|
+
/* @__PURE__ */ o("div", {
|
|
221
241
|
style: {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
242
|
+
display: "flex",
|
|
243
|
+
alignItems: "center",
|
|
244
|
+
justifyContent: "space-between",
|
|
225
245
|
marginBottom: "6px"
|
|
226
246
|
},
|
|
227
|
-
children: "
|
|
247
|
+
children: [/* @__PURE__ */ a("span", {
|
|
248
|
+
style: {
|
|
249
|
+
fontSize: "12px",
|
|
250
|
+
fontWeight: 600,
|
|
251
|
+
color: d.textSecondary
|
|
252
|
+
},
|
|
253
|
+
children: "Embed snippet"
|
|
254
|
+
}), /* @__PURE__ */ o("label", {
|
|
255
|
+
style: {
|
|
256
|
+
display: "flex",
|
|
257
|
+
alignItems: "center",
|
|
258
|
+
gap: "6px"
|
|
259
|
+
},
|
|
260
|
+
children: [/* @__PURE__ */ a("span", {
|
|
261
|
+
style: {
|
|
262
|
+
fontSize: "12px",
|
|
263
|
+
color: d.textTertiary
|
|
264
|
+
},
|
|
265
|
+
children: "Theme"
|
|
266
|
+
}), /* @__PURE__ */ a("select", {
|
|
267
|
+
value: k,
|
|
268
|
+
onChange: (e) => {
|
|
269
|
+
let t = e.target.value;
|
|
270
|
+
(t === "light" || t === "dark" || t === "auto") && A(t);
|
|
271
|
+
},
|
|
272
|
+
style: {
|
|
273
|
+
padding: "4px 8px",
|
|
274
|
+
borderRadius: "6px",
|
|
275
|
+
border: `1px solid ${d.borderDefault}`,
|
|
276
|
+
background: d.bgSecondary,
|
|
277
|
+
color: d.textPrimary,
|
|
278
|
+
fontSize: "12px",
|
|
279
|
+
cursor: "pointer"
|
|
280
|
+
},
|
|
281
|
+
children: s.map((e) => /* @__PURE__ */ a("option", {
|
|
282
|
+
value: e.value,
|
|
283
|
+
children: e.label
|
|
284
|
+
}, e.value))
|
|
285
|
+
})]
|
|
286
|
+
})]
|
|
228
287
|
}),
|
|
229
288
|
/* @__PURE__ */ a("textarea", {
|
|
230
289
|
readOnly: !0,
|
|
231
|
-
value:
|
|
290
|
+
value: F,
|
|
232
291
|
style: {
|
|
233
292
|
width: "100%",
|
|
234
293
|
minHeight: "96px",
|
|
235
294
|
padding: "10px",
|
|
236
295
|
borderRadius: "8px",
|
|
237
|
-
border: `1px solid ${
|
|
238
|
-
background:
|
|
239
|
-
color:
|
|
296
|
+
border: `1px solid ${d.borderDefault}`,
|
|
297
|
+
background: d.bgSecondary,
|
|
298
|
+
color: d.textPrimary,
|
|
240
299
|
fontFamily: "monospace",
|
|
241
300
|
fontSize: "12px",
|
|
242
301
|
resize: "vertical"
|
|
@@ -244,19 +303,19 @@ function c({ dashboardId: c, colors: l }) {
|
|
|
244
303
|
}),
|
|
245
304
|
/* @__PURE__ */ a("button", {
|
|
246
305
|
style: {
|
|
247
|
-
...
|
|
306
|
+
...V,
|
|
248
307
|
marginTop: "8px"
|
|
249
308
|
},
|
|
250
|
-
onClick: () => F
|
|
309
|
+
onClick: () => z(F),
|
|
251
310
|
type: "button",
|
|
252
|
-
children:
|
|
311
|
+
children: D ? "Copied ✓" : "Copy snippet"
|
|
253
312
|
})
|
|
254
313
|
]
|
|
255
314
|
}) : /* @__PURE__ */ o("div", {
|
|
256
315
|
style: {
|
|
257
316
|
fontSize: "12px",
|
|
258
|
-
color:
|
|
259
|
-
background:
|
|
317
|
+
color: d.textSecondary,
|
|
318
|
+
background: d.bgSecondary,
|
|
260
319
|
borderRadius: "8px",
|
|
261
320
|
padding: "10px 12px",
|
|
262
321
|
marginBottom: "16px"
|
|
@@ -272,37 +331,37 @@ function c({ dashboardId: c, colors: l }) {
|
|
|
272
331
|
gap: "8px"
|
|
273
332
|
},
|
|
274
333
|
children: [/* @__PURE__ */ a("button", {
|
|
275
|
-
style:
|
|
276
|
-
onClick: () =>
|
|
277
|
-
disabled:
|
|
334
|
+
style: V,
|
|
335
|
+
onClick: () => f && y(f.id),
|
|
336
|
+
disabled: m,
|
|
278
337
|
type: "button",
|
|
279
338
|
children: "Rotate link"
|
|
280
339
|
}), /* @__PURE__ */ a("button", {
|
|
281
340
|
style: {
|
|
282
|
-
...
|
|
283
|
-
color:
|
|
284
|
-
borderColor:
|
|
341
|
+
...V,
|
|
342
|
+
color: d.statusErrorText,
|
|
343
|
+
borderColor: d.statusErrorText
|
|
285
344
|
},
|
|
286
345
|
onClick: () => {
|
|
287
|
-
|
|
346
|
+
f && window.confirm("Unpublish this dashboard? The embed will stop working immediately for anyone using the link.") && b(f.id);
|
|
288
347
|
},
|
|
289
|
-
disabled:
|
|
348
|
+
disabled: m,
|
|
290
349
|
type: "button",
|
|
291
350
|
children: "Unpublish"
|
|
292
351
|
})]
|
|
293
352
|
})] }),
|
|
294
|
-
(
|
|
353
|
+
(j || h) && /* @__PURE__ */ a("div", {
|
|
295
354
|
style: {
|
|
296
355
|
marginTop: "12px",
|
|
297
356
|
fontSize: "12px",
|
|
298
|
-
color:
|
|
357
|
+
color: d.statusErrorText
|
|
299
358
|
},
|
|
300
|
-
children:
|
|
359
|
+
children: j ?? h?.message
|
|
301
360
|
})
|
|
302
361
|
]
|
|
303
362
|
});
|
|
304
363
|
}
|
|
305
364
|
//#endregion
|
|
306
|
-
export {
|
|
365
|
+
export { u as default };
|
|
307
366
|
|
|
308
367
|
//# sourceMappingURL=EmbedTab.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmbedTab.js","names":[],"sources":["../../../../../src/bigconsole/components/dashboard/ShareDialog/EmbedTab.tsx"],"sourcesContent":["/**\n * EmbedTab — \"Publish to web\" section of the dashboard Share dialog.\n *\n * Owner-facing UI to publish a dashboard as an anonymous-link embed serving LIVE\n * data, manage the allowed parent domains, copy the iframe snippet, rotate the\n * link key, and unpublish. Backed by {@link useDashboardEmbedPolicy}; all\n * mutations are gateway-enforced by `@rbac(manage_embed)` (owner-only).\n *\n * Self-contained (inline styles driven by the dialog's `colors`) so it slots into\n * the existing hand-styled ShareDialog without a broader refactor.\n */\n\nimport React, { useEffect, useMemo, useState } from 'react';\nimport { useDashboardEmbedPolicy } from '../../../hooks/useDashboardEmbedPolicy';\n\ninterface DialogColors {\n bgPrimary: string;\n bgSecondary: string;\n bgTertiary: string;\n textPrimary: string;\n textSecondary: string;\n textTertiary: string;\n borderDefault: string;\n actionPrimaryBg: string;\n actionPrimaryBgHover: string;\n statusSuccessBg: string;\n statusErrorText: string;\n}\n\nexport interface EmbedTabProps {\n dashboardId: string;\n colors: DialogColors;\n}\n\n/** Validate an exact https origin (matches the backend allowlist rules). */\nfunction normalizeOrigin(raw: string): string | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n try {\n const url = new URL(trimmed);\n if (url.protocol !== 'https:') return null;\n if ((url.pathname !== '' && url.pathname !== '/') || url.search || url.hash) return null;\n return url.origin;\n } catch {\n return null;\n }\n}\n\nexport function EmbedTab({ dashboardId, colors }: EmbedTabProps) {\n const { policy, secret, loading, error, fetchPolicy, publishToWeb, setAllowedOrigins, rotateKey, unpublish } =\n useDashboardEmbedPolicy();\n\n const [domainInput, setDomainInput] = useState('');\n const [pendingDomains, setPendingDomains] = useState<string[]>([]);\n const [attested, setAttested] = useState(false);\n const [copied, setCopied] = useState(false);\n const [localError, setLocalError] = useState<string | null>(null);\n\n useEffect(() => {\n if (dashboardId) fetchPolicy(dashboardId);\n }, [dashboardId, fetchPolicy]);\n\n const isPublished = policy?.status === 'ACTIVE';\n const domains = isPublished ? (policy?.allowedParentOrigins ?? []) : pendingDomains;\n\n // The copyable iframe snippet: from the one-time secret if present (fresh\n // publish/rotate), else reconstructed from the locator for an already-published\n // policy (the fragment key is only available in the one-time secret).\n const iframeSnippet = useMemo(() => {\n if (secret?.iframeSnippet) return secret.iframeSnippet;\n return null;\n }, [secret]);\n\n const addDomain = () => {\n setLocalError(null);\n const origin = normalizeOrigin(domainInput);\n if (!origin) {\n setLocalError('Enter a valid https origin, e.g. https://example.com');\n return;\n }\n if (domains.includes(origin)) {\n setDomainInput('');\n return;\n }\n if (isPublished && policy) {\n void setAllowedOrigins(policy.id, [...domains, origin]);\n } else {\n setPendingDomains((prev) => [...prev, origin]);\n }\n setDomainInput('');\n };\n\n const removeDomain = (origin: string) => {\n if (isPublished && policy) {\n void setAllowedOrigins(\n policy.id,\n domains.filter((d) => d !== origin)\n );\n } else {\n setPendingDomains((prev) => prev.filter((d) => d !== origin));\n }\n };\n\n const handlePublish = async () => {\n setLocalError(null);\n if (pendingDomains.length === 0) {\n setLocalError('Add at least one allowed website domain before publishing.');\n return;\n }\n if (!attested) {\n setLocalError('Please confirm this dashboard is safe to be public.');\n return;\n }\n await publishToWeb(dashboardId, pendingDomains, true);\n };\n\n const handleCopy = async (text: string) => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n } catch {\n setLocalError('Could not copy — select and copy manually.');\n }\n };\n\n const primaryBtn: React.CSSProperties = {\n padding: '8px 14px',\n borderRadius: '8px',\n border: 'none',\n background: colors.actionPrimaryBg,\n color: '#fff',\n fontSize: '13px',\n fontWeight: 600,\n cursor: 'pointer',\n };\n const ghostBtn: React.CSSProperties = {\n padding: '8px 14px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: 'transparent',\n color: colors.textPrimary,\n fontSize: '13px',\n cursor: 'pointer',\n };\n const inputStyle: React.CSSProperties = {\n flex: 1,\n padding: '8px 10px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontSize: '13px',\n };\n\n return (\n <div style={{ padding: '20px 24px' }}>\n {/* Status banner */}\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 14px',\n borderRadius: '10px',\n background: colors.bgSecondary,\n marginBottom: '16px',\n }}\n >\n <div>\n <div style={{ fontSize: '14px', fontWeight: 600, color: colors.textPrimary }}>Publish to the web</div>\n <div style={{ fontSize: '12px', color: colors.textSecondary, marginTop: '2px' }}>\n {isPublished\n ? 'This dashboard is live. Anyone with the link on an allowed website sees its live data.'\n : 'Embed this dashboard on your website. Viewers see live data — no login required.'}\n </div>\n </div>\n <span\n style={{\n fontSize: '11px',\n fontWeight: 700,\n padding: '3px 8px',\n borderRadius: '999px',\n color: '#fff',\n background: isPublished ? colors.statusSuccessBg : colors.textTertiary,\n }}\n >\n {isPublished ? 'PUBLISHED' : 'PRIVATE'}\n </span>\n </div>\n\n {/* Allowed domains */}\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '6px' }}>\n Allowed website domains\n </div>\n <div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}>\n <input\n style={inputStyle}\n placeholder=\"https://yoursite.com\"\n value={domainInput}\n onChange={(e) => setDomainInput(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') addDomain();\n }}\n />\n <button style={ghostBtn} onClick={addDomain} type=\"button\">\n Add\n </button>\n </div>\n {domains.length === 0 ? (\n <div style={{ fontSize: '12px', color: colors.textTertiary }}>\n No domains yet. Only listed https domains may frame this dashboard.\n </div>\n ) : (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>\n {domains.map((d) => (\n <div\n key={d}\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '6px 10px',\n borderRadius: '8px',\n background: colors.bgTertiary,\n }}\n >\n <span style={{ fontSize: '13px', color: colors.textPrimary }}>{d}</span>\n <button\n style={{ ...ghostBtn, padding: '2px 8px', color: colors.statusErrorText }}\n onClick={() => removeDomain(d)}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n ))}\n </div>\n )}\n </div>\n\n {/* Publish CTA (unpublished) */}\n {!isPublished && (\n <div style={{ marginBottom: '8px' }}>\n <label style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '12px' }}>\n <input type=\"checkbox\" checked={attested} onChange={(e) => setAttested(e.target.checked)} />\n <span style={{ fontSize: '12px', color: colors.textSecondary }}>\n I understand this makes the dashboard's live data visible to anyone with the link, and I confirm it\n is safe to be public.\n </span>\n </label>\n <button style={primaryBtn} onClick={handlePublish} disabled={loading} type=\"button\">\n {loading ? 'Publishing…' : 'Publish to web'}\n </button>\n </div>\n )}\n\n {/* Embed snippet + management (published) */}\n {isPublished && (\n <>\n {iframeSnippet ? (\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '6px' }}>\n Embed snippet\n </div>\n <textarea\n readOnly\n value={iframeSnippet}\n style={{\n width: '100%',\n minHeight: '96px',\n padding: '10px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontFamily: 'monospace',\n fontSize: '12px',\n resize: 'vertical',\n }}\n />\n <button style={{ ...ghostBtn, marginTop: '8px' }} onClick={() => handleCopy(iframeSnippet)} type=\"button\">\n {copied ? 'Copied ✓' : 'Copy snippet'}\n </button>\n </div>\n ) : (\n <div\n style={{\n fontSize: '12px',\n color: colors.textSecondary,\n background: colors.bgSecondary,\n borderRadius: '8px',\n padding: '10px 12px',\n marginBottom: '16px',\n }}\n >\n The embed link is already active. For security the snippet (which contains the secret key) is only shown\n once at publish time — use <strong>Rotate link</strong> to generate a fresh snippet.\n </div>\n )}\n\n <div style={{ display: 'flex', gap: '8px' }}>\n <button style={ghostBtn} onClick={() => policy && rotateKey(policy.id)} disabled={loading} type=\"button\">\n Rotate link\n </button>\n <button\n style={{ ...ghostBtn, color: colors.statusErrorText, borderColor: colors.statusErrorText }}\n onClick={() => {\n if (\n policy &&\n window.confirm(\n 'Unpublish this dashboard? The embed will stop working immediately for anyone using the link.'\n )\n ) {\n void unpublish(policy.id);\n }\n }}\n disabled={loading}\n type=\"button\"\n >\n Unpublish\n </button>\n </div>\n </>\n )}\n\n {(localError || error) && (\n <div style={{ marginTop: '12px', fontSize: '12px', color: colors.statusErrorText }}>\n {localError ?? error?.message}\n </div>\n )}\n </div>\n );\n}\n\nexport default EmbedTab;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EmbedTab.js","names":[],"sources":["../../../../../src/bigconsole/components/dashboard/ShareDialog/EmbedTab.tsx"],"sourcesContent":["/**\n * EmbedTab — \"Publish to web\" section of the dashboard Share dialog.\n *\n * Owner-facing UI to publish a dashboard as an anonymous-link embed serving LIVE\n * data, manage the allowed parent domains, copy the iframe snippet, rotate the\n * link key, and unpublish. Backed by {@link useDashboardEmbedPolicy}; all\n * mutations are gateway-enforced by `@rbac(manage_embed)` (owner-only).\n *\n * Self-contained (inline styles driven by the dialog's `colors`) so it slots into\n * the existing hand-styled ShareDialog without a broader refactor.\n */\n\nimport React, { useEffect, useMemo, useState } from 'react';\nimport { useDashboardEmbedPolicy } from '../../../hooks/useDashboardEmbedPolicy';\n\ninterface DialogColors {\n bgPrimary: string;\n bgSecondary: string;\n bgTertiary: string;\n textPrimary: string;\n textSecondary: string;\n textTertiary: string;\n borderDefault: string;\n actionPrimaryBg: string;\n actionPrimaryBgHover: string;\n statusSuccessBg: string;\n statusErrorText: string;\n}\n\nexport interface EmbedTabProps {\n dashboardId: string;\n colors: DialogColors;\n}\n\n/** Embed color-mode baked into the iframe URL (BOFF-5734). */\ntype EmbedSnippetTheme = 'light' | 'dark' | 'auto';\n\nconst THEME_OPTIONS: ReadonlyArray<{ value: EmbedSnippetTheme; label: string }> = [\n { value: 'light', label: 'Light' },\n { value: 'dark', label: 'Dark' },\n { value: 'auto', label: 'Auto (match viewer)' },\n];\n\n/**\n * Bake the chosen color mode into an iframe snippet by adding `?theme=` to its\n * `src` URL. `light` is the default and is left implicit so existing/plain\n * snippets stay unchanged. The secret `#key=` fragment is preserved untouched.\n *\n * Inserted via string splicing (before the fragment) rather than `URL`/\n * `URLSearchParams`, which re-serialize ALL params and could subtly re-encode\n * existing ones (e.g. `%20` → `+`). `theme` is a fixed enum, so no encoding is\n * needed; every other part of the URL is passed through byte-for-byte.\n */\nexport function withThemeInSnippet(snippet: string, theme: EmbedSnippetTheme): string {\n if (theme === 'light') return snippet;\n // Require whitespace before `src` so we match the real attribute, never a\n // substring like `data-src`/`srcdoc` (the snippet is backend-generated with\n // each attribute on its own indented line).\n return snippet.replace(/(?<=\\s)src=\"([^\"]*)\"/, (_match, url: string) => {\n const hashIdx = url.indexOf('#');\n const base = hashIdx === -1 ? url : url.slice(0, hashIdx);\n const fragment = hashIdx === -1 ? '' : url.slice(hashIdx);\n const sep = base.includes('?') ? '&' : '?';\n return `src=\"${base}${sep}theme=${theme}${fragment}\"`;\n });\n}\n\n/** Validate an exact https origin (matches the backend allowlist rules). */\nfunction normalizeOrigin(raw: string): string | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n try {\n const url = new URL(trimmed);\n if (url.protocol !== 'https:') return null;\n if ((url.pathname !== '' && url.pathname !== '/') || url.search || url.hash) return null;\n return url.origin;\n } catch {\n return null;\n }\n}\n\nexport function EmbedTab({ dashboardId, colors }: EmbedTabProps) {\n const { policy, secret, loading, error, fetchPolicy, publishToWeb, setAllowedOrigins, rotateKey, unpublish } =\n useDashboardEmbedPolicy();\n\n const [domainInput, setDomainInput] = useState('');\n const [pendingDomains, setPendingDomains] = useState<string[]>([]);\n const [attested, setAttested] = useState(false);\n const [copied, setCopied] = useState(false);\n const [theme, setTheme] = useState<EmbedSnippetTheme>('light');\n const [localError, setLocalError] = useState<string | null>(null);\n\n useEffect(() => {\n if (dashboardId) fetchPolicy(dashboardId);\n }, [dashboardId, fetchPolicy]);\n\n const isPublished = policy?.status === 'ACTIVE';\n const domains = isPublished ? (policy?.allowedParentOrigins ?? []) : pendingDomains;\n\n // The copyable iframe snippet: from the one-time secret if present (fresh\n // publish/rotate), else reconstructed from the locator for an already-published\n // policy (the fragment key is only available in the one-time secret).\n const iframeSnippet = useMemo(() => {\n if (secret?.iframeSnippet) return withThemeInSnippet(secret.iframeSnippet, theme);\n return null;\n }, [secret, theme]);\n\n const addDomain = () => {\n setLocalError(null);\n const origin = normalizeOrigin(domainInput);\n if (!origin) {\n setLocalError('Enter a valid https origin, e.g. https://example.com');\n return;\n }\n if (domains.includes(origin)) {\n setDomainInput('');\n return;\n }\n if (isPublished && policy) {\n void setAllowedOrigins(policy.id, [...domains, origin]);\n } else {\n setPendingDomains((prev) => [...prev, origin]);\n }\n setDomainInput('');\n };\n\n const removeDomain = (origin: string) => {\n if (isPublished && policy) {\n void setAllowedOrigins(\n policy.id,\n domains.filter((d) => d !== origin)\n );\n } else {\n setPendingDomains((prev) => prev.filter((d) => d !== origin));\n }\n };\n\n const handlePublish = async () => {\n setLocalError(null);\n if (pendingDomains.length === 0) {\n setLocalError('Add at least one allowed website domain before publishing.');\n return;\n }\n if (!attested) {\n setLocalError('Please confirm this dashboard is safe to be public.');\n return;\n }\n await publishToWeb(dashboardId, pendingDomains, true);\n };\n\n const handleCopy = async (text: string) => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n } catch {\n setLocalError('Could not copy — select and copy manually.');\n }\n };\n\n const primaryBtn: React.CSSProperties = {\n padding: '8px 14px',\n borderRadius: '8px',\n border: 'none',\n background: colors.actionPrimaryBg,\n color: '#fff',\n fontSize: '13px',\n fontWeight: 600,\n cursor: 'pointer',\n };\n const ghostBtn: React.CSSProperties = {\n padding: '8px 14px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: 'transparent',\n color: colors.textPrimary,\n fontSize: '13px',\n cursor: 'pointer',\n };\n const inputStyle: React.CSSProperties = {\n flex: 1,\n padding: '8px 10px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontSize: '13px',\n };\n\n return (\n <div style={{ padding: '20px 24px' }}>\n {/* Status banner */}\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 14px',\n borderRadius: '10px',\n background: colors.bgSecondary,\n marginBottom: '16px',\n }}\n >\n <div>\n <div style={{ fontSize: '14px', fontWeight: 600, color: colors.textPrimary }}>Publish to the web</div>\n <div style={{ fontSize: '12px', color: colors.textSecondary, marginTop: '2px' }}>\n {isPublished\n ? 'This dashboard is live. Anyone with the link on an allowed website sees its live data.'\n : 'Embed this dashboard on your website. Viewers see live data — no login required.'}\n </div>\n </div>\n <span\n style={{\n fontSize: '11px',\n fontWeight: 700,\n padding: '3px 8px',\n borderRadius: '999px',\n color: '#fff',\n background: isPublished ? colors.statusSuccessBg : colors.textTertiary,\n }}\n >\n {isPublished ? 'PUBLISHED' : 'PRIVATE'}\n </span>\n </div>\n\n {/* Allowed domains */}\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '6px' }}>\n Allowed website domains\n </div>\n <div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}>\n <input\n style={inputStyle}\n placeholder=\"https://yoursite.com\"\n value={domainInput}\n onChange={(e) => setDomainInput(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') addDomain();\n }}\n />\n <button style={ghostBtn} onClick={addDomain} type=\"button\">\n Add\n </button>\n </div>\n {domains.length === 0 ? (\n <div style={{ fontSize: '12px', color: colors.textTertiary }}>\n No domains yet. Only listed https domains may frame this dashboard.\n </div>\n ) : (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>\n {domains.map((d) => (\n <div\n key={d}\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '6px 10px',\n borderRadius: '8px',\n background: colors.bgTertiary,\n }}\n >\n <span style={{ fontSize: '13px', color: colors.textPrimary }}>{d}</span>\n <button\n style={{ ...ghostBtn, padding: '2px 8px', color: colors.statusErrorText }}\n onClick={() => removeDomain(d)}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n ))}\n </div>\n )}\n </div>\n\n {/* Publish CTA (unpublished) */}\n {!isPublished && (\n <div style={{ marginBottom: '8px' }}>\n <label style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '12px' }}>\n <input type=\"checkbox\" checked={attested} onChange={(e) => setAttested(e.target.checked)} />\n <span style={{ fontSize: '12px', color: colors.textSecondary }}>\n I understand this makes the dashboard's live data visible to anyone with the link, and I confirm it\n is safe to be public.\n </span>\n </label>\n <button style={primaryBtn} onClick={handlePublish} disabled={loading} type=\"button\">\n {loading ? 'Publishing…' : 'Publish to web'}\n </button>\n </div>\n )}\n\n {/* Embed snippet + management (published) */}\n {isPublished && (\n <>\n {iframeSnippet ? (\n <div style={{ marginBottom: '16px' }}>\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n marginBottom: '6px',\n }}\n >\n <span style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary }}>Embed snippet</span>\n <label style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>\n <span style={{ fontSize: '12px', color: colors.textTertiary }}>Theme</span>\n <select\n value={theme}\n onChange={(e) => {\n // The options are fixed, but narrow instead of casting so an\n // unexpected value can never slip into state.\n const value = e.target.value;\n if (value === 'light' || value === 'dark' || value === 'auto') {\n setTheme(value);\n }\n }}\n style={{\n padding: '4px 8px',\n borderRadius: '6px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontSize: '12px',\n cursor: 'pointer',\n }}\n >\n {THEME_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </label>\n </div>\n <textarea\n readOnly\n value={iframeSnippet}\n style={{\n width: '100%',\n minHeight: '96px',\n padding: '10px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontFamily: 'monospace',\n fontSize: '12px',\n resize: 'vertical',\n }}\n />\n <button style={{ ...ghostBtn, marginTop: '8px' }} onClick={() => handleCopy(iframeSnippet)} type=\"button\">\n {copied ? 'Copied ✓' : 'Copy snippet'}\n </button>\n </div>\n ) : (\n <div\n style={{\n fontSize: '12px',\n color: colors.textSecondary,\n background: colors.bgSecondary,\n borderRadius: '8px',\n padding: '10px 12px',\n marginBottom: '16px',\n }}\n >\n The embed link is already active. For security the snippet (which contains the secret key) is only shown\n once at publish time — use <strong>Rotate link</strong> to generate a fresh snippet.\n </div>\n )}\n\n <div style={{ display: 'flex', gap: '8px' }}>\n <button style={ghostBtn} onClick={() => policy && rotateKey(policy.id)} disabled={loading} type=\"button\">\n Rotate link\n </button>\n <button\n style={{ ...ghostBtn, color: colors.statusErrorText, borderColor: colors.statusErrorText }}\n onClick={() => {\n if (\n policy &&\n window.confirm(\n 'Unpublish this dashboard? The embed will stop working immediately for anyone using the link.'\n )\n ) {\n void unpublish(policy.id);\n }\n }}\n disabled={loading}\n type=\"button\"\n >\n Unpublish\n </button>\n </div>\n </>\n )}\n\n {(localError || error) && (\n <div style={{ marginTop: '12px', fontSize: '12px', color: colors.statusErrorText }}>\n {localError ?? error?.message}\n </div>\n )}\n </div>\n );\n}\n\nexport default EmbedTab;\n"],"mappings":"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"}
|