@burdenoff/microfe-bigconsole 2026.717.2 → 2026.717.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.
@@ -9,17 +9,117 @@ function i(e) {
9
9
  default: return "bg-bg-sunken text-text-secondary";
10
10
  }
11
11
  }
12
- var a = e(function({ widget: e, data: a, onClick: o }) {
13
- let s = t(() => a?.columns || [], [a?.columns]), c = t(() => a?.tasks || [], [a?.tasks]), l = t(() => {
12
+ var a = [
13
+ "status",
14
+ "state",
15
+ "column",
16
+ "stage",
17
+ "category",
18
+ "phase",
19
+ "group"
20
+ ], o = [
21
+ "title",
22
+ "name",
23
+ "summary",
24
+ "label",
25
+ "identifier",
26
+ "id"
27
+ ];
28
+ function s(e) {
29
+ return typeof e == "string" && e.length > 0 ? e : void 0;
30
+ }
31
+ function c(e) {
32
+ let t = (e) => e.filter((e) => !!e && typeof e == "object" && !Array.isArray(e));
33
+ if (Array.isArray(e)) return t(e);
34
+ if (e && typeof e == "object") {
35
+ let n = e;
36
+ for (let e of [
37
+ "tasks",
38
+ "rows",
39
+ "items",
40
+ "data",
41
+ "nodes",
42
+ "results",
43
+ "records"
44
+ ]) if (Array.isArray(n[e])) return t(n[e]);
45
+ let r = Object.values(n).find((e) => Array.isArray(e));
46
+ if (Array.isArray(r)) return t(r);
47
+ }
48
+ return [];
49
+ }
50
+ function l(e, t) {
51
+ for (let n of t) if (n && n in e) return n;
52
+ }
53
+ function u(e) {
54
+ let t = s(e)?.toLowerCase();
55
+ if (t) {
56
+ if (t === "urgent" || t === "high") return "high";
57
+ if (t === "medium" || t === "normal") return "medium";
58
+ if (t === "low") return "low";
59
+ }
60
+ }
61
+ function d(e, t) {
62
+ if (e && typeof e == "object" && !Array.isArray(e) && Array.isArray(e.columns)) {
63
+ let t = e;
64
+ return {
65
+ columns: t.columns ?? [],
66
+ tasks: t.tasks ?? []
67
+ };
68
+ }
69
+ let n = c(e);
70
+ if (n.length === 0) return {
71
+ columns: [],
72
+ tasks: []
73
+ };
74
+ let r = n[0], i = l(r, [
75
+ s(t?.columnField),
76
+ s(t?.groupField),
77
+ s(t?.statusField),
78
+ ...a,
79
+ s(t?.xAxisField),
80
+ s(t?.labelField)
81
+ ]);
82
+ if (!i) return {
83
+ columns: [],
84
+ tasks: []
85
+ };
86
+ let d = l(r, [
87
+ s(t?.cardTitleField),
88
+ s(t?.titleField),
89
+ ...o,
90
+ s(t?.labelField)
91
+ ]), f = {}, p = [], m = n.map((e, t) => {
92
+ let n = e[i], r = n == null || n === "" ? "Uncategorized" : String(n);
93
+ r in f || (f[r] = r, p.push(r));
94
+ let a = e.id ?? e.identifier ?? e.key, o = Array.isArray(e.labels) ? e.labels.filter((e) => typeof e == "string") : void 0;
95
+ return {
96
+ id: a == null ? `task-${t}` : String(a),
97
+ title: d && e[d] != null ? String(e[d]) : `Item ${t + 1}`,
98
+ column: r,
99
+ assignee: s(e.assignee),
100
+ priority: u(e.priority ?? e.priorityLabel),
101
+ labels: o && o.length > 0 ? o : void 0
102
+ };
103
+ });
104
+ return {
105
+ columns: p.map((e) => ({
106
+ id: e,
107
+ title: f[e] ?? e
108
+ })),
109
+ tasks: m
110
+ };
111
+ }
112
+ var f = e(function({ widget: e, data: a, onClick: o }) {
113
+ let s = e.config, { columns: c, tasks: l } = t(() => d(a, s), [a, s]), u = t(() => {
14
114
  let e = {};
15
- return s.forEach((t) => {
16
- e[t.id] = c.filter((e) => e.column === t.id);
115
+ return c.forEach((t) => {
116
+ e[t.id] = l.filter((e) => e.column === t.id);
17
117
  }), e;
18
- }, [s, c]);
19
- return s.length ? /* @__PURE__ */ n("div", {
118
+ }, [c, l]);
119
+ return c.length ? /* @__PURE__ */ n("div", {
20
120
  className: "flex h-full p-2 gap-2 overflow-x-auto",
21
- children: s.map((e) => {
22
- let t = l[e.id] || [];
121
+ children: c.map((e) => {
122
+ let t = u[e.id] || [];
23
123
  return /* @__PURE__ */ r("div", {
24
124
  className: "flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg",
25
125
  children: [/* @__PURE__ */ r("div", {
@@ -87,6 +187,6 @@ var a = e(function({ widget: e, data: a, onClick: o }) {
87
187
  });
88
188
  });
89
189
  //#endregion
90
- export { a as default };
190
+ export { f as default };
91
191
 
92
192
  //# sourceMappingURL=KanbanWidget.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"KanbanWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanWidget.tsx"],"sourcesContent":["/**\n * KanbanWidget Component\n *\n * Simple kanban board view with columns and tasks.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KanbanWidgetProps {\n widget: Widget;\n data?: KanbanData;\n onClick?: (task: KanbanTask) => void;\n}\n\nexport interface KanbanColumn {\n id: string;\n title: string;\n color?: string;\n}\n\nexport interface KanbanTask {\n id: string;\n title: string;\n column: string;\n priority?: 'high' | 'medium' | 'low';\n assignee?: string;\n labels?: string[];\n dueDate?: string;\n}\n\nexport interface KanbanData {\n columns?: KanbanColumn[];\n tasks?: KanbanTask[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getPriorityColor(priority?: string): string {\n switch (priority) {\n case 'high':\n return 'bg-state-error/20 text-state-error';\n case 'medium':\n return 'bg-state-warning/20 text-state-warning';\n case 'low':\n return 'bg-state-success/20 text-state-success';\n default:\n return 'bg-bg-sunken text-text-secondary';\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KanbanWidget: FC<KanbanWidgetProps> = memo(function KanbanWidget({ widget: _widget, data, onClick }) {\n const columns = useMemo(() => data?.columns || [], [data?.columns]);\n const tasks = useMemo(() => data?.tasks || [], [data?.tasks]);\n\n // Group tasks by column\n const tasksByColumn = useMemo(() => {\n const grouped: Record<string, KanbanTask[]> = {};\n columns.forEach((col) => {\n grouped[col.id] = tasks.filter((t) => t.column === col.id);\n });\n return grouped;\n }, [columns, tasks]);\n\n if (!columns.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2\"\n />\n </svg>\n <p>No board data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex h-full p-2 gap-2 overflow-x-auto\">\n {columns.map((column) => {\n const columnTasks = tasksByColumn[column.id] || [];\n\n return (\n <div key={column.id} className=\"flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg\">\n {/* Column Header */}\n <div className=\"flex items-center gap-2 p-2 border-b border-border-default\">\n <div\n className=\"w-2 h-2 rounded-full\"\n style={{ backgroundColor: column.color || 'var(--color-text-muted)' }}\n />\n <span className=\"text-xs font-medium text-text-primary truncate\">{column.title}</span>\n <span className=\"ml-auto text-xs text-text-tertiary\">{columnTasks.length}</span>\n </div>\n\n {/* Tasks */}\n <div className=\"flex-1 p-2 space-y-2 overflow-y-auto\">\n {columnTasks.map((task) => (\n <div\n key={task.id}\n className={`\n p-2 bg-bg-surface rounded border border-border-default\n ${onClick ? 'cursor-pointer hover:border-action-primary-bg' : ''}\n transition-colors\n `}\n onClick={() => onClick?.(task)}\n >\n <p className=\"text-xs font-medium text-text-primary line-clamp-2\">{task.title}</p>\n\n <div className=\"flex items-center gap-1 mt-2 flex-wrap\">\n {task.priority && (\n <span className={`px-1.5 py-0.5 text-[10px] rounded ${getPriorityColor(task.priority)}`}>\n {task.priority}\n </span>\n )}\n {task.assignee && (\n <span className=\"px-1.5 py-0.5 text-[10px] rounded bg-action-primary-bg/10 text-action-primary-text\">\n {task.assignee}\n </span>\n )}\n </div>\n </div>\n ))}\n\n {columnTasks.length === 0 && <p className=\"text-[10px] text-text-tertiary text-center py-4\">No tasks</p>}\n </div>\n </div>\n );\n })}\n </div>\n );\n});\n\nexport default KanbanWidget;\n"],"mappings":";;;AA4CA,SAAS,EAAiB,GAA2B;AACnD,SAAQ,GAAR;EACE,KAAK,OACH,QAAO;EACT,KAAK,SACH,QAAO;EACT,KAAK,MACH,QAAO;EACT,QACE,QAAO;;;AAQb,IAAa,IAAsC,EAAK,SAAsB,EAAE,QAAQ,GAAS,SAAM,cAAW;CAChH,IAAM,IAAU,QAAc,GAAM,WAAW,EAAE,EAAE,CAAC,GAAM,QAAQ,CAAC,EAC7D,IAAQ,QAAc,GAAM,SAAS,EAAE,EAAE,CAAC,GAAM,MAAM,CAAC,EAGvD,IAAgB,QAAc;EAClC,IAAM,IAAwC,EAAE;AAIhD,SAHA,EAAQ,SAAS,MAAQ;AACvB,KAAQ,EAAI,MAAM,EAAM,QAAQ,MAAM,EAAE,WAAW,EAAI,GAAG;IAC1D,EACK;IACN,CAAC,GAAS,EAAM,CAAC;AAyBpB,QAvBK,EAAQ,SAwBX,kBAAC,OAAD;EAAK,WAAU;YACZ,EAAQ,KAAK,MAAW;GACvB,IAAM,IAAc,EAAc,EAAO,OAAO,EAAE;AAElD,UACE,kBAAC,OAAD;IAAqB,WAAU;cAA/B,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,iBAAiB,EAAO,SAAS,2BAA2B;OACrE,CAAA;MACF,kBAAC,QAAD;OAAM,WAAU;iBAAkD,EAAO;OAAa,CAAA;MACtF,kBAAC,QAAD;OAAM,WAAU;iBAAsC,EAAY;OAAc,CAAA;MAC5E;QAGN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,EAAY,KAAK,MAChB,kBAAC,OAAD;MAEE,WAAW;;wBAEL,IAAU,kDAAkD,GAAG;;;MAGrE,eAAe,IAAU,EAAK;gBAPhC,CASE,kBAAC,KAAD;OAAG,WAAU;iBAAsD,EAAK;OAAU,CAAA,EAElF,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,EAAK,YACJ,kBAAC,QAAD;QAAM,WAAW,qCAAqC,EAAiB,EAAK,SAAS;kBAClF,EAAK;QACD,CAAA,EAER,EAAK,YACJ,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAK;QACD,CAAA,CAEL;SACF;QAtBC,EAAK,GAsBN,CACN,EAED,EAAY,WAAW,KAAK,kBAAC,KAAD;MAAG,WAAU;gBAAkD;MAAY,CAAA,CACpG;OACF;MA1CI,EAAO,GA0CX;IAER;EACE,CAAA,GAxEJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IACE,WAAU;IACV,MAAK;IACL,QAAO;IACP,SAAQ;cAER,kBAAC,QAAD;KACE,eAAc;KACd,gBAAe;KACf,aAAa;KACb,GAAE;KACF,CAAA;IACE,CAAA,EACN,kBAAC,KAAD,EAAA,UAAG,iBAAiB,CAAA,CAChB;;EACF,CAAA;EAyDV"}
1
+ {"version":3,"file":"KanbanWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanWidget.tsx"],"sourcesContent":["/**\n * KanbanWidget Component\n *\n * Simple kanban board view with columns and tasks.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KanbanWidgetProps {\n widget: Widget;\n data?: KanbanData;\n onClick?: (task: KanbanTask) => void;\n}\n\nexport interface KanbanColumn {\n id: string;\n title: string;\n color?: string;\n}\n\nexport interface KanbanTask {\n id: string;\n title: string;\n column: string;\n priority?: 'high' | 'medium' | 'low';\n assignee?: string;\n labels?: string[];\n dueDate?: string;\n}\n\nexport interface KanbanData {\n columns?: KanbanColumn[];\n tasks?: KanbanTask[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getPriorityColor(priority?: string): string {\n switch (priority) {\n case 'high':\n return 'bg-state-error/20 text-state-error';\n case 'medium':\n return 'bg-state-warning/20 text-state-warning';\n case 'low':\n return 'bg-state-success/20 text-state-success';\n default:\n return 'bg-bg-sunken text-text-secondary';\n }\n}\n\n// Field names we try, in order, when the widget config doesn't name one\n// explicitly. A DataSink/parser typically emits a flat array of rows (e.g. the\n// Linear \"Kanban board\" parser -> [{ id, title, status, assignee, ... }]), so\n// the board is derived from a status/column field on those rows.\nconst COLUMN_FIELD_CANDIDATES = ['status', 'state', 'column', 'stage', 'category', 'phase', 'group'];\nconst TITLE_FIELD_CANDIDATES = ['title', 'name', 'summary', 'label', 'identifier', 'id'];\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/** Coerce parser output into an array of object rows (handles common envelopes). */\nfunction asRows(data: unknown): Record<string, unknown>[] {\n const onlyObjects = (arr: unknown[]): Record<string, unknown>[] =>\n arr.filter((r): r is Record<string, unknown> => !!r && typeof r === 'object' && !Array.isArray(r));\n\n if (Array.isArray(data)) return onlyObjects(data);\n\n if (data && typeof data === 'object') {\n const obj = data as Record<string, unknown>;\n for (const key of ['tasks', 'rows', 'items', 'data', 'nodes', 'results', 'records']) {\n if (Array.isArray(obj[key])) return onlyObjects(obj[key] as unknown[]);\n }\n const firstArray = Object.values(obj).find((v) => Array.isArray(v));\n if (Array.isArray(firstArray)) return onlyObjects(firstArray);\n }\n return [];\n}\n\n/** First key that actually exists on the sample row. */\nfunction firstPresent(row: Record<string, unknown>, keys: (string | undefined)[]): string | undefined {\n for (const key of keys) {\n if (key && key in row) return key;\n }\n return undefined;\n}\n\nfunction normalizePriority(value: unknown): KanbanTask['priority'] {\n const v = asString(value)?.toLowerCase();\n if (!v) return undefined;\n if (v === 'urgent' || v === 'high') return 'high';\n if (v === 'medium' || v === 'normal') return 'medium';\n if (v === 'low') return 'low';\n return undefined;\n}\n\n/**\n * Normalize widget data into the board's { columns, tasks } shape.\n *\n * KanbanWidget previously required the exact canonical shape and rendered\n * \"No board data\" for any DataSink/parser output that was a plain array of\n * rows (which is what parsers actually emit) — BOFF-4110. This adapts a flat\n * array into columns (distinct values of a status/column field, in first-seen\n * order) plus one card per row, honoring config field mappings with fallbacks.\n */\nfunction normalizeKanban(\n data: unknown,\n config?: Record<string, unknown>\n): { columns: KanbanColumn[]; tasks: KanbanTask[] } {\n // Canonical shape passes through untouched (back-compat).\n if (data && typeof data === 'object' && !Array.isArray(data) && Array.isArray((data as KanbanData).columns)) {\n const d = data as KanbanData;\n return { columns: d.columns ?? [], tasks: d.tasks ?? [] };\n }\n\n const rows = asRows(data);\n if (rows.length === 0) return { columns: [], tasks: [] };\n\n const sample = rows[0];\n const columnField = firstPresent(sample, [\n asString(config?.columnField),\n asString(config?.groupField),\n asString(config?.statusField),\n ...COLUMN_FIELD_CANDIDATES,\n asString(config?.xAxisField),\n asString(config?.labelField),\n ]);\n if (!columnField) return { columns: [], tasks: [] };\n\n const titleField = firstPresent(sample, [\n asString(config?.cardTitleField),\n asString(config?.titleField),\n ...TITLE_FIELD_CANDIDATES,\n asString(config?.labelField),\n ]);\n\n const columnTitles: Record<string, string> = {};\n const columnOrder: string[] = [];\n const tasks: KanbanTask[] = rows.map((row, i) => {\n const columnRaw = row[columnField];\n const column = columnRaw == null || columnRaw === '' ? 'Uncategorized' : String(columnRaw);\n if (!(column in columnTitles)) {\n columnTitles[column] = column;\n columnOrder.push(column);\n }\n const idRaw = row.id ?? row.identifier ?? row.key;\n const labels = Array.isArray(row.labels) ? row.labels.filter((l): l is string => typeof l === 'string') : undefined;\n return {\n id: idRaw != null ? String(idRaw) : `task-${i}`,\n title: titleField && row[titleField] != null ? String(row[titleField]) : `Item ${i + 1}`,\n column,\n assignee: asString(row.assignee),\n priority: normalizePriority(row.priority ?? row.priorityLabel),\n labels: labels && labels.length > 0 ? labels : undefined,\n };\n });\n\n const columns: KanbanColumn[] = columnOrder.map((id) => ({ id, title: columnTitles[id] ?? id }));\n return { columns, tasks };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KanbanWidget: FC<KanbanWidgetProps> = memo(function KanbanWidget({ widget, data, onClick }) {\n const config = widget.config as Record<string, unknown> | undefined;\n const { columns, tasks } = useMemo(() => normalizeKanban(data, config), [data, config]);\n\n // Group tasks by column\n const tasksByColumn = useMemo(() => {\n const grouped: Record<string, KanbanTask[]> = {};\n columns.forEach((col) => {\n grouped[col.id] = tasks.filter((t) => t.column === col.id);\n });\n return grouped;\n }, [columns, tasks]);\n\n if (!columns.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2\"\n />\n </svg>\n <p>No board data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex h-full p-2 gap-2 overflow-x-auto\">\n {columns.map((column) => {\n const columnTasks = tasksByColumn[column.id] || [];\n\n return (\n <div key={column.id} className=\"flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg\">\n {/* Column Header */}\n <div className=\"flex items-center gap-2 p-2 border-b border-border-default\">\n <div\n className=\"w-2 h-2 rounded-full\"\n style={{ backgroundColor: column.color || 'var(--color-text-muted)' }}\n />\n <span className=\"text-xs font-medium text-text-primary truncate\">{column.title}</span>\n <span className=\"ml-auto text-xs text-text-tertiary\">{columnTasks.length}</span>\n </div>\n\n {/* Tasks */}\n <div className=\"flex-1 p-2 space-y-2 overflow-y-auto\">\n {columnTasks.map((task) => (\n <div\n key={task.id}\n className={`\n p-2 bg-bg-surface rounded border border-border-default\n ${onClick ? 'cursor-pointer hover:border-action-primary-bg' : ''}\n transition-colors\n `}\n onClick={() => onClick?.(task)}\n >\n <p className=\"text-xs font-medium text-text-primary line-clamp-2\">{task.title}</p>\n\n <div className=\"flex items-center gap-1 mt-2 flex-wrap\">\n {task.priority && (\n <span className={`px-1.5 py-0.5 text-[10px] rounded ${getPriorityColor(task.priority)}`}>\n {task.priority}\n </span>\n )}\n {task.assignee && (\n <span className=\"px-1.5 py-0.5 text-[10px] rounded bg-action-primary-bg/10 text-action-primary-text\">\n {task.assignee}\n </span>\n )}\n </div>\n </div>\n ))}\n\n {columnTasks.length === 0 && <p className=\"text-[10px] text-text-tertiary text-center py-4\">No tasks</p>}\n </div>\n </div>\n );\n })}\n </div>\n );\n});\n\nexport default KanbanWidget;\n"],"mappings":"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"}
@@ -41,7 +41,8 @@ var g = c(function({ widget: c, data: g, onClick: _, onDrilldown: v }) {
41
41
  let y = c.config, b = u(() => {
42
42
  if (!g) return;
43
43
  let e = (e, t) => t.split(".").reduce((e, t) => e && typeof e == "object" ? e[t] : void 0, e);
44
- if ("value" in g && g.value !== void 0) return g;
44
+ if (typeof g == "number" || typeof g == "string") return { value: g };
45
+ if (typeof g == "object" && "value" in g && g.value !== void 0) return g;
45
46
  let t = y?.valueField;
46
47
  if (t && typeof g == "object") {
47
48
  let n = e(g, t);
@@ -1 +1 @@
1
- {"version":3,"file":"MetricCardWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/MetricCardWidget.tsx"],"sourcesContent":["/**\n * MetricCardWidget Component\n *\n * Displays a single KPI/metric with trend, comparison, and optional sparkline.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, MetricCardConfig } from '../../../types';\nimport { TrendIndicator, type TrendDirection } from './TrendIndicator';\nimport { ComparisonBadge } from './ComparisonBadge';\nimport { SparklineChart, type SparklineDataPoint } from './SparklineChart';\nimport { firstObjectRow, inferValueField, unwrapRows } from '../utils/inferFields';\nimport { resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MetricCardWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Fetched data */\n data?: MetricCardData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives metric data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface MetricCardData {\n /** Primary value to display */\n value: number | string;\n /** Previous value for comparison */\n previousValue?: number;\n /** Percentage change from previous */\n changePercent?: number;\n /** Trend direction override */\n trend?: TrendDirection;\n /** Sparkline data points */\n sparklineData?: SparklineDataPoint[];\n /** Target value for comparison */\n target?: number;\n /** Unit label (e.g., \"users\", \"$\") */\n unit?: string;\n /** Whether the metric is a currency */\n isCurrency?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Format a number based on configuration\n */\nfunction formatValue(value: number | string, config?: MetricCardConfig): string {\n if (typeof value === 'string') return value;\n\n const { format = 'number', prefix, suffix } = config || {};\n\n let formatted: string;\n\n switch (format) {\n case 'currency':\n formatted = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n }).format(value);\n break;\n\n case 'percentage':\n formatted = `${(value * 100).toFixed(1)}%`;\n break;\n\n case 'compact':\n formatted = new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n break;\n\n case 'number':\n default:\n formatted = new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n }).format(value);\n }\n\n // Apply prefix/suffix if provided\n if (prefix) formatted = `${prefix}${formatted}`;\n if (suffix) formatted = `${formatted}${suffix}`;\n\n return formatted;\n}\n\n/**\n * Calculate trend direction from change percent\n */\nfunction calculateTrend(changePercent?: number): TrendDirection {\n if (changePercent === undefined || changePercent === 0) return 'flat';\n return changePercent > 0 ? 'up' : 'down';\n}\n\n/**\n * Get sparkline color based on trend\n */\nfunction getSparklineColor(trend: TrendDirection, positiveIsGood: boolean): 'success' | 'error' | 'muted' {\n if (trend === 'flat') return 'muted';\n const isGood = positiveIsGood ? trend === 'up' : trend === 'down';\n return isGood ? 'success' : 'error';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const MetricCardWidget: FC<MetricCardWidgetProps> = memo(function MetricCardWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as MetricCardConfig | undefined;\n\n // Normalize data - extract value from object if valueField is configured\n const data = useMemo((): MetricCardData | undefined => {\n if (!rawData) return undefined;\n\n // Helper to get nested value using dot notation (e.g., \"summary.totalSales\")\n const getNestedValue = (obj: Record<string, unknown>, path: string): unknown => {\n return path.split('.').reduce((current, key) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj as unknown);\n };\n\n // If data already has 'value' property, use it directly\n if ('value' in rawData && rawData.value !== undefined) {\n return rawData as MetricCardData;\n }\n\n // If valueField is configured, extract the value from that field (supports dot notation)\n const valueField = (config as unknown as Record<string, unknown> | undefined)?.valueField as string | undefined;\n if (valueField && typeof rawData === 'object') {\n const extractedValue = getNestedValue(rawData as unknown as Record<string, unknown>, valueField);\n if (extractedValue !== undefined) {\n // Also try to extract previousValue if previousValueField is configured\n const previousValueField = (config as unknown as Record<string, unknown> | undefined)?.previousValueField as\n string | undefined;\n const previousValue = previousValueField\n ? (getNestedValue(rawData as unknown as Record<string, unknown>, previousValueField) as number | undefined)\n : undefined;\n\n return {\n value: extractedValue as number | string,\n previousValue,\n changePercent:\n previousValue !== undefined && typeof extractedValue === 'number'\n ? ((extractedValue - previousValue) / previousValue) * 100\n : undefined,\n };\n }\n }\n\n // Try common field names at top level\n const commonFields = ['value', 'total', 'amount', 'count', 'sum', 'totalSales', 'totalRevenue', 'totalOrders'];\n for (const field of commonFields) {\n if (typeof rawData === 'object' && field in rawData) {\n const val = (rawData as unknown as Record<string, unknown>)[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n // Try looking in nested 'summary' object (common pattern)\n if (typeof rawData === 'object' && 'summary' in rawData) {\n const summary = (rawData as Record<string, unknown>).summary as Record<string, unknown> | undefined;\n if (summary && typeof summary === 'object') {\n for (const field of commonFields) {\n if (field in summary) {\n const val = summary[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n }\n }\n\n // If rawData is a number or string directly, use it\n if (typeof rawData === 'number' || typeof rawData === 'string') {\n return { value: rawData };\n }\n\n // BOFF-2246 smart default: if rawData is an array (or envelope around an\n // array) with object rows, use the configured valueField if it resolves\n // on any row (supports dotted paths like \"metrics.revenue\") — otherwise\n // pick the first numeric field from row[0]. This path only fires when\n // the earlier top-level extraction didn't find anything, but an explicit\n // `config.valueField` (flat or dotted) must always win over inference.\n const firstRow = firstObjectRow(rawData);\n if (firstRow) {\n const configuredField = (config as unknown as Record<string, unknown> | undefined)?.valueField as\n string | undefined;\n if (configuredField) {\n // Configured field is authoritative. Scan every row for it so sparse\n // envelopes like `{data:[{id:1},{revenue:120}]}` with\n // `valueField:\"revenue\"` still resolve. If no row carries it, return\n // undefined rather than substituting an unrelated numeric column\n // from row[0] (that would silently misrepresent the user's mapping).\n const rows = unwrapRows(rawData) ?? [firstRow];\n for (const r of rows) {\n if (r && typeof r === 'object') {\n const resolved = getNestedValue(r as Record<string, unknown>, configuredField);\n if (typeof resolved === 'number' || typeof resolved === 'string') {\n return { value: resolved };\n }\n }\n }\n return undefined;\n }\n const inferredField = inferValueField(firstRow);\n if (inferredField) {\n const val = firstRow[inferredField];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n return undefined;\n }, [rawData, config]);\n\n // Calculate derived values\n const trend = useMemo(() => {\n return data?.trend || calculateTrend(data?.changePercent);\n }, [data?.trend, data?.changePercent]);\n\n const positiveIsGood = config?.positiveIsGood ?? true;\n\n const formattedValue = useMemo(() => {\n // Treat 0 / \"\" as real values — only a missing value renders the em dash.\n // (`if (!data?.value)` would wrongly blank out a legitimate 0, e.g. an ED\n // boarding-compliance of 0% or an available-bed count of 0.)\n if (data?.value === undefined || data?.value === null) return '—';\n return formatValue(data.value, config);\n }, [data?.value, config]);\n\n const sparklineColor = useMemo(() => {\n return getSparklineColor(trend, positiveIsGood);\n }, [trend, positiveIsGood]);\n\n // Conditional formatting on the metric value (threshold-driven color, not\n // just trend direction). Falls back to the default primary text color.\n const valueConditionalClass = useMemo(() => {\n const style = resolveConditionalStyle(data?.value, config?.conditionalRules);\n return conditionalStyleToClassName(style);\n }, [data?.value, config?.conditionalRules]);\n\n // Handle click\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown) {\n onDrilldown({\n value: data?.value,\n changePercent: data?.changePercent,\n trend,\n target: data?.target,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, data, trend]);\n\n // Determine if clickable\n const isClickable = Boolean(onDrilldown || (onClick && widget.drilldowns?.length));\n\n return (\n <div\n className={`\n flex flex-col h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken/50 transition-colors' : ''}\n `}\n onClick={handleClick}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Subtitle/Label */}\n {config?.subtitle && <p className=\"text-xs text-text-secondary mb-1 truncate\">{config.subtitle}</p>}\n\n {/* Main Value */}\n <div className=\"flex items-baseline gap-2 mb-2\">\n <span\n className={`text-3xl font-semibold tracking-tight ${valueConditionalClass || 'text-text-primary'}`}\n title={String(data?.value)}\n >\n {formattedValue}\n </span>\n\n {/* Trend Indicator (inline) */}\n {config?.showTrend !== false && data?.changePercent !== undefined && (\n <TrendIndicator direction={trend} size=\"md\" positiveIsGood={positiveIsGood} />\n )}\n </div>\n\n {/* Comparison Badge */}\n {config?.showComparison !== false && data?.changePercent !== undefined && (\n <div className=\"mb-3\">\n <ComparisonBadge\n value={data.changePercent}\n label={config?.comparisonLabel}\n positiveIsGood={positiveIsGood}\n size=\"sm\"\n />\n </div>\n )}\n\n {/* Target Progress */}\n {data?.target !== undefined && typeof data.value === 'number' && (\n <div className=\"mb-3\">\n <div className=\"flex justify-between text-xs text-text-secondary mb-1\">\n <span>Target: {formatValue(data.target, config)}</span>\n <span>{((data.value / data.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1.5 bg-bg-sunken rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full transition-all ${\n data.value >= data.target ? 'bg-state-success' : 'bg-action-primary-bg'\n }`}\n style={{\n width: `${Math.min((data.value / data.target) * 100, 100)}%`,\n }}\n />\n </div>\n </div>\n )}\n\n {/* Sparkline */}\n {config?.showSparkline !== false && data?.sparklineData?.length ? (\n <div className=\"mt-auto pt-2\">\n <SparklineChart data={data.sparklineData} width={120} height={32} color={sparklineColor} showFill />\n </div>\n ) : null}\n\n {/* Drilldown Indicator */}\n {isClickable && (\n <div className=\"mt-auto pt-2 flex items-center justify-end\">\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n View details\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </span>\n </div>\n )}\n </div>\n );\n});\n\nexport default MetricCardWidget;\n"],"mappings":"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"}
1
+ {"version":3,"file":"MetricCardWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/MetricCardWidget.tsx"],"sourcesContent":["/**\n * MetricCardWidget Component\n *\n * Displays a single KPI/metric with trend, comparison, and optional sparkline.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, MetricCardConfig } from '../../../types';\nimport { TrendIndicator, type TrendDirection } from './TrendIndicator';\nimport { ComparisonBadge } from './ComparisonBadge';\nimport { SparklineChart, type SparklineDataPoint } from './SparklineChart';\nimport { firstObjectRow, inferValueField, unwrapRows } from '../utils/inferFields';\nimport { resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MetricCardWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Fetched data */\n data?: MetricCardData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives metric data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface MetricCardData {\n /** Primary value to display */\n value: number | string;\n /** Previous value for comparison */\n previousValue?: number;\n /** Percentage change from previous */\n changePercent?: number;\n /** Trend direction override */\n trend?: TrendDirection;\n /** Sparkline data points */\n sparklineData?: SparklineDataPoint[];\n /** Target value for comparison */\n target?: number;\n /** Unit label (e.g., \"users\", \"$\") */\n unit?: string;\n /** Whether the metric is a currency */\n isCurrency?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Format a number based on configuration\n */\nfunction formatValue(value: number | string, config?: MetricCardConfig): string {\n if (typeof value === 'string') return value;\n\n const { format = 'number', prefix, suffix } = config || {};\n\n let formatted: string;\n\n switch (format) {\n case 'currency':\n formatted = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n }).format(value);\n break;\n\n case 'percentage':\n formatted = `${(value * 100).toFixed(1)}%`;\n break;\n\n case 'compact':\n formatted = new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n break;\n\n case 'number':\n default:\n formatted = new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n }).format(value);\n }\n\n // Apply prefix/suffix if provided\n if (prefix) formatted = `${prefix}${formatted}`;\n if (suffix) formatted = `${formatted}${suffix}`;\n\n return formatted;\n}\n\n/**\n * Calculate trend direction from change percent\n */\nfunction calculateTrend(changePercent?: number): TrendDirection {\n if (changePercent === undefined || changePercent === 0) return 'flat';\n return changePercent > 0 ? 'up' : 'down';\n}\n\n/**\n * Get sparkline color based on trend\n */\nfunction getSparklineColor(trend: TrendDirection, positiveIsGood: boolean): 'success' | 'error' | 'muted' {\n if (trend === 'flat') return 'muted';\n const isGood = positiveIsGood ? trend === 'up' : trend === 'down';\n return isGood ? 'success' : 'error';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const MetricCardWidget: FC<MetricCardWidgetProps> = memo(function MetricCardWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as MetricCardConfig | undefined;\n\n // Normalize data - extract value from object if valueField is configured\n const data = useMemo((): MetricCardData | undefined => {\n if (!rawData) return undefined;\n\n // Helper to get nested value using dot notation (e.g., \"summary.totalSales\")\n const getNestedValue = (obj: Record<string, unknown>, path: string): unknown => {\n return path.split('.').reduce((current, key) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj as unknown);\n };\n\n // A parser can return a bare scalar (e.g. `. | length` -> 127). Handle that\n // up front: the `in` operator below throws \"Cannot use 'in' operator to\n // search for 'value' in 127\" when rawData is a primitive, which the widget\n // error boundary surfaces as \"Failed to render\" (BOFF-4110).\n if (typeof rawData === 'number' || typeof rawData === 'string') {\n return { value: rawData };\n }\n\n // If data already has 'value' property, use it directly. Guard the `in`\n // operator with an object check — it only accepts an object on the RHS.\n if (typeof rawData === 'object' && 'value' in rawData && rawData.value !== undefined) {\n return rawData as MetricCardData;\n }\n\n // If valueField is configured, extract the value from that field (supports dot notation)\n const valueField = (config as unknown as Record<string, unknown> | undefined)?.valueField as string | undefined;\n if (valueField && typeof rawData === 'object') {\n const extractedValue = getNestedValue(rawData as unknown as Record<string, unknown>, valueField);\n if (extractedValue !== undefined) {\n // Also try to extract previousValue if previousValueField is configured\n const previousValueField = (config as unknown as Record<string, unknown> | undefined)?.previousValueField as\n string | undefined;\n const previousValue = previousValueField\n ? (getNestedValue(rawData as unknown as Record<string, unknown>, previousValueField) as number | undefined)\n : undefined;\n\n return {\n value: extractedValue as number | string,\n previousValue,\n changePercent:\n previousValue !== undefined && typeof extractedValue === 'number'\n ? ((extractedValue - previousValue) / previousValue) * 100\n : undefined,\n };\n }\n }\n\n // Try common field names at top level\n const commonFields = ['value', 'total', 'amount', 'count', 'sum', 'totalSales', 'totalRevenue', 'totalOrders'];\n for (const field of commonFields) {\n if (typeof rawData === 'object' && field in rawData) {\n const val = (rawData as unknown as Record<string, unknown>)[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n // Try looking in nested 'summary' object (common pattern)\n if (typeof rawData === 'object' && 'summary' in rawData) {\n const summary = (rawData as Record<string, unknown>).summary as Record<string, unknown> | undefined;\n if (summary && typeof summary === 'object') {\n for (const field of commonFields) {\n if (field in summary) {\n const val = summary[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n }\n }\n\n // If rawData is a number or string directly, use it\n if (typeof rawData === 'number' || typeof rawData === 'string') {\n return { value: rawData };\n }\n\n // BOFF-2246 smart default: if rawData is an array (or envelope around an\n // array) with object rows, use the configured valueField if it resolves\n // on any row (supports dotted paths like \"metrics.revenue\") — otherwise\n // pick the first numeric field from row[0]. This path only fires when\n // the earlier top-level extraction didn't find anything, but an explicit\n // `config.valueField` (flat or dotted) must always win over inference.\n const firstRow = firstObjectRow(rawData);\n if (firstRow) {\n const configuredField = (config as unknown as Record<string, unknown> | undefined)?.valueField as\n string | undefined;\n if (configuredField) {\n // Configured field is authoritative. Scan every row for it so sparse\n // envelopes like `{data:[{id:1},{revenue:120}]}` with\n // `valueField:\"revenue\"` still resolve. If no row carries it, return\n // undefined rather than substituting an unrelated numeric column\n // from row[0] (that would silently misrepresent the user's mapping).\n const rows = unwrapRows(rawData) ?? [firstRow];\n for (const r of rows) {\n if (r && typeof r === 'object') {\n const resolved = getNestedValue(r as Record<string, unknown>, configuredField);\n if (typeof resolved === 'number' || typeof resolved === 'string') {\n return { value: resolved };\n }\n }\n }\n return undefined;\n }\n const inferredField = inferValueField(firstRow);\n if (inferredField) {\n const val = firstRow[inferredField];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n return undefined;\n }, [rawData, config]);\n\n // Calculate derived values\n const trend = useMemo(() => {\n return data?.trend || calculateTrend(data?.changePercent);\n }, [data?.trend, data?.changePercent]);\n\n const positiveIsGood = config?.positiveIsGood ?? true;\n\n const formattedValue = useMemo(() => {\n // Treat 0 / \"\" as real values — only a missing value renders the em dash.\n // (`if (!data?.value)` would wrongly blank out a legitimate 0, e.g. an ED\n // boarding-compliance of 0% or an available-bed count of 0.)\n if (data?.value === undefined || data?.value === null) return '—';\n return formatValue(data.value, config);\n }, [data?.value, config]);\n\n const sparklineColor = useMemo(() => {\n return getSparklineColor(trend, positiveIsGood);\n }, [trend, positiveIsGood]);\n\n // Conditional formatting on the metric value (threshold-driven color, not\n // just trend direction). Falls back to the default primary text color.\n const valueConditionalClass = useMemo(() => {\n const style = resolveConditionalStyle(data?.value, config?.conditionalRules);\n return conditionalStyleToClassName(style);\n }, [data?.value, config?.conditionalRules]);\n\n // Handle click\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown) {\n onDrilldown({\n value: data?.value,\n changePercent: data?.changePercent,\n trend,\n target: data?.target,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, data, trend]);\n\n // Determine if clickable\n const isClickable = Boolean(onDrilldown || (onClick && widget.drilldowns?.length));\n\n return (\n <div\n className={`\n flex flex-col h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken/50 transition-colors' : ''}\n `}\n onClick={handleClick}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Subtitle/Label */}\n {config?.subtitle && <p className=\"text-xs text-text-secondary mb-1 truncate\">{config.subtitle}</p>}\n\n {/* Main Value */}\n <div className=\"flex items-baseline gap-2 mb-2\">\n <span\n className={`text-3xl font-semibold tracking-tight ${valueConditionalClass || 'text-text-primary'}`}\n title={String(data?.value)}\n >\n {formattedValue}\n </span>\n\n {/* Trend Indicator (inline) */}\n {config?.showTrend !== false && data?.changePercent !== undefined && (\n <TrendIndicator direction={trend} size=\"md\" positiveIsGood={positiveIsGood} />\n )}\n </div>\n\n {/* Comparison Badge */}\n {config?.showComparison !== false && data?.changePercent !== undefined && (\n <div className=\"mb-3\">\n <ComparisonBadge\n value={data.changePercent}\n label={config?.comparisonLabel}\n positiveIsGood={positiveIsGood}\n size=\"sm\"\n />\n </div>\n )}\n\n {/* Target Progress */}\n {data?.target !== undefined && typeof data.value === 'number' && (\n <div className=\"mb-3\">\n <div className=\"flex justify-between text-xs text-text-secondary mb-1\">\n <span>Target: {formatValue(data.target, config)}</span>\n <span>{((data.value / data.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1.5 bg-bg-sunken rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full transition-all ${\n data.value >= data.target ? 'bg-state-success' : 'bg-action-primary-bg'\n }`}\n style={{\n width: `${Math.min((data.value / data.target) * 100, 100)}%`,\n }}\n />\n </div>\n </div>\n )}\n\n {/* Sparkline */}\n {config?.showSparkline !== false && data?.sparklineData?.length ? (\n <div className=\"mt-auto pt-2\">\n <SparklineChart data={data.sparklineData} width={120} height={32} color={sparklineColor} showFill />\n </div>\n ) : null}\n\n {/* Drilldown Indicator */}\n {isClickable && (\n <div className=\"mt-auto pt-2 flex items-center justify-end\">\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n View details\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </span>\n </div>\n )}\n </div>\n );\n});\n\nexport default MetricCardWidget;\n"],"mappings":"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"}
@@ -222,9 +222,9 @@ var E = i(function() {
222
222
  children: /* @__PURE__ */ w("div", {
223
223
  className: "text-center",
224
224
  children: [
225
- /* @__PURE__ */ C(c, { className: "w-12 h-12 text-state-error mx-auto mb-spacing-md" }),
225
+ /* @__PURE__ */ C(c, { className: "w-12 h-12 text-state-error mx-auto mb-4" }),
226
226
  /* @__PURE__ */ C("p", {
227
- className: "text-state-error text-body-sm font-medium mb-spacing-sm",
227
+ className: "text-state-error text-body-sm font-medium mb-2",
228
228
  children: "Failed to load DataSink"
229
229
  }),
230
230
  /* @__PURE__ */ C("p", {
@@ -239,13 +239,13 @@ var E = i(function() {
239
239
  className: "max-w-3xl mx-auto p-card-padding",
240
240
  children: [
241
241
  /* @__PURE__ */ w("div", {
242
- className: "mb-spacing-lg",
242
+ className: "mb-6",
243
243
  children: [
244
244
  /* @__PURE__ */ w(p, {
245
245
  onClick: $,
246
246
  variant: "ghost",
247
247
  size: "sm",
248
- className: "mb-spacing-md",
248
+ className: "mb-4",
249
249
  children: [/* @__PURE__ */ C(l, { className: "w-4 h-4 mr-2" }), "Back to DataSinks"]
250
250
  }),
251
251
  /* @__PURE__ */ C("h1", {
@@ -253,13 +253,13 @@ var E = i(function() {
253
253
  children: D ? "Edit DataSink" : "Create DataSink"
254
254
  }),
255
255
  /* @__PURE__ */ C("p", {
256
- className: "text-body-sm text-text-secondary mt-spacing-xs",
256
+ className: "text-body-sm text-text-secondary mt-1",
257
257
  children: D ? "Update the DataSink configuration." : "Create a new DataSink to store and cache data. Use the unique key to connect workflows."
258
258
  })
259
259
  ]
260
260
  }),
261
261
  I && /* @__PURE__ */ w("div", {
262
- className: "mb-spacing-lg p-4 bg-state-error/10 border border-state-error/30 rounded-lg flex items-start gap-3",
262
+ className: "mb-6 p-4 bg-state-error/10 border border-state-error/30 rounded-lg flex items-start gap-3",
263
263
  children: [/* @__PURE__ */ C(c, { className: "w-5 h-5 text-state-error flex-shrink-0 mt-0.5" }), /* @__PURE__ */ w("div", { children: [/* @__PURE__ */ C("p", {
264
264
  className: "text-sm font-medium text-state-error",
265
265
  children: "Failed to save DataSink"
@@ -269,12 +269,12 @@ var E = i(function() {
269
269
  })] })]
270
270
  }),
271
271
  /* @__PURE__ */ C(ue, {
272
- className: "mb-spacing-lg",
272
+ className: "mb-6",
273
273
  children: "A data sink is the named, structured store your dashboards read from. Give it a unique key, configure how long data is cached and how it refreshes, then point a workflow at that key to push data in. Get the basics right here and everything downstream connects to it."
274
274
  }),
275
275
  /* @__PURE__ */ w("form", {
276
276
  onSubmit: we,
277
- className: "space-y-spacing-lg",
277
+ className: "space-y-6",
278
278
  children: [
279
279
  /* @__PURE__ */ w(le, {
280
280
  treatment: "glass",
@@ -298,7 +298,7 @@ var E = i(function() {
298
298
  className: "space-y-5",
299
299
  children: [
300
300
  /* @__PURE__ */ w("div", {
301
- className: "space-y-spacing-sm",
301
+ className: "space-y-2",
302
302
  children: [
303
303
  /* @__PURE__ */ w(b, {
304
304
  htmlFor: "name",
@@ -322,7 +322,7 @@ var E = i(function() {
322
322
  ]
323
323
  }),
324
324
  /* @__PURE__ */ w("div", {
325
- className: "space-y-spacing-sm",
325
+ className: "space-y-2",
326
326
  children: [
327
327
  /* @__PURE__ */ w(b, {
328
328
  htmlFor: "key",
@@ -361,7 +361,7 @@ var E = i(function() {
361
361
  ]
362
362
  }),
363
363
  /* @__PURE__ */ w("div", {
364
- className: "space-y-spacing-sm",
364
+ className: "space-y-2",
365
365
  children: [/* @__PURE__ */ C(b, {
366
366
  htmlFor: "description",
367
367
  children: "Description"
@@ -395,7 +395,7 @@ var E = i(function() {
395
395
  }), /* @__PURE__ */ w(h, {
396
396
  className: "space-y-5",
397
397
  children: [/* @__PURE__ */ w("div", {
398
- className: "space-y-spacing-sm",
398
+ className: "space-y-2",
399
399
  children: [
400
400
  /* @__PURE__ */ C(b, { children: "Cache Duration" }),
401
401
  /* @__PURE__ */ C(r, {
@@ -437,7 +437,7 @@ var E = i(function() {
437
437
  })]
438
438
  })
439
439
  }), /* @__PURE__ */ C(h, { children: /* @__PURE__ */ w("div", {
440
- className: "space-y-spacing-sm",
440
+ className: "space-y-2",
441
441
  children: [
442
442
  /* @__PURE__ */ C(x, {
443
443
  value: O.data,
@@ -496,7 +496,7 @@ var E = i(function() {
496
496
  description: "Connect a workflow to this sink's key to start pushing data."
497
497
  }) })] }),
498
498
  /* @__PURE__ */ w("div", {
499
- className: "flex items-center justify-end gap-spacing-md pt-spacing-md",
499
+ className: "flex items-center justify-end gap-4 pt-4",
500
500
  children: [/* @__PURE__ */ C(p, {
501
501
  type: "button",
502
502
  onClick: $,
@@ -512,7 +512,7 @@ var E = i(function() {
512
512
  ]
513
513
  }),
514
514
  !D && /* @__PURE__ */ w("div", {
515
- className: "mt-spacing-lg p-4 bg-bg-sunken rounded-lg border border-border-default",
515
+ className: "mt-6 p-4 bg-bg-sunken rounded-lg border border-border-default",
516
516
  children: [/* @__PURE__ */ C("h3", {
517
517
  className: "text-sm font-medium text-text-primary mb-2",
518
518
  children: "What's Next?"
@@ -1 +1 @@
1
- {"version":3,"file":"DataSinkBuilderPage.js","names":[],"sources":["../../../src/bigconsole/pages/DataSinkBuilderPage.tsx"],"sourcesContent":["/**\n * DataSink Builder Page\n *\n * Simplified page for creating and editing DataSinks.\n * DataSinks are now standalone - workflows push data to them via unique keys.\n * Required fields: name, key\n */\n\nimport { memo, useState, useMemo, useEffect, useCallback } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useAppNavigate } from '../contexts';\nimport { isNative, nativeCopyText, nativeToast } from '../../utils/nativeBridge';\nimport { useDataSinkOperations } from '../hooks/useDataSinkOperations';\nimport { CacheTTLSelector } from '../components/datasink/CacheTTLSelector';\nimport { RefreshTypeSelector } from '../components/datasink/RefreshTypeSelector';\nimport type { CreateDataSinkInput, UpdateDataSinkInput, DataSinkRefreshType } from '../types';\n\n// fe-libs UI components\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Input } from '@burdenoff/fe-libs/ui';\nimport { Textarea } from '@burdenoff/fe-libs/ui';\nimport { Label } from '@burdenoff/fe-libs/ui';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { GlassCard, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\n// Lucide icons\nimport { ArrowLeft, Save, Loader2, AlertCircle, Database, Settings, Eye, Copy, Check } from 'lucide-react';\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\ninterface RefreshConfig {\n type: 'manual' | 'scheduled';\n schedule?: {\n cron: string;\n timezone: string;\n };\n}\n\ninterface FormState {\n name: string;\n key: string;\n description: string;\n ttl: number;\n refresh: RefreshConfig;\n data: string; // JSON string for data payload\n}\n\ninterface FormErrors {\n name?: string;\n key?: string;\n data?: string;\n}\n\n// ============================================================================\n// HELPERS\n// ============================================================================\n\n/**\n * Generate a key from name (slug format)\n */\nfunction generateKey(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .substring(0, 64);\n}\n\n/**\n * Validate form data\n */\nfunction validateForm(form: FormState): FormErrors {\n const errors: FormErrors = {};\n\n if (!form.name.trim()) {\n errors.name = 'Name is required';\n } else if (form.name.length < 2) {\n errors.name = 'Name must be at least 2 characters';\n } else if (form.name.length > 100) {\n errors.name = 'Name must be less than 100 characters';\n }\n\n if (!form.key.trim()) {\n errors.key = 'Key is required';\n } else if (!/^[a-z0-9_]+$/.test(form.key)) {\n errors.key = 'Key can only contain lowercase letters, numbers, and underscores';\n } else if (form.key.length > 64) {\n errors.key = 'Key must be less than 64 characters';\n }\n\n // Validate JSON data if provided\n if (form.data && form.data.trim()) {\n try {\n JSON.parse(form.data);\n } catch {\n errors.data = 'Invalid JSON format';\n }\n }\n\n return errors;\n}\n\n// ============================================================================\n// COMPONENT\n// ============================================================================\n\nexport const DataSinkBuilderPage = memo(function DataSinkBuilderPage() {\n const { dataSinkId } = useParams<{ dataSinkId?: string }>();\n const navigate = useAppNavigate();\n\n const isEdit = dataSinkId !== 'new' && !!dataSinkId;\n\n // Form state\n const [form, setForm] = useState<FormState>({\n name: '',\n key: '',\n description: '',\n ttl: 300, // 5 minutes default\n refresh: {\n type: 'manual',\n },\n data: '{}', // Default empty JSON object\n });\n const [errors, setErrors] = useState<FormErrors>({});\n const [keyManuallyEdited, setKeyManuallyEdited] = useState(false);\n const [isSaving, setIsSaving] = useState(false);\n const [submitError, setSubmitError] = useState<string | null>(null);\n const [keyCopied, setKeyCopied] = useState(false);\n\n // Data preview state\n const [isLoadingPreview, setIsLoadingPreview] = useState(false);\n const [previewData, setPreviewData] = useState<unknown[] | null>(null);\n const [previewError, setPreviewError] = useState<string | null>(null);\n\n // Operations\n const { loading, error, dataSinks, fetchDataSink, createDataSink, updateDataSink, getDataSinkData } =\n useDataSinkOperations({\n skipFetch: true,\n });\n\n // Get existing data sink if editing\n const existingDataSink = isEdit ? dataSinks.find((ds) => ds.id === dataSinkId) : null;\n\n // Load existing data sink for editing\n useEffect(() => {\n if (isEdit && dataSinkId && !existingDataSink) {\n fetchDataSink(dataSinkId);\n }\n }, [isEdit, dataSinkId, existingDataSink, fetchDataSink]);\n\n // Track if form has been initialized\n const [formInitialized, setFormInitialized] = useState(false);\n\n // Populate form with existing data when editing (only once)\n useEffect(() => {\n if (existingDataSink && !formInitialized) {\n // Format existing data as pretty JSON string\n let dataString = '';\n if (existingDataSink.data) {\n try {\n // Check if data is already a string (JSON string from API)\n if (typeof existingDataSink.data === 'string') {\n // Parse and re-stringify to ensure proper formatting\n const parsed = JSON.parse(existingDataSink.data);\n dataString = JSON.stringify(parsed, null, 2);\n } else {\n // It's an object, just stringify it\n dataString = JSON.stringify(existingDataSink.data, null, 2);\n }\n } catch {\n // If parsing fails, use as-is if string, otherwise empty\n dataString = typeof existingDataSink.data === 'string' ? existingDataSink.data : '';\n }\n }\n\n setForm({\n name: existingDataSink.name || '',\n key: existingDataSink.key || '',\n description: existingDataSink.description || '',\n ttl: existingDataSink.ttl || 300,\n refresh: existingDataSink.refresh\n ? {\n type: existingDataSink.refresh.type === 'scheduled' ? 'scheduled' : 'manual',\n schedule: existingDataSink.refresh.schedule\n ? {\n cron: existingDataSink.refresh.schedule.cron,\n timezone: existingDataSink.refresh.schedule.timezone,\n }\n : undefined,\n }\n : { type: 'manual' },\n data: dataString,\n });\n setKeyManuallyEdited(true); // Don't auto-generate key when editing\n setFormInitialized(true); // Mark as initialized\n setErrors({}); // Clear any errors\n }\n }, [existingDataSink, formInitialized]);\n\n // Auto-generate key from name\n const handleNameChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const name = e.target.value;\n setForm((prev) => ({\n ...prev,\n name,\n key: keyManuallyEdited ? prev.key : generateKey(name),\n }));\n setErrors((prev) => ({ ...prev, name: undefined }));\n },\n [keyManuallyEdited]\n );\n\n // Handle key change\n const handleKeyChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {\n const key = e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '');\n setForm((prev) => ({ ...prev, key }));\n setKeyManuallyEdited(true);\n setErrors((prev) => ({ ...prev, key: undefined }));\n }, []);\n\n // Handle description change\n const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setForm((prev) => ({ ...prev, description: e.target.value }));\n }, []);\n\n // Handle TTL change\n const handleTTLChange = useCallback((ttl: number | null) => {\n setForm((prev) => ({ ...prev, ttl: ttl || 0 }));\n }, []);\n\n // Handle refresh config change\n const handleRefreshChange = useCallback((config: RefreshConfig) => {\n setForm((prev) => ({ ...prev, refresh: config }));\n }, []);\n\n // Handle data change\n const handleDataChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setForm((prev) => ({ ...prev, data: e.target.value }));\n setErrors((prev) => ({ ...prev, data: undefined }));\n }, []);\n\n // Format JSON data\n const handleFormatData = useCallback(() => {\n try {\n const parsed = JSON.parse(form.data);\n setForm((prev) => ({ ...prev, data: JSON.stringify(parsed, null, 2) }));\n setErrors((prev) => ({ ...prev, data: undefined }));\n } catch {\n setErrors((prev) => ({ ...prev, data: 'Invalid JSON format' }));\n }\n }, [form.data]);\n\n // Copy key to clipboard\n const handleCopyKey = useCallback(async () => {\n if (form.key) {\n await nativeCopyText(form.key);\n setKeyCopied(true);\n if (isNative()) void nativeToast('Key copied');\n setTimeout(() => setKeyCopied(false), 2000);\n }\n }, [form.key]);\n\n // Handle back navigation\n const handleBack = useCallback(() => {\n navigate('/datasinks');\n }, [navigate]);\n\n // Load data preview\n const handleShowData = useCallback(async () => {\n if (!isEdit || !existingDataSink) return;\n\n setIsLoadingPreview(true);\n setPreviewError(null);\n\n try {\n const result = await getDataSinkData({ key: existingDataSink.key });\n if (result?.raw) {\n // Get first 20 items if it's an array, otherwise wrap in array\n const data = Array.isArray(result.raw) ? result.raw.slice(0, 20) : [result.raw];\n setPreviewData(data);\n } else {\n setPreviewData([]);\n }\n } catch (err) {\n setPreviewError(err instanceof Error ? err.message : 'Failed to load data');\n } finally {\n setIsLoadingPreview(false);\n }\n }, [isEdit, existingDataSink, getDataSinkData]);\n\n // Handle form submission\n const handleSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n setSubmitError(null);\n\n // Validate form\n const validationErrors = validateForm(form);\n if (Object.keys(validationErrors).length > 0) {\n setErrors(validationErrors);\n return;\n }\n\n setIsSaving(true);\n try {\n // Parse data JSON\n let parsedData: Record<string, unknown> = {};\n if (form.data && form.data.trim()) {\n try {\n parsedData = JSON.parse(form.data) as Record<string, unknown>;\n } catch {\n setErrors((prev) => ({ ...prev, data: 'Invalid JSON format' }));\n setIsSaving(false);\n return;\n }\n }\n\n if (isEdit && existingDataSink) {\n // Update existing\n const input: UpdateDataSinkInput = {\n id: existingDataSink.id,\n version: existingDataSink.version,\n name: form.name,\n description: form.description || undefined,\n ttl: form.ttl,\n refresh: {\n type: form.refresh.type as DataSinkRefreshType,\n schedule: form.refresh.schedule,\n },\n data: parsedData, // Include data in update\n };\n const result = await updateDataSink(input);\n if (!result) {\n throw new Error('Failed to update DataSink');\n }\n } else {\n // Create new - simplified flow (standalone DataSinks)\n const input: CreateDataSinkInput = {\n key: form.key,\n name: form.name,\n description: form.description || undefined,\n ttl: form.ttl,\n refresh: {\n type: form.refresh.type as DataSinkRefreshType,\n schedule: form.refresh.schedule,\n },\n data: parsedData, // Include data in create\n };\n const result = await createDataSink(input);\n if (!result) {\n throw new Error('Failed to create DataSink');\n }\n }\n navigate('/datasinks');\n } catch (err) {\n setSubmitError(err instanceof Error ? err.message : 'Failed to save DataSink');\n } finally {\n setIsSaving(false);\n }\n },\n [form, isEdit, existingDataSink, createDataSink, updateDataSink, navigate]\n );\n\n // Check if form is valid\n const isFormValid = useMemo(() => {\n const nameValid = form.name.trim().length >= 2;\n const keyValid = form.key.trim().length > 0;\n\n // Check JSON validity if data is provided\n let dataValid = true;\n if (form.data && form.data.trim()) {\n try {\n JSON.parse(form.data);\n } catch {\n dataValid = false;\n }\n }\n\n return nameValid && keyValid && dataValid;\n }, [form.name, form.key, form.data]);\n\n // Loading state\n if (loading && isEdit) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"w-8 h-8 text-text-secondary animate-spin\" />\n </div>\n );\n }\n\n // Error state\n if (error) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px] p-card-padding\">\n <div className=\"text-center\">\n <AlertCircle className=\"w-12 h-12 text-state-error mx-auto mb-spacing-md\" />\n <p className=\"text-state-error text-body-sm font-medium mb-spacing-sm\">Failed to load DataSink</p>\n <p className=\"text-text-tertiary text-body-xs\">{error.message}</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"min-h-screen bg-bg-canvas\">\n <div className=\"max-w-3xl mx-auto p-card-padding\">\n {/* Header */}\n <div className=\"mb-spacing-lg\">\n <Button onClick={handleBack} variant=\"ghost\" size=\"sm\" className=\"mb-spacing-md\">\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n Back to DataSinks\n </Button>\n <h1 className=\"text-heading-h1 font-bold text-text-primary\">\n {isEdit ? 'Edit DataSink' : 'Create DataSink'}\n </h1>\n <p className=\"text-body-sm text-text-secondary mt-spacing-xs\">\n {isEdit\n ? 'Update the DataSink configuration.'\n : 'Create a new DataSink to store and cache data. Use the unique key to connect workflows.'}\n </p>\n </div>\n\n {/* Error Alert */}\n {submitError && (\n <div className=\"mb-spacing-lg p-4 bg-state-error/10 border border-state-error/30 rounded-lg flex items-start gap-3\">\n <AlertCircle className=\"w-5 h-5 text-state-error flex-shrink-0 mt-0.5\" />\n <div>\n <p className=\"text-sm font-medium text-state-error\">Failed to save DataSink</p>\n <p className=\"text-xs text-state-error/80 mt-1\">{submitError}</p>\n </div>\n </div>\n )}\n\n <PagePurpose className=\"mb-spacing-lg\">\n A data sink is the named, structured store your dashboards read from. Give it a unique key, configure how long\n data is cached and how it refreshes, then point a workflow at that key to push data in. Get the basics right\n here and everything downstream connects to it.\n </PagePurpose>\n\n {/* Form */}\n <form onSubmit={handleSubmit} className=\"space-y-spacing-lg\">\n {/* Basic Information — primary emphasis zone */}\n <GlassCard treatment=\"glass\" glow>\n <CardHeader className=\"pb-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full bg-action-primary-bg/10 flex items-center justify-center\">\n <Database className=\"w-4 h-4 text-action-primary-bg\" />\n </div>\n <div>\n <CardTitle className=\"text-base\">Basic Information</CardTitle>\n <p className=\"text-xs text-text-secondary mt-0.5\">Essential details for your DataSink</p>\n </div>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n {/* Name field */}\n <div className=\"space-y-spacing-sm\">\n <Label htmlFor=\"name\">\n Name <span className=\"text-state-error\">*</span>\n </Label>\n <Input\n id=\"name\"\n type=\"text\"\n value={form.name}\n onChange={handleNameChange}\n placeholder=\"e.g., Sales Metrics\"\n className={errors.name ? 'border-state-error' : ''}\n />\n {errors.name && <p className=\"text-body-xs text-state-error\">{errors.name}</p>}\n </div>\n\n {/* Key field */}\n <div className=\"space-y-spacing-sm\">\n <Label htmlFor=\"key\">\n Unique Key <span className=\"text-state-error\">*</span>\n </Label>\n <div className=\"flex gap-2\">\n <Input\n id=\"key\"\n type=\"text\"\n value={form.key}\n onChange={handleKeyChange}\n placeholder=\"sales_metrics\"\n disabled={isEdit}\n className={`font-mono flex-1 ${errors.key ? 'border-state-error' : ''}`}\n />\n {form.key && (\n <Button type=\"button\" onClick={handleCopyKey} variant=\"outline\" size=\"sm\" className=\"px-3\">\n {keyCopied ? <Check className=\"w-4 h-4 text-state-success\" /> : <Copy className=\"w-4 h-4\" />}\n </Button>\n )}\n </div>\n {errors.key && <p className=\"text-body-xs text-state-error\">{errors.key}</p>}\n <p className=\"text-body-xs text-text-tertiary\">\n {isEdit\n ? 'Key cannot be changed. Use this key in your workflows to push data.'\n : 'Auto-generated from name. This key will be used by workflows to push data.'}\n </p>\n </div>\n\n {/* Description field */}\n <div className=\"space-y-spacing-sm\">\n <Label htmlFor=\"description\">Description</Label>\n <Textarea\n id=\"description\"\n value={form.description}\n onChange={handleDescriptionChange}\n placeholder=\"Describe what data this sink stores...\"\n rows={2}\n className=\"resize-none\"\n />\n </div>\n </CardContent>\n </GlassCard>\n\n {/* Configuration */}\n <Card>\n <CardHeader className=\"pb-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full bg-action-primary-bg/10 flex items-center justify-center\">\n <Settings className=\"w-4 h-4 text-action-primary-bg\" />\n </div>\n <div>\n <CardTitle className=\"text-base\">Configuration</CardTitle>\n <p className=\"text-xs text-text-secondary mt-0.5\">Cache and refresh settings</p>\n </div>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n {/* Cache TTL */}\n <div className=\"space-y-spacing-sm\">\n <Label>Cache Duration</Label>\n <CacheTTLSelector value={form.ttl} onChange={handleTTLChange} />\n <p className=\"text-body-xs text-text-tertiary\">\n How long cached data remains valid before requiring refresh.\n </p>\n </div>\n\n {/* Refresh Type */}\n <RefreshTypeSelector value={form.refresh} onChange={handleRefreshChange} />\n </CardContent>\n </Card>\n\n {/* Data Payload */}\n <Card>\n <CardHeader className=\"pb-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full bg-action-primary-bg/10 flex items-center justify-center\">\n <Database className=\"w-4 h-4 text-action-primary-bg\" />\n </div>\n <div>\n <CardTitle className=\"text-base\">Data Payload</CardTitle>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n {isEdit ? 'Edit the JSON data stored in this DataSink' : 'Optional initial data (JSON format)'}\n </p>\n </div>\n </div>\n <Button type=\"button\" onClick={handleFormatData} variant=\"outline\" size=\"sm\">\n Format JSON\n </Button>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-spacing-sm\">\n <Textarea\n value={form.data}\n onChange={handleDataChange}\n placeholder='{\"key\": \"value\"}'\n rows={10}\n className={`font-mono text-xs resize-y ${errors.data ? 'border-state-error' : ''}`}\n />\n {errors.data && <p className=\"text-body-xs text-state-error\">{errors.data}</p>}\n <p className=\"text-body-xs text-text-tertiary\">\n Enter valid JSON data. This data will be stored and can be accessed by widgets via parsers.\n </p>\n </div>\n </CardContent>\n </Card>\n\n {/* Data Preview - Only show for existing DataSinks */}\n {isEdit && existingDataSink && (\n <Card>\n <CardHeader className=\"pb-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full bg-action-primary-bg/10 flex items-center justify-center\">\n <Eye className=\"w-4 h-4 text-action-primary-bg\" />\n </div>\n <div>\n <CardTitle className=\"text-base\">Data Preview</CardTitle>\n <p className=\"text-xs text-text-secondary mt-0.5\">Preview the first 20 records</p>\n </div>\n </div>\n <Button\n type=\"button\"\n onClick={handleShowData}\n variant=\"outline\"\n size=\"sm\"\n disabled={isLoadingPreview}\n >\n {isLoadingPreview ? (\n <>\n <Loader2 className=\"w-4 h-4 mr-2 animate-spin\" />\n Loading...\n </>\n ) : (\n <>\n <Eye className=\"w-4 h-4 mr-2\" />\n Show Data\n </>\n )}\n </Button>\n </div>\n </CardHeader>\n {(previewData || previewError) && (\n <CardContent>\n {previewError ? (\n <div className=\"p-4 bg-state-error/10 border border-state-error/30 rounded-md\">\n <p className=\"text-sm text-state-error\">{previewError}</p>\n </div>\n ) : previewData && previewData.length > 0 ? (\n <pre className=\"p-4 bg-bg-sunken rounded-md overflow-auto max-h-[300px] text-xs font-mono\">\n {JSON.stringify(previewData, null, 2)}\n </pre>\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No data available yet\"\n description=\"Connect a workflow to this sink's key to start pushing data.\"\n />\n )}\n </CardContent>\n )}\n </Card>\n )}\n\n {/* Submit */}\n <div className=\"flex items-center justify-end gap-spacing-md pt-spacing-md\">\n <Button type=\"button\" onClick={handleBack} variant=\"secondary\">\n Cancel\n </Button>\n <Button type=\"submit\" disabled={!isFormValid || isSaving} variant=\"default\">\n {isSaving ? (\n <>\n <Loader2 className=\"w-4 h-4 mr-2 animate-spin\" />\n Saving...\n </>\n ) : (\n <>\n <Save className=\"w-4 h-4 mr-2\" />\n {isEdit ? 'Save Changes' : 'Create DataSink'}\n </>\n )}\n </Button>\n </div>\n </form>\n\n {/* Info Box for New DataSinks */}\n {!isEdit && (\n <div className=\"mt-spacing-lg p-4 bg-bg-sunken rounded-lg border border-border-default\">\n <h3 className=\"text-sm font-medium text-text-primary mb-2\">What&apos;s Next?</h3>\n <ol className=\"list-decimal list-inside text-xs text-text-secondary space-y-1\">\n <li>Create this DataSink to get your unique key</li>\n <li>\n Go to <strong>Workflows</strong> and create a FluidGrids workflow\n </li>\n <li>Use the unique key in your workflow to push data to this DataSink</li>\n <li>\n Create <strong>Parsers</strong> to transform the data using JQ or JSONata\n </li>\n </ol>\n </div>\n )}\n </div>\n </div>\n );\n});\n\nexport default DataSinkBuilderPage;\n"],"mappings":"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"}
1
+ {"version":3,"file":"DataSinkBuilderPage.js","names":[],"sources":["../../../src/bigconsole/pages/DataSinkBuilderPage.tsx"],"sourcesContent":["/**\n * DataSink Builder Page\n *\n * Simplified page for creating and editing DataSinks.\n * DataSinks are now standalone - workflows push data to them via unique keys.\n * Required fields: name, key\n */\n\nimport { memo, useState, useMemo, useEffect, useCallback } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useAppNavigate } from '../contexts';\nimport { isNative, nativeCopyText, nativeToast } from '../../utils/nativeBridge';\nimport { useDataSinkOperations } from '../hooks/useDataSinkOperations';\nimport { CacheTTLSelector } from '../components/datasink/CacheTTLSelector';\nimport { RefreshTypeSelector } from '../components/datasink/RefreshTypeSelector';\nimport type { CreateDataSinkInput, UpdateDataSinkInput, DataSinkRefreshType } from '../types';\n\n// fe-libs UI components\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Input } from '@burdenoff/fe-libs/ui';\nimport { Textarea } from '@burdenoff/fe-libs/ui';\nimport { Label } from '@burdenoff/fe-libs/ui';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { GlassCard, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\n// Lucide icons\nimport { ArrowLeft, Save, Loader2, AlertCircle, Database, Settings, Eye, Copy, Check } from 'lucide-react';\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\ninterface RefreshConfig {\n type: 'manual' | 'scheduled';\n schedule?: {\n cron: string;\n timezone: string;\n };\n}\n\ninterface FormState {\n name: string;\n key: string;\n description: string;\n ttl: number;\n refresh: RefreshConfig;\n data: string; // JSON string for data payload\n}\n\ninterface FormErrors {\n name?: string;\n key?: string;\n data?: string;\n}\n\n// ============================================================================\n// HELPERS\n// ============================================================================\n\n/**\n * Generate a key from name (slug format)\n */\nfunction generateKey(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .substring(0, 64);\n}\n\n/**\n * Validate form data\n */\nfunction validateForm(form: FormState): FormErrors {\n const errors: FormErrors = {};\n\n if (!form.name.trim()) {\n errors.name = 'Name is required';\n } else if (form.name.length < 2) {\n errors.name = 'Name must be at least 2 characters';\n } else if (form.name.length > 100) {\n errors.name = 'Name must be less than 100 characters';\n }\n\n if (!form.key.trim()) {\n errors.key = 'Key is required';\n } else if (!/^[a-z0-9_]+$/.test(form.key)) {\n errors.key = 'Key can only contain lowercase letters, numbers, and underscores';\n } else if (form.key.length > 64) {\n errors.key = 'Key must be less than 64 characters';\n }\n\n // Validate JSON data if provided\n if (form.data && form.data.trim()) {\n try {\n JSON.parse(form.data);\n } catch {\n errors.data = 'Invalid JSON format';\n }\n }\n\n return errors;\n}\n\n// ============================================================================\n// COMPONENT\n// ============================================================================\n\nexport const DataSinkBuilderPage = memo(function DataSinkBuilderPage() {\n const { dataSinkId } = useParams<{ dataSinkId?: string }>();\n const navigate = useAppNavigate();\n\n const isEdit = dataSinkId !== 'new' && !!dataSinkId;\n\n // Form state\n const [form, setForm] = useState<FormState>({\n name: '',\n key: '',\n description: '',\n ttl: 300, // 5 minutes default\n refresh: {\n type: 'manual',\n },\n data: '{}', // Default empty JSON object\n });\n const [errors, setErrors] = useState<FormErrors>({});\n const [keyManuallyEdited, setKeyManuallyEdited] = useState(false);\n const [isSaving, setIsSaving] = useState(false);\n const [submitError, setSubmitError] = useState<string | null>(null);\n const [keyCopied, setKeyCopied] = useState(false);\n\n // Data preview state\n const [isLoadingPreview, setIsLoadingPreview] = useState(false);\n const [previewData, setPreviewData] = useState<unknown[] | null>(null);\n const [previewError, setPreviewError] = useState<string | null>(null);\n\n // Operations\n const { loading, error, dataSinks, fetchDataSink, createDataSink, updateDataSink, getDataSinkData } =\n useDataSinkOperations({\n skipFetch: true,\n });\n\n // Get existing data sink if editing\n const existingDataSink = isEdit ? dataSinks.find((ds) => ds.id === dataSinkId) : null;\n\n // Load existing data sink for editing\n useEffect(() => {\n if (isEdit && dataSinkId && !existingDataSink) {\n fetchDataSink(dataSinkId);\n }\n }, [isEdit, dataSinkId, existingDataSink, fetchDataSink]);\n\n // Track if form has been initialized\n const [formInitialized, setFormInitialized] = useState(false);\n\n // Populate form with existing data when editing (only once)\n useEffect(() => {\n if (existingDataSink && !formInitialized) {\n // Format existing data as pretty JSON string\n let dataString = '';\n if (existingDataSink.data) {\n try {\n // Check if data is already a string (JSON string from API)\n if (typeof existingDataSink.data === 'string') {\n // Parse and re-stringify to ensure proper formatting\n const parsed = JSON.parse(existingDataSink.data);\n dataString = JSON.stringify(parsed, null, 2);\n } else {\n // It's an object, just stringify it\n dataString = JSON.stringify(existingDataSink.data, null, 2);\n }\n } catch {\n // If parsing fails, use as-is if string, otherwise empty\n dataString = typeof existingDataSink.data === 'string' ? existingDataSink.data : '';\n }\n }\n\n setForm({\n name: existingDataSink.name || '',\n key: existingDataSink.key || '',\n description: existingDataSink.description || '',\n ttl: existingDataSink.ttl || 300,\n refresh: existingDataSink.refresh\n ? {\n type: existingDataSink.refresh.type === 'scheduled' ? 'scheduled' : 'manual',\n schedule: existingDataSink.refresh.schedule\n ? {\n cron: existingDataSink.refresh.schedule.cron,\n timezone: existingDataSink.refresh.schedule.timezone,\n }\n : undefined,\n }\n : { type: 'manual' },\n data: dataString,\n });\n setKeyManuallyEdited(true); // Don't auto-generate key when editing\n setFormInitialized(true); // Mark as initialized\n setErrors({}); // Clear any errors\n }\n }, [existingDataSink, formInitialized]);\n\n // Auto-generate key from name\n const handleNameChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const name = e.target.value;\n setForm((prev) => ({\n ...prev,\n name,\n key: keyManuallyEdited ? prev.key : generateKey(name),\n }));\n setErrors((prev) => ({ ...prev, name: undefined }));\n },\n [keyManuallyEdited]\n );\n\n // Handle key change\n const handleKeyChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {\n const key = e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '');\n setForm((prev) => ({ ...prev, key }));\n setKeyManuallyEdited(true);\n setErrors((prev) => ({ ...prev, key: undefined }));\n }, []);\n\n // Handle description change\n const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setForm((prev) => ({ ...prev, description: e.target.value }));\n }, []);\n\n // Handle TTL change\n const handleTTLChange = useCallback((ttl: number | null) => {\n setForm((prev) => ({ ...prev, ttl: ttl || 0 }));\n }, []);\n\n // Handle refresh config change\n const handleRefreshChange = useCallback((config: RefreshConfig) => {\n setForm((prev) => ({ ...prev, refresh: config }));\n }, []);\n\n // Handle data change\n const handleDataChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setForm((prev) => ({ ...prev, data: e.target.value }));\n setErrors((prev) => ({ ...prev, data: undefined }));\n }, []);\n\n // Format JSON data\n const handleFormatData = useCallback(() => {\n try {\n const parsed = JSON.parse(form.data);\n setForm((prev) => ({ ...prev, data: JSON.stringify(parsed, null, 2) }));\n setErrors((prev) => ({ ...prev, data: undefined }));\n } catch {\n setErrors((prev) => ({ ...prev, data: 'Invalid JSON format' }));\n }\n }, [form.data]);\n\n // Copy key to clipboard\n const handleCopyKey = useCallback(async () => {\n if (form.key) {\n await nativeCopyText(form.key);\n setKeyCopied(true);\n if (isNative()) void nativeToast('Key copied');\n setTimeout(() => setKeyCopied(false), 2000);\n }\n }, [form.key]);\n\n // Handle back navigation\n const handleBack = useCallback(() => {\n navigate('/datasinks');\n }, [navigate]);\n\n // Load data preview\n const handleShowData = useCallback(async () => {\n if (!isEdit || !existingDataSink) return;\n\n setIsLoadingPreview(true);\n setPreviewError(null);\n\n try {\n const result = await getDataSinkData({ key: existingDataSink.key });\n if (result?.raw) {\n // Get first 20 items if it's an array, otherwise wrap in array\n const data = Array.isArray(result.raw) ? result.raw.slice(0, 20) : [result.raw];\n setPreviewData(data);\n } else {\n setPreviewData([]);\n }\n } catch (err) {\n setPreviewError(err instanceof Error ? err.message : 'Failed to load data');\n } finally {\n setIsLoadingPreview(false);\n }\n }, [isEdit, existingDataSink, getDataSinkData]);\n\n // Handle form submission\n const handleSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n setSubmitError(null);\n\n // Validate form\n const validationErrors = validateForm(form);\n if (Object.keys(validationErrors).length > 0) {\n setErrors(validationErrors);\n return;\n }\n\n setIsSaving(true);\n try {\n // Parse data JSON\n let parsedData: Record<string, unknown> = {};\n if (form.data && form.data.trim()) {\n try {\n parsedData = JSON.parse(form.data) as Record<string, unknown>;\n } catch {\n setErrors((prev) => ({ ...prev, data: 'Invalid JSON format' }));\n setIsSaving(false);\n return;\n }\n }\n\n if (isEdit && existingDataSink) {\n // Update existing\n const input: UpdateDataSinkInput = {\n id: existingDataSink.id,\n version: existingDataSink.version,\n name: form.name,\n description: form.description || undefined,\n ttl: form.ttl,\n refresh: {\n type: form.refresh.type as DataSinkRefreshType,\n schedule: form.refresh.schedule,\n },\n data: parsedData, // Include data in update\n };\n const result = await updateDataSink(input);\n if (!result) {\n throw new Error('Failed to update DataSink');\n }\n } else {\n // Create new - simplified flow (standalone DataSinks)\n const input: CreateDataSinkInput = {\n key: form.key,\n name: form.name,\n description: form.description || undefined,\n ttl: form.ttl,\n refresh: {\n type: form.refresh.type as DataSinkRefreshType,\n schedule: form.refresh.schedule,\n },\n data: parsedData, // Include data in create\n };\n const result = await createDataSink(input);\n if (!result) {\n throw new Error('Failed to create DataSink');\n }\n }\n navigate('/datasinks');\n } catch (err) {\n setSubmitError(err instanceof Error ? err.message : 'Failed to save DataSink');\n } finally {\n setIsSaving(false);\n }\n },\n [form, isEdit, existingDataSink, createDataSink, updateDataSink, navigate]\n );\n\n // Check if form is valid\n const isFormValid = useMemo(() => {\n const nameValid = form.name.trim().length >= 2;\n const keyValid = form.key.trim().length > 0;\n\n // Check JSON validity if data is provided\n let dataValid = true;\n if (form.data && form.data.trim()) {\n try {\n JSON.parse(form.data);\n } catch {\n dataValid = false;\n }\n }\n\n return nameValid && keyValid && dataValid;\n }, [form.name, form.key, form.data]);\n\n // Loading state\n if (loading && isEdit) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"w-8 h-8 text-text-secondary animate-spin\" />\n </div>\n );\n }\n\n // Error state\n if (error) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px] p-card-padding\">\n <div className=\"text-center\">\n <AlertCircle className=\"w-12 h-12 text-state-error mx-auto mb-4\" />\n <p className=\"text-state-error text-body-sm font-medium mb-2\">Failed to load DataSink</p>\n <p className=\"text-text-tertiary text-body-xs\">{error.message}</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"min-h-screen bg-bg-canvas\">\n <div className=\"max-w-3xl mx-auto p-card-padding\">\n {/* Header */}\n <div className=\"mb-6\">\n <Button onClick={handleBack} variant=\"ghost\" size=\"sm\" className=\"mb-4\">\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n Back to DataSinks\n </Button>\n <h1 className=\"text-heading-h1 font-bold text-text-primary\">\n {isEdit ? 'Edit DataSink' : 'Create DataSink'}\n </h1>\n <p className=\"text-body-sm text-text-secondary mt-1\">\n {isEdit\n ? 'Update the DataSink configuration.'\n : 'Create a new DataSink to store and cache data. Use the unique key to connect workflows.'}\n </p>\n </div>\n\n {/* Error Alert */}\n {submitError && (\n <div className=\"mb-6 p-4 bg-state-error/10 border border-state-error/30 rounded-lg flex items-start gap-3\">\n <AlertCircle className=\"w-5 h-5 text-state-error flex-shrink-0 mt-0.5\" />\n <div>\n <p className=\"text-sm font-medium text-state-error\">Failed to save DataSink</p>\n <p className=\"text-xs text-state-error/80 mt-1\">{submitError}</p>\n </div>\n </div>\n )}\n\n <PagePurpose className=\"mb-6\">\n A data sink is the named, structured store your dashboards read from. Give it a unique key, configure how long\n data is cached and how it refreshes, then point a workflow at that key to push data in. Get the basics right\n here and everything downstream connects to it.\n </PagePurpose>\n\n {/* Form */}\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Basic Information — primary emphasis zone */}\n <GlassCard treatment=\"glass\" glow>\n <CardHeader className=\"pb-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full bg-action-primary-bg/10 flex items-center justify-center\">\n <Database className=\"w-4 h-4 text-action-primary-bg\" />\n </div>\n <div>\n <CardTitle className=\"text-base\">Basic Information</CardTitle>\n <p className=\"text-xs text-text-secondary mt-0.5\">Essential details for your DataSink</p>\n </div>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n {/* Name field */}\n <div className=\"space-y-2\">\n <Label htmlFor=\"name\">\n Name <span className=\"text-state-error\">*</span>\n </Label>\n <Input\n id=\"name\"\n type=\"text\"\n value={form.name}\n onChange={handleNameChange}\n placeholder=\"e.g., Sales Metrics\"\n className={errors.name ? 'border-state-error' : ''}\n />\n {errors.name && <p className=\"text-body-xs text-state-error\">{errors.name}</p>}\n </div>\n\n {/* Key field */}\n <div className=\"space-y-2\">\n <Label htmlFor=\"key\">\n Unique Key <span className=\"text-state-error\">*</span>\n </Label>\n <div className=\"flex gap-2\">\n <Input\n id=\"key\"\n type=\"text\"\n value={form.key}\n onChange={handleKeyChange}\n placeholder=\"sales_metrics\"\n disabled={isEdit}\n className={`font-mono flex-1 ${errors.key ? 'border-state-error' : ''}`}\n />\n {form.key && (\n <Button type=\"button\" onClick={handleCopyKey} variant=\"outline\" size=\"sm\" className=\"px-3\">\n {keyCopied ? <Check className=\"w-4 h-4 text-state-success\" /> : <Copy className=\"w-4 h-4\" />}\n </Button>\n )}\n </div>\n {errors.key && <p className=\"text-body-xs text-state-error\">{errors.key}</p>}\n <p className=\"text-body-xs text-text-tertiary\">\n {isEdit\n ? 'Key cannot be changed. Use this key in your workflows to push data.'\n : 'Auto-generated from name. This key will be used by workflows to push data.'}\n </p>\n </div>\n\n {/* Description field */}\n <div className=\"space-y-2\">\n <Label htmlFor=\"description\">Description</Label>\n <Textarea\n id=\"description\"\n value={form.description}\n onChange={handleDescriptionChange}\n placeholder=\"Describe what data this sink stores...\"\n rows={2}\n className=\"resize-none\"\n />\n </div>\n </CardContent>\n </GlassCard>\n\n {/* Configuration */}\n <Card>\n <CardHeader className=\"pb-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full bg-action-primary-bg/10 flex items-center justify-center\">\n <Settings className=\"w-4 h-4 text-action-primary-bg\" />\n </div>\n <div>\n <CardTitle className=\"text-base\">Configuration</CardTitle>\n <p className=\"text-xs text-text-secondary mt-0.5\">Cache and refresh settings</p>\n </div>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n {/* Cache TTL */}\n <div className=\"space-y-2\">\n <Label>Cache Duration</Label>\n <CacheTTLSelector value={form.ttl} onChange={handleTTLChange} />\n <p className=\"text-body-xs text-text-tertiary\">\n How long cached data remains valid before requiring refresh.\n </p>\n </div>\n\n {/* Refresh Type */}\n <RefreshTypeSelector value={form.refresh} onChange={handleRefreshChange} />\n </CardContent>\n </Card>\n\n {/* Data Payload */}\n <Card>\n <CardHeader className=\"pb-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full bg-action-primary-bg/10 flex items-center justify-center\">\n <Database className=\"w-4 h-4 text-action-primary-bg\" />\n </div>\n <div>\n <CardTitle className=\"text-base\">Data Payload</CardTitle>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n {isEdit ? 'Edit the JSON data stored in this DataSink' : 'Optional initial data (JSON format)'}\n </p>\n </div>\n </div>\n <Button type=\"button\" onClick={handleFormatData} variant=\"outline\" size=\"sm\">\n Format JSON\n </Button>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n <Textarea\n value={form.data}\n onChange={handleDataChange}\n placeholder='{\"key\": \"value\"}'\n rows={10}\n className={`font-mono text-xs resize-y ${errors.data ? 'border-state-error' : ''}`}\n />\n {errors.data && <p className=\"text-body-xs text-state-error\">{errors.data}</p>}\n <p className=\"text-body-xs text-text-tertiary\">\n Enter valid JSON data. This data will be stored and can be accessed by widgets via parsers.\n </p>\n </div>\n </CardContent>\n </Card>\n\n {/* Data Preview - Only show for existing DataSinks */}\n {isEdit && existingDataSink && (\n <Card>\n <CardHeader className=\"pb-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full bg-action-primary-bg/10 flex items-center justify-center\">\n <Eye className=\"w-4 h-4 text-action-primary-bg\" />\n </div>\n <div>\n <CardTitle className=\"text-base\">Data Preview</CardTitle>\n <p className=\"text-xs text-text-secondary mt-0.5\">Preview the first 20 records</p>\n </div>\n </div>\n <Button\n type=\"button\"\n onClick={handleShowData}\n variant=\"outline\"\n size=\"sm\"\n disabled={isLoadingPreview}\n >\n {isLoadingPreview ? (\n <>\n <Loader2 className=\"w-4 h-4 mr-2 animate-spin\" />\n Loading...\n </>\n ) : (\n <>\n <Eye className=\"w-4 h-4 mr-2\" />\n Show Data\n </>\n )}\n </Button>\n </div>\n </CardHeader>\n {(previewData || previewError) && (\n <CardContent>\n {previewError ? (\n <div className=\"p-4 bg-state-error/10 border border-state-error/30 rounded-md\">\n <p className=\"text-sm text-state-error\">{previewError}</p>\n </div>\n ) : previewData && previewData.length > 0 ? (\n <pre className=\"p-4 bg-bg-sunken rounded-md overflow-auto max-h-[300px] text-xs font-mono\">\n {JSON.stringify(previewData, null, 2)}\n </pre>\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No data available yet\"\n description=\"Connect a workflow to this sink's key to start pushing data.\"\n />\n )}\n </CardContent>\n )}\n </Card>\n )}\n\n {/* Submit */}\n <div className=\"flex items-center justify-end gap-4 pt-4\">\n <Button type=\"button\" onClick={handleBack} variant=\"secondary\">\n Cancel\n </Button>\n <Button type=\"submit\" disabled={!isFormValid || isSaving} variant=\"default\">\n {isSaving ? (\n <>\n <Loader2 className=\"w-4 h-4 mr-2 animate-spin\" />\n Saving...\n </>\n ) : (\n <>\n <Save className=\"w-4 h-4 mr-2\" />\n {isEdit ? 'Save Changes' : 'Create DataSink'}\n </>\n )}\n </Button>\n </div>\n </form>\n\n {/* Info Box for New DataSinks */}\n {!isEdit && (\n <div className=\"mt-6 p-4 bg-bg-sunken rounded-lg border border-border-default\">\n <h3 className=\"text-sm font-medium text-text-primary mb-2\">What&apos;s Next?</h3>\n <ol className=\"list-decimal list-inside text-xs text-text-secondary space-y-1\">\n <li>Create this DataSink to get your unique key</li>\n <li>\n Go to <strong>Workflows</strong> and create a FluidGrids workflow\n </li>\n <li>Use the unique key in your workflow to push data to this DataSink</li>\n <li>\n Create <strong>Parsers</strong> to transform the data using JQ or JSONata\n </li>\n </ol>\n </div>\n )}\n </div>\n </div>\n );\n});\n\nexport default DataSinkBuilderPage;\n"],"mappings":"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"}