@burdenoff/microfe-bigconsole 2026.730.2 → 2026.730.4
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/dist/bigconsole/components/dashboard/DashboardCanvas.js +73 -68
- package/dist/bigconsole/components/dashboard/DashboardCanvas.js.map +1 -1
- package/dist/bigconsole/components/dashboard/TemplateSetupBanner.js +82 -0
- package/dist/bigconsole/components/dashboard/TemplateSetupBanner.js.map +1 -0
- package/dist/bigconsole/components/embed/EmbedErrorBoundary.js +25 -0
- package/dist/bigconsole/components/embed/EmbedErrorBoundary.js.map +1 -0
- package/dist/bigconsole/components/embed/EmbedWidgetRenderer.js +36 -0
- package/dist/bigconsole/components/embed/EmbedWidgetRenderer.js.map +1 -0
- package/dist/bigconsole/components/embed/ReadOnlyDashboardRenderer.js +62 -0
- package/dist/bigconsole/components/embed/ReadOnlyDashboardRenderer.js.map +1 -0
- package/dist/bigconsole/components/embed/index.js +7 -0
- package/dist/bigconsole/components/embed/renderer-manifest.js +77 -0
- package/dist/bigconsole/components/embed/renderer-manifest.js.map +1 -0
- package/dist/bigconsole/components/embed/types.js +6 -0
- package/dist/bigconsole/components/embed/types.js.map +1 -0
- package/dist/bigconsole/components/embed/validate.js +29 -0
- package/dist/bigconsole/components/embed/validate.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/KpiComparisonEmbed.js +34 -0
- package/dist/bigconsole/components/embed/widgets/KpiComparisonEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/ListEmbed.js +36 -0
- package/dist/bigconsole/components/embed/widgets/ListEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/MetricCardEmbed.js +20 -0
- package/dist/bigconsole/components/embed/widgets/MetricCardEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/TableEmbed.js +40 -0
- package/dist/bigconsole/components/embed/widgets/TableEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/TextEmbed.js +20 -0
- package/dist/bigconsole/components/embed/widgets/TextEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/shared.js +29 -0
- package/dist/bigconsole/components/embed/widgets/shared.js.map +1 -0
- package/dist/bigconsole/components/widgets/WidgetWrapper.js +2 -1
- package/dist/bigconsole/components/widgets/WidgetWrapper.js.map +1 -1
- package/dist/bigconsole/components/widgets/states/EmptyState.js +3 -3
- package/dist/bigconsole/components/widgets/states/EmptyState.js.map +1 -1
- package/package.json +5 -1
|
@@ -15,36 +15,37 @@ import u from "./DashboardToolbar.js";
|
|
|
15
15
|
import d from "./GlobalFiltersBar/GlobalFiltersBar.js";
|
|
16
16
|
import "./GlobalFiltersBar/index.js";
|
|
17
17
|
import ee from "./LiveBoardBanner.js";
|
|
18
|
-
import te from "./
|
|
18
|
+
import te from "./TemplateSetupBanner.js";
|
|
19
|
+
import ne from "./PageTabsManager/PageTabsManager.js";
|
|
19
20
|
import "./PageTabsManager/index.js";
|
|
20
|
-
import
|
|
21
|
+
import re from "./ShareDialog/ShareDialog.js";
|
|
21
22
|
import "./ShareDialog/index.js";
|
|
22
|
-
import
|
|
23
|
-
import { WidgetInspector as
|
|
23
|
+
import ie from "./WidgetPalette.js";
|
|
24
|
+
import { WidgetInspector as ae } from "../builder/WidgetInspector.js";
|
|
24
25
|
import "../builder/index.js";
|
|
25
|
-
import { calculatePositionFromDrop as
|
|
26
|
-
import { memo as f, useCallback as p, useEffect as
|
|
26
|
+
import { calculatePositionFromDrop as oe, findNextAvailablePosition as se } from "../../utils/widgetPositioning.js";
|
|
27
|
+
import { memo as f, useCallback as p, useEffect as ce, useMemo as le, useRef as m, useState as h } from "react";
|
|
27
28
|
import { jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
28
29
|
//#region src/bigconsole/components/dashboard/DashboardCanvas.tsx
|
|
29
|
-
var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste:
|
|
30
|
-
let
|
|
30
|
+
var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: ue, onSave: de, isSaving: b = !1, onExport: fe, pages: x = [], activePageId: pe, onPageChange: me, onPagesReorder: S, onPageAdd: he, onPageRename: ge, onPageDuplicate: _e, onPageDelete: ve, showFilters: ye = !0, showPageTabs: be = !0, enableCrossFilter: xe = !0 }) {
|
|
31
|
+
let C = m(null), w = m(null), [T, Se] = h(1200), [Ce, E] = h(!1), [D, O] = h(null), [k, A] = h(!1), [we, j] = h(null), M = t((e) => e.viewMode === "edit"), { workspaceId: N, apiGatewayUrl: P, authToken: F } = r(), { installedWidgets: Te, loading: Ee } = c({
|
|
31
32
|
workspaceId: N,
|
|
32
33
|
apiGatewayUrl: P,
|
|
33
34
|
authToken: F,
|
|
34
35
|
enabled: M
|
|
35
|
-
}), { definitions:
|
|
36
|
+
}), { definitions: De } = a({
|
|
36
37
|
workspaceId: N,
|
|
37
38
|
apiGatewayUrl: P,
|
|
38
39
|
authToken: F,
|
|
39
40
|
enabled: M
|
|
40
|
-
}), I = t((e) => e.zoomLevel), L = t((e) => e.sidebarCollapsed), R = t((e) => e.toggleSidebar), z = !L, B = t((e) => e.setGlobalFilterValues),
|
|
41
|
+
}), I = t((e) => e.zoomLevel), L = t((e) => e.sidebarCollapsed), R = t((e) => e.toggleSidebar), z = !L, B = t((e) => e.setGlobalFilterValues), Oe = p((e) => {
|
|
41
42
|
B(e);
|
|
42
|
-
}, [B]), { createWidget: V } = o(v, f, { skipFetch: !0 }), H = e((e) => e.widgets), U =
|
|
43
|
+
}, [B]), { createWidget: V } = o(v, f, { skipFetch: !0 }), H = e((e) => e.widgets), U = le(() => {
|
|
43
44
|
let e = Array.from(H.values());
|
|
44
45
|
return v ? e.filter((e) => e.pageId === v) : e;
|
|
45
|
-
}, [H, v]),
|
|
46
|
-
|
|
47
|
-
let e =
|
|
46
|
+
}, [H, v]), ke = e((e) => e.selectedWidgetId), W = e((e) => e.selectWidget), G = e((e) => e.addWidget), K = e((e) => e.updateWidgetPosition), q = e((e) => e.configPanelOpen), Ae = e((e) => e.closeConfigPanel);
|
|
47
|
+
ce(() => {
|
|
48
|
+
let e = C.current;
|
|
48
49
|
if (!e) return;
|
|
49
50
|
let t = () => {
|
|
50
51
|
Se(e.offsetWidth);
|
|
@@ -55,7 +56,7 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
55
56
|
}
|
|
56
57
|
return window.addEventListener("resize", t), () => window.removeEventListener("resize", t);
|
|
57
58
|
}, []);
|
|
58
|
-
let
|
|
59
|
+
let J = p((e) => {
|
|
59
60
|
e.forEach((e) => {
|
|
60
61
|
K(e.i, {
|
|
61
62
|
x: e.x,
|
|
@@ -64,17 +65,17 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
64
65
|
height: e.h
|
|
65
66
|
});
|
|
66
67
|
});
|
|
67
|
-
}, [K]),
|
|
68
|
+
}, [K]), je = p((e) => {
|
|
68
69
|
W(e);
|
|
69
70
|
}, [W]), Y = p(async (e, t) => {
|
|
70
|
-
|
|
71
|
+
O(null);
|
|
71
72
|
let n = t?.definition || (e.type ? i(e.type) : null), r = {
|
|
72
73
|
width: n?.defaultSize?.width ?? 4,
|
|
73
74
|
height: n?.defaultSize?.height ?? 4
|
|
74
75
|
}, a;
|
|
75
76
|
if (t?.position) a = t.position;
|
|
76
77
|
else {
|
|
77
|
-
let { x: e, y: t } =
|
|
78
|
+
let { x: e, y: t } = se(U, r, { cols: 12 });
|
|
78
79
|
a = {
|
|
79
80
|
x: e,
|
|
80
81
|
y: t,
|
|
@@ -102,7 +103,7 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
102
103
|
W(t.id);
|
|
103
104
|
return;
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
+
O("Could not create the widget. Check the connection and try again.");
|
|
106
107
|
return;
|
|
107
108
|
}
|
|
108
109
|
let s = {
|
|
@@ -135,12 +136,12 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
135
136
|
G,
|
|
136
137
|
W,
|
|
137
138
|
U
|
|
138
|
-
]),
|
|
139
|
-
|
|
140
|
-
}, []),
|
|
141
|
-
|
|
142
|
-
}, []),
|
|
143
|
-
e.preventDefault(),
|
|
139
|
+
]), Me = p((e) => {
|
|
140
|
+
A(!0), j(e);
|
|
141
|
+
}, []), Ne = p(() => {
|
|
142
|
+
A(!1), j(null);
|
|
143
|
+
}, []), Pe = p((e) => {
|
|
144
|
+
e.preventDefault(), A(!1), j(null);
|
|
144
145
|
try {
|
|
145
146
|
let t = e.dataTransfer.getData("application/json");
|
|
146
147
|
if (!t) return;
|
|
@@ -154,12 +155,12 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
154
155
|
defaultConfig: r.defaultConfig,
|
|
155
156
|
defaultSize: r.defaultSize || a.defaultSize
|
|
156
157
|
});
|
|
157
|
-
let s =
|
|
158
|
+
let s = C.current?.getBoundingClientRect();
|
|
158
159
|
if (!s) return;
|
|
159
160
|
let c = {
|
|
160
161
|
width: a.defaultSize.width,
|
|
161
162
|
height: a.defaultSize.height
|
|
162
|
-
}, { x: l, y: u } =
|
|
163
|
+
}, { x: l, y: u } = oe(e.clientX, e.clientY, s, T, c, { cols: 12 }), d = {
|
|
163
164
|
x: l,
|
|
164
165
|
y: u,
|
|
165
166
|
width: c.width,
|
|
@@ -176,9 +177,9 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
176
177
|
} catch (e) {
|
|
177
178
|
console.error("Failed to handle widget drop:", e);
|
|
178
179
|
}
|
|
179
|
-
}, [
|
|
180
|
+
}, [T, Y]), Fe = p((e) => {
|
|
180
181
|
e.preventDefault(), e.dataTransfer.dropEffect = "copy";
|
|
181
|
-
}, []),
|
|
182
|
+
}, []), Ie = p((e, t) => {
|
|
182
183
|
let n = t || i(e);
|
|
183
184
|
Y({
|
|
184
185
|
type: e,
|
|
@@ -191,8 +192,8 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
191
192
|
L && R(), window.requestAnimationFrame(() => {
|
|
192
193
|
document.querySelector("input[placeholder=\"Search widgets...\"]")?.focus();
|
|
193
194
|
});
|
|
194
|
-
}, [L, R]), { exportPNG: Q, exportPDF: $, isExporting:
|
|
195
|
-
containerRef:
|
|
195
|
+
}, [L, R]), { exportPNG: Q, exportPDF: $, isExporting: Le } = s({
|
|
196
|
+
containerRef: w,
|
|
196
197
|
name: y || "dashboard"
|
|
197
198
|
});
|
|
198
199
|
return /* @__PURE__ */ _("div", {
|
|
@@ -204,9 +205,9 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
204
205
|
onAddWidget: Z,
|
|
205
206
|
onTogglePalette: X,
|
|
206
207
|
isPaletteOpen: z,
|
|
207
|
-
onPaste:
|
|
208
|
-
onSave:
|
|
209
|
-
isSaving:
|
|
208
|
+
onPaste: ue,
|
|
209
|
+
onSave: de,
|
|
210
|
+
isSaving: b,
|
|
210
211
|
onExport: fe,
|
|
211
212
|
onExportImage: p(() => {
|
|
212
213
|
Q();
|
|
@@ -214,35 +215,39 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
214
215
|
onExportPdf: p(() => {
|
|
215
216
|
$();
|
|
216
217
|
}, [$]),
|
|
217
|
-
isExportingImage:
|
|
218
|
-
onShare: () =>
|
|
218
|
+
isExportingImage: Le,
|
|
219
|
+
onShare: () => E(!0)
|
|
219
220
|
}),
|
|
220
|
-
|
|
221
|
+
D && /* @__PURE__ */ _("div", {
|
|
221
222
|
className: "flex items-center justify-between gap-3 border-b border-status-error-border bg-status-error-bg px-4 py-2 text-sm text-status-error-text",
|
|
222
223
|
role: "alert",
|
|
223
|
-
children: [/* @__PURE__ */ g("span", { children:
|
|
224
|
+
children: [/* @__PURE__ */ g("span", { children: D }), /* @__PURE__ */ g("button", {
|
|
224
225
|
type: "button",
|
|
225
226
|
className: "font-medium underline underline-offset-2",
|
|
226
|
-
onClick: () =>
|
|
227
|
+
onClick: () => O(null),
|
|
227
228
|
children: "Dismiss"
|
|
228
229
|
})]
|
|
229
230
|
}),
|
|
231
|
+
/* @__PURE__ */ g(te, {
|
|
232
|
+
widgets: U,
|
|
233
|
+
isEditMode: M
|
|
234
|
+
}),
|
|
230
235
|
/* @__PURE__ */ g(ee, {
|
|
231
236
|
widgets: U,
|
|
232
237
|
isEditMode: M
|
|
233
238
|
}),
|
|
234
239
|
ye && /* @__PURE__ */ g(d, {
|
|
235
240
|
dashboardId: f,
|
|
236
|
-
onFilterChange:
|
|
241
|
+
onFilterChange: Oe,
|
|
237
242
|
collapsible: !0
|
|
238
243
|
}),
|
|
239
|
-
be && (M ||
|
|
240
|
-
pages:
|
|
241
|
-
activePageId: pe ||
|
|
244
|
+
be && (M || x.length > 0) && /* @__PURE__ */ g(ne, {
|
|
245
|
+
pages: x,
|
|
246
|
+
activePageId: pe || x[0]?.id,
|
|
242
247
|
editable: M,
|
|
243
248
|
onPageSelect: me || (() => {}),
|
|
244
|
-
onPagesReorder:
|
|
245
|
-
|
|
249
|
+
onPagesReorder: S ? (e) => {
|
|
250
|
+
S(e.map((e) => x.find((t) => t.id === e)).filter(Boolean));
|
|
246
251
|
} : void 0,
|
|
247
252
|
onPageCreate: he,
|
|
248
253
|
onPageRename: ge,
|
|
@@ -255,30 +260,30 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
255
260
|
marginRight: q ? "var(--bc-inspector-width, 420px)" : void 0,
|
|
256
261
|
transition: "margin-right 0.2s ease"
|
|
257
262
|
},
|
|
258
|
-
children: [M && /* @__PURE__ */ g(
|
|
263
|
+
children: [M && /* @__PURE__ */ g(ie, {
|
|
259
264
|
isOpen: z,
|
|
260
265
|
onClose: X,
|
|
261
|
-
onWidgetSelect:
|
|
262
|
-
onDragStart:
|
|
263
|
-
onDragEnd:
|
|
264
|
-
installedWidgets:
|
|
265
|
-
installedWidgetsLoading:
|
|
266
|
-
definitions:
|
|
266
|
+
onWidgetSelect: Ie,
|
|
267
|
+
onDragStart: Me,
|
|
268
|
+
onDragEnd: Ne,
|
|
269
|
+
installedWidgets: Te,
|
|
270
|
+
installedWidgetsLoading: Ee,
|
|
271
|
+
definitions: De
|
|
267
272
|
}), /* @__PURE__ */ _("div", {
|
|
268
|
-
ref:
|
|
273
|
+
ref: C,
|
|
269
274
|
className: "flex-1 min-w-0 overflow-auto bg-bg-sunken",
|
|
270
|
-
onDrop:
|
|
271
|
-
onDragOver:
|
|
275
|
+
onDrop: Pe,
|
|
276
|
+
onDragOver: Fe,
|
|
272
277
|
onClick: () => {
|
|
273
278
|
M && W(null);
|
|
274
279
|
},
|
|
275
280
|
style: {
|
|
276
281
|
position: "relative",
|
|
277
|
-
border:
|
|
278
|
-
backgroundColor:
|
|
282
|
+
border: k ? "2px dashed var(--color-action-primary-bg)" : "none",
|
|
283
|
+
backgroundColor: k ? "var(--color-bg-hover)" : void 0,
|
|
279
284
|
transition: "border-color 0.2s, background-color 0.2s"
|
|
280
285
|
},
|
|
281
|
-
children: [
|
|
286
|
+
children: [k && /* @__PURE__ */ g("div", {
|
|
282
287
|
style: {
|
|
283
288
|
position: "absolute",
|
|
284
289
|
top: "50%",
|
|
@@ -300,12 +305,12 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
300
305
|
},
|
|
301
306
|
children: [
|
|
302
307
|
"Drop to add ",
|
|
303
|
-
/* @__PURE__ */ g("strong", { children:
|
|
308
|
+
/* @__PURE__ */ g("strong", { children: we }),
|
|
304
309
|
" widget"
|
|
305
310
|
]
|
|
306
311
|
})
|
|
307
312
|
}), /* @__PURE__ */ g("div", {
|
|
308
|
-
ref:
|
|
313
|
+
ref: w,
|
|
309
314
|
className: "min-h-full origin-top-left transition-transform",
|
|
310
315
|
style: {
|
|
311
316
|
transform: `scale(${I / 100})`,
|
|
@@ -317,10 +322,10 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
317
322
|
children: /* @__PURE__ */ g(l, {
|
|
318
323
|
widgets: U,
|
|
319
324
|
isEditMode: M,
|
|
320
|
-
width:
|
|
321
|
-
onLayoutChange:
|
|
322
|
-
onWidgetSelect:
|
|
323
|
-
selectedWidgetId:
|
|
325
|
+
width: T,
|
|
326
|
+
onLayoutChange: J,
|
|
327
|
+
onWidgetSelect: je,
|
|
328
|
+
selectedWidgetId: ke
|
|
324
329
|
})
|
|
325
330
|
}) : /* @__PURE__ */ _("div", {
|
|
326
331
|
className: "flex flex-col items-center justify-center min-h-[400px] p-8",
|
|
@@ -370,15 +375,15 @@ var v = f(function({ dashboardId: f, pageId: v, title: y, onPaste: le, onSave: u
|
|
|
370
375
|
})]
|
|
371
376
|
})]
|
|
372
377
|
}),
|
|
373
|
-
/* @__PURE__ */ g(
|
|
378
|
+
/* @__PURE__ */ g(ae, {
|
|
374
379
|
isOpen: q,
|
|
375
|
-
onClose:
|
|
380
|
+
onClose: Ae
|
|
376
381
|
}),
|
|
377
|
-
/* @__PURE__ */ g(
|
|
382
|
+
/* @__PURE__ */ g(re, {
|
|
378
383
|
dashboardId: f,
|
|
379
384
|
dashboardName: y || "Dashboard",
|
|
380
385
|
isOpen: Ce,
|
|
381
|
-
onClose: () =>
|
|
386
|
+
onClose: () => E(!1)
|
|
382
387
|
})
|
|
383
388
|
]
|
|
384
389
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardCanvas.js","names":[],"sources":["../../../../src/bigconsole/components/dashboard/DashboardCanvas.tsx"],"sourcesContent":["/**\n * DashboardCanvas Component\n *\n * Main canvas for dashboard with grid layout and widget rendering.\n * Phase 2: Added GlobalFiltersBar and PageTabsManager integration.\n * Phase 3: Added WidgetPalette sidebar for drag-and-drop widget creation.\n */\n\nimport { type FC, memo, useRef, useState, useCallback, useEffect, useMemo } from 'react';\nimport type { Layout } from 'react-grid-layout';\nimport type { Widget, WidgetType } from '../../types';\nimport { useDashboardStore, useWidgetStore } from '../../store';\nimport { useWidgetOperations } from '../../hooks/useWidgetOperations';\nimport { useInstalledWidgets } from '../../hooks/useInstalledWidgets';\nimport { useWidgetCatalog } from '../../hooks/useWidgetCatalog';\nimport { useDashboardImageExport } from '../../hooks/useDashboardImageExport';\nimport { useBigConsole } from '../../context';\nimport { DashboardGrid } from './DashboardGrid';\nimport { DashboardToolbar } from './DashboardToolbar';\nimport { GlobalFiltersBar } from './GlobalFiltersBar';\nimport { LiveBoardBanner } from './LiveBoardBanner';\nimport { PageTabsManager, type DashboardPageTab } from './PageTabsManager';\nimport { ShareDialog } from './ShareDialog';\nimport { WidgetPalette } from './WidgetPalette';\nimport { WidgetInspector } from '../builder';\nimport { getWidgetDefinition, type WidgetDefinition } from '../widgets/WidgetRegistry';\n// Phase 2: Cross-filtering context\nimport { CrossFilterProvider } from '../../contexts';\n// Smart widget positioning\nimport { findNextAvailablePosition, calculatePositionFromDrop } from '../../utils/widgetPositioning';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DashboardCanvasProps {\n /** Dashboard ID */\n dashboardId: string;\n /** Page ID (for loading widgets) */\n pageId?: string;\n /** Dashboard title */\n title?: string;\n /** Callback to paste widget */\n onPaste?: () => void;\n /** Callback when dashboard is saved */\n onSave?: () => void;\n /** Whether save is in progress */\n isSaving?: boolean;\n /** Callback to export dashboard */\n onExport?: () => void;\n /** Dashboard pages for PageTabsManager */\n pages?: DashboardPageTab[];\n /** Active page ID */\n activePageId?: string;\n /** Callback when page changes */\n onPageChange?: (pageId: string) => void;\n /** Callback when pages are reordered */\n onPagesReorder?: (pages: DashboardPageTab[]) => void;\n /** Callback when page is added */\n onPageAdd?: () => void;\n /** Callback when page is renamed */\n onPageRename?: (pageId: string, newName: string) => void;\n /** Callback when page is duplicated */\n onPageDuplicate?: (pageId: string) => void;\n /** Callback when page is deleted */\n onPageDelete?: (pageId: string) => void;\n /** Whether to show global filters bar */\n showFilters?: boolean;\n /** Whether to show page tabs */\n showPageTabs?: boolean;\n /** Whether to enable cross-filtering (widget clicks update filters) */\n enableCrossFilter?: boolean;\n}\n\ninterface WidgetPlacement {\n position: { x: number; y: number; width: number; height: number };\n definition?: WidgetDefinition | null;\n}\n\ninterface PaletteDropData {\n type: WidgetType;\n name?: string;\n category?: WidgetDefinition['category'];\n defaultConfig?: Record<string, unknown>;\n defaultSize?: WidgetDefinition['defaultSize'];\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const DashboardCanvas: FC<DashboardCanvasProps> = memo(function DashboardCanvas({\n dashboardId,\n pageId,\n title,\n onPaste,\n onSave,\n isSaving = false,\n onExport,\n pages = [],\n activePageId,\n onPageChange,\n onPagesReorder,\n onPageAdd,\n onPageRename,\n onPageDuplicate,\n onPageDelete,\n showFilters = true,\n showPageTabs = true,\n enableCrossFilter = true,\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n // Inner board element (the grid surface) — rasterized for PNG/PDF board-pack export.\n const boardRef = useRef<HTMLDivElement>(null);\n const [containerWidth, setContainerWidth] = useState(1200);\n const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);\n const [widgetCreateError, setWidgetCreateError] = useState<string | null>(null);\n const [isDraggingWidget, setIsDraggingWidget] = useState(false);\n const [dragWidgetType, setDragWidgetType] = useState<WidgetType | null>(null);\n\n // Store state\n const isEditMode = useDashboardStore((state) => state.viewMode === 'edit');\n\n // Fetch installed widgets only in edit mode — the palette and add-widget dialog\n // are not usable in read-only view, so skip the request for viewer-only sessions.\n const { workspaceId, apiGatewayUrl, authToken } = useBigConsole();\n const { installedWidgets, loading: installedWidgetsLoading } = useInstalledWidgets({\n workspaceId,\n apiGatewayUrl,\n authToken,\n enabled: isEditMode,\n });\n // Built-in widget catalog from the backend (getWidgetTypes); falls back to the\n // bundled registry while loading or if the supergraph hasn't adopted it yet.\n const { definitions: widgetCatalog } = useWidgetCatalog({\n workspaceId,\n apiGatewayUrl,\n authToken,\n enabled: isEditMode,\n });\n const zoomLevel = useDashboardStore((state) => state.zoomLevel);\n const sidebarCollapsed = useDashboardStore((state) => state.sidebarCollapsed);\n const toggleSidebar = useDashboardStore((state) => state.toggleSidebar);\n const isPaletteOpen = !sidebarCollapsed;\n const setGlobalFilterValues = useDashboardStore((state) => state.setGlobalFilterValues);\n\n // Handle filter changes - update store for widget access\n const handleFilterChange = useCallback(\n (filterValues: Record<string, unknown>) => {\n // Update global filter values in dashboard store\n setGlobalFilterValues(filterValues);\n\n // Widgets can now access filter values via useGlobalFilterValues() hook\n // When backend supports filter parameters, widget queries will include these values\n },\n [setGlobalFilterValues]\n );\n\n // Widget operations hook (connects to backend) - skipFetch since parent page fetches\n const { createWidget: createWidgetApi } = useWidgetOperations(pageId, dashboardId, { skipFetch: true });\n\n const widgetsMap = useWidgetStore((state) => state.widgets);\n // Filter widgets to only show widgets for the current page\n const widgetsArray = useMemo(() => {\n const allWidgets = Array.from(widgetsMap.values());\n if (!pageId) return allWidgets;\n return allWidgets.filter((widget) => widget.pageId === pageId);\n }, [widgetsMap, pageId]);\n const selectedWidgetId = useWidgetStore((state) => state.selectedWidgetId);\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n const addWidgetToStore = useWidgetStore((state) => state.addWidget);\n const updateWidgetPosition = useWidgetStore((state) => state.updateWidgetPosition);\n const configPanelOpen = useWidgetStore((state) => state.configPanelOpen);\n const closeConfigPanel = useWidgetStore((state) => state.closeConfigPanel);\n\n // Keep react-grid-layout's explicit width in sync with the actual canvas.\n // A ResizeObserver is required because opening the docked inspector or widget\n // palette resizes this element without resizing the browser window.\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const updateWidth = () => {\n setContainerWidth(container.offsetWidth);\n };\n\n updateWidth();\n\n if (typeof ResizeObserver !== 'undefined') {\n const observer = new ResizeObserver(updateWidth);\n observer.observe(container);\n return () => observer.disconnect();\n }\n\n window.addEventListener('resize', updateWidth);\n return () => window.removeEventListener('resize', updateWidth);\n }, []);\n\n // Handle layout change\n const handleLayoutChange = useCallback(\n (layout: Layout[]) => {\n layout.forEach((item) => {\n updateWidgetPosition(item.i, {\n x: item.x,\n y: item.y,\n width: item.w,\n height: item.h,\n });\n });\n },\n [updateWidgetPosition]\n );\n\n // Handle widget select\n const handleWidgetSelect = useCallback(\n (widgetId: string) => {\n selectWidget(widgetId);\n },\n [selectWidget]\n );\n\n // Create with defaults, select the result, then configure it in the inspector.\n const handleAddWidget = useCallback(\n async (widgetData: Partial<Widget>, placement?: WidgetPlacement) => {\n setWidgetCreateError(null);\n const definition = placement?.definition || (widgetData.type ? getWidgetDefinition(widgetData.type) : null);\n const widgetSize = {\n width: definition?.defaultSize?.width ?? 4,\n height: definition?.defaultSize?.height ?? 4,\n };\n\n let position: { x: number; y: number; width: number; height: number };\n if (placement?.position) {\n position = placement.position;\n } else {\n // Use smart placement algorithm - horizontal first, then next row\n const { x, y } = findNextAvailablePosition(widgetsArray, widgetSize, { cols: 12 });\n position = {\n x,\n y,\n width: widgetSize.width,\n height: widgetSize.height,\n };\n }\n\n // Preserve legacy binding fields when creating from imported/default data.\n const extendedData = widgetData as Partial<Widget> & { dataSinkId?: string; parserId?: string };\n\n // Try to create via API first\n if (pageId) {\n const widget = await createWidgetApi({\n pageId,\n dashboardId,\n type: widgetData.type!,\n title: widgetData.title || 'Untitled Widget',\n description: widgetData.description,\n // v2.0: Renderer and template support\n renderer: widgetData.renderer,\n template: widgetData.template,\n config: widgetData.config || {},\n position,\n refreshInterval: widgetData.refreshInterval ?? undefined,\n // v2.0: Data source fields (DataSink → Parser → Widget)\n dataSinkId: extendedData.dataSinkId,\n parserId: extendedData.parserId,\n });\n\n if (widget) {\n selectWidget(widget.id);\n return;\n }\n\n setWidgetCreateError('Could not create the widget. Check the connection and try again.');\n return;\n }\n\n // Fallback to local store if API fails or no pageId\n const newWidget: Widget = {\n id: crypto.randomUUID(),\n dashboardId,\n pageId: pageId || 'local',\n type: widgetData.type!,\n title: widgetData.title || 'Untitled Widget',\n description: widgetData.description,\n // v2.0: Renderer and template support\n renderer: widgetData.renderer,\n template: widgetData.template,\n refreshInterval: widgetData.refreshInterval,\n config: widgetData.config || {},\n position,\n positionX: position.x,\n positionY: position.y,\n positionWidth: position.width,\n positionHeight: position.height,\n // v2.0: Data source fields (DataSink → Parser → Widget)\n dataSinkId: extendedData.dataSinkId,\n parserId: extendedData.parserId,\n translations: [],\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n addWidgetToStore(newWidget);\n selectWidget(newWidget.id);\n },\n [dashboardId, pageId, createWidgetApi, addWidgetToStore, selectWidget, widgetsArray]\n );\n\n // Handle drag start from palette\n const handlePaletteDragStart = useCallback((type: WidgetType) => {\n setIsDraggingWidget(true);\n setDragWidgetType(type);\n }, []);\n\n // Handle drag end from palette\n const handlePaletteDragEnd = useCallback(() => {\n setIsDraggingWidget(false);\n setDragWidgetType(null);\n }, []);\n\n // Handle drop on canvas - create immediately with defaults.\n const handleCanvasDrop = useCallback(\n (e: React.DragEvent) => {\n e.preventDefault();\n setIsDraggingWidget(false);\n setDragWidgetType(null);\n\n try {\n const data = e.dataTransfer.getData('application/json');\n if (!data) return;\n\n const parsedData: unknown = JSON.parse(data);\n if (!parsedData || typeof parsedData !== 'object' || !('type' in parsedData)) return;\n const widgetData = parsedData as PaletteDropData;\n const definition = getWidgetDefinition(widgetData.type);\n\n // If the dropped data includes defaultConfig (from WidgetPalette drag serialization),\n // reconstruct a full WidgetDefinition so installed/store widget identity is preserved.\n let droppedDefinition: WidgetDefinition | null = null;\n if (widgetData.defaultConfig && widgetData.category) {\n droppedDefinition = {\n ...definition,\n name: widgetData.name || definition.name,\n category: widgetData.category,\n defaultConfig: widgetData.defaultConfig,\n defaultSize: widgetData.defaultSize || definition.defaultSize,\n };\n }\n\n // Calculate drop position relative to canvas\n const canvasRect = containerRef.current?.getBoundingClientRect();\n if (!canvasRect) return;\n\n // Use utility to calculate grid position from drop coordinates\n const widgetSize = {\n width: definition.defaultSize.width,\n height: definition.defaultSize.height,\n };\n\n const { x: gridX, y: gridY } = calculatePositionFromDrop(\n e.clientX,\n e.clientY,\n canvasRect,\n containerWidth,\n widgetSize,\n { cols: 12 }\n );\n\n const position = {\n x: gridX,\n y: gridY,\n width: widgetSize.width,\n height: widgetSize.height,\n };\n\n void handleAddWidget(\n {\n type: widgetData.type,\n title: widgetData.name || definition.name,\n config: widgetData.defaultConfig || definition.defaultConfig,\n },\n { position, definition: droppedDefinition }\n );\n } catch (error) {\n console.error('Failed to handle widget drop:', error);\n }\n },\n [containerWidth, handleAddWidget]\n );\n\n // Handle drag over to allow drop\n const handleCanvasDragOver = useCallback((e: React.DragEvent) => {\n e.preventDefault();\n e.dataTransfer.dropEffect = 'copy';\n }, []);\n\n // Handle widget select from palette (quick add without drag)\n const handlePaletteWidgetSelect = useCallback(\n (type: WidgetType, paletteDefinition?: WidgetDefinition) => {\n const definition = paletteDefinition || getWidgetDefinition(type);\n handleAddWidget({\n type,\n title: definition.name,\n config: definition.defaultConfig,\n });\n },\n [handleAddWidget]\n );\n\n // Toggle palette\n const togglePalette = useCallback(() => {\n toggleSidebar();\n }, [toggleSidebar]);\n\n const openWidgetPalette = useCallback(() => {\n if (sidebarCollapsed) toggleSidebar();\n window.requestAnimationFrame(() => {\n document.querySelector<HTMLInputElement>('input[placeholder=\"Search widgets...\"]')?.focus();\n });\n }, [sidebarCollapsed, toggleSidebar]);\n\n // Board-pack export (PNG + PDF) — rasterizes the inner board element.\n const {\n exportPNG,\n exportPDF,\n isExporting: isExportingImage,\n } = useDashboardImageExport({ containerRef: boardRef, name: title || 'dashboard' });\n const handleExportImage = useCallback(() => {\n void exportPNG();\n }, [exportPNG]);\n const handleExportPdf = useCallback(() => {\n void exportPDF();\n }, [exportPDF]);\n\n return (\n <div className=\"flex flex-col h-full min-w-0 overflow-hidden\">\n {/* Toolbar */}\n <DashboardToolbar\n dashboardId={dashboardId}\n title={title}\n onAddWidget={openWidgetPalette}\n onTogglePalette={togglePalette}\n isPaletteOpen={isPaletteOpen}\n onPaste={onPaste}\n onSave={onSave}\n isSaving={isSaving}\n onExport={onExport}\n onExportImage={handleExportImage}\n onExportPdf={handleExportPdf}\n isExportingImage={isExportingImage}\n onShare={() => setIsShareDialogOpen(true)}\n />\n\n {widgetCreateError && (\n <div\n className=\"flex items-center justify-between gap-3 border-b border-status-error-border bg-status-error-bg px-4 py-2 text-sm text-status-error-text\"\n role=\"alert\"\n >\n <span>{widgetCreateError}</span>\n <button\n type=\"button\"\n className=\"font-medium underline underline-offset-2\"\n onClick={() => setWidgetCreateError(null)}\n >\n Dismiss\n </button>\n </div>\n )}\n\n {/* Live wall-board freshness banner — renders only when the page has\n auto-refreshing widgets (e.g. the Live Bed Board) and not in edit mode. */}\n <LiveBoardBanner widgets={widgetsArray} isEditMode={isEditMode} />\n\n {/* Global Filters Bar */}\n {showFilters && <GlobalFiltersBar dashboardId={dashboardId} onFilterChange={handleFilterChange} collapsible />}\n\n {/* Page Tabs - always show in edit mode so user can add first page */}\n {showPageTabs && (isEditMode || pages.length > 0) && (\n <PageTabsManager\n pages={pages}\n activePageId={activePageId || pages[0]?.id}\n editable={isEditMode}\n onPageSelect={onPageChange || (() => {})}\n onPagesReorder={\n onPagesReorder\n ? (pageIds) => {\n // Convert string[] to DashboardPageTab[] for the callback\n const reorderedPages = pageIds.map((id) => pages.find((p) => p.id === id)!).filter(Boolean);\n onPagesReorder(reorderedPages);\n }\n : undefined\n }\n onPageCreate={onPageAdd}\n onPageRename={onPageRename}\n onPageDuplicate={onPageDuplicate}\n onPageDelete={onPageDelete}\n />\n )}\n\n {/* Main content area with palette and canvas.\n When the properties inspector is open we reserve equal space on the\n right so the (fixed, non-modal) panel docks beside the canvas instead\n of overlaying the selected widget. */}\n <div\n className=\"flex flex-1 min-w-0 overflow-hidden\"\n style={{\n marginRight: configPanelOpen ? 'var(--bc-inspector-width, 420px)' : undefined,\n transition: 'margin-right 0.2s ease',\n }}\n >\n {/* Widget Palette Sidebar */}\n {isEditMode && (\n <WidgetPalette\n isOpen={isPaletteOpen}\n onClose={togglePalette}\n onWidgetSelect={handlePaletteWidgetSelect}\n onDragStart={handlePaletteDragStart}\n onDragEnd={handlePaletteDragEnd}\n installedWidgets={installedWidgets}\n installedWidgetsLoading={installedWidgetsLoading}\n definitions={widgetCatalog}\n />\n )}\n\n {/* Canvas area */}\n <div\n ref={containerRef}\n className=\"flex-1 min-w-0 overflow-auto bg-bg-sunken\"\n onDrop={handleCanvasDrop}\n onDragOver={handleCanvasDragOver}\n onClick={() => {\n if (isEditMode) {\n selectWidget(null);\n }\n }}\n style={{\n position: 'relative',\n border: isDraggingWidget ? '2px dashed var(--color-action-primary-bg)' : 'none',\n backgroundColor: isDraggingWidget ? 'var(--color-bg-hover)' : undefined,\n transition: 'border-color 0.2s, background-color 0.2s',\n }}\n >\n {/* Drop zone indicator */}\n {isDraggingWidget && (\n <div\n style={{\n position: 'absolute',\n top: '50%',\n left: '50%',\n transform: 'translate(-50%, -50%)',\n padding: 'var(--space-pagePadding)',\n backgroundColor: 'var(--color-bg-surface)',\n borderRadius: 'var(--radius-card)',\n boxShadow: 'var(--shadow-elevation-2)',\n textAlign: 'center',\n pointerEvents: 'none',\n zIndex: 100,\n }}\n >\n <p style={{ fontSize: 'var(--font-size-body-md)', color: 'var(--color-text-primary)', margin: 0 }}>\n Drop to add <strong>{dragWidgetType}</strong> widget\n </p>\n </div>\n )}\n\n <div\n ref={boardRef}\n className=\"min-h-full origin-top-left transition-transform\"\n style={{\n transform: `scale(${zoomLevel / 100})`,\n width: `${100 / (zoomLevel / 100)}%`,\n }}\n >\n {widgetsArray.length > 0 ? (\n <CrossFilterProvider\n enabled={enableCrossFilter}\n onCrossFilter={(_filters) => {\n // Cross-filter applied - widget queries can access filter values\n }}\n >\n <DashboardGrid\n widgets={widgetsArray}\n isEditMode={isEditMode}\n width={containerWidth}\n onLayoutChange={handleLayoutChange}\n onWidgetSelect={handleWidgetSelect}\n selectedWidgetId={selectedWidgetId}\n />\n </CrossFilterProvider>\n ) : (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] p-8\">\n <div className=\"w-16 h-16 mb-4 flex items-center justify-center rounded-full bg-bg-surface\">\n <svg className=\"w-8 h-8 text-text-secondary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-text-primary mb-2\">No widgets yet</h3>\n <p className=\"text-sm text-text-secondary mb-4 text-center max-w-[300px]\">\n Add widgets to build your dashboard. Click the "Add Widget" button to get started.\n </p>\n {isEditMode && (\n <button\n type=\"button\"\n onClick={openWidgetPalette}\n className=\"\n inline-flex items-center gap-2\n px-4 py-2\n text-sm font-medium\n text-action-primary-fg\n bg-action-primary-bg\n hover:bg-action-primary-bgHover\n rounded-md\n transition-colors\n \"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 4v16m8-8H4\" />\n </svg>\n Add Widget\n </button>\n )}\n </div>\n )}\n </div>\n </div>\n </div>\n\n {/* Properties Panel */}\n <WidgetInspector isOpen={configPanelOpen} onClose={closeConfigPanel} />\n\n {/* Share Dialog */}\n <ShareDialog\n dashboardId={dashboardId}\n dashboardName={title || 'Dashboard'}\n isOpen={isShareDialogOpen}\n onClose={() => setIsShareDialogOpen(false)}\n />\n </div>\n );\n});\n\nexport default DashboardCanvas;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardCanvas.js","names":[],"sources":["../../../../src/bigconsole/components/dashboard/DashboardCanvas.tsx"],"sourcesContent":["/**\n * DashboardCanvas Component\n *\n * Main canvas for dashboard with grid layout and widget rendering.\n * Phase 2: Added GlobalFiltersBar and PageTabsManager integration.\n * Phase 3: Added WidgetPalette sidebar for drag-and-drop widget creation.\n */\n\nimport { type FC, memo, useRef, useState, useCallback, useEffect, useMemo } from 'react';\nimport type { Layout } from 'react-grid-layout';\nimport type { Widget, WidgetType } from '../../types';\nimport { useDashboardStore, useWidgetStore } from '../../store';\nimport { useWidgetOperations } from '../../hooks/useWidgetOperations';\nimport { useInstalledWidgets } from '../../hooks/useInstalledWidgets';\nimport { useWidgetCatalog } from '../../hooks/useWidgetCatalog';\nimport { useDashboardImageExport } from '../../hooks/useDashboardImageExport';\nimport { useBigConsole } from '../../context';\nimport { DashboardGrid } from './DashboardGrid';\nimport { DashboardToolbar } from './DashboardToolbar';\nimport { GlobalFiltersBar } from './GlobalFiltersBar';\nimport { LiveBoardBanner } from './LiveBoardBanner';\nimport { TemplateSetupBanner } from './TemplateSetupBanner';\nimport { PageTabsManager, type DashboardPageTab } from './PageTabsManager';\nimport { ShareDialog } from './ShareDialog';\nimport { WidgetPalette } from './WidgetPalette';\nimport { WidgetInspector } from '../builder';\nimport { getWidgetDefinition, type WidgetDefinition } from '../widgets/WidgetRegistry';\n// Phase 2: Cross-filtering context\nimport { CrossFilterProvider } from '../../contexts';\n// Smart widget positioning\nimport { findNextAvailablePosition, calculatePositionFromDrop } from '../../utils/widgetPositioning';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DashboardCanvasProps {\n /** Dashboard ID */\n dashboardId: string;\n /** Page ID (for loading widgets) */\n pageId?: string;\n /** Dashboard title */\n title?: string;\n /** Callback to paste widget */\n onPaste?: () => void;\n /** Callback when dashboard is saved */\n onSave?: () => void;\n /** Whether save is in progress */\n isSaving?: boolean;\n /** Callback to export dashboard */\n onExport?: () => void;\n /** Dashboard pages for PageTabsManager */\n pages?: DashboardPageTab[];\n /** Active page ID */\n activePageId?: string;\n /** Callback when page changes */\n onPageChange?: (pageId: string) => void;\n /** Callback when pages are reordered */\n onPagesReorder?: (pages: DashboardPageTab[]) => void;\n /** Callback when page is added */\n onPageAdd?: () => void;\n /** Callback when page is renamed */\n onPageRename?: (pageId: string, newName: string) => void;\n /** Callback when page is duplicated */\n onPageDuplicate?: (pageId: string) => void;\n /** Callback when page is deleted */\n onPageDelete?: (pageId: string) => void;\n /** Whether to show global filters bar */\n showFilters?: boolean;\n /** Whether to show page tabs */\n showPageTabs?: boolean;\n /** Whether to enable cross-filtering (widget clicks update filters) */\n enableCrossFilter?: boolean;\n}\n\ninterface WidgetPlacement {\n position: { x: number; y: number; width: number; height: number };\n definition?: WidgetDefinition | null;\n}\n\ninterface PaletteDropData {\n type: WidgetType;\n name?: string;\n category?: WidgetDefinition['category'];\n defaultConfig?: Record<string, unknown>;\n defaultSize?: WidgetDefinition['defaultSize'];\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const DashboardCanvas: FC<DashboardCanvasProps> = memo(function DashboardCanvas({\n dashboardId,\n pageId,\n title,\n onPaste,\n onSave,\n isSaving = false,\n onExport,\n pages = [],\n activePageId,\n onPageChange,\n onPagesReorder,\n onPageAdd,\n onPageRename,\n onPageDuplicate,\n onPageDelete,\n showFilters = true,\n showPageTabs = true,\n enableCrossFilter = true,\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n // Inner board element (the grid surface) — rasterized for PNG/PDF board-pack export.\n const boardRef = useRef<HTMLDivElement>(null);\n const [containerWidth, setContainerWidth] = useState(1200);\n const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);\n const [widgetCreateError, setWidgetCreateError] = useState<string | null>(null);\n const [isDraggingWidget, setIsDraggingWidget] = useState(false);\n const [dragWidgetType, setDragWidgetType] = useState<WidgetType | null>(null);\n\n // Store state\n const isEditMode = useDashboardStore((state) => state.viewMode === 'edit');\n\n // Fetch installed widgets only in edit mode — the palette and add-widget dialog\n // are not usable in read-only view, so skip the request for viewer-only sessions.\n const { workspaceId, apiGatewayUrl, authToken } = useBigConsole();\n const { installedWidgets, loading: installedWidgetsLoading } = useInstalledWidgets({\n workspaceId,\n apiGatewayUrl,\n authToken,\n enabled: isEditMode,\n });\n // Built-in widget catalog from the backend (getWidgetTypes); falls back to the\n // bundled registry while loading or if the supergraph hasn't adopted it yet.\n const { definitions: widgetCatalog } = useWidgetCatalog({\n workspaceId,\n apiGatewayUrl,\n authToken,\n enabled: isEditMode,\n });\n const zoomLevel = useDashboardStore((state) => state.zoomLevel);\n const sidebarCollapsed = useDashboardStore((state) => state.sidebarCollapsed);\n const toggleSidebar = useDashboardStore((state) => state.toggleSidebar);\n const isPaletteOpen = !sidebarCollapsed;\n const setGlobalFilterValues = useDashboardStore((state) => state.setGlobalFilterValues);\n\n // Handle filter changes - update store for widget access\n const handleFilterChange = useCallback(\n (filterValues: Record<string, unknown>) => {\n // Update global filter values in dashboard store\n setGlobalFilterValues(filterValues);\n\n // Widgets can now access filter values via useGlobalFilterValues() hook\n // When backend supports filter parameters, widget queries will include these values\n },\n [setGlobalFilterValues]\n );\n\n // Widget operations hook (connects to backend) - skipFetch since parent page fetches\n const { createWidget: createWidgetApi } = useWidgetOperations(pageId, dashboardId, { skipFetch: true });\n\n const widgetsMap = useWidgetStore((state) => state.widgets);\n // Filter widgets to only show widgets for the current page\n const widgetsArray = useMemo(() => {\n const allWidgets = Array.from(widgetsMap.values());\n if (!pageId) return allWidgets;\n return allWidgets.filter((widget) => widget.pageId === pageId);\n }, [widgetsMap, pageId]);\n const selectedWidgetId = useWidgetStore((state) => state.selectedWidgetId);\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n const addWidgetToStore = useWidgetStore((state) => state.addWidget);\n const updateWidgetPosition = useWidgetStore((state) => state.updateWidgetPosition);\n const configPanelOpen = useWidgetStore((state) => state.configPanelOpen);\n const closeConfigPanel = useWidgetStore((state) => state.closeConfigPanel);\n\n // Keep react-grid-layout's explicit width in sync with the actual canvas.\n // A ResizeObserver is required because opening the docked inspector or widget\n // palette resizes this element without resizing the browser window.\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const updateWidth = () => {\n setContainerWidth(container.offsetWidth);\n };\n\n updateWidth();\n\n if (typeof ResizeObserver !== 'undefined') {\n const observer = new ResizeObserver(updateWidth);\n observer.observe(container);\n return () => observer.disconnect();\n }\n\n window.addEventListener('resize', updateWidth);\n return () => window.removeEventListener('resize', updateWidth);\n }, []);\n\n // Handle layout change\n const handleLayoutChange = useCallback(\n (layout: Layout[]) => {\n layout.forEach((item) => {\n updateWidgetPosition(item.i, {\n x: item.x,\n y: item.y,\n width: item.w,\n height: item.h,\n });\n });\n },\n [updateWidgetPosition]\n );\n\n // Handle widget select\n const handleWidgetSelect = useCallback(\n (widgetId: string) => {\n selectWidget(widgetId);\n },\n [selectWidget]\n );\n\n // Create with defaults, select the result, then configure it in the inspector.\n const handleAddWidget = useCallback(\n async (widgetData: Partial<Widget>, placement?: WidgetPlacement) => {\n setWidgetCreateError(null);\n const definition = placement?.definition || (widgetData.type ? getWidgetDefinition(widgetData.type) : null);\n const widgetSize = {\n width: definition?.defaultSize?.width ?? 4,\n height: definition?.defaultSize?.height ?? 4,\n };\n\n let position: { x: number; y: number; width: number; height: number };\n if (placement?.position) {\n position = placement.position;\n } else {\n // Use smart placement algorithm - horizontal first, then next row\n const { x, y } = findNextAvailablePosition(widgetsArray, widgetSize, { cols: 12 });\n position = {\n x,\n y,\n width: widgetSize.width,\n height: widgetSize.height,\n };\n }\n\n // Preserve legacy binding fields when creating from imported/default data.\n const extendedData = widgetData as Partial<Widget> & { dataSinkId?: string; parserId?: string };\n\n // Try to create via API first\n if (pageId) {\n const widget = await createWidgetApi({\n pageId,\n dashboardId,\n type: widgetData.type!,\n title: widgetData.title || 'Untitled Widget',\n description: widgetData.description,\n // v2.0: Renderer and template support\n renderer: widgetData.renderer,\n template: widgetData.template,\n config: widgetData.config || {},\n position,\n refreshInterval: widgetData.refreshInterval ?? undefined,\n // v2.0: Data source fields (DataSink → Parser → Widget)\n dataSinkId: extendedData.dataSinkId,\n parserId: extendedData.parserId,\n });\n\n if (widget) {\n selectWidget(widget.id);\n return;\n }\n\n setWidgetCreateError('Could not create the widget. Check the connection and try again.');\n return;\n }\n\n // Fallback to local store if API fails or no pageId\n const newWidget: Widget = {\n id: crypto.randomUUID(),\n dashboardId,\n pageId: pageId || 'local',\n type: widgetData.type!,\n title: widgetData.title || 'Untitled Widget',\n description: widgetData.description,\n // v2.0: Renderer and template support\n renderer: widgetData.renderer,\n template: widgetData.template,\n refreshInterval: widgetData.refreshInterval,\n config: widgetData.config || {},\n position,\n positionX: position.x,\n positionY: position.y,\n positionWidth: position.width,\n positionHeight: position.height,\n // v2.0: Data source fields (DataSink → Parser → Widget)\n dataSinkId: extendedData.dataSinkId,\n parserId: extendedData.parserId,\n translations: [],\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n addWidgetToStore(newWidget);\n selectWidget(newWidget.id);\n },\n [dashboardId, pageId, createWidgetApi, addWidgetToStore, selectWidget, widgetsArray]\n );\n\n // Handle drag start from palette\n const handlePaletteDragStart = useCallback((type: WidgetType) => {\n setIsDraggingWidget(true);\n setDragWidgetType(type);\n }, []);\n\n // Handle drag end from palette\n const handlePaletteDragEnd = useCallback(() => {\n setIsDraggingWidget(false);\n setDragWidgetType(null);\n }, []);\n\n // Handle drop on canvas - create immediately with defaults.\n const handleCanvasDrop = useCallback(\n (e: React.DragEvent) => {\n e.preventDefault();\n setIsDraggingWidget(false);\n setDragWidgetType(null);\n\n try {\n const data = e.dataTransfer.getData('application/json');\n if (!data) return;\n\n const parsedData: unknown = JSON.parse(data);\n if (!parsedData || typeof parsedData !== 'object' || !('type' in parsedData)) return;\n const widgetData = parsedData as PaletteDropData;\n const definition = getWidgetDefinition(widgetData.type);\n\n // If the dropped data includes defaultConfig (from WidgetPalette drag serialization),\n // reconstruct a full WidgetDefinition so installed/store widget identity is preserved.\n let droppedDefinition: WidgetDefinition | null = null;\n if (widgetData.defaultConfig && widgetData.category) {\n droppedDefinition = {\n ...definition,\n name: widgetData.name || definition.name,\n category: widgetData.category,\n defaultConfig: widgetData.defaultConfig,\n defaultSize: widgetData.defaultSize || definition.defaultSize,\n };\n }\n\n // Calculate drop position relative to canvas\n const canvasRect = containerRef.current?.getBoundingClientRect();\n if (!canvasRect) return;\n\n // Use utility to calculate grid position from drop coordinates\n const widgetSize = {\n width: definition.defaultSize.width,\n height: definition.defaultSize.height,\n };\n\n const { x: gridX, y: gridY } = calculatePositionFromDrop(\n e.clientX,\n e.clientY,\n canvasRect,\n containerWidth,\n widgetSize,\n { cols: 12 }\n );\n\n const position = {\n x: gridX,\n y: gridY,\n width: widgetSize.width,\n height: widgetSize.height,\n };\n\n void handleAddWidget(\n {\n type: widgetData.type,\n title: widgetData.name || definition.name,\n config: widgetData.defaultConfig || definition.defaultConfig,\n },\n { position, definition: droppedDefinition }\n );\n } catch (error) {\n console.error('Failed to handle widget drop:', error);\n }\n },\n [containerWidth, handleAddWidget]\n );\n\n // Handle drag over to allow drop\n const handleCanvasDragOver = useCallback((e: React.DragEvent) => {\n e.preventDefault();\n e.dataTransfer.dropEffect = 'copy';\n }, []);\n\n // Handle widget select from palette (quick add without drag)\n const handlePaletteWidgetSelect = useCallback(\n (type: WidgetType, paletteDefinition?: WidgetDefinition) => {\n const definition = paletteDefinition || getWidgetDefinition(type);\n handleAddWidget({\n type,\n title: definition.name,\n config: definition.defaultConfig,\n });\n },\n [handleAddWidget]\n );\n\n // Toggle palette\n const togglePalette = useCallback(() => {\n toggleSidebar();\n }, [toggleSidebar]);\n\n const openWidgetPalette = useCallback(() => {\n if (sidebarCollapsed) toggleSidebar();\n window.requestAnimationFrame(() => {\n document.querySelector<HTMLInputElement>('input[placeholder=\"Search widgets...\"]')?.focus();\n });\n }, [sidebarCollapsed, toggleSidebar]);\n\n // Board-pack export (PNG + PDF) — rasterizes the inner board element.\n const {\n exportPNG,\n exportPDF,\n isExporting: isExportingImage,\n } = useDashboardImageExport({ containerRef: boardRef, name: title || 'dashboard' });\n const handleExportImage = useCallback(() => {\n void exportPNG();\n }, [exportPNG]);\n const handleExportPdf = useCallback(() => {\n void exportPDF();\n }, [exportPDF]);\n\n return (\n <div className=\"flex flex-col h-full min-w-0 overflow-hidden\">\n {/* Toolbar */}\n <DashboardToolbar\n dashboardId={dashboardId}\n title={title}\n onAddWidget={openWidgetPalette}\n onTogglePalette={togglePalette}\n isPaletteOpen={isPaletteOpen}\n onPaste={onPaste}\n onSave={onSave}\n isSaving={isSaving}\n onExport={onExport}\n onExportImage={handleExportImage}\n onExportPdf={handleExportPdf}\n isExportingImage={isExportingImage}\n onShare={() => setIsShareDialogOpen(true)}\n />\n\n {widgetCreateError && (\n <div\n className=\"flex items-center justify-between gap-3 border-b border-status-error-border bg-status-error-bg px-4 py-2 text-sm text-status-error-text\"\n role=\"alert\"\n >\n <span>{widgetCreateError}</span>\n <button\n type=\"button\"\n className=\"font-medium underline underline-offset-2\"\n onClick={() => setWidgetCreateError(null)}\n >\n Dismiss\n </button>\n </div>\n )}\n\n {/* Live wall-board freshness banner — renders only when the page has\n auto-refreshing widgets (e.g. the Live Bed Board) and not in edit mode. */}\n <TemplateSetupBanner widgets={widgetsArray} isEditMode={isEditMode} />\n <LiveBoardBanner widgets={widgetsArray} isEditMode={isEditMode} />\n\n {/* Global Filters Bar */}\n {showFilters && <GlobalFiltersBar dashboardId={dashboardId} onFilterChange={handleFilterChange} collapsible />}\n\n {/* Page Tabs - always show in edit mode so user can add first page */}\n {showPageTabs && (isEditMode || pages.length > 0) && (\n <PageTabsManager\n pages={pages}\n activePageId={activePageId || pages[0]?.id}\n editable={isEditMode}\n onPageSelect={onPageChange || (() => {})}\n onPagesReorder={\n onPagesReorder\n ? (pageIds) => {\n // Convert string[] to DashboardPageTab[] for the callback\n const reorderedPages = pageIds.map((id) => pages.find((p) => p.id === id)!).filter(Boolean);\n onPagesReorder(reorderedPages);\n }\n : undefined\n }\n onPageCreate={onPageAdd}\n onPageRename={onPageRename}\n onPageDuplicate={onPageDuplicate}\n onPageDelete={onPageDelete}\n />\n )}\n\n {/* Main content area with palette and canvas.\n When the properties inspector is open we reserve equal space on the\n right so the (fixed, non-modal) panel docks beside the canvas instead\n of overlaying the selected widget. */}\n <div\n className=\"flex flex-1 min-w-0 overflow-hidden\"\n style={{\n marginRight: configPanelOpen ? 'var(--bc-inspector-width, 420px)' : undefined,\n transition: 'margin-right 0.2s ease',\n }}\n >\n {/* Widget Palette Sidebar */}\n {isEditMode && (\n <WidgetPalette\n isOpen={isPaletteOpen}\n onClose={togglePalette}\n onWidgetSelect={handlePaletteWidgetSelect}\n onDragStart={handlePaletteDragStart}\n onDragEnd={handlePaletteDragEnd}\n installedWidgets={installedWidgets}\n installedWidgetsLoading={installedWidgetsLoading}\n definitions={widgetCatalog}\n />\n )}\n\n {/* Canvas area */}\n <div\n ref={containerRef}\n className=\"flex-1 min-w-0 overflow-auto bg-bg-sunken\"\n onDrop={handleCanvasDrop}\n onDragOver={handleCanvasDragOver}\n onClick={() => {\n if (isEditMode) {\n selectWidget(null);\n }\n }}\n style={{\n position: 'relative',\n border: isDraggingWidget ? '2px dashed var(--color-action-primary-bg)' : 'none',\n backgroundColor: isDraggingWidget ? 'var(--color-bg-hover)' : undefined,\n transition: 'border-color 0.2s, background-color 0.2s',\n }}\n >\n {/* Drop zone indicator */}\n {isDraggingWidget && (\n <div\n style={{\n position: 'absolute',\n top: '50%',\n left: '50%',\n transform: 'translate(-50%, -50%)',\n padding: 'var(--space-pagePadding)',\n backgroundColor: 'var(--color-bg-surface)',\n borderRadius: 'var(--radius-card)',\n boxShadow: 'var(--shadow-elevation-2)',\n textAlign: 'center',\n pointerEvents: 'none',\n zIndex: 100,\n }}\n >\n <p style={{ fontSize: 'var(--font-size-body-md)', color: 'var(--color-text-primary)', margin: 0 }}>\n Drop to add <strong>{dragWidgetType}</strong> widget\n </p>\n </div>\n )}\n\n <div\n ref={boardRef}\n className=\"min-h-full origin-top-left transition-transform\"\n style={{\n transform: `scale(${zoomLevel / 100})`,\n width: `${100 / (zoomLevel / 100)}%`,\n }}\n >\n {widgetsArray.length > 0 ? (\n <CrossFilterProvider\n enabled={enableCrossFilter}\n onCrossFilter={(_filters) => {\n // Cross-filter applied - widget queries can access filter values\n }}\n >\n <DashboardGrid\n widgets={widgetsArray}\n isEditMode={isEditMode}\n width={containerWidth}\n onLayoutChange={handleLayoutChange}\n onWidgetSelect={handleWidgetSelect}\n selectedWidgetId={selectedWidgetId}\n />\n </CrossFilterProvider>\n ) : (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] p-8\">\n <div className=\"w-16 h-16 mb-4 flex items-center justify-center rounded-full bg-bg-surface\">\n <svg className=\"w-8 h-8 text-text-secondary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-text-primary mb-2\">No widgets yet</h3>\n <p className=\"text-sm text-text-secondary mb-4 text-center max-w-[300px]\">\n Add widgets to build your dashboard. Click the "Add Widget" button to get started.\n </p>\n {isEditMode && (\n <button\n type=\"button\"\n onClick={openWidgetPalette}\n className=\"\n inline-flex items-center gap-2\n px-4 py-2\n text-sm font-medium\n text-action-primary-fg\n bg-action-primary-bg\n hover:bg-action-primary-bgHover\n rounded-md\n transition-colors\n \"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 4v16m8-8H4\" />\n </svg>\n Add Widget\n </button>\n )}\n </div>\n )}\n </div>\n </div>\n </div>\n\n {/* Properties Panel */}\n <WidgetInspector isOpen={configPanelOpen} onClose={closeConfigPanel} />\n\n {/* Share Dialog */}\n <ShareDialog\n dashboardId={dashboardId}\n dashboardName={title || 'Dashboard'}\n isOpen={isShareDialogOpen}\n onClose={() => setIsShareDialogOpen(false)}\n />\n </div>\n );\n});\n\nexport default DashboardCanvas;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { memo as e, useMemo as t } from "react";
|
|
2
|
+
import { DatabaseZap as n } from "lucide-react";
|
|
3
|
+
import { Button as r } from "@burdenoff/fe-libs/ui";
|
|
4
|
+
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
5
|
+
//#region src/bigconsole/components/dashboard/TemplateSetupBanner.tsx
|
|
6
|
+
var o = "templateDataSinkSlot";
|
|
7
|
+
function s(e) {
|
|
8
|
+
if (e.dataSinkId) return null;
|
|
9
|
+
let t = e.metadata?.[o];
|
|
10
|
+
return typeof t == "string" && t.length > 0 ? t : null;
|
|
11
|
+
}
|
|
12
|
+
var c = e(function({ widgets: e, isEditMode: o = !1, onConnectData: c }) {
|
|
13
|
+
let { unconnectedCount: l, slots: u } = t(() => {
|
|
14
|
+
let t = /* @__PURE__ */ new Set(), n = 0;
|
|
15
|
+
for (let r of e) {
|
|
16
|
+
let e = s(r);
|
|
17
|
+
e && (n += 1, t.add(e));
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
unconnectedCount: n,
|
|
21
|
+
slots: Array.from(t)
|
|
22
|
+
};
|
|
23
|
+
}, [e]);
|
|
24
|
+
if (o || l === 0) return null;
|
|
25
|
+
let d = u.length === 1 ? "data source" : "data sources";
|
|
26
|
+
return /* @__PURE__ */ a("div", {
|
|
27
|
+
role: "status",
|
|
28
|
+
style: {
|
|
29
|
+
display: "flex",
|
|
30
|
+
alignItems: "center",
|
|
31
|
+
gap: "var(--space-sm)",
|
|
32
|
+
padding: "var(--space-sm) var(--space-md)",
|
|
33
|
+
marginBottom: "var(--space-sm)",
|
|
34
|
+
borderRadius: "var(--radius-md)",
|
|
35
|
+
border: "1px solid var(--color-border-info)",
|
|
36
|
+
background: "var(--color-surface-info-subtle)",
|
|
37
|
+
color: "var(--color-text-info)"
|
|
38
|
+
},
|
|
39
|
+
children: [
|
|
40
|
+
/* @__PURE__ */ i(n, {
|
|
41
|
+
style: {
|
|
42
|
+
height: "var(--size-icon-sm)",
|
|
43
|
+
width: "var(--size-icon-sm)",
|
|
44
|
+
flexShrink: 0
|
|
45
|
+
},
|
|
46
|
+
"aria-hidden": "true"
|
|
47
|
+
}),
|
|
48
|
+
/* @__PURE__ */ a("div", {
|
|
49
|
+
style: {
|
|
50
|
+
flex: 1,
|
|
51
|
+
minWidth: 0
|
|
52
|
+
},
|
|
53
|
+
children: [/* @__PURE__ */ i("div", {
|
|
54
|
+
style: { fontWeight: "var(--font-weight-medium)" },
|
|
55
|
+
children: "This dashboard came from a template"
|
|
56
|
+
}), /* @__PURE__ */ a("div", {
|
|
57
|
+
style: { fontSize: "var(--font-size-sm)" },
|
|
58
|
+
children: [
|
|
59
|
+
l,
|
|
60
|
+
" ",
|
|
61
|
+
l === 1 ? "widget needs" : "widgets need",
|
|
62
|
+
" connecting to your data — ",
|
|
63
|
+
u.length,
|
|
64
|
+
" ",
|
|
65
|
+
d,
|
|
66
|
+
" to set up."
|
|
67
|
+
]
|
|
68
|
+
})]
|
|
69
|
+
}),
|
|
70
|
+
c ? /* @__PURE__ */ i(r, {
|
|
71
|
+
size: "sm",
|
|
72
|
+
onClick: () => c(u),
|
|
73
|
+
style: { flexShrink: 0 },
|
|
74
|
+
children: "Connect data"
|
|
75
|
+
}) : null
|
|
76
|
+
]
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
//#endregion
|
|
80
|
+
export { c as default };
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=TemplateSetupBanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplateSetupBanner.js","names":[],"sources":["../../../../src/bigconsole/components/dashboard/TemplateSetupBanner.tsx"],"sourcesContent":["/**\n * TemplateSetupBanner\n *\n * Shown on a board installed from a store template that still has widgets\n * without a data source.\n *\n * WHY THIS EXISTS:\n * Template installs deliberately do not require every data-sink slot to be\n * bound up front — a brand-new workspace has no sinks at all, so a hard\n * requirement would make the template gallery unusable for exactly the users\n * it exists to serve. The cost of that choice is that the board renders\n * immediately with some widgets holding no data.\n *\n * A workflow can get away with the equivalent (FluidGrids just toasts \"go\n * configure your credential\") because nothing runs until you press Test. A\n * dashboard renders the instant you open it, so an unconnected widget is\n * visually indistinguishable from a crashed one. This banner is what\n * reframes the screen from \"broken\" to \"setup in progress\", and it does more\n * work for that than any per-widget treatment.\n *\n * It derives its state from the widgets themselves — each widget installed from\n * a template carries its originating slot in `metadata.templateDataSinkSlot` —\n * so it needs no extra query and lights up for any such board.\n *\n * Semantic status tokens only, no raw colors.\n *\n * @module bigconsole/components/dashboard/TemplateSetupBanner\n */\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { DatabaseZap } from 'lucide-react';\nimport { type FC, memo, useMemo } from 'react';\n\nimport type { Widget } from '../../types';\n\n/** Metadata key written by the backend template installer. */\nconst WIDGET_TEMPLATE_SLOT_KEY = 'templateDataSinkSlot';\n\nexport interface TemplateSetupBannerProps {\n widgets: Widget[];\n /** Hidden while editing: the builder has its own binding affordances. */\n isEditMode?: boolean;\n /** Opens the guided connect flow. Omit to render the banner without a CTA. */\n onConnectData?: (slots: string[]) => void;\n}\n\n/** A widget that came from a template and still has no sink bound. */\nfunction needsData(widget: Widget): string | null {\n if (widget.dataSinkId) return null;\n const slot = widget.metadata?.[WIDGET_TEMPLATE_SLOT_KEY];\n return typeof slot === 'string' && slot.length > 0 ? slot : null;\n}\n\nexport const TemplateSetupBanner: FC<TemplateSetupBannerProps> = memo(function TemplateSetupBanner({\n widgets,\n isEditMode = false,\n onConnectData,\n}) {\n const { unconnectedCount, slots } = useMemo(() => {\n const found = new Set<string>();\n let count = 0;\n for (const widget of widgets) {\n const slot = needsData(widget);\n if (!slot) continue;\n count += 1;\n found.add(slot);\n }\n return { unconnectedCount: count, slots: Array.from(found) };\n }, [widgets]);\n\n if (isEditMode || unconnectedCount === 0) return null;\n\n const sourceWord = slots.length === 1 ? 'data source' : 'data sources';\n const widgetWord = unconnectedCount === 1 ? 'widget needs' : 'widgets need';\n\n return (\n <div\n role=\"status\"\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 'var(--space-sm)',\n padding: 'var(--space-sm) var(--space-md)',\n marginBottom: 'var(--space-sm)',\n borderRadius: 'var(--radius-md)',\n border: '1px solid var(--color-border-info)',\n background: 'var(--color-surface-info-subtle)',\n color: 'var(--color-text-info)',\n }}\n >\n <DatabaseZap\n style={{ height: 'var(--size-icon-sm)', width: 'var(--size-icon-sm)', flexShrink: 0 }}\n aria-hidden=\"true\"\n />\n\n <div style={{ flex: 1, minWidth: 0 }}>\n <div style={{ fontWeight: 'var(--font-weight-medium)' }}>This dashboard came from a template</div>\n <div style={{ fontSize: 'var(--font-size-sm)' }}>\n {unconnectedCount} {widgetWord} connecting to your data — {slots.length} {sourceWord} to set up.\n </div>\n </div>\n\n {onConnectData ? (\n <Button size=\"sm\" onClick={() => onConnectData(slots)} style={{ flexShrink: 0 }}>\n Connect data\n </Button>\n ) : null}\n </div>\n );\n});\n\nexport default TemplateSetupBanner;\n"],"mappings":";;;;;AAmCA,IAAM,IAA2B;AAWjC,SAAS,EAAU,GAA+B;AAChD,KAAI,EAAO,WAAY,QAAO;CAC9B,IAAM,IAAO,EAAO,WAAW;AAC/B,QAAO,OAAO,KAAS,YAAY,EAAK,SAAS,IAAI,IAAO;;AAG9D,IAAa,IAAoD,EAAK,SAA6B,EACjG,YACA,gBAAa,IACb,oBACC;CACD,IAAM,EAAE,qBAAkB,aAAU,QAAc;EAChD,IAAM,oBAAQ,IAAI,KAAa,EAC3B,IAAQ;AACZ,OAAK,IAAM,KAAU,GAAS;GAC5B,IAAM,IAAO,EAAU,EAAO;AACzB,SACL,KAAS,GACT,EAAM,IAAI,EAAK;;AAEjB,SAAO;GAAE,kBAAkB;GAAO,OAAO,MAAM,KAAK,EAAM;GAAE;IAC3D,CAAC,EAAQ,CAAC;AAEb,KAAI,KAAc,MAAqB,EAAG,QAAO;CAEjD,IAAM,IAAa,EAAM,WAAW,IAAI,gBAAgB;AAGxD,QACE,kBAAC,OAAD;EACE,MAAK;EACL,OAAO;GACL,SAAS;GACT,YAAY;GACZ,KAAK;GACL,SAAS;GACT,cAAc;GACd,cAAc;GACd,QAAQ;GACR,YAAY;GACZ,OAAO;GACR;YAZH;GAcE,kBAAC,GAAD;IACE,OAAO;KAAE,QAAQ;KAAuB,OAAO;KAAuB,YAAY;KAAG;IACrF,eAAY;IACZ,CAAA;GAEF,kBAAC,OAAD;IAAK,OAAO;KAAE,MAAM;KAAG,UAAU;KAAG;cAApC,CACE,kBAAC,OAAD;KAAK,OAAO,EAAE,YAAY,6BAA6B;eAAE;KAAyC,CAAA,EAClG,kBAAC,OAAD;KAAK,OAAO,EAAE,UAAU,uBAAuB;eAA/C;MACG;MAAiB;MAzBP,MAAqB,IAAI,iBAAiB;MAyBtB;MAA4B,EAAM;MAAO;MAAE;MAAW;MACjF;OACF;;GAEL,IACC,kBAAC,GAAD;IAAQ,MAAK;IAAK,eAAe,EAAc,EAAM;IAAE,OAAO,EAAE,YAAY,GAAG;cAAE;IAExE,CAAA,GACP;GACA;;EAER"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Component as e } from "react";
|
|
2
|
+
import { jsxs as t } from "react/jsx-runtime";
|
|
3
|
+
//#region src/bigconsole/components/embed/EmbedErrorBoundary.tsx
|
|
4
|
+
var n = class extends e {
|
|
5
|
+
state = { hasError: !1 };
|
|
6
|
+
static getDerivedStateFromError() {
|
|
7
|
+
return { hasError: !0 };
|
|
8
|
+
}
|
|
9
|
+
componentDidCatch(e, t) {}
|
|
10
|
+
render() {
|
|
11
|
+
return this.state.hasError ? /* @__PURE__ */ t("div", {
|
|
12
|
+
role: "alert",
|
|
13
|
+
className: "flex h-full w-full items-center justify-center bg-surface p-4 text-center text-sm text-secondary",
|
|
14
|
+
children: [
|
|
15
|
+
"This ",
|
|
16
|
+
this.props.label ?? "content",
|
|
17
|
+
" could not be displayed."
|
|
18
|
+
]
|
|
19
|
+
}) : this.props.children;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { n as EmbedErrorBoundary };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=EmbedErrorBoundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmbedErrorBoundary.js","names":[],"sources":["../../../../src/bigconsole/components/embed/EmbedErrorBoundary.tsx"],"sourcesContent":["/**\n * Customer-facing error boundary for embedded widgets/dashboards (BOFF-2986).\n *\n * Renders a neutral, safe message and NEVER surfaces internal details (stack\n * traces, ids, endpoints, config). One failing widget must not tear down the\n * whole dashboard, so this wraps each widget individually as well as the page.\n */\n\nimport { Component, type ErrorInfo, type ReactNode } from 'react';\n\ninterface Props {\n readonly children: ReactNode;\n /** Short, non-sensitive label for what failed (e.g. \"widget\"). */\n readonly label?: string;\n}\n\ninterface State {\n readonly hasError: boolean;\n}\n\nexport class EmbedErrorBoundary extends Component<Props, State> {\n state: State = { hasError: false };\n\n static getDerivedStateFromError(): State {\n return { hasError: true };\n }\n\n componentDidCatch(_error: Error, _info: ErrorInfo): void {\n // Intentionally no external reporting from the anonymous embed context — the\n // safe render below is the only user-visible outcome.\n }\n\n render(): ReactNode {\n if (this.state.hasError) {\n return (\n <div\n role=\"alert\"\n className=\"flex h-full w-full items-center justify-center bg-surface p-4 text-center text-sm text-secondary\"\n >\n This {this.props.label ?? 'content'} could not be displayed.\n </div>\n );\n }\n return this.props.children;\n }\n}\n"],"mappings":";;;AAoBA,IAAa,IAAb,cAAwC,EAAwB;CAC9D,QAAe,EAAE,UAAU,IAAO;CAElC,OAAO,2BAAkC;AACvC,SAAO,EAAE,UAAU,IAAM;;CAG3B,kBAAkB,GAAe,GAAwB;CAKzD,SAAoB;AAWlB,SAVI,KAAK,MAAM,WAEX,kBAAC,OAAD;GACE,MAAK;GACL,WAAU;aAFZ;IAGC;IACO,KAAK,MAAM,SAAS;IAAU;IAChC;OAGH,KAAK,MAAM"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { EMBED_WIDGET_COVERAGE as e } from "./renderer-manifest.js";
|
|
2
|
+
import { EmbedErrorBoundary as t } from "./EmbedErrorBoundary.js";
|
|
3
|
+
import { MetricCardEmbed as n } from "./widgets/MetricCardEmbed.js";
|
|
4
|
+
import { KpiComparisonEmbed as r } from "./widgets/KpiComparisonEmbed.js";
|
|
5
|
+
import { TableEmbed as i } from "./widgets/TableEmbed.js";
|
|
6
|
+
import { ListEmbed as a } from "./widgets/ListEmbed.js";
|
|
7
|
+
import { TextEmbed as o } from "./widgets/TextEmbed.js";
|
|
8
|
+
import { jsx as s } from "react/jsx-runtime";
|
|
9
|
+
//#region src/bigconsole/components/embed/EmbedWidgetRenderer.tsx
|
|
10
|
+
var c = {
|
|
11
|
+
metric_card: n,
|
|
12
|
+
kpi_card_comparison: r,
|
|
13
|
+
table: i,
|
|
14
|
+
list: a,
|
|
15
|
+
text: o
|
|
16
|
+
};
|
|
17
|
+
function l() {
|
|
18
|
+
return Object.keys(c);
|
|
19
|
+
}
|
|
20
|
+
function u({ title: e }) {
|
|
21
|
+
return /* @__PURE__ */ s("div", {
|
|
22
|
+
className: "flex h-full w-full items-center justify-center bg-surface p-4 text-center text-sm text-secondary",
|
|
23
|
+
children: e ? `“${e}” is not available in this embed.` : "This widget is not available in this embed."
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function d(n) {
|
|
27
|
+
let { widget: r } = n, i = e[r.type]?.support === "implemented" ? c[r.type] : void 0;
|
|
28
|
+
return i ? /* @__PURE__ */ s(t, {
|
|
29
|
+
label: "widget",
|
|
30
|
+
children: /* @__PURE__ */ s(i, { ...n })
|
|
31
|
+
}) : /* @__PURE__ */ s(u, { title: r.title });
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { d as EmbedWidgetRenderer, l as getImplementedRendererTypes };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=EmbedWidgetRenderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmbedWidgetRenderer.js","names":[],"sources":["../../../../src/bigconsole/components/embed/EmbedWidgetRenderer.tsx"],"sourcesContent":["/**\n * Strict embed widget dispatcher (BOFF-2986).\n *\n * Maps a sanitized widget DTO to its embed renderer via an EXACT registry.\n * There is NO silent fallback (unlike the authenticated WidgetRendererFactory):\n * a type that is not `implemented` in the coverage manifest renders a neutral\n * \"not available\" placeholder, never a guessed or partial widget. Each widget is\n * wrapped in its own error boundary so one failure can't take down the page.\n */\n\nimport type { ComponentType } from 'react';\nimport type { WidgetType } from '../../types';\nimport type { EmbedWidgetRenderProps } from './types';\nimport { EMBED_WIDGET_COVERAGE } from './renderer-manifest';\nimport { EmbedErrorBoundary } from './EmbedErrorBoundary';\nimport { MetricCardEmbed } from './widgets/MetricCardEmbed';\nimport { KpiComparisonEmbed } from './widgets/KpiComparisonEmbed';\nimport { TableEmbed } from './widgets/TableEmbed';\nimport { ListEmbed } from './widgets/ListEmbed';\nimport { TextEmbed } from './widgets/TextEmbed';\n\n/**\n * Registry of implemented renderers. Every key here MUST be marked\n * `implemented` in EMBED_WIDGET_COVERAGE, and vice-versa — the completeness test\n * asserts the two stay in lockstep, so a renderer can never be silently missing\n * for a type the manifest claims to support.\n */\nconst IMPLEMENTED_RENDERERS: Partial<Record<WidgetType, ComponentType<EmbedWidgetRenderProps>>> = {\n metric_card: MetricCardEmbed,\n kpi_card_comparison: KpiComparisonEmbed,\n table: TableEmbed,\n list: ListEmbed,\n text: TextEmbed,\n};\n\nexport function getImplementedRendererTypes(): WidgetType[] {\n return Object.keys(IMPLEMENTED_RENDERERS) as WidgetType[];\n}\n\nfunction UnavailableWidget({ title }: { title?: string }) {\n return (\n <div className=\"flex h-full w-full items-center justify-center bg-surface p-4 text-center text-sm text-secondary\">\n {title ? `“${title}” is not available in this embed.` : 'This widget is not available in this embed.'}\n </div>\n );\n}\n\nexport function EmbedWidgetRenderer(props: EmbedWidgetRenderProps) {\n const { widget } = props;\n const coverage = EMBED_WIDGET_COVERAGE[widget.type];\n const Renderer = coverage?.support === 'implemented' ? IMPLEMENTED_RENDERERS[widget.type] : undefined;\n\n if (!Renderer) {\n return <UnavailableWidget title={widget.title} />;\n }\n\n return (\n <EmbedErrorBoundary label=\"widget\">\n <Renderer {...props} />\n </EmbedErrorBoundary>\n );\n}\n"],"mappings":";;;;;;;;;AA2BA,IAAM,IAA4F;CAChG,aAAa;CACb,qBAAqB;CACrB,OAAO;CACP,MAAM;CACN,MAAM;CACP;AAED,SAAgB,IAA4C;AAC1D,QAAO,OAAO,KAAK,EAAsB;;AAG3C,SAAS,EAAkB,EAAE,YAA6B;AACxD,QACE,kBAAC,OAAD;EAAK,WAAU;YACZ,IAAQ,IAAI,EAAM,qCAAqC;EACpD,CAAA;;AAIV,SAAgB,EAAoB,GAA+B;CACjE,IAAM,EAAE,cAAW,GAEb,IADW,EAAsB,EAAO,OACnB,YAAY,gBAAgB,EAAsB,EAAO,QAAQ,KAAA;AAM5F,QAJK,IAKH,kBAAC,GAAD;EAAoB,OAAM;YACxB,kBAAC,GAAD,EAAU,GAAI,GAAS,CAAA;EACJ,CAAA,GANd,kBAAC,GAAD,EAAmB,OAAO,EAAO,OAAS,CAAA"}
|