@burdenoff/microfe-bigconsole 2026.804.2 → 2026.805.1

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.
@@ -1,66 +1,233 @@
1
- import { memo as e } from "react";
2
- import { ArrowRight as t, Upload as n, Workflow as r } from "lucide-react";
3
- import { Button as i, Dialog as a, DialogContent as o, DialogDescription as s, DialogFooter as c, DialogHeader as l, DialogTitle as u } from "@burdenoff/fe-libs/ui";
4
- import { jsx as d, jsxs as f } from "react/jsx-runtime";
1
+ import { parseCsv as e } from "../../utils/csv.js";
2
+ import { memo as t, useCallback as n, useEffect as r, useRef as i, useState as a } from "react";
3
+ import { AlertTriangle as o, ArrowRight as s, FileUp as c, Loader2 as l, Upload as u, Workflow as d } from "lucide-react";
4
+ import { Button as f, Dialog as p, DialogContent as m, DialogDescription as h, DialogFooter as g, DialogHeader as _, DialogTitle as v } from "@burdenoff/fe-libs/ui";
5
+ import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
5
6
  //#region src/bigconsole/components/datasink/ImportDataGuideDialog.tsx
6
- var p = [
7
- "Go to the Workflows page and create a new workflow.",
8
- "Add a source node to fetch your data, then a node that writes to this data sink.",
9
- "Run the workflow — the imported data appears here in the data preview."
10
- ], m = e(function({ isOpen: e, dataSinkName: m, onClose: h, onGoToWorkflows: g }) {
11
- return /* @__PURE__ */ d(a, {
12
- open: e,
13
- onOpenChange: (e) => !e && h(),
14
- children: /* @__PURE__ */ f(o, {
7
+ var S = 6, C = 1e7, w = 5e4, T = t(function({ isOpen: t, dataSinkName: T, hasExistingData: E, onClose: D, onGoToWorkflows: O, onImportCsv: k }) {
8
+ let A = i(null), j = i(0), [M, N] = a(null), [P, F] = a(null), [I, L] = a(null), [R, z] = a(null), [B, V] = a(!1), H = n(() => {
9
+ j.current += 1, N(null), F(null), L(null), z(null), V(!1);
10
+ }, []);
11
+ r(() => {
12
+ t || H();
13
+ }, [t, H]);
14
+ let U = n(async (t) => {
15
+ let n = t.target.files?.[0];
16
+ if (t.target.value = "", !n) return;
17
+ let r = j.current += 1;
18
+ if (F(null), L(null), z(null), N(n.name), n.size > C) {
19
+ L("This CSV is larger than 10 MB. Import large datasets with a workflow instead.");
20
+ return;
21
+ }
22
+ try {
23
+ let t = await n.text();
24
+ if (r !== j.current) return;
25
+ let i = e(t);
26
+ if (i.rows.length > w) {
27
+ L(`This CSV has ${i.rows.length.toLocaleString()} rows (limit ${w.toLocaleString()}). Import large datasets with a workflow instead.`);
28
+ return;
29
+ }
30
+ F(i);
31
+ } catch (e) {
32
+ if (r !== j.current) return;
33
+ F(null), L(e instanceof Error ? e.message : "Failed to parse the CSV file.");
34
+ }
35
+ }, []), W = n(async () => {
36
+ if (!(!P || !k)) {
37
+ V(!0), z(null);
38
+ try {
39
+ await k(P.rows) ? D() : z("Import failed. Please try again.");
40
+ } catch (e) {
41
+ z(e instanceof Error ? e.message : "Import failed. Please try again.");
42
+ } finally {
43
+ V(!1);
44
+ }
45
+ }
46
+ }, [
47
+ P,
48
+ k,
49
+ D
50
+ ]), G = n(() => A.current?.click(), []), K = P?.columns.slice(0, S) ?? [], q = P ? P.columns.length - K.length : 0;
51
+ return /* @__PURE__ */ b(p, {
52
+ open: t,
53
+ onOpenChange: (e) => !e && !B && D(),
54
+ children: /* @__PURE__ */ x(m, {
15
55
  className: "sm:max-w-lg",
16
56
  children: [
17
- /* @__PURE__ */ f(l, { children: [/* @__PURE__ */ f("div", {
57
+ /* @__PURE__ */ x(_, { children: [/* @__PURE__ */ x("div", {
18
58
  className: "flex items-center gap-3",
19
- children: [/* @__PURE__ */ d("div", {
59
+ children: [/* @__PURE__ */ b("div", {
20
60
  className: "flex size-10 items-center justify-center rounded-lg bg-action-primary-bg/10",
21
- children: /* @__PURE__ */ d(n, { className: "size-5 text-action-primary-bg" })
22
- }), /* @__PURE__ */ d(u, { children: "Import data" })]
23
- }), /* @__PURE__ */ d(s, { children: m ? `Populate “${m} with a workflow` : "Populate this data sink with a workflow" })] }),
24
- /* @__PURE__ */ f("div", {
25
- className: "py-2 space-y-4",
26
- children: [/* @__PURE__ */ f("p", {
27
- className: "text-sm text-text-primary leading-relaxed",
28
- children: [
29
- "Data is imported into a data sink by building a ",
30
- /* @__PURE__ */ d("strong", { children: "workflow" }),
31
- ". A workflow fetches data from your source (an API, database, file, or app) and writes it into this data sink so your dashboards can read it."
32
- ]
33
- }), /* @__PURE__ */ d("ol", {
34
- className: "space-y-3",
35
- children: p.map((e, t) => /* @__PURE__ */ f("li", {
36
- className: "flex items-start gap-3",
37
- children: [/* @__PURE__ */ d("span", {
38
- className: "flex size-6 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-primary",
39
- children: t + 1
40
- }), /* @__PURE__ */ d("p", {
61
+ children: /* @__PURE__ */ b(u, { className: "size-5 text-action-primary-bg" })
62
+ }), /* @__PURE__ */ b(v, { children: "Import data" })]
63
+ }), /* @__PURE__ */ b(h, { children: T ? `Add data to “${T}”` : "Add data to this data sink" })] }),
64
+ /* @__PURE__ */ x("div", {
65
+ className: "py-2 space-y-5",
66
+ children: [
67
+ /* @__PURE__ */ x("section", {
68
+ className: "space-y-3",
69
+ children: [/* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("h3", {
70
+ className: "text-sm font-semibold text-text-primary",
71
+ children: "Build a workflow"
72
+ }), /* @__PURE__ */ b("p", {
41
73
  className: "text-sm text-text-secondary leading-relaxed",
42
- children: e
74
+ children: "The recommended way to keep this data sink up to date — build a repeatable, scheduled pipeline that fetches from your source (an API, database, or app) and writes into this data sink on every run."
75
+ })] }), /* @__PURE__ */ x(f, {
76
+ variant: "default",
77
+ onClick: O,
78
+ disabled: B,
79
+ className: "w-full",
80
+ children: [
81
+ /* @__PURE__ */ b(d, { className: "size-4" }),
82
+ "Go to Workflows",
83
+ /* @__PURE__ */ b(s, { className: "size-4" })
84
+ ]
43
85
  })]
44
- }, t))
45
- })]
86
+ }),
87
+ k && /* @__PURE__ */ x("div", {
88
+ className: "flex items-center gap-3",
89
+ children: [
90
+ /* @__PURE__ */ b("span", { className: "h-px flex-1 bg-border-subtle" }),
91
+ /* @__PURE__ */ b("span", {
92
+ className: "text-xs font-medium uppercase tracking-wide text-text-tertiary",
93
+ children: "Or"
94
+ }),
95
+ /* @__PURE__ */ b("span", { className: "h-px flex-1 bg-border-subtle" })
96
+ ]
97
+ }),
98
+ k && /* @__PURE__ */ x("section", {
99
+ className: "space-y-3",
100
+ children: [
101
+ /* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("h3", {
102
+ className: "text-sm font-semibold text-text-primary",
103
+ children: "Upload a CSV file"
104
+ }), /* @__PURE__ */ b("p", {
105
+ className: "text-sm text-text-secondary leading-relaxed",
106
+ children: "For a quick, one-off import, upload a spreadsheet export and it becomes available to your dashboards right away."
107
+ })] }),
108
+ E && /* @__PURE__ */ x("div", {
109
+ className: "flex items-start gap-2 rounded-lg border border-status-warning-border bg-status-warning-bg px-3 py-2",
110
+ children: [/* @__PURE__ */ b(o, { className: "size-4 shrink-0 text-status-warning-text mt-0.5" }), /* @__PURE__ */ b("p", {
111
+ className: "text-sm text-status-warning-text",
112
+ children: "This data sink already has data — importing a CSV will replace it."
113
+ })]
114
+ }),
115
+ /* @__PURE__ */ b("input", {
116
+ ref: A,
117
+ type: "file",
118
+ accept: ".csv,text/csv",
119
+ className: "hidden",
120
+ onChange: U,
121
+ "aria-label": "Choose a CSV file to import"
122
+ }),
123
+ !P && /* @__PURE__ */ x("button", {
124
+ type: "button",
125
+ onClick: G,
126
+ className: "flex w-full flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border-default bg-bg-sunken px-4 py-6 text-center transition-colors hover:border-action-primary-bg hover:bg-action-primary-bg/5",
127
+ children: [
128
+ /* @__PURE__ */ b(c, { className: "size-6 text-text-tertiary" }),
129
+ /* @__PURE__ */ b("span", {
130
+ className: "text-sm font-medium text-text-primary",
131
+ children: M ? "Choose a different CSV file" : "Choose a CSV file"
132
+ }),
133
+ /* @__PURE__ */ b("span", {
134
+ className: "text-xs text-text-tertiary",
135
+ children: "The first row is used as column names."
136
+ })
137
+ ]
138
+ }),
139
+ I && /* @__PURE__ */ b("div", {
140
+ className: "rounded-lg border border-status-error-border bg-status-error-bg px-3 py-2",
141
+ children: /* @__PURE__ */ b("p", {
142
+ className: "text-sm text-status-error-text",
143
+ children: I
144
+ })
145
+ }),
146
+ P && /* @__PURE__ */ x("div", {
147
+ className: "space-y-3 rounded-lg border border-border-default bg-bg-sunken p-4",
148
+ children: [
149
+ /* @__PURE__ */ x("div", {
150
+ className: "flex items-center justify-between gap-3",
151
+ children: [/* @__PURE__ */ b("span", {
152
+ className: "truncate text-sm font-medium text-text-primary",
153
+ title: M ?? void 0,
154
+ children: M
155
+ }), /* @__PURE__ */ b("button", {
156
+ type: "button",
157
+ onClick: G,
158
+ disabled: B,
159
+ className: "shrink-0 text-xs font-medium text-action-primary-bg hover:underline disabled:cursor-not-allowed disabled:opacity-50 disabled:no-underline",
160
+ children: "Change file"
161
+ })]
162
+ }),
163
+ /* @__PURE__ */ x("p", {
164
+ className: "text-sm text-text-secondary",
165
+ children: [
166
+ "Ready to import ",
167
+ /* @__PURE__ */ b("strong", {
168
+ className: "text-text-primary",
169
+ children: P.rows.length
170
+ }),
171
+ " ",
172
+ P.rows.length === 1 ? "row" : "rows",
173
+ " ×",
174
+ " ",
175
+ /* @__PURE__ */ b("strong", {
176
+ className: "text-text-primary",
177
+ children: P.columns.length
178
+ }),
179
+ " ",
180
+ P.columns.length === 1 ? "column" : "columns",
181
+ "."
182
+ ]
183
+ }),
184
+ /* @__PURE__ */ x("div", {
185
+ className: "flex flex-wrap gap-1.5",
186
+ children: [K.map((e) => /* @__PURE__ */ b("span", {
187
+ className: "rounded-full bg-bg-surface px-2 py-0.5 text-xs text-text-secondary border border-border-subtle",
188
+ children: e
189
+ }, e)), q > 0 && /* @__PURE__ */ x("span", {
190
+ className: "rounded-full px-2 py-0.5 text-xs text-text-tertiary",
191
+ children: [
192
+ "+",
193
+ q,
194
+ " more"
195
+ ]
196
+ })]
197
+ }),
198
+ R && /* @__PURE__ */ b("p", {
199
+ className: "text-sm text-status-error-text",
200
+ children: R
201
+ }),
202
+ /* @__PURE__ */ b(f, {
203
+ onClick: W,
204
+ disabled: B,
205
+ className: "w-full",
206
+ children: B ? /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(l, { className: "size-4 animate-spin" }), "Importing…"] }) : /* @__PURE__ */ x(y, { children: [
207
+ /* @__PURE__ */ b(u, { className: "size-4" }),
208
+ "Import ",
209
+ P.rows.length,
210
+ " ",
211
+ P.rows.length === 1 ? "row" : "rows"
212
+ ] })
213
+ })
214
+ ]
215
+ })
216
+ ]
217
+ })
218
+ ]
46
219
  }),
47
- /* @__PURE__ */ f(c, { children: [/* @__PURE__ */ d(i, {
220
+ /* @__PURE__ */ b(g, { children: /* @__PURE__ */ b(f, {
48
221
  variant: "outline",
49
- onClick: h,
222
+ onClick: D,
223
+ disabled: B,
50
224
  children: "Cancel"
51
- }), /* @__PURE__ */ f(i, {
52
- onClick: g,
53
- children: [
54
- /* @__PURE__ */ d(r, { className: "size-4" }),
55
- "Go to Workflows",
56
- /* @__PURE__ */ d(t, { className: "size-4" })
57
- ]
58
- })] })
225
+ }) })
59
226
  ]
60
227
  })
61
228
  });
62
229
  });
63
230
  //#endregion
64
- export { m as default };
231
+ export { T as default };
65
232
 
66
233
  //# sourceMappingURL=ImportDataGuideDialog.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ImportDataGuideDialog.js","names":[],"sources":["../../../../src/bigconsole/components/datasink/ImportDataGuideDialog.tsx"],"sourcesContent":["/**\n * Guides a user on how to import data into a DataSink (via a workflow) and\n * navigates them to the Workflows page. Named to avoid confusion with\n * ImportDataSinkDialog, which imports a DataSink config from JSON.\n */\n\nimport { type FC, memo } from 'react';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Button,\n} from '@burdenoff/fe-libs/ui';\nimport { Upload, Workflow, ArrowRight } from 'lucide-react';\n\nexport interface ImportDataGuideDialogProps {\n /** Whether dialog is open */\n isOpen: boolean;\n /** DataSink name for context */\n dataSinkName?: string;\n /** Callback when dialog is closed */\n onClose: () => void;\n /** Callback when the user chooses to go to the Workflows page */\n onGoToWorkflows: () => void;\n}\n\nconst STEPS = [\n 'Go to the Workflows page and create a new workflow.',\n 'Add a source node to fetch your data, then a node that writes to this data sink.',\n 'Run the workflow — the imported data appears here in the data preview.',\n];\n\nexport const ImportDataGuideDialog: FC<ImportDataGuideDialogProps> = memo(function ImportDataGuideDialog({\n isOpen,\n dataSinkName,\n onClose,\n onGoToWorkflows,\n}) {\n return (\n <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>\n <DialogContent className=\"sm:max-w-lg\">\n <DialogHeader>\n <div className=\"flex items-center gap-3\">\n <div className=\"flex size-10 items-center justify-center rounded-lg bg-action-primary-bg/10\">\n <Upload className=\"size-5 text-action-primary-bg\" />\n </div>\n <DialogTitle>Import data</DialogTitle>\n </div>\n <DialogDescription>\n {dataSinkName ? `Populate “${dataSinkName}” with a workflow` : 'Populate this data sink with a workflow'}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"py-2 space-y-4\">\n <p className=\"text-sm text-text-primary leading-relaxed\">\n Data is imported into a data sink by building a <strong>workflow</strong>. A workflow fetches data from your\n source (an API, database, file, or app) and writes it into this data sink so your dashboards can read it.\n </p>\n\n <ol className=\"space-y-3\">\n {STEPS.map((step, index) => (\n <li key={index} className=\"flex items-start gap-3\">\n <span className=\"flex size-6 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-primary\">\n {index + 1}\n </span>\n <p className=\"text-sm text-text-secondary leading-relaxed\">{step}</p>\n </li>\n ))}\n </ol>\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" onClick={onClose}>\n Cancel\n </Button>\n <Button onClick={onGoToWorkflows}>\n <Workflow className=\"size-4\" />\n Go to Workflows\n <ArrowRight className=\"size-4\" />\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n});\n\nexport default ImportDataGuideDialog;\n"],"mappings":";;;;;AA6BA,IAAM,IAAQ;CACZ;CACA;CACA;CACD,EAEY,IAAwD,EAAK,SAA+B,EACvG,WACA,iBACA,YACA,sBACC;AACD,QACE,kBAAC,GAAD;EAAQ,MAAM;EAAQ,eAAe,MAAS,CAAC,KAAQ,GAAS;YAC9D,kBAAC,GAAD;GAAe,WAAU;aAAzB;IACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD,EAAQ,WAAU,iCAAkC,CAAA;MAChD,CAAA,EACN,kBAAC,GAAD,EAAA,UAAa,eAAyB,CAAA,CAClC;QACN,kBAAC,GAAD,EAAA,UACG,IAAe,aAAa,EAAa,qBAAqB,2CAC7C,CAAA,CACP,EAAA,CAAA;IAEf,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAb;OAAyD;OACP,kBAAC,UAAD,EAAA,UAAQ,YAAiB,CAAA;;OAEvE;SAEJ,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAM,KAAK,GAAM,MAChB,kBAAC,MAAD;OAAgB,WAAU;iBAA1B,CACE,kBAAC,QAAD;QAAM,WAAU;kBACb,IAAQ;QACJ,CAAA,EACP,kBAAC,KAAD;QAAG,WAAU;kBAA+C;QAAS,CAAA,CAClE;SALI,EAKJ,CACL;MACC,CAAA,CACD;;IAEN,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,SAAS;eAAS;KAEnC,CAAA,EACT,kBAAC,GAAD;KAAQ,SAAS;eAAjB;MACE,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;;MAE/B,kBAAC,GAAD,EAAY,WAAU,UAAW,CAAA;MAC1B;OACI,EAAA,CAAA;IACD;;EACT,CAAA;EAEX"}
1
+ {"version":3,"file":"ImportDataGuideDialog.js","names":[],"sources":["../../../../src/bigconsole/components/datasink/ImportDataGuideDialog.tsx"],"sourcesContent":["/**\n * Import data into a DataSink. Offers two methods:\n * 1. Build a workflow — a repeatable, scheduled pipeline (the primary path).\n * 2. Upload a CSV file directly (parsed client-side, written to the sink) —\n * the quickest path for one-off or small datasets.\n *\n * Named to avoid confusion with ImportDataSinkDialog, which imports a DataSink\n * *config* from JSON.\n */\n\nimport { type ChangeEvent, type FC, memo, useCallback, useEffect, useRef, useState } from 'react';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Button,\n} from '@burdenoff/fe-libs/ui';\nimport { Upload, Workflow, ArrowRight, FileUp, Loader2, AlertTriangle } from 'lucide-react';\n\nimport { parseCsv, type ParsedCsv } from '../../utils';\n\nexport interface ImportDataGuideDialogProps {\n /** Whether dialog is open */\n isOpen: boolean;\n /** DataSink name for context */\n dataSinkName?: string;\n /** Whether the sink already holds data — a CSV import would replace it. */\n hasExistingData?: boolean;\n /** Callback when dialog is closed */\n onClose: () => void;\n /** Callback when the user chooses to go to the Workflows page */\n onGoToWorkflows: () => void;\n /**\n * When provided, enables the direct CSV upload method. Receives the parsed\n * rows and should persist them to the DataSink. Resolves true on success\n * (the dialog then closes) or false on a handled failure.\n */\n onImportCsv?: (rows: Record<string, unknown>[]) => Promise<boolean>;\n}\n\nconst MAX_PREVIEW_COLUMNS = 6;\n\n/** Guard against reading a huge file fully into memory (~3-4x its size). */\nconst MAX_CSV_BYTES = 10_000_000; // 10 MB\n\n/** Cap rows so a single updateDataSink payload stays a sane size. */\nconst MAX_CSV_ROWS = 50_000;\n\nexport const ImportDataGuideDialog: FC<ImportDataGuideDialogProps> = memo(function ImportDataGuideDialog({\n isOpen,\n dataSinkName,\n hasExistingData,\n onClose,\n onGoToWorkflows,\n onImportCsv,\n}) {\n const fileInputRef = useRef<HTMLInputElement>(null);\n // Monotonic token for the in-flight file read; bumped on every new selection\n // or reset so a slow read that resolves late can detect it was superseded.\n const readSeqRef = useRef(0);\n const [fileName, setFileName] = useState<string | null>(null);\n const [parsed, setParsed] = useState<ParsedCsv | null>(null);\n const [parseError, setParseError] = useState<string | null>(null);\n const [importError, setImportError] = useState<string | null>(null);\n const [isImporting, setIsImporting] = useState(false);\n\n const resetUploadState = useCallback(() => {\n readSeqRef.current += 1; // invalidate any in-flight file read\n setFileName(null);\n setParsed(null);\n setParseError(null);\n setImportError(null);\n setIsImporting(false);\n }, []);\n\n // Clear any prior selection whenever the dialog is (re)opened or closed.\n useEffect(() => {\n if (!isOpen) resetUploadState();\n }, [isOpen, resetUploadState]);\n\n const handleFileChange = useCallback(async (event: ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Reset the input so selecting the same file again re-triggers onChange.\n event.target.value = '';\n if (!file) return;\n\n const seq = (readSeqRef.current += 1);\n setParsed(null);\n setParseError(null);\n setImportError(null);\n setFileName(file.name);\n\n if (file.size > MAX_CSV_BYTES) {\n setParseError('This CSV is larger than 10 MB. Import large datasets with a workflow instead.');\n return;\n }\n\n try {\n const text = await file.text();\n if (seq !== readSeqRef.current) return; // superseded by a newer selection/close\n const result = parseCsv(text);\n if (result.rows.length > MAX_CSV_ROWS) {\n setParseError(\n `This CSV has ${result.rows.length.toLocaleString()} rows (limit ${MAX_CSV_ROWS.toLocaleString()}). Import large datasets with a workflow instead.`\n );\n return;\n }\n setParsed(result);\n } catch (err) {\n if (seq !== readSeqRef.current) return;\n setParsed(null);\n setParseError(err instanceof Error ? err.message : 'Failed to parse the CSV file.');\n }\n }, []);\n\n const handleConfirmImport = useCallback(async () => {\n if (!parsed || !onImportCsv) return;\n setIsImporting(true);\n setImportError(null);\n try {\n const ok = await onImportCsv(parsed.rows);\n if (ok) {\n onClose();\n } else {\n setImportError('Import failed. Please try again.');\n }\n } catch (err) {\n setImportError(err instanceof Error ? err.message : 'Import failed. Please try again.');\n } finally {\n setIsImporting(false);\n }\n }, [parsed, onImportCsv, onClose]);\n\n const openFilePicker = useCallback(() => fileInputRef.current?.click(), []);\n\n const previewColumns = parsed?.columns.slice(0, MAX_PREVIEW_COLUMNS) ?? [];\n const extraColumns = parsed ? parsed.columns.length - previewColumns.length : 0;\n\n return (\n <Dialog open={isOpen} onOpenChange={(open) => !open && !isImporting && onClose()}>\n <DialogContent className=\"sm:max-w-lg\">\n <DialogHeader>\n <div className=\"flex items-center gap-3\">\n <div className=\"flex size-10 items-center justify-center rounded-lg bg-action-primary-bg/10\">\n <Upload className=\"size-5 text-action-primary-bg\" />\n </div>\n <DialogTitle>Import data</DialogTitle>\n </div>\n <DialogDescription>\n {dataSinkName ? `Add data to “${dataSinkName}”` : 'Add data to this data sink'}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"py-2 space-y-5\">\n <section className=\"space-y-3\">\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">Build a workflow</h3>\n <p className=\"text-sm text-text-secondary leading-relaxed\">\n The recommended way to keep this data sink up to date — build a repeatable, scheduled pipeline that\n fetches from your source (an API, database, or app) and writes into this data sink on every run.\n </p>\n </div>\n <Button variant=\"default\" onClick={onGoToWorkflows} disabled={isImporting} className=\"w-full\">\n <Workflow className=\"size-4\" />\n Go to Workflows\n <ArrowRight className=\"size-4\" />\n </Button>\n </section>\n\n {onImportCsv && (\n <div className=\"flex items-center gap-3\">\n <span className=\"h-px flex-1 bg-border-subtle\" />\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-tertiary\">Or</span>\n <span className=\"h-px flex-1 bg-border-subtle\" />\n </div>\n )}\n\n {onImportCsv && (\n <section className=\"space-y-3\">\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">Upload a CSV file</h3>\n <p className=\"text-sm text-text-secondary leading-relaxed\">\n For a quick, one-off import, upload a spreadsheet export and it becomes available to your dashboards\n right away.\n </p>\n </div>\n\n {hasExistingData && (\n <div className=\"flex items-start gap-2 rounded-lg border border-status-warning-border bg-status-warning-bg px-3 py-2\">\n <AlertTriangle className=\"size-4 shrink-0 text-status-warning-text mt-0.5\" />\n <p className=\"text-sm text-status-warning-text\">\n This data sink already has data — importing a CSV will replace it.\n </p>\n </div>\n )}\n\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\".csv,text/csv\"\n className=\"hidden\"\n onChange={handleFileChange}\n aria-label=\"Choose a CSV file to import\"\n />\n\n {!parsed && (\n <button\n type=\"button\"\n onClick={openFilePicker}\n className=\"flex w-full flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border-default bg-bg-sunken px-4 py-6 text-center transition-colors hover:border-action-primary-bg hover:bg-action-primary-bg/5\"\n >\n <FileUp className=\"size-6 text-text-tertiary\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {fileName ? 'Choose a different CSV file' : 'Choose a CSV file'}\n </span>\n <span className=\"text-xs text-text-tertiary\">The first row is used as column names.</span>\n </button>\n )}\n\n {parseError && (\n <div className=\"rounded-lg border border-status-error-border bg-status-error-bg px-3 py-2\">\n <p className=\"text-sm text-status-error-text\">{parseError}</p>\n </div>\n )}\n\n {parsed && (\n <div className=\"space-y-3 rounded-lg border border-border-default bg-bg-sunken p-4\">\n <div className=\"flex items-center justify-between gap-3\">\n <span className=\"truncate text-sm font-medium text-text-primary\" title={fileName ?? undefined}>\n {fileName}\n </span>\n <button\n type=\"button\"\n onClick={openFilePicker}\n disabled={isImporting}\n className=\"shrink-0 text-xs font-medium text-action-primary-bg hover:underline disabled:cursor-not-allowed disabled:opacity-50 disabled:no-underline\"\n >\n Change file\n </button>\n </div>\n <p className=\"text-sm text-text-secondary\">\n Ready to import <strong className=\"text-text-primary\">{parsed.rows.length}</strong>{' '}\n {parsed.rows.length === 1 ? 'row' : 'rows'} ×{' '}\n <strong className=\"text-text-primary\">{parsed.columns.length}</strong>{' '}\n {parsed.columns.length === 1 ? 'column' : 'columns'}.\n </p>\n <div className=\"flex flex-wrap gap-1.5\">\n {previewColumns.map((col) => (\n <span\n key={col}\n className=\"rounded-full bg-bg-surface px-2 py-0.5 text-xs text-text-secondary border border-border-subtle\"\n >\n {col}\n </span>\n ))}\n {extraColumns > 0 && (\n <span className=\"rounded-full px-2 py-0.5 text-xs text-text-tertiary\">+{extraColumns} more</span>\n )}\n </div>\n\n {importError && <p className=\"text-sm text-status-error-text\">{importError}</p>}\n\n <Button onClick={handleConfirmImport} disabled={isImporting} className=\"w-full\">\n {isImporting ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n Importing…\n </>\n ) : (\n <>\n <Upload className=\"size-4\" />\n Import {parsed.rows.length} {parsed.rows.length === 1 ? 'row' : 'rows'}\n </>\n )}\n </Button>\n </div>\n )}\n </section>\n )}\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" onClick={onClose} disabled={isImporting}>\n Cancel\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n});\n\nexport default ImportDataGuideDialog;\n"],"mappings":"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"}