@burdenoff/microfe-bigconsole 2026.1008.11 → 2026.1008.13
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/BigConsoleRoutes.d.ts.map +1 -1
- package/dist/bigconsole/BigConsoleRoutes.js +136 -127
- package/dist/bigconsole/BigConsoleRoutes.js.map +1 -1
- package/dist/bigconsole/assistant/assistantProduct.d.ts.map +1 -1
- package/dist/bigconsole/assistant/assistantProduct.js +3 -1
- package/dist/bigconsole/assistant/assistantProduct.js.map +1 -1
- package/dist/bigconsole/components/builder/inspector/DataBindingSection.js +31 -31
- package/dist/bigconsole/components/dashboard/CreateDashboardDialog.js +44 -44
- package/dist/bigconsole/components/dashboard/CreateDashboardDialog.js.map +1 -1
- package/dist/bigconsole/components/dashboard/DashboardCanvas.js +36 -36
- package/dist/bigconsole/components/dashboard/DashboardCanvas.js.map +1 -1
- package/dist/bigconsole/components/dashboard/DashboardToolbar.js +59 -59
- package/dist/bigconsole/components/dashboard/DrilldownBreadcrumb.js +15 -15
- package/dist/bigconsole/components/dashboard/GlobalFiltersBar/FilterChip.js +30 -30
- package/dist/bigconsole/components/dashboard/GlobalFiltersBar/FilterEditor.js +41 -41
- package/dist/bigconsole/components/dashboard/GlobalFiltersBar/FilterPresetSelector.js +34 -34
- package/dist/bigconsole/components/dashboard/GlobalFiltersBar/GlobalFiltersBar.js +34 -34
- package/dist/bigconsole/components/dashboard/ImportDashboardDialog.js +110 -110
- package/dist/bigconsole/components/dashboard/ImportDashboardDialog.js.map +1 -1
- package/dist/bigconsole/components/dashboard/PublishToStoreDialog.js +21 -21
- package/dist/bigconsole/components/dashboard/SavedViewsDropdown/SavedViewsDropdown.js +46 -46
- package/dist/bigconsole/components/dashboard/ShareDialog/EmbedTab.js +91 -91
- package/dist/bigconsole/components/dashboard/ShareDialog/EmbedTab.js.map +1 -1
- package/dist/bigconsole/components/dashboard/ShareDialog/ShareDialog.js +101 -101
- package/dist/bigconsole/components/dashboard/ShareDialog/ShareDialog.js.map +1 -1
- package/dist/bigconsole/components/dashboard/TemplateInstallOptions.js +31 -31
- package/dist/bigconsole/components/dashboard/TemplateLayoutPreview.js +15 -15
- package/dist/bigconsole/components/dashboard/TemplatePicker/TemplatePicker.js +1 -1
- package/dist/bigconsole/components/dashboard/WidgetPalette.js +46 -46
- package/dist/bigconsole/components/datasink/AlertExecutionHistory.js +37 -37
- package/dist/bigconsole/components/datasink/AlertRuleEditor.js +66 -66
- package/dist/bigconsole/components/datasink/CacheTTLSelector.js +35 -35
- package/dist/bigconsole/components/datasink/DataSinkCard.js +29 -29
- package/dist/bigconsole/components/datasink/DataSinkDeleteDialog.js +18 -18
- package/dist/bigconsole/components/datasink/DataSinkRefreshDialog.js +20 -20
- package/dist/bigconsole/components/datasink/ImportDataGuideDialog.js +47 -47
- package/dist/bigconsole/components/datasink/ImportDataSinkDialog.js +91 -91
- package/dist/bigconsole/components/datasink/RefreshTypeSelector.js +35 -35
- package/dist/bigconsole/components/parser/DataSinkSelector.js +40 -40
- package/dist/bigconsole/components/parser/ImportParserDialog.js +120 -120
- package/dist/bigconsole/components/parser/ImportParserDialog.js.map +1 -1
- package/dist/bigconsole/components/parser/ParserSelector.js +51 -51
- package/dist/bigconsole/components/pipeline/PipelineScopePicker.js +10 -10
- package/dist/bigconsole/components/preview/AiPreviewMiniPanel.js +27 -27
- package/dist/bigconsole/components/preview/AiPreviewPanel.js +30 -30
- package/dist/bigconsole/components/widgets/ActionConfigDialog.js +4 -4
- package/dist/bigconsole/components/widgets/DrilldownConfigPanel.js +93 -93
- package/dist/bigconsole/components/widgets/DrilldownConfigPanel.js.map +1 -1
- package/dist/bigconsole/components/widgets/PropertiesPanel.d.ts.map +1 -1
- package/dist/bigconsole/components/widgets/PropertiesPanel.js +507 -499
- package/dist/bigconsole/components/widgets/PropertiesPanel.js.map +1 -1
- package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js +4 -4
- package/dist/bigconsole/components/widgets/chart/ChartConfig.js +48 -48
- package/dist/bigconsole/components/widgets/comments/WidgetComments.js +46 -46
- package/dist/bigconsole/components/widgets/event-logs/EventLogViewer.js +61 -61
- package/dist/bigconsole/components/widgets/form/FormWidget.js +36 -36
- package/dist/bigconsole/components/widgets/gauge/GaugeConfig.js +42 -42
- package/dist/bigconsole/components/widgets/heatmap/HeatmapConfig.js +25 -25
- package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js +4 -4
- package/dist/bigconsole/components/widgets/list/ListConfig.js +30 -30
- package/dist/bigconsole/components/widgets/metric-card/MetricCardConfig.js +38 -38
- package/dist/bigconsole/components/widgets/metric-card/SparklineChart.js +9 -9
- package/dist/bigconsole/components/widgets/metric-card/TrendIndicator.js +9 -9
- package/dist/bigconsole/components/widgets/pivot-table/PivotTableWidget.js +49 -49
- package/dist/bigconsole/components/widgets/progress/ProgressConfig.js +39 -39
- package/dist/bigconsole/components/widgets/table/TableConfig.js +73 -73
- package/dist/bigconsole/components/widgets/webhook-config/WebhookConfigPanel.js +64 -64
- package/dist/bigconsole/components/workflow/WorkflowOpenGuard.js +15 -15
- package/dist/bigconsole/components/workflow/WorkflowSinkLinkForm.js +15 -15
- package/dist/bigconsole/customWidget/CustomWidgetBuilderSheet.js +9 -9
- package/dist/bigconsole/customWidget/CustomWidgetProposalPanel.js +67 -67
- package/dist/bigconsole/customWidget/CustomWidgetProposalPanel.js.map +1 -1
- package/dist/bigconsole/hooks/useDashboardLayoutBaseline.d.ts +15 -0
- package/dist/bigconsole/hooks/useDashboardLayoutBaseline.d.ts.map +1 -0
- package/dist/bigconsole/hooks/useDashboardLayoutBaseline.js +34 -0
- package/dist/bigconsole/hooks/useDashboardLayoutBaseline.js.map +1 -0
- package/dist/bigconsole/hooks/useDrilldown.js +3 -3
- package/dist/bigconsole/hooks/useGlobalFilters.js +3 -3
- package/dist/bigconsole/pages/AdminPage.js +130 -130
- package/dist/bigconsole/pages/DashboardBuilderPage.d.ts.map +1 -1
- package/dist/bigconsole/pages/DashboardBuilderPage.js +72 -60
- package/dist/bigconsole/pages/DashboardBuilderPage.js.map +1 -1
- package/dist/bigconsole/pages/DashboardViewPage.js +1 -1
- package/dist/bigconsole/pages/DataSinkAlertsPage.d.ts.map +1 -1
- package/dist/bigconsole/pages/DataSinkAlertsPage.js +30 -23
- package/dist/bigconsole/pages/DataSinkAlertsPage.js.map +1 -1
- package/dist/bigconsole/pages/DataSinkBuilderPage.d.ts.map +1 -1
- package/dist/bigconsole/pages/DataSinkBuilderPage.js +273 -264
- package/dist/bigconsole/pages/DataSinkBuilderPage.js.map +1 -1
- package/dist/bigconsole/pages/DataSinkViewPage.js +1 -1
- package/dist/bigconsole/pages/DataSinksPage.js +1 -1
- package/dist/bigconsole/pages/DataSourcesPage.js +5 -5
- package/dist/bigconsole/pages/InsightsPage.d.ts.map +1 -1
- package/dist/bigconsole/pages/InsightsPage.js +279 -265
- package/dist/bigconsole/pages/InsightsPage.js.map +1 -1
- package/dist/bigconsole/pages/OverviewPage.js +47 -47
- package/dist/bigconsole/pages/OverviewPage.js.map +1 -1
- package/dist/bigconsole/pages/ParserBuilderPage.d.ts.map +1 -1
- package/dist/bigconsole/pages/ParserBuilderPage.js +229 -213
- package/dist/bigconsole/pages/ParserBuilderPage.js.map +1 -1
- package/dist/bigconsole/pages/ParserViewPage.js +97 -97
- package/dist/bigconsole/pages/PipelineBuilderPage.d.ts.map +1 -1
- package/dist/bigconsole/pages/PipelineBuilderPage.js +256 -214
- package/dist/bigconsole/pages/PipelineBuilderPage.js.map +1 -1
- package/dist/bigconsole/pages/PipelineViewPage.js +146 -146
- package/dist/bigconsole/pages/PipelinesPage.js +88 -88
- package/dist/bigconsole/pages/PipelinesPage.js.map +1 -1
- package/dist/bigconsole/pages/SettingsPage.d.ts.map +1 -1
- package/dist/bigconsole/pages/SettingsPage.js +179 -173
- package/dist/bigconsole/pages/SettingsPage.js.map +1 -1
- package/dist/bigconsole/pages/WidgetDemoPage.js +42 -42
- package/dist/bigconsole/pages/WorkflowViewPage.js +1 -1
- package/dist/bigconsole/pages/WorkflowsPage.js +141 -141
- package/dist/bigconsole/pages/WorkflowsPage.js.map +1 -1
- package/dist/bigconsole/providers/BigConsoleQuotaProvider.js +12 -12
- package/dist/bigconsole/renderers/adapters/AdaptiveCardRenderer.js +34 -34
- package/dist/bigconsole/renderers/adapters/BlockKitRenderer.js +79 -79
- package/dist/bigconsole/renderers/adapters/CustomRenderer.js +46 -46
- package/dist/bigconsole/store/widgetStore.d.ts +12 -0
- package/dist/bigconsole/store/widgetStore.d.ts.map +1 -1
- package/dist/bigconsole/store/widgetStore.js +3 -1
- package/dist/bigconsole/store/widgetStore.js.map +1 -1
- package/dist/bigconsole/utils/formDirty.d.ts +83 -0
- package/dist/bigconsole/utils/formDirty.d.ts.map +1 -0
- package/dist/bigconsole/utils/formDirty.js +23 -0
- package/dist/bigconsole/utils/formDirty.js.map +1 -0
- package/dist/bigconsole/utils/insightsDraft.d.ts +30 -0
- package/dist/bigconsole/utils/insightsDraft.d.ts.map +1 -0
- package/dist/bigconsole/utils/insightsDraft.js +10 -0
- package/dist/bigconsole/utils/insightsDraft.js.map +1 -0
- package/dist/bigconsole/utils/layoutBaseline.d.ts +131 -0
- package/dist/bigconsole/utils/layoutBaseline.d.ts.map +1 -0
- package/dist/bigconsole/utils/layoutBaseline.js +98 -0
- package/dist/bigconsole/utils/layoutBaseline.js.map +1 -0
- package/dist/bigconsole/utils/widgetFormDirty.d.ts +17 -0
- package/dist/bigconsole/utils/widgetFormDirty.d.ts.map +1 -0
- package/dist/bigconsole/utils/widgetFormDirty.js +15 -0
- package/dist/bigconsole/utils/widgetFormDirty.js.map +1 -0
- package/dist/translations/en.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -7,8 +7,8 @@ import { localProposalSource as ne } from "./proposalSource.js";
|
|
|
7
7
|
import { memo as a, useCallback as o, useEffect as re, useMemo as s, useState as c } from "react";
|
|
8
8
|
import { AlertTriangle as l, Check as u, Loader2 as d, RefreshCw as ie, Sparkles as ae, X as oe } from "lucide-react";
|
|
9
9
|
import { Button as f, Input as p, Label as m, Textarea as se } from "@burdenoff/fe-libs/ui";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { useI18n as ce } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
11
|
+
import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
12
12
|
//#region src/bigconsole/customWidget/CustomWidgetProposalPanel.tsx
|
|
13
13
|
var v = "w-full", y = {
|
|
14
14
|
loading: "neutral",
|
|
@@ -61,7 +61,7 @@ function S(e) {
|
|
|
61
61
|
return 0;
|
|
62
62
|
}
|
|
63
63
|
var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T, onClose: E, source: D = ne }) {
|
|
64
|
-
let { t: O } =
|
|
64
|
+
let { t: O } = ce(), k = s(() => ({
|
|
65
65
|
loading: {
|
|
66
66
|
title: O("bigconsole.widget.customWidget.preview.loading.title", "Building preview…"),
|
|
67
67
|
body: O("bigconsole.widget.customWidget.preview.loading.body", "Running the parser against the selected DataSink.")
|
|
@@ -160,43 +160,43 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
|
|
|
160
160
|
E,
|
|
161
161
|
O
|
|
162
162
|
]), Q = J.issues.filter((e) => e.severity === "error"), $ = J.issues.filter((e) => e.severity === "warning");
|
|
163
|
-
return /* @__PURE__ */
|
|
163
|
+
return /* @__PURE__ */ g("section", {
|
|
164
164
|
"data-testid": "custom-widget-proposal-panel",
|
|
165
165
|
"aria-label": O("bigconsole.widget.customWidget.panel.ariaLabel", "AI custom widget builder"),
|
|
166
166
|
className: "flex h-full w-full min-w-0 flex-col overflow-y-auto p-4",
|
|
167
|
-
children: /* @__PURE__ */
|
|
167
|
+
children: /* @__PURE__ */ _(t, { children: [/* @__PURE__ */ g(e, {
|
|
168
168
|
title: O("bigconsole.widget.customWidget.prompt.label", "Describe the widget you need"),
|
|
169
169
|
description: "Plain English. The assistant proposes a typed widget you can edit before anything is saved.",
|
|
170
|
-
icon: /* @__PURE__ */
|
|
170
|
+
icon: /* @__PURE__ */ g(ae, {
|
|
171
171
|
className: "size-4",
|
|
172
172
|
"aria-hidden": "true"
|
|
173
173
|
}),
|
|
174
|
-
actions: /* @__PURE__ */
|
|
174
|
+
actions: /* @__PURE__ */ _("div", {
|
|
175
175
|
className: "flex flex-wrap gap-2",
|
|
176
|
-
children: [/* @__PURE__ */
|
|
176
|
+
children: [/* @__PURE__ */ _(f, {
|
|
177
177
|
"data-testid": "custom-widget-generate",
|
|
178
178
|
onClick: ue,
|
|
179
179
|
disabled: !A.trim() || N,
|
|
180
|
-
children: [N ? /* @__PURE__ */
|
|
180
|
+
children: [N ? /* @__PURE__ */ g(d, {
|
|
181
181
|
className: "mr-2 size-4 animate-spin",
|
|
182
182
|
"aria-hidden": "true"
|
|
183
183
|
}) : null, j ? O("bigconsole.widget.customWidget.action.regenerate", "Regenerate") : O("bigconsole.widget.customWidget.action.generateProposal", "Generate proposal")]
|
|
184
|
-
}), j ? /* @__PURE__ */
|
|
184
|
+
}), j ? /* @__PURE__ */ _(f, {
|
|
185
185
|
variant: "outline",
|
|
186
186
|
"data-testid": "custom-widget-reject",
|
|
187
187
|
onClick: de,
|
|
188
|
-
children: [/* @__PURE__ */
|
|
188
|
+
children: [/* @__PURE__ */ g(oe, {
|
|
189
189
|
className: "mr-1 size-4",
|
|
190
190
|
"aria-hidden": "true"
|
|
191
191
|
}), O("bigconsole.widget.customWidget.action.reject", "Reject")]
|
|
192
192
|
}) : null]
|
|
193
193
|
}),
|
|
194
|
-
children: /* @__PURE__ */
|
|
194
|
+
children: /* @__PURE__ */ _("div", {
|
|
195
195
|
className: "flex flex-col gap-2",
|
|
196
|
-
children: [/* @__PURE__ */
|
|
196
|
+
children: [/* @__PURE__ */ g(m, {
|
|
197
197
|
htmlFor: "custom-widget-prompt",
|
|
198
198
|
children: "What should it show?"
|
|
199
|
-
}), /* @__PURE__ */
|
|
199
|
+
}), /* @__PURE__ */ g(se, {
|
|
200
200
|
id: "custom-widget-prompt",
|
|
201
201
|
"data-testid": "custom-widget-prompt",
|
|
202
202
|
value: A,
|
|
@@ -205,95 +205,95 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
|
|
|
205
205
|
rows: 3
|
|
206
206
|
})]
|
|
207
207
|
})
|
|
208
|
-
}), j ? /* @__PURE__ */
|
|
209
|
-
/* @__PURE__ */
|
|
208
|
+
}), j ? /* @__PURE__ */ _(h, { children: [
|
|
209
|
+
/* @__PURE__ */ _(e, {
|
|
210
210
|
emphasis: "primary",
|
|
211
211
|
tone: y[Y],
|
|
212
212
|
title: k[Y].title,
|
|
213
213
|
description: "Rendered by the real widget renderer against the real parser output — what you approve is what you get.",
|
|
214
|
-
meta: /* @__PURE__ */
|
|
214
|
+
meta: /* @__PURE__ */ g("span", {
|
|
215
215
|
"data-testid": "custom-widget-preview-state",
|
|
216
216
|
"data-state": Y,
|
|
217
217
|
className: "rounded-pill bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary",
|
|
218
218
|
children: Y
|
|
219
219
|
}),
|
|
220
|
-
children: [/* @__PURE__ */
|
|
220
|
+
children: [/* @__PURE__ */ g("div", {
|
|
221
221
|
"data-testid": "custom-widget-preview",
|
|
222
222
|
className: "min-h-[220px] min-w-0 rounded-card border border-[var(--color-border-subtle)] bg-bg-surface p-2",
|
|
223
|
-
children: Y === "loading" ? /* @__PURE__ */
|
|
223
|
+
children: Y === "loading" ? /* @__PURE__ */ _("div", {
|
|
224
224
|
className: "flex h-[200px] items-center justify-center text-text-secondary",
|
|
225
|
-
children: [/* @__PURE__ */
|
|
225
|
+
children: [/* @__PURE__ */ g(d, {
|
|
226
226
|
className: "mr-2 size-4 animate-spin",
|
|
227
227
|
"aria-hidden": "true"
|
|
228
|
-
}), /* @__PURE__ */
|
|
228
|
+
}), /* @__PURE__ */ g("span", {
|
|
229
229
|
className: "text-sm",
|
|
230
230
|
children: k.loading.body
|
|
231
231
|
})]
|
|
232
|
-
}) : Y === "valid" ? /* @__PURE__ */
|
|
232
|
+
}) : Y === "valid" ? /* @__PURE__ */ g("div", {
|
|
233
233
|
className: "h-[200px]",
|
|
234
|
-
children: /* @__PURE__ */
|
|
234
|
+
children: /* @__PURE__ */ g(n, {
|
|
235
235
|
widgetId: `preview-${j.id}`,
|
|
236
|
-
children: /* @__PURE__ */
|
|
236
|
+
children: /* @__PURE__ */ g(ee, {
|
|
237
237
|
widget: b(j, O("bigconsole.widget.customWidget.untitledWidget", "Untitled widget")),
|
|
238
238
|
data: z
|
|
239
239
|
})
|
|
240
240
|
})
|
|
241
|
-
}) : /* @__PURE__ */
|
|
241
|
+
}) : /* @__PURE__ */ _("div", {
|
|
242
242
|
className: "flex h-[200px] flex-col items-center justify-center gap-2 px-4 text-center",
|
|
243
243
|
children: [
|
|
244
|
-
/* @__PURE__ */
|
|
244
|
+
/* @__PURE__ */ g(l, {
|
|
245
245
|
className: "size-5 text-[var(--color-status-warning-text)]",
|
|
246
246
|
"aria-hidden": "true"
|
|
247
247
|
}),
|
|
248
|
-
/* @__PURE__ */
|
|
248
|
+
/* @__PURE__ */ g("p", {
|
|
249
249
|
className: "text-sm text-text-secondary",
|
|
250
250
|
children: k[Y].body
|
|
251
251
|
}),
|
|
252
|
-
Y === "access-error" ? /* @__PURE__ */
|
|
252
|
+
Y === "access-error" ? /* @__PURE__ */ _(f, {
|
|
253
253
|
size: "sm",
|
|
254
254
|
variant: "outline",
|
|
255
255
|
onClick: () => X((e) => ({ ...e })),
|
|
256
|
-
children: [/* @__PURE__ */
|
|
256
|
+
children: [/* @__PURE__ */ g(ie, {
|
|
257
257
|
className: "mr-1 size-3.5",
|
|
258
258
|
"aria-hidden": "true"
|
|
259
259
|
}), O("bigconsole.widget.customWidget.action.retry", "Retry")]
|
|
260
260
|
}) : null
|
|
261
261
|
]
|
|
262
262
|
})
|
|
263
|
-
}), j.rationale ? /* @__PURE__ */
|
|
263
|
+
}), j.rationale ? /* @__PURE__ */ g("p", {
|
|
264
264
|
className: "mt-2 text-xs text-text-secondary",
|
|
265
265
|
"data-testid": "custom-widget-rationale",
|
|
266
266
|
children: j.rationale
|
|
267
267
|
}) : null]
|
|
268
268
|
}),
|
|
269
|
-
Q.length > 0 || $.length > 0 ? /* @__PURE__ */
|
|
269
|
+
Q.length > 0 || $.length > 0 ? /* @__PURE__ */ g(e, {
|
|
270
270
|
tone: Q.length > 0 ? "danger" : "warning",
|
|
271
271
|
padding: "compact",
|
|
272
272
|
headingLevel: 3,
|
|
273
273
|
title: Q.length > 0 ? `${Q.length} ${Q.length === 1 ? "problem" : "problems"} to fix before approving` : `${$.length} ${$.length === 1 ? "warning" : "warnings"}`,
|
|
274
|
-
children: /* @__PURE__ */
|
|
274
|
+
children: /* @__PURE__ */ g("ul", {
|
|
275
275
|
"data-testid": "custom-widget-issues",
|
|
276
276
|
className: "flex min-w-0 flex-col gap-1",
|
|
277
|
-
children: [...Q, ...$].map((e) => /* @__PURE__ */
|
|
277
|
+
children: [...Q, ...$].map((e) => /* @__PURE__ */ g("li", {
|
|
278
278
|
"data-severity": e.severity,
|
|
279
279
|
className: `break-words text-xs ${e.severity === "error" ? "text-[var(--color-status-error-text)]" : "text-[var(--color-status-warning-text)]"}`,
|
|
280
280
|
children: e.message
|
|
281
281
|
}, `${e.code}-${e.path ?? ""}`))
|
|
282
282
|
})
|
|
283
283
|
}) : null,
|
|
284
|
-
/* @__PURE__ */
|
|
284
|
+
/* @__PURE__ */ g(e, {
|
|
285
285
|
title: O("bigconsole.widget.customWidget.proposal.heading", "Proposal"),
|
|
286
286
|
description: "Every field is editable — the assistant proposes, you dispose.",
|
|
287
287
|
"data-testid": "custom-widget-proposal-fields",
|
|
288
|
-
children: /* @__PURE__ */
|
|
288
|
+
children: /* @__PURE__ */ _("div", {
|
|
289
289
|
className: "flex flex-col gap-3",
|
|
290
290
|
children: [
|
|
291
|
-
/* @__PURE__ */
|
|
291
|
+
/* @__PURE__ */ _("div", {
|
|
292
292
|
className: "flex flex-col gap-1",
|
|
293
|
-
children: [/* @__PURE__ */
|
|
293
|
+
children: [/* @__PURE__ */ g(m, {
|
|
294
294
|
htmlFor: "custom-widget-title",
|
|
295
295
|
children: O("bigconsole.widget.customWidget.field.title", "Title")
|
|
296
|
-
}), /* @__PURE__ */
|
|
296
|
+
}), /* @__PURE__ */ g(p, {
|
|
297
297
|
id: "custom-widget-title",
|
|
298
298
|
"data-testid": "custom-widget-title",
|
|
299
299
|
className: v,
|
|
@@ -307,12 +307,12 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
|
|
|
307
307
|
}))
|
|
308
308
|
})]
|
|
309
309
|
}),
|
|
310
|
-
/* @__PURE__ */
|
|
310
|
+
/* @__PURE__ */ _("div", {
|
|
311
311
|
className: "flex flex-col gap-1",
|
|
312
|
-
children: [/* @__PURE__ */
|
|
312
|
+
children: [/* @__PURE__ */ g(m, {
|
|
313
313
|
htmlFor: "custom-widget-type",
|
|
314
314
|
children: O("bigconsole.widget.customWidget.field.widgetType", "Widget type")
|
|
315
|
-
}), /* @__PURE__ */
|
|
315
|
+
}), /* @__PURE__ */ g("select", {
|
|
316
316
|
id: "custom-widget-type",
|
|
317
317
|
"data-testid": "custom-widget-type",
|
|
318
318
|
value: j.widgetType,
|
|
@@ -321,18 +321,18 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
|
|
|
321
321
|
widgetType: e.target.value
|
|
322
322
|
})),
|
|
323
323
|
className: "w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg",
|
|
324
|
-
children: te.map((e) => /* @__PURE__ */
|
|
324
|
+
children: te.map((e) => /* @__PURE__ */ g("option", {
|
|
325
325
|
value: e,
|
|
326
326
|
children: e
|
|
327
327
|
}, e))
|
|
328
328
|
})]
|
|
329
329
|
}),
|
|
330
|
-
/* @__PURE__ */
|
|
330
|
+
/* @__PURE__ */ _("div", {
|
|
331
331
|
className: "flex flex-col gap-1",
|
|
332
|
-
children: [/* @__PURE__ */
|
|
332
|
+
children: [/* @__PURE__ */ g(m, {
|
|
333
333
|
htmlFor: "custom-widget-sink",
|
|
334
334
|
children: O("bigconsole.widget.customWidget.field.dataSink", "DataSink")
|
|
335
|
-
}), /* @__PURE__ */
|
|
335
|
+
}), /* @__PURE__ */ _("select", {
|
|
336
336
|
id: "custom-widget-sink",
|
|
337
337
|
"data-testid": "custom-widget-sink",
|
|
338
338
|
value: j.binding.dataSinkId,
|
|
@@ -348,21 +348,21 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
|
|
|
348
348
|
}));
|
|
349
349
|
},
|
|
350
350
|
className: "w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg",
|
|
351
|
-
children: [/* @__PURE__ */
|
|
351
|
+
children: [/* @__PURE__ */ g("option", {
|
|
352
352
|
value: "",
|
|
353
353
|
children: O("bigconsole.widget.customWidget.field.selectDataSink", "Select a DataSink…")
|
|
354
|
-
}), a.map((e) => /* @__PURE__ */
|
|
354
|
+
}), a.map((e) => /* @__PURE__ */ g("option", {
|
|
355
355
|
value: e.id,
|
|
356
356
|
children: e.name || e.key
|
|
357
357
|
}, e.id))]
|
|
358
358
|
})]
|
|
359
359
|
}),
|
|
360
|
-
/* @__PURE__ */
|
|
360
|
+
/* @__PURE__ */ _("div", {
|
|
361
361
|
className: "flex flex-col gap-1",
|
|
362
|
-
children: [/* @__PURE__ */
|
|
362
|
+
children: [/* @__PURE__ */ g(m, {
|
|
363
363
|
htmlFor: "custom-widget-parser",
|
|
364
364
|
children: O("bigconsole.widget.customWidget.field.parser", "Parser")
|
|
365
|
-
}), /* @__PURE__ */
|
|
365
|
+
}), /* @__PURE__ */ _("select", {
|
|
366
366
|
id: "custom-widget-parser",
|
|
367
367
|
"data-testid": "custom-widget-parser",
|
|
368
368
|
value: j.binding.parserId ?? "",
|
|
@@ -378,29 +378,29 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
|
|
|
378
378
|
}));
|
|
379
379
|
},
|
|
380
380
|
className: "w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg",
|
|
381
|
-
children: [/* @__PURE__ */
|
|
381
|
+
children: [/* @__PURE__ */ g("option", {
|
|
382
382
|
value: "",
|
|
383
383
|
children: O("bigconsole.widget.customWidget.field.noParser", "No parser")
|
|
384
|
-
}), C.map((e) => /* @__PURE__ */
|
|
384
|
+
}), C.map((e) => /* @__PURE__ */ g("option", {
|
|
385
385
|
value: e.id,
|
|
386
386
|
children: e.name
|
|
387
387
|
}, e.id))]
|
|
388
388
|
})]
|
|
389
389
|
}),
|
|
390
|
-
Object.keys(j.fieldMapping).length > 0 ? /* @__PURE__ */
|
|
390
|
+
Object.keys(j.fieldMapping).length > 0 ? /* @__PURE__ */ _("fieldset", {
|
|
391
391
|
className: "flex flex-col gap-2",
|
|
392
392
|
"data-testid": "custom-widget-field-mapping",
|
|
393
393
|
children: [
|
|
394
|
-
/* @__PURE__ */
|
|
394
|
+
/* @__PURE__ */ g("legend", {
|
|
395
395
|
className: "text-sm font-medium text-text-primary",
|
|
396
396
|
children: O("bigconsole.widget.customWidget.fieldMapping.legend", "Field mapping")
|
|
397
397
|
}),
|
|
398
|
-
Object.entries(j.fieldMapping).map(([e, t]) => /* @__PURE__ */
|
|
398
|
+
Object.entries(j.fieldMapping).map(([e, t]) => /* @__PURE__ */ _("div", {
|
|
399
399
|
className: "flex flex-col gap-1",
|
|
400
|
-
children: [/* @__PURE__ */
|
|
400
|
+
children: [/* @__PURE__ */ g(m, {
|
|
401
401
|
htmlFor: `custom-widget-field-${e}`,
|
|
402
402
|
children: e
|
|
403
|
-
}), /* @__PURE__ */
|
|
403
|
+
}), /* @__PURE__ */ g(p, {
|
|
404
404
|
id: `custom-widget-field-${e}`,
|
|
405
405
|
"data-testid": `custom-widget-field-${e}`,
|
|
406
406
|
value: t,
|
|
@@ -413,7 +413,7 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
|
|
|
413
413
|
}))
|
|
414
414
|
})]
|
|
415
415
|
}, e)),
|
|
416
|
-
q?.length ? /* @__PURE__ */
|
|
416
|
+
q?.length ? /* @__PURE__ */ g("p", {
|
|
417
417
|
className: "break-words text-xs text-text-secondary",
|
|
418
418
|
children: O("bigconsole.widget.customWidget.fieldMapping.available", "Available: {{fields}}", { fields: q.join(", ") })
|
|
419
419
|
}) : null
|
|
@@ -422,44 +422,44 @@ var C = a(function({ sinks: a, parsers: C, resolvePreviewData: w, onApprove: T,
|
|
|
422
422
|
]
|
|
423
423
|
})
|
|
424
424
|
}),
|
|
425
|
-
L ? /* @__PURE__ */
|
|
425
|
+
L ? /* @__PURE__ */ g(e, {
|
|
426
426
|
tone: "danger",
|
|
427
427
|
padding: "compact",
|
|
428
428
|
headingLevel: 3,
|
|
429
429
|
title: "Could not add the widget",
|
|
430
|
-
children: /* @__PURE__ */
|
|
430
|
+
children: /* @__PURE__ */ g("p", {
|
|
431
431
|
role: "alert",
|
|
432
432
|
className: "break-words text-xs text-[var(--color-status-error-text)]",
|
|
433
433
|
"data-testid": "custom-widget-approve-error",
|
|
434
434
|
children: L
|
|
435
435
|
})
|
|
436
436
|
}) : null,
|
|
437
|
-
/* @__PURE__ */
|
|
437
|
+
/* @__PURE__ */ _("div", {
|
|
438
438
|
className: "flex flex-wrap gap-2 border-t border-[var(--color-border-subtle)] pt-3",
|
|
439
|
-
children: [/* @__PURE__ */
|
|
439
|
+
children: [/* @__PURE__ */ _(f, {
|
|
440
440
|
"data-testid": "custom-widget-approve",
|
|
441
441
|
onClick: Z,
|
|
442
442
|
disabled: !J.canApprove || F,
|
|
443
|
-
children: [F ? /* @__PURE__ */
|
|
443
|
+
children: [F ? /* @__PURE__ */ g(d, {
|
|
444
444
|
className: "mr-2 size-4 animate-spin",
|
|
445
445
|
"aria-hidden": "true"
|
|
446
|
-
}) : /* @__PURE__ */
|
|
446
|
+
}) : /* @__PURE__ */ g(u, {
|
|
447
447
|
className: "mr-1 size-4",
|
|
448
448
|
"aria-hidden": "true"
|
|
449
449
|
}), O("bigconsole.widget.customWidget.action.approveAndAdd", "Approve & add to dashboard")]
|
|
450
|
-
}), /* @__PURE__ */
|
|
450
|
+
}), /* @__PURE__ */ g(f, {
|
|
451
451
|
variant: "outline",
|
|
452
452
|
onClick: E,
|
|
453
453
|
"data-testid": "custom-widget-cancel",
|
|
454
454
|
children: O("bigconsole.widget.customWidget.action.cancel", "Cancel")
|
|
455
455
|
})]
|
|
456
456
|
})
|
|
457
|
-
] }) : /* @__PURE__ */
|
|
457
|
+
] }) : /* @__PURE__ */ g(e, {
|
|
458
458
|
emphasis: "muted",
|
|
459
459
|
padding: "compact",
|
|
460
460
|
title: "No proposal yet",
|
|
461
461
|
headingLevel: 3,
|
|
462
|
-
children: /* @__PURE__ */
|
|
462
|
+
children: /* @__PURE__ */ g("p", {
|
|
463
463
|
className: "text-sm text-text-secondary",
|
|
464
464
|
"data-testid": "custom-widget-empty",
|
|
465
465
|
children: O("bigconsole.widget.customWidget.empty", "No proposal yet. Describe a widget and the assistant will propose one for you to review — nothing is added to the dashboard until you approve it.")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CustomWidgetProposalPanel.js","names":[],"sources":["../../../src/bigconsole/customWidget/CustomWidgetProposalPanel.tsx"],"sourcesContent":["/**\n * Custom-widget proposal review panel (BOFF-5532).\n *\n * The authoring surface for the AI custom-widget builder: prompt in, typed\n * proposal out, reviewed and edited by a human, then approved into a real\n * widget. Nothing is written to the dashboard until the reviewer approves.\n *\n * Deliberate choices:\n * - The preview renders through the *real* widget renderer with the *real*\n * parser output, so what the reviewer approves is what they will get.\n * - Bindings are chosen from the user's permitted sinks and parsers only, and\n * the panel says plainly when a proposal points somewhere they cannot reach.\n * - Every proposal field is editable before approval — the AI proposes, the\n * reviewer disposes.\n */\n\nimport { type FC, memo, useCallback, useEffect, useMemo, useState } from 'react';\nimport { Button, Input, Label, Textarea } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { AlertTriangle, Check, Loader2, RefreshCw, Sparkles, X } from 'lucide-react';\n\nimport type { Widget } from '../types';\nimport { BigConsoleRenderer } from '../renderers/adapters/BigConsoleRenderer';\nimport { WidgetErrorBoundary } from '../components/widgets/WidgetErrorBoundary';\nimport {\n PROPOSABLE_WIDGET_TYPES,\n derivePreviewState,\n validateProposal,\n type CustomWidgetProposal,\n type ProposableWidgetType,\n type ProposalPreviewState,\n} from './proposalContract';\nimport {\n localProposalSource,\n type CustomWidgetProposalSource,\n type ProposalCandidateParser,\n type ProposalCandidateSink,\n} from './proposalSource';\nimport { Section, SectionStack, type SectionTone } from '../components/common';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CustomWidgetProposalPanelProps {\n /** Sinks the user may bind to (already RBAC-filtered by the API). */\n sinks: readonly ProposalCandidateSink[];\n /** Parsers the user may bind to. */\n parsers: readonly ProposalCandidateParser[];\n /**\n * Resolves the rows a binding would produce, for the preview. Should reject\n * (or throw) when the user cannot read the binding, so the panel can show the\n * access-failure state.\n */\n resolvePreviewData: (proposal: CustomWidgetProposal) => Promise<unknown>;\n /** Persists an approved proposal. Resolves once the widget exists. */\n onApprove: (proposal: CustomWidgetProposal) => Promise<void>;\n /** Closes the panel without persisting anything. */\n onClose: () => void;\n /** Swap for the GraphQL-backed source once BOFF-3021's backend half lands. */\n source?: CustomWidgetProposalSource;\n}\n\nconst FIELD_CLASSES = 'w-full';\n\n/**\n * The preview is the thing being judged, so its tone tracks the binding state.\n * The copy itself is translated per render (`previewCopy` below); only the tone\n * ladder is static.\n */\nconst PREVIEW_TONES: Record<ProposalPreviewState, SectionTone> = {\n loading: 'neutral',\n valid: 'neutral',\n 'no-data': 'warning',\n 'field-mismatch': 'warning',\n 'access-error': 'danger',\n};\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/** Builds a throwaway Widget so the preview can use the production renderer. */\nfunction toDraftWidget(proposal: CustomWidgetProposal, untitledLabel: string): Widget {\n const now = new Date(0).toISOString();\n return {\n id: `preview-${proposal.id}`,\n pageId: 'preview',\n type: proposal.widgetType,\n title: proposal.presentation.title || untitledLabel,\n description: proposal.presentation.description,\n dataSinkId: proposal.binding.dataSinkId || null,\n parserId: proposal.binding.parserId || null,\n config: { ...proposal.presentation.config, ...proposal.fieldMapping },\n position: { x: 0, y: 0, width: 6, height: 4 },\n positionX: 0,\n positionY: 0,\n positionWidth: 6,\n positionHeight: 4,\n createdAt: now,\n updatedAt: now,\n } as Widget;\n}\n\n/** Field names present in the resolved preview payload, for mismatch checks. */\nfunction extractFields(data: unknown): string[] | undefined {\n if (Array.isArray(data)) {\n const first = data.find((row) => row && typeof row === 'object');\n return first ? Object.keys(first as Record<string, unknown>) : [];\n }\n if (data && typeof data === 'object') return Object.keys(data as Record<string, unknown>);\n return undefined;\n}\n\nfunction countRows(data: unknown): number {\n if (Array.isArray(data)) return data.length;\n if (data && typeof data === 'object') {\n const cells = (data as { cells?: unknown }).cells;\n if (Array.isArray(cells)) return cells.length;\n return Object.keys(data as Record<string, unknown>).length;\n }\n return 0;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const CustomWidgetProposalPanel: FC<CustomWidgetProposalPanelProps> = memo(function CustomWidgetProposalPanel({\n sinks,\n parsers,\n resolvePreviewData,\n onApprove,\n onClose,\n source = localProposalSource,\n}) {\n const { t } = useI18n();\n\n const previewCopy = useMemo<Record<ProposalPreviewState, { title: string; body: string }>>(\n () => ({\n loading: {\n title: t('bigconsole.widget.customWidget.preview.loading.title', 'Building preview…'),\n body: t(\n 'bigconsole.widget.customWidget.preview.loading.body',\n 'Running the parser against the selected DataSink.'\n ),\n },\n valid: {\n title: t('bigconsole.widget.customWidget.preview.valid.title', 'Preview'),\n body: '',\n },\n 'no-data': {\n title: t('bigconsole.widget.customWidget.preview.noData.title', 'No data returned'),\n body: t(\n 'bigconsole.widget.customWidget.preview.noData.body',\n 'The binding is valid but the parser returned no rows. The widget will render empty until the sink has data.'\n ),\n },\n 'field-mismatch': {\n title: t('bigconsole.widget.customWidget.preview.fieldMismatch.title', 'Field mismatch'),\n body: t(\n 'bigconsole.widget.customWidget.preview.fieldMismatch.body',\n 'Some mapped fields are not present in the parser output. Fix the mapping below before approving.'\n ),\n },\n 'access-error': {\n title: t('bigconsole.widget.customWidget.preview.accessError.title', 'Cannot read this data'),\n body: t(\n 'bigconsole.widget.customWidget.preview.accessError.body',\n 'You do not have access to the proposed DataSink or parser in this workspace.'\n ),\n },\n }),\n [t]\n );\n\n const [prompt, setPrompt] = useState('');\n const [proposal, setProposal] = useState<CustomWidgetProposal | null>(null);\n const [isProposing, setIsProposing] = useState(false);\n const [isApproving, setIsApproving] = useState(false);\n const [approveError, setApproveError] = useState<string | null>(null);\n\n const [previewData, setPreviewData] = useState<unknown>(undefined);\n const [previewLoading, setPreviewLoading] = useState(false);\n const [previewAccessError, setPreviewAccessError] = useState(false);\n\n const permittedDataSinkIds = useMemo(() => sinks.map((sink) => sink.id), [sinks]);\n const permittedParserIds = useMemo(() => parsers.map((parser) => parser.id), [parsers]);\n const availableFields = useMemo(() => extractFields(previewData), [previewData]);\n\n const validation = useMemo(\n () =>\n proposal\n ? validateProposal(proposal, { permittedDataSinkIds, permittedParserIds, availableFields })\n : { issues: [], canApprove: false },\n [proposal, permittedDataSinkIds, permittedParserIds, availableFields]\n );\n\n const previewState = useMemo(\n () =>\n derivePreviewState(validation, {\n loading: previewLoading,\n accessError: previewAccessError,\n rowCount: countRows(previewData),\n }),\n [validation, previewLoading, previewAccessError, previewData]\n );\n\n // Re-resolve the preview whenever the binding changes.\n const bindingKey = proposal\n ? `${proposal.binding.dataSinkId}|${proposal.binding.parserId ?? ''}|${proposal.binding.dataPath ?? ''}`\n : '';\n\n useEffect(() => {\n if (!proposal || !proposal.binding.dataSinkId) {\n setPreviewData(undefined);\n return;\n }\n let cancelled = false;\n setPreviewLoading(true);\n setPreviewAccessError(false);\n\n resolvePreviewData(proposal)\n .then((data) => {\n if (cancelled) return;\n setPreviewData(data);\n })\n .catch(() => {\n if (cancelled) return;\n setPreviewData(undefined);\n setPreviewAccessError(true);\n })\n .finally(() => {\n if (!cancelled) setPreviewLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n // `bindingKey` captures every input that changes the resolved data.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [bindingKey, resolvePreviewData]);\n\n const handlePropose = useCallback(async () => {\n if (!prompt.trim()) return;\n setIsProposing(true);\n setApproveError(null);\n try {\n const next = await source.propose({ prompt: prompt.trim(), sinks, parsers });\n setProposal(next);\n } finally {\n setIsProposing(false);\n }\n }, [prompt, source, sinks, parsers]);\n\n const patchProposal = useCallback((patch: (current: CustomWidgetProposal) => CustomWidgetProposal) => {\n setProposal((current) => (current ? patch(current) : current));\n }, []);\n\n const handleReject = useCallback(() => {\n setProposal(null);\n setPreviewData(undefined);\n setPreviewAccessError(false);\n setApproveError(null);\n }, []);\n\n const handleApprove = useCallback(async () => {\n if (!proposal || !validation.canApprove) return;\n setIsApproving(true);\n setApproveError(null);\n try {\n await onApprove(proposal);\n onClose();\n } catch (error) {\n setApproveError(\n error instanceof Error\n ? error.message\n : t('bigconsole.widget.customWidget.error.couldNotSaveWidget', 'Could not save the widget.')\n );\n } finally {\n setIsApproving(false);\n }\n }, [proposal, validation.canApprove, onApprove, onClose, t]);\n\n const errors = validation.issues.filter((issue) => issue.severity === 'error');\n const warnings = validation.issues.filter((issue) => issue.severity === 'warning');\n\n return (\n <section\n data-testid=\"custom-widget-proposal-panel\"\n aria-label={t('bigconsole.widget.customWidget.panel.ariaLabel', 'AI custom widget builder')}\n className=\"flex h-full w-full min-w-0 flex-col overflow-y-auto p-4\"\n >\n {/*\n Five stages — prompt → preview → validation → editable proposal →\n approval — used to be five bare divs separated by one uniform gap. Each\n is now a labelled section, and the preview (the thing the reviewer is\n actually judging) is the panel's single `primary` block (BOFF-5636).\n */}\n <SectionStack>\n {/* ---------- Prompt ---------- */}\n <Section\n title={t('bigconsole.widget.customWidget.prompt.label', 'Describe the widget you need')}\n description=\"Plain English. The assistant proposes a typed widget you can edit before anything is saved.\"\n icon={<Sparkles className=\"size-4\" aria-hidden=\"true\" />}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <Button\n data-testid=\"custom-widget-generate\"\n onClick={handlePropose}\n disabled={!prompt.trim() || isProposing}\n >\n {isProposing ? <Loader2 className=\"mr-2 size-4 animate-spin\" aria-hidden=\"true\" /> : null}\n {proposal\n ? t('bigconsole.widget.customWidget.action.regenerate', 'Regenerate')\n : t('bigconsole.widget.customWidget.action.generateProposal', 'Generate proposal')}\n </Button>\n {proposal ? (\n <Button variant=\"outline\" data-testid=\"custom-widget-reject\" onClick={handleReject}>\n <X className=\"mr-1 size-4\" aria-hidden=\"true\" />\n {t('bigconsole.widget.customWidget.action.reject', 'Reject')}\n </Button>\n ) : null}\n </div>\n }\n >\n <div className=\"flex flex-col gap-2\">\n <Label htmlFor=\"custom-widget-prompt\">What should it show?</Label>\n <Textarea\n id=\"custom-widget-prompt\"\n data-testid=\"custom-widget-prompt\"\n value={prompt}\n onChange={(event) => setPrompt(event.target.value)}\n placeholder={t(\n 'bigconsole.widget.customWidget.prompt.placeholder',\n 'e.g. a funnel of signup conversion stages'\n )}\n rows={3}\n />\n </div>\n </Section>\n\n {!proposal ? (\n <Section emphasis=\"muted\" padding=\"compact\" title=\"No proposal yet\" headingLevel={3}>\n <p className=\"text-sm text-text-secondary\" data-testid=\"custom-widget-empty\">\n {t(\n 'bigconsole.widget.customWidget.empty',\n 'No proposal yet. Describe a widget and the assistant will propose one for you to review — nothing is added to the dashboard until you approve it.'\n )}\n </p>\n </Section>\n ) : (\n <>\n {/* ---------- Preview ---------- */}\n <Section\n emphasis=\"primary\"\n tone={PREVIEW_TONES[previewState]}\n title={previewCopy[previewState].title}\n description=\"Rendered by the real widget renderer against the real parser output — what you approve is what you get.\"\n meta={\n <span\n data-testid=\"custom-widget-preview-state\"\n data-state={previewState}\n className=\"rounded-pill bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {previewState}\n </span>\n }\n >\n <div\n data-testid=\"custom-widget-preview\"\n className=\"min-h-[220px] min-w-0 rounded-card border border-[var(--color-border-subtle)] bg-bg-surface p-2\"\n >\n {previewState === 'loading' ? (\n <div className=\"flex h-[200px] items-center justify-center text-text-secondary\">\n <Loader2 className=\"mr-2 size-4 animate-spin\" aria-hidden=\"true\" />\n <span className=\"text-sm\">{previewCopy.loading.body}</span>\n </div>\n ) : previewState === 'valid' ? (\n <div className=\"h-[200px]\">\n <WidgetErrorBoundary widgetId={`preview-${proposal.id}`}>\n <BigConsoleRenderer\n widget={toDraftWidget(\n proposal,\n t('bigconsole.widget.customWidget.untitledWidget', 'Untitled widget')\n )}\n data={previewData as Record<string, unknown>}\n />\n </WidgetErrorBoundary>\n </div>\n ) : (\n <div className=\"flex h-[200px] flex-col items-center justify-center gap-2 px-4 text-center\">\n <AlertTriangle className=\"size-5 text-[var(--color-status-warning-text)]\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-secondary\">{previewCopy[previewState].body}</p>\n {previewState === 'access-error' ? (\n <Button size=\"sm\" variant=\"outline\" onClick={() => patchProposal((current) => ({ ...current }))}>\n <RefreshCw className=\"mr-1 size-3.5\" aria-hidden=\"true\" />\n {t('bigconsole.widget.customWidget.action.retry', 'Retry')}\n </Button>\n ) : null}\n </div>\n )}\n </div>\n\n {proposal.rationale ? (\n <p className=\"mt-2 text-xs text-text-secondary\" data-testid=\"custom-widget-rationale\">\n {proposal.rationale}\n </p>\n ) : null}\n </Section>\n\n {/* ---------- Validation ---------- */}\n {errors.length > 0 || warnings.length > 0 ? (\n <Section\n tone={errors.length > 0 ? 'danger' : 'warning'}\n padding=\"compact\"\n headingLevel={3}\n title={\n errors.length > 0\n ? `${errors.length} ${errors.length === 1 ? 'problem' : 'problems'} to fix before approving`\n : `${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`\n }\n >\n <ul data-testid=\"custom-widget-issues\" className=\"flex min-w-0 flex-col gap-1\">\n {[...errors, ...warnings].map((issue) => (\n <li\n key={`${issue.code}-${issue.path ?? ''}`}\n data-severity={issue.severity}\n className={`break-words text-xs ${\n issue.severity === 'error'\n ? 'text-[var(--color-status-error-text)]'\n : 'text-[var(--color-status-warning-text)]'\n }`}\n >\n {issue.message}\n </li>\n ))}\n </ul>\n </Section>\n ) : null}\n\n {/* ---------- Editable proposal ---------- */}\n <Section\n title={t('bigconsole.widget.customWidget.proposal.heading', 'Proposal')}\n description=\"Every field is editable — the assistant proposes, you dispose.\"\n data-testid=\"custom-widget-proposal-fields\"\n >\n <div className=\"flex flex-col gap-3\">\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-title\">\n {t('bigconsole.widget.customWidget.field.title', 'Title')}\n </Label>\n <Input\n id=\"custom-widget-title\"\n data-testid=\"custom-widget-title\"\n className={FIELD_CLASSES}\n value={proposal.presentation.title}\n onChange={(event) =>\n patchProposal((current) => ({\n ...current,\n presentation: { ...current.presentation, title: event.target.value },\n }))\n }\n />\n </div>\n\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-type\">\n {t('bigconsole.widget.customWidget.field.widgetType', 'Widget type')}\n </Label>\n <select\n id=\"custom-widget-type\"\n data-testid=\"custom-widget-type\"\n value={proposal.widgetType}\n onChange={(event) =>\n patchProposal((current) => ({\n ...current,\n widgetType: event.target.value as ProposableWidgetType,\n }))\n }\n className=\"w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n {PROPOSABLE_WIDGET_TYPES.map((type) => (\n <option key={type} value={type}>\n {type}\n </option>\n ))}\n </select>\n </div>\n\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-sink\">\n {t('bigconsole.widget.customWidget.field.dataSink', 'DataSink')}\n </Label>\n <select\n id=\"custom-widget-sink\"\n data-testid=\"custom-widget-sink\"\n value={proposal.binding.dataSinkId}\n onChange={(event) => {\n const sink = sinks.find((candidate) => candidate.id === event.target.value);\n patchProposal((current) => ({\n ...current,\n binding: { ...current.binding, dataSinkId: event.target.value, dataSinkKey: sink?.key },\n }));\n }}\n className=\"w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"\">\n {t('bigconsole.widget.customWidget.field.selectDataSink', 'Select a DataSink…')}\n </option>\n {sinks.map((sink) => (\n <option key={sink.id} value={sink.id}>\n {sink.name || sink.key}\n </option>\n ))}\n </select>\n </div>\n\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-parser\">\n {t('bigconsole.widget.customWidget.field.parser', 'Parser')}\n </Label>\n <select\n id=\"custom-widget-parser\"\n data-testid=\"custom-widget-parser\"\n value={proposal.binding.parserId ?? ''}\n onChange={(event) => {\n const parser = parsers.find((candidate) => candidate.id === event.target.value);\n patchProposal((current) => ({\n ...current,\n binding: {\n ...current.binding,\n parserId: event.target.value || undefined,\n parserName: parser?.name,\n },\n }));\n }}\n className=\"w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"\">{t('bigconsole.widget.customWidget.field.noParser', 'No parser')}</option>\n {parsers.map((parser) => (\n <option key={parser.id} value={parser.id}>\n {parser.name}\n </option>\n ))}\n </select>\n </div>\n\n {/* Field mapping */}\n {Object.keys(proposal.fieldMapping).length > 0 ? (\n <fieldset className=\"flex flex-col gap-2\" data-testid=\"custom-widget-field-mapping\">\n <legend className=\"text-sm font-medium text-text-primary\">\n {t('bigconsole.widget.customWidget.fieldMapping.legend', 'Field mapping')}\n </legend>\n {Object.entries(proposal.fieldMapping).map(([role, field]) => (\n <div key={role} className=\"flex flex-col gap-1\">\n <Label htmlFor={`custom-widget-field-${role}`}>{role}</Label>\n <Input\n id={`custom-widget-field-${role}`}\n data-testid={`custom-widget-field-${role}`}\n value={field}\n onChange={(event) =>\n patchProposal((current) => ({\n ...current,\n fieldMapping: { ...current.fieldMapping, [role]: event.target.value },\n }))\n }\n />\n </div>\n ))}\n {availableFields?.length ? (\n <p className=\"break-words text-xs text-text-secondary\">\n {t('bigconsole.widget.customWidget.fieldMapping.available', 'Available: {{fields}}', {\n fields: availableFields.join(', '),\n })}\n </p>\n ) : null}\n </fieldset>\n ) : null}\n </div>\n </Section>\n\n {/* ---------- Approval ---------- */}\n {approveError ? (\n <Section tone=\"danger\" padding=\"compact\" headingLevel={3} title=\"Could not add the widget\">\n <p\n role=\"alert\"\n className=\"break-words text-xs text-[var(--color-status-error-text)]\"\n data-testid=\"custom-widget-approve-error\"\n >\n {approveError}\n </p>\n </Section>\n ) : null}\n\n <div className=\"flex flex-wrap gap-2 border-t border-[var(--color-border-subtle)] pt-3\">\n <Button\n data-testid=\"custom-widget-approve\"\n onClick={handleApprove}\n disabled={!validation.canApprove || isApproving}\n >\n {isApproving ? (\n <Loader2 className=\"mr-2 size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Check className=\"mr-1 size-4\" aria-hidden=\"true\" />\n )}\n {t('bigconsole.widget.customWidget.action.approveAndAdd', 'Approve & add to dashboard')}\n </Button>\n <Button variant=\"outline\" onClick={onClose} data-testid=\"custom-widget-cancel\">\n {t('bigconsole.widget.customWidget.action.cancel', 'Cancel')}\n </Button>\n </div>\n </>\n )}\n </SectionStack>\n </section>\n );\n});\n\nexport default CustomWidgetProposalPanel;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CustomWidgetProposalPanel.js","names":[],"sources":["../../../src/bigconsole/customWidget/CustomWidgetProposalPanel.tsx"],"sourcesContent":["/**\n * Custom-widget proposal review panel (BOFF-5532).\n *\n * The authoring surface for the AI custom-widget builder: prompt in, typed\n * proposal out, reviewed and edited by a human, then approved into a real\n * widget. Nothing is written to the dashboard until the reviewer approves.\n *\n * Deliberate choices:\n * - The preview renders through the *real* widget renderer with the *real*\n * parser output, so what the reviewer approves is what they will get.\n * - Bindings are chosen from the user's permitted sinks and parsers only, and\n * the panel says plainly when a proposal points somewhere they cannot reach.\n * - Every proposal field is editable before approval — the AI proposes, the\n * reviewer disposes.\n */\n\nimport { type FC, memo, useCallback, useEffect, useMemo, useState } from 'react';\nimport { Button, Input, Label, Textarea } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { AlertTriangle, Check, Loader2, RefreshCw, Sparkles, X } from 'lucide-react';\n\nimport type { Widget } from '../types';\nimport { BigConsoleRenderer } from '../renderers/adapters/BigConsoleRenderer';\nimport { WidgetErrorBoundary } from '../components/widgets/WidgetErrorBoundary';\nimport {\n PROPOSABLE_WIDGET_TYPES,\n derivePreviewState,\n validateProposal,\n type CustomWidgetProposal,\n type ProposableWidgetType,\n type ProposalPreviewState,\n} from './proposalContract';\nimport {\n localProposalSource,\n type CustomWidgetProposalSource,\n type ProposalCandidateParser,\n type ProposalCandidateSink,\n} from './proposalSource';\nimport { Section, SectionStack, type SectionTone } from '../components/common';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CustomWidgetProposalPanelProps {\n /** Sinks the user may bind to (already RBAC-filtered by the API). */\n sinks: readonly ProposalCandidateSink[];\n /** Parsers the user may bind to. */\n parsers: readonly ProposalCandidateParser[];\n /**\n * Resolves the rows a binding would produce, for the preview. Should reject\n * (or throw) when the user cannot read the binding, so the panel can show the\n * access-failure state.\n */\n resolvePreviewData: (proposal: CustomWidgetProposal) => Promise<unknown>;\n /** Persists an approved proposal. Resolves once the widget exists. */\n onApprove: (proposal: CustomWidgetProposal) => Promise<void>;\n /** Closes the panel without persisting anything. */\n onClose: () => void;\n /** Swap for the GraphQL-backed source once BOFF-3021's backend half lands. */\n source?: CustomWidgetProposalSource;\n}\n\nconst FIELD_CLASSES = 'w-full';\n\n/**\n * The preview is the thing being judged, so its tone tracks the binding state.\n * The copy itself is translated per render (`previewCopy` below); only the tone\n * ladder is static.\n */\nconst PREVIEW_TONES: Record<ProposalPreviewState, SectionTone> = {\n loading: 'neutral',\n valid: 'neutral',\n 'no-data': 'warning',\n 'field-mismatch': 'warning',\n 'access-error': 'danger',\n};\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/** Builds a throwaway Widget so the preview can use the production renderer. */\nfunction toDraftWidget(proposal: CustomWidgetProposal, untitledLabel: string): Widget {\n const now = new Date(0).toISOString();\n return {\n id: `preview-${proposal.id}`,\n pageId: 'preview',\n type: proposal.widgetType,\n title: proposal.presentation.title || untitledLabel,\n description: proposal.presentation.description,\n dataSinkId: proposal.binding.dataSinkId || null,\n parserId: proposal.binding.parserId || null,\n config: { ...proposal.presentation.config, ...proposal.fieldMapping },\n position: { x: 0, y: 0, width: 6, height: 4 },\n positionX: 0,\n positionY: 0,\n positionWidth: 6,\n positionHeight: 4,\n createdAt: now,\n updatedAt: now,\n } as Widget;\n}\n\n/** Field names present in the resolved preview payload, for mismatch checks. */\nfunction extractFields(data: unknown): string[] | undefined {\n if (Array.isArray(data)) {\n const first = data.find((row) => row && typeof row === 'object');\n return first ? Object.keys(first as Record<string, unknown>) : [];\n }\n if (data && typeof data === 'object') return Object.keys(data as Record<string, unknown>);\n return undefined;\n}\n\nfunction countRows(data: unknown): number {\n if (Array.isArray(data)) return data.length;\n if (data && typeof data === 'object') {\n const cells = (data as { cells?: unknown }).cells;\n if (Array.isArray(cells)) return cells.length;\n return Object.keys(data as Record<string, unknown>).length;\n }\n return 0;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const CustomWidgetProposalPanel: FC<CustomWidgetProposalPanelProps> = memo(function CustomWidgetProposalPanel({\n sinks,\n parsers,\n resolvePreviewData,\n onApprove,\n onClose,\n source = localProposalSource,\n}) {\n const { t } = useI18n();\n\n const previewCopy = useMemo<Record<ProposalPreviewState, { title: string; body: string }>>(\n () => ({\n loading: {\n title: t('bigconsole.widget.customWidget.preview.loading.title', 'Building preview…'),\n body: t(\n 'bigconsole.widget.customWidget.preview.loading.body',\n 'Running the parser against the selected DataSink.'\n ),\n },\n valid: {\n title: t('bigconsole.widget.customWidget.preview.valid.title', 'Preview'),\n body: '',\n },\n 'no-data': {\n title: t('bigconsole.widget.customWidget.preview.noData.title', 'No data returned'),\n body: t(\n 'bigconsole.widget.customWidget.preview.noData.body',\n 'The binding is valid but the parser returned no rows. The widget will render empty until the sink has data.'\n ),\n },\n 'field-mismatch': {\n title: t('bigconsole.widget.customWidget.preview.fieldMismatch.title', 'Field mismatch'),\n body: t(\n 'bigconsole.widget.customWidget.preview.fieldMismatch.body',\n 'Some mapped fields are not present in the parser output. Fix the mapping below before approving.'\n ),\n },\n 'access-error': {\n title: t('bigconsole.widget.customWidget.preview.accessError.title', 'Cannot read this data'),\n body: t(\n 'bigconsole.widget.customWidget.preview.accessError.body',\n 'You do not have access to the proposed DataSink or parser in this workspace.'\n ),\n },\n }),\n [t]\n );\n\n const [prompt, setPrompt] = useState('');\n const [proposal, setProposal] = useState<CustomWidgetProposal | null>(null);\n const [isProposing, setIsProposing] = useState(false);\n const [isApproving, setIsApproving] = useState(false);\n const [approveError, setApproveError] = useState<string | null>(null);\n\n const [previewData, setPreviewData] = useState<unknown>(undefined);\n const [previewLoading, setPreviewLoading] = useState(false);\n const [previewAccessError, setPreviewAccessError] = useState(false);\n\n const permittedDataSinkIds = useMemo(() => sinks.map((sink) => sink.id), [sinks]);\n const permittedParserIds = useMemo(() => parsers.map((parser) => parser.id), [parsers]);\n const availableFields = useMemo(() => extractFields(previewData), [previewData]);\n\n const validation = useMemo(\n () =>\n proposal\n ? validateProposal(proposal, { permittedDataSinkIds, permittedParserIds, availableFields })\n : { issues: [], canApprove: false },\n [proposal, permittedDataSinkIds, permittedParserIds, availableFields]\n );\n\n const previewState = useMemo(\n () =>\n derivePreviewState(validation, {\n loading: previewLoading,\n accessError: previewAccessError,\n rowCount: countRows(previewData),\n }),\n [validation, previewLoading, previewAccessError, previewData]\n );\n\n // Re-resolve the preview whenever the binding changes.\n const bindingKey = proposal\n ? `${proposal.binding.dataSinkId}|${proposal.binding.parserId ?? ''}|${proposal.binding.dataPath ?? ''}`\n : '';\n\n useEffect(() => {\n if (!proposal || !proposal.binding.dataSinkId) {\n setPreviewData(undefined);\n return;\n }\n let cancelled = false;\n setPreviewLoading(true);\n setPreviewAccessError(false);\n\n resolvePreviewData(proposal)\n .then((data) => {\n if (cancelled) return;\n setPreviewData(data);\n })\n .catch(() => {\n if (cancelled) return;\n setPreviewData(undefined);\n setPreviewAccessError(true);\n })\n .finally(() => {\n if (!cancelled) setPreviewLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n // `bindingKey` captures every input that changes the resolved data.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [bindingKey, resolvePreviewData]);\n\n const handlePropose = useCallback(async () => {\n if (!prompt.trim()) return;\n setIsProposing(true);\n setApproveError(null);\n try {\n const next = await source.propose({ prompt: prompt.trim(), sinks, parsers });\n setProposal(next);\n } finally {\n setIsProposing(false);\n }\n }, [prompt, source, sinks, parsers]);\n\n const patchProposal = useCallback((patch: (current: CustomWidgetProposal) => CustomWidgetProposal) => {\n setProposal((current) => (current ? patch(current) : current));\n }, []);\n\n const handleReject = useCallback(() => {\n setProposal(null);\n setPreviewData(undefined);\n setPreviewAccessError(false);\n setApproveError(null);\n }, []);\n\n const handleApprove = useCallback(async () => {\n if (!proposal || !validation.canApprove) return;\n setIsApproving(true);\n setApproveError(null);\n try {\n await onApprove(proposal);\n onClose();\n } catch (error) {\n setApproveError(\n error instanceof Error\n ? error.message\n : t('bigconsole.widget.customWidget.error.couldNotSaveWidget', 'Could not save the widget.')\n );\n } finally {\n setIsApproving(false);\n }\n }, [proposal, validation.canApprove, onApprove, onClose, t]);\n\n const errors = validation.issues.filter((issue) => issue.severity === 'error');\n const warnings = validation.issues.filter((issue) => issue.severity === 'warning');\n\n return (\n <section\n data-testid=\"custom-widget-proposal-panel\"\n aria-label={t('bigconsole.widget.customWidget.panel.ariaLabel', 'AI custom widget builder')}\n className=\"flex h-full w-full min-w-0 flex-col overflow-y-auto p-4\"\n >\n {/*\n Five stages — prompt → preview → validation → editable proposal →\n approval — used to be five bare divs separated by one uniform gap. Each\n is now a labelled section, and the preview (the thing the reviewer is\n actually judging) is the panel's single `primary` block (BOFF-5636).\n */}\n <SectionStack>\n {/* ---------- Prompt ---------- */}\n <Section\n title={t('bigconsole.widget.customWidget.prompt.label', 'Describe the widget you need')}\n description=\"Plain English. The assistant proposes a typed widget you can edit before anything is saved.\"\n icon={<Sparkles className=\"size-4\" aria-hidden=\"true\" />}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <Button\n data-testid=\"custom-widget-generate\"\n onClick={handlePropose}\n disabled={!prompt.trim() || isProposing}\n >\n {isProposing ? <Loader2 className=\"mr-2 size-4 animate-spin\" aria-hidden=\"true\" /> : null}\n {proposal\n ? t('bigconsole.widget.customWidget.action.regenerate', 'Regenerate')\n : t('bigconsole.widget.customWidget.action.generateProposal', 'Generate proposal')}\n </Button>\n {proposal ? (\n <Button variant=\"outline\" data-testid=\"custom-widget-reject\" onClick={handleReject}>\n <X className=\"mr-1 size-4\" aria-hidden=\"true\" />\n {t('bigconsole.widget.customWidget.action.reject', 'Reject')}\n </Button>\n ) : null}\n </div>\n }\n >\n <div className=\"flex flex-col gap-2\">\n <Label htmlFor=\"custom-widget-prompt\">What should it show?</Label>\n <Textarea\n id=\"custom-widget-prompt\"\n data-testid=\"custom-widget-prompt\"\n value={prompt}\n onChange={(event) => setPrompt(event.target.value)}\n placeholder={t(\n 'bigconsole.widget.customWidget.prompt.placeholder',\n 'e.g. a funnel of signup conversion stages'\n )}\n rows={3}\n />\n </div>\n </Section>\n\n {!proposal ? (\n <Section emphasis=\"muted\" padding=\"compact\" title=\"No proposal yet\" headingLevel={3}>\n <p className=\"text-sm text-text-secondary\" data-testid=\"custom-widget-empty\">\n {t(\n 'bigconsole.widget.customWidget.empty',\n 'No proposal yet. Describe a widget and the assistant will propose one for you to review — nothing is added to the dashboard until you approve it.'\n )}\n </p>\n </Section>\n ) : (\n <>\n {/* ---------- Preview ---------- */}\n <Section\n emphasis=\"primary\"\n tone={PREVIEW_TONES[previewState]}\n title={previewCopy[previewState].title}\n description=\"Rendered by the real widget renderer against the real parser output — what you approve is what you get.\"\n meta={\n <span\n data-testid=\"custom-widget-preview-state\"\n data-state={previewState}\n className=\"rounded-pill bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {previewState}\n </span>\n }\n >\n <div\n data-testid=\"custom-widget-preview\"\n className=\"min-h-[220px] min-w-0 rounded-card border border-[var(--color-border-subtle)] bg-bg-surface p-2\"\n >\n {previewState === 'loading' ? (\n <div className=\"flex h-[200px] items-center justify-center text-text-secondary\">\n <Loader2 className=\"mr-2 size-4 animate-spin\" aria-hidden=\"true\" />\n <span className=\"text-sm\">{previewCopy.loading.body}</span>\n </div>\n ) : previewState === 'valid' ? (\n <div className=\"h-[200px]\">\n <WidgetErrorBoundary widgetId={`preview-${proposal.id}`}>\n <BigConsoleRenderer\n widget={toDraftWidget(\n proposal,\n t('bigconsole.widget.customWidget.untitledWidget', 'Untitled widget')\n )}\n data={previewData as Record<string, unknown>}\n />\n </WidgetErrorBoundary>\n </div>\n ) : (\n <div className=\"flex h-[200px] flex-col items-center justify-center gap-2 px-4 text-center\">\n <AlertTriangle className=\"size-5 text-[var(--color-status-warning-text)]\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-secondary\">{previewCopy[previewState].body}</p>\n {previewState === 'access-error' ? (\n <Button size=\"sm\" variant=\"outline\" onClick={() => patchProposal((current) => ({ ...current }))}>\n <RefreshCw className=\"mr-1 size-3.5\" aria-hidden=\"true\" />\n {t('bigconsole.widget.customWidget.action.retry', 'Retry')}\n </Button>\n ) : null}\n </div>\n )}\n </div>\n\n {proposal.rationale ? (\n <p className=\"mt-2 text-xs text-text-secondary\" data-testid=\"custom-widget-rationale\">\n {proposal.rationale}\n </p>\n ) : null}\n </Section>\n\n {/* ---------- Validation ---------- */}\n {errors.length > 0 || warnings.length > 0 ? (\n <Section\n tone={errors.length > 0 ? 'danger' : 'warning'}\n padding=\"compact\"\n headingLevel={3}\n title={\n errors.length > 0\n ? `${errors.length} ${errors.length === 1 ? 'problem' : 'problems'} to fix before approving`\n : `${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`\n }\n >\n <ul data-testid=\"custom-widget-issues\" className=\"flex min-w-0 flex-col gap-1\">\n {[...errors, ...warnings].map((issue) => (\n <li\n key={`${issue.code}-${issue.path ?? ''}`}\n data-severity={issue.severity}\n className={`break-words text-xs ${\n issue.severity === 'error'\n ? 'text-[var(--color-status-error-text)]'\n : 'text-[var(--color-status-warning-text)]'\n }`}\n >\n {issue.message}\n </li>\n ))}\n </ul>\n </Section>\n ) : null}\n\n {/* ---------- Editable proposal ---------- */}\n <Section\n title={t('bigconsole.widget.customWidget.proposal.heading', 'Proposal')}\n description=\"Every field is editable — the assistant proposes, you dispose.\"\n data-testid=\"custom-widget-proposal-fields\"\n >\n <div className=\"flex flex-col gap-3\">\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-title\">\n {t('bigconsole.widget.customWidget.field.title', 'Title')}\n </Label>\n <Input\n id=\"custom-widget-title\"\n data-testid=\"custom-widget-title\"\n className={FIELD_CLASSES}\n value={proposal.presentation.title}\n onChange={(event) =>\n patchProposal((current) => ({\n ...current,\n presentation: { ...current.presentation, title: event.target.value },\n }))\n }\n />\n </div>\n\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-type\">\n {t('bigconsole.widget.customWidget.field.widgetType', 'Widget type')}\n </Label>\n <select\n id=\"custom-widget-type\"\n data-testid=\"custom-widget-type\"\n value={proposal.widgetType}\n onChange={(event) =>\n patchProposal((current) => ({\n ...current,\n widgetType: event.target.value as ProposableWidgetType,\n }))\n }\n className=\"w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n {PROPOSABLE_WIDGET_TYPES.map((type) => (\n <option key={type} value={type}>\n {type}\n </option>\n ))}\n </select>\n </div>\n\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-sink\">\n {t('bigconsole.widget.customWidget.field.dataSink', 'DataSink')}\n </Label>\n <select\n id=\"custom-widget-sink\"\n data-testid=\"custom-widget-sink\"\n value={proposal.binding.dataSinkId}\n onChange={(event) => {\n const sink = sinks.find((candidate) => candidate.id === event.target.value);\n patchProposal((current) => ({\n ...current,\n binding: { ...current.binding, dataSinkId: event.target.value, dataSinkKey: sink?.key },\n }));\n }}\n className=\"w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"\">\n {t('bigconsole.widget.customWidget.field.selectDataSink', 'Select a DataSink…')}\n </option>\n {sinks.map((sink) => (\n <option key={sink.id} value={sink.id}>\n {sink.name || sink.key}\n </option>\n ))}\n </select>\n </div>\n\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"custom-widget-parser\">\n {t('bigconsole.widget.customWidget.field.parser', 'Parser')}\n </Label>\n <select\n id=\"custom-widget-parser\"\n data-testid=\"custom-widget-parser\"\n value={proposal.binding.parserId ?? ''}\n onChange={(event) => {\n const parser = parsers.find((candidate) => candidate.id === event.target.value);\n patchProposal((current) => ({\n ...current,\n binding: {\n ...current.binding,\n parserId: event.target.value || undefined,\n parserName: parser?.name,\n },\n }));\n }}\n className=\"w-full rounded-md border border-[var(--color-border-default)] bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"\">{t('bigconsole.widget.customWidget.field.noParser', 'No parser')}</option>\n {parsers.map((parser) => (\n <option key={parser.id} value={parser.id}>\n {parser.name}\n </option>\n ))}\n </select>\n </div>\n\n {/* Field mapping */}\n {Object.keys(proposal.fieldMapping).length > 0 ? (\n <fieldset className=\"flex flex-col gap-2\" data-testid=\"custom-widget-field-mapping\">\n <legend className=\"text-sm font-medium text-text-primary\">\n {t('bigconsole.widget.customWidget.fieldMapping.legend', 'Field mapping')}\n </legend>\n {Object.entries(proposal.fieldMapping).map(([role, field]) => (\n <div key={role} className=\"flex flex-col gap-1\">\n <Label htmlFor={`custom-widget-field-${role}`}>{role}</Label>\n <Input\n id={`custom-widget-field-${role}`}\n data-testid={`custom-widget-field-${role}`}\n value={field}\n onChange={(event) =>\n patchProposal((current) => ({\n ...current,\n fieldMapping: { ...current.fieldMapping, [role]: event.target.value },\n }))\n }\n />\n </div>\n ))}\n {availableFields?.length ? (\n <p className=\"break-words text-xs text-text-secondary\">\n {t('bigconsole.widget.customWidget.fieldMapping.available', 'Available: {{fields}}', {\n fields: availableFields.join(', '),\n })}\n </p>\n ) : null}\n </fieldset>\n ) : null}\n </div>\n </Section>\n\n {/* ---------- Approval ---------- */}\n {approveError ? (\n <Section tone=\"danger\" padding=\"compact\" headingLevel={3} title=\"Could not add the widget\">\n <p\n role=\"alert\"\n className=\"break-words text-xs text-[var(--color-status-error-text)]\"\n data-testid=\"custom-widget-approve-error\"\n >\n {approveError}\n </p>\n </Section>\n ) : null}\n\n <div className=\"flex flex-wrap gap-2 border-t border-[var(--color-border-subtle)] pt-3\">\n <Button\n data-testid=\"custom-widget-approve\"\n onClick={handleApprove}\n disabled={!validation.canApprove || isApproving}\n >\n {isApproving ? (\n <Loader2 className=\"mr-2 size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Check className=\"mr-1 size-4\" aria-hidden=\"true\" />\n )}\n {t('bigconsole.widget.customWidget.action.approveAndAdd', 'Approve & add to dashboard')}\n </Button>\n <Button variant=\"outline\" onClick={onClose} data-testid=\"custom-widget-cancel\">\n {t('bigconsole.widget.customWidget.action.cancel', 'Cancel')}\n </Button>\n </div>\n </>\n )}\n </SectionStack>\n </section>\n );\n});\n\nexport default CustomWidgetProposalPanel;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LayoutItem } from '../utils/layoutBaseline';
|
|
2
|
+
|
|
3
|
+
export interface DashboardLayoutBaseline {
|
|
4
|
+
/** The board's positions differ from the last loaded or saved ones, because a person placed something. */
|
|
5
|
+
dirty: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Re-take the baseline after the positions reached the server. `sent` is exactly
|
|
8
|
+
* what was sent; `editRevisionAtSave` is `layoutEditRevision` read BEFORE the
|
|
9
|
+
* request began, so a placement made while it was in flight stays dirty.
|
|
10
|
+
* Call it on success only: a failed save must leave the layout dirty.
|
|
11
|
+
*/
|
|
12
|
+
markSaved: (sent: ReadonlyArray<LayoutItem>, editRevisionAtSave: number) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function useDashboardLayoutBaseline(dashboardId: string | undefined): DashboardLayoutBaseline;
|
|
15
|
+
//# sourceMappingURL=useDashboardLayoutBaseline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDashboardLayoutBaseline.d.ts","sourceRoot":"","sources":["../../../src/bigconsole/hooks/useDashboardLayoutBaseline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,uBAAuB;IACtC,0GAA0G;IAC1G,KAAK,EAAE,OAAO,CAAC;IACf;;;;;OAKG;IACH,SAAS,EAAE,CAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,EAAE,kBAAkB,EAAE,MAAM,KAAK,IAAI,CAAC;CAClF;AAED,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,uBAAuB,CA0BnG"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useWidgetStore as e } from "../store/widgetStore.js";
|
|
2
|
+
import "../store/index.js";
|
|
3
|
+
import { INITIAL_LAYOUT_BASELINE as t, isLayoutBaselineDirty as n, layoutFromWidgets as r, normaliseLayout as i, reduceLayoutBaseline as a } from "../utils/layoutBaseline.js";
|
|
4
|
+
import { useCallback as o, useEffect as s, useMemo as c, useReducer as l, useState as u } from "react";
|
|
5
|
+
//#region src/bigconsole/hooks/useDashboardLayoutBaseline.ts
|
|
6
|
+
function d(d) {
|
|
7
|
+
let f = e((e) => e.widgets), p = e((e) => e.layoutEditRevision), [m, h] = l(a, t), g = c(() => r(f.values()), [f]), [_, v] = u(d);
|
|
8
|
+
_ !== d && (v(d), h({ type: "reset" })), s(() => {
|
|
9
|
+
h({
|
|
10
|
+
type: "observe",
|
|
11
|
+
layout: g,
|
|
12
|
+
editRevision: p
|
|
13
|
+
});
|
|
14
|
+
}, [
|
|
15
|
+
d,
|
|
16
|
+
g,
|
|
17
|
+
p
|
|
18
|
+
]);
|
|
19
|
+
let y = o((e, t) => {
|
|
20
|
+
h({
|
|
21
|
+
type: "saved",
|
|
22
|
+
layout: i(e),
|
|
23
|
+
editRevision: t
|
|
24
|
+
});
|
|
25
|
+
}, []);
|
|
26
|
+
return {
|
|
27
|
+
dirty: n(m, g),
|
|
28
|
+
markSaved: y
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { d as useDashboardLayoutBaseline };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=useDashboardLayoutBaseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDashboardLayoutBaseline.js","names":[],"sources":["../../../src/bigconsole/hooks/useDashboardLayoutBaseline.ts"],"sourcesContent":["/**\n * useDashboardLayoutBaseline (BOFF-8060, N3/N4: the dashboard leave guard)\n *\n * The React shell around `utils/layoutBaseline.ts`: \"does the dashboard builder\n * hold moved or resized widgets that are not saved yet?\". The rules, and why a\n * layout event alone is not a dirty signal, are documented there; this file only\n * feeds the reducer from the widget store and exposes the answer.\n *\n * ★ It reads the store, never the grid. The canvas does not hand react-grid-layout\n * an `onLayoutChange` (`DashboardGrid` wires `onDragStop` / `onResizeStop` only,\n * with `compactType={null}`), so mount and compaction events never reach the store\n * at all; what does reach it is load, create, delete, save write-back (all\n * absorbed while the baseline floats) and the person's placements (which freeze\n * it, through `layoutEditRevision`).\n */\n\nimport { useCallback, useEffect, useMemo, useReducer, useState } from 'react';\nimport { useWidgetStore } from '../store';\nimport {\n INITIAL_LAYOUT_BASELINE,\n isLayoutBaselineDirty,\n layoutFromWidgets,\n normaliseLayout,\n reduceLayoutBaseline,\n type LayoutItem,\n} from '../utils/layoutBaseline';\n\nexport interface DashboardLayoutBaseline {\n /** The board's positions differ from the last loaded or saved ones, because a person placed something. */\n dirty: boolean;\n /**\n * Re-take the baseline after the positions reached the server. `sent` is exactly\n * what was sent; `editRevisionAtSave` is `layoutEditRevision` read BEFORE the\n * request began, so a placement made while it was in flight stays dirty.\n * Call it on success only: a failed save must leave the layout dirty.\n */\n markSaved: (sent: ReadonlyArray<LayoutItem>, editRevisionAtSave: number) => void;\n}\n\nexport function useDashboardLayoutBaseline(dashboardId: string | undefined): DashboardLayoutBaseline {\n const widgets = useWidgetStore((state) => state.widgets);\n const editRevision = useWidgetStore((state) => state.layoutEditRevision);\n const [baseline, dispatch] = useReducer(reduceLayoutBaseline, INITIAL_LAYOUT_BASELINE);\n\n const layout = useMemo(() => layoutFromWidgets(widgets.values()), [widgets]);\n\n // A different dashboard on the same mounted page (the route param changed):\n // forget the old baseline in the SAME render, so the old dashboard's frozen\n // baseline is never compared against the new one's widgets. This is React's\n // \"adjust state when a prop changes\" pattern; the effect below then re-seeds it.\n const [trackedDashboardId, setTrackedDashboardId] = useState(dashboardId);\n if (trackedDashboardId !== dashboardId) {\n setTrackedDashboardId(dashboardId);\n dispatch({ type: 'reset' });\n }\n\n useEffect(() => {\n dispatch({ type: 'observe', layout, editRevision });\n }, [dashboardId, layout, editRevision]);\n\n const markSaved = useCallback((sent: ReadonlyArray<LayoutItem>, editRevisionAtSave: number) => {\n dispatch({ type: 'saved', layout: normaliseLayout(sent), editRevision: editRevisionAtSave });\n }, []);\n\n return { dirty: isLayoutBaselineDirty(baseline, layout), markSaved };\n}\n"],"mappings":";;;;;AAuCA,SAAgB,EAA2B,GAA0D;CACnG,IAAM,IAAU,GAAgB,MAAU,EAAM,QAAQ,EAClD,IAAe,GAAgB,MAAU,EAAM,mBAAmB,EAClE,CAAC,GAAU,KAAY,EAAW,GAAsB,EAAwB,EAEhF,IAAS,QAAc,EAAkB,EAAQ,QAAQ,CAAC,EAAE,CAAC,EAAQ,CAAC,EAMtE,CAAC,GAAoB,KAAyB,EAAS,EAAY;AAMzE,CALI,MAAuB,MACzB,EAAsB,EAAY,EAClC,EAAS,EAAE,MAAM,SAAS,CAAC,GAG7B,QAAgB;AACd,IAAS;GAAE,MAAM;GAAW;GAAQ;GAAc,CAAC;IAClD;EAAC;EAAa;EAAQ;EAAa,CAAC;CAEvC,IAAM,IAAY,GAAa,GAAiC,MAA+B;AAC7F,IAAS;GAAE,MAAM;GAAS,QAAQ,EAAgB,EAAK;GAAE,cAAc;GAAoB,CAAC;IAC3F,EAAE,CAAC;AAEN,QAAO;EAAE,OAAO,EAAsB,GAAU,EAAO;EAAE;EAAW"}
|