@datarecce/ui 0.1.40 → 0.1.41

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.
Files changed (32) hide show
  1. package/dist/api.d.mts +1 -1
  2. package/dist/{components-DTLQ2djq.js → components-DfXnN1Hx.js} +592 -13
  3. package/dist/components-DfXnN1Hx.js.map +1 -0
  4. package/dist/{components-B6oaPB5f.mjs → components-jh6r4tQn.mjs} +593 -14
  5. package/dist/components-jh6r4tQn.mjs.map +1 -0
  6. package/dist/components.d.mts +1 -1
  7. package/dist/components.js +1 -1
  8. package/dist/components.mjs +1 -1
  9. package/dist/hooks.d.mts +1 -1
  10. package/dist/{index-CbF0x3kW.d.mts → index-B5bpmv0i.d.mts} +70 -70
  11. package/dist/{index-CbF0x3kW.d.mts.map → index-B5bpmv0i.d.mts.map} +1 -1
  12. package/dist/index-B9lSPJTi.d.ts.map +1 -1
  13. package/dist/index.d.mts +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.mjs +1 -1
  16. package/dist/theme.d.mts +1 -1
  17. package/dist/types.d.mts +1 -1
  18. package/package.json +1 -1
  19. package/recce-source/docs/plans/2024-12-31-csv-download-design.md +121 -0
  20. package/recce-source/docs/plans/2024-12-31-csv-download-implementation.md +930 -0
  21. package/recce-source/js/src/components/run/RunResultPane.tsx +138 -14
  22. package/recce-source/js/src/lib/csv/extractors.test.ts +456 -0
  23. package/recce-source/js/src/lib/csv/extractors.ts +468 -0
  24. package/recce-source/js/src/lib/csv/format.test.ts +211 -0
  25. package/recce-source/js/src/lib/csv/format.ts +44 -0
  26. package/recce-source/js/src/lib/csv/index.test.ts +155 -0
  27. package/recce-source/js/src/lib/csv/index.ts +109 -0
  28. package/recce-source/js/src/lib/hooks/useCSVExport.ts +136 -0
  29. package/recce-source/recce/mcp_server.py +54 -30
  30. package/recce-source/recce/models/check.py +10 -2
  31. package/dist/components-B6oaPB5f.mjs.map +0 -1
  32. package/dist/components-DTLQ2djq.js.map +0 -1
@@ -43,7 +43,7 @@ import TextField from "@mui/material/TextField";
43
43
  import Menu from "@mui/material/Menu";
44
44
  import MenuItem from "@mui/material/MenuItem";
45
45
  import { VscCircleLarge, VscDiffAdded, VscDiffModified, VscDiffRemoved, VscFeedback, VscGitPullRequest, VscHistory, VscKebabVertical } from "react-icons/vsc";
46
- import { PiBookmarkSimple, PiCaretDown, PiChatText, PiCheck, PiCheckCircle, PiCircle, PiCopy, PiInfo, PiInfoFill, PiMoon, PiNotePencil, PiPencilSimple, PiPlusCircle, PiRepeat, PiSun, PiTrashFill, PiTrashSimple, PiWarning, PiX } from "react-icons/pi";
46
+ import { PiBookmarkSimple, PiCaretDown, PiChatText, PiCheck, PiCheckCircle, PiCircle, PiCopy, PiDownloadSimple, PiImage, PiInfo, PiInfoFill, PiMoon, PiNotePencil, PiPencilSimple, PiPlusCircle, PiRepeat, PiSun, PiTable, PiTrashFill, PiTrashSimple, PiWarning, PiX } from "react-icons/pi";
47
47
  import MuiTooltip from "@mui/material/Tooltip";
48
48
  import { useCopyToClipboard, useInterval } from "usehooks-ts";
49
49
  import ListSubheader from "@mui/material/ListSubheader";
@@ -4430,6 +4430,512 @@ function AuthModal({ handleParentClose, parentOpen = false, ignoreCookie = false
4430
4430
  });
4431
4431
  }
4432
4432
 
4433
+ //#endregion
4434
+ //#region recce-source/js/src/lib/csv/format.ts
4435
+ /**
4436
+ * CSV formatting utilities with Excel-friendly output
4437
+ */
4438
+ /**
4439
+ * Escape a value for CSV format
4440
+ * - Wrap in quotes if contains comma, quote, or newline
4441
+ * - Escape quotes by doubling them
4442
+ */
4443
+ function escapeCSVValue(value) {
4444
+ if (value === null || value === void 0) return "";
4445
+ const stringValue = typeof value === "object" ? JSON.stringify(value) : String(value);
4446
+ if (stringValue.includes(",") || stringValue.includes("\"") || stringValue.includes("\n") || stringValue.includes("\r")) return `"${stringValue.replace(/"/g, "\"\"")}"`;
4447
+ return stringValue;
4448
+ }
4449
+ /**
4450
+ * Convert tabular data to CSV string
4451
+ * @param columns - Column headers
4452
+ * @param rows - Row data (array of arrays)
4453
+ * @returns CSV string with UTF-8 BOM for Excel compatibility
4454
+ */
4455
+ function toCSV(columns, rows) {
4456
+ return "" + [columns.map(escapeCSVValue).join(","), ...rows.map((row) => row.map(escapeCSVValue).join(","))].join("\r\n");
4457
+ }
4458
+
4459
+ //#endregion
4460
+ //#region recce-source/js/src/lib/csv/extractors.ts
4461
+ /**
4462
+ * Format a cell value for inline diff mode
4463
+ * If base and current are the same, return the value
4464
+ * If different, return "(base_value) (current_value)"
4465
+ */
4466
+ function formatInlineDiffCell(baseValue, currentValue) {
4467
+ if ((baseValue == null ? "" : String(baseValue)) === (currentValue == null ? "" : String(currentValue))) return baseValue;
4468
+ return `${baseValue == null ? "" : `(${baseValue})`} ${currentValue == null ? "" : `(${currentValue})`}`.trim();
4469
+ }
4470
+ /**
4471
+ * Extract columns and rows from a DataFrame
4472
+ */
4473
+ function extractDataFrame(df) {
4474
+ if (!df || !df.columns || !df.data) return null;
4475
+ return {
4476
+ columns: df.columns.map((col) => col.name),
4477
+ rows: df.data.map((row) => [...row])
4478
+ };
4479
+ }
4480
+ /**
4481
+ * Extract CSV data from query result (single environment)
4482
+ */
4483
+ function extractQuery(result) {
4484
+ return extractDataFrame(result);
4485
+ }
4486
+ /**
4487
+ * Extract CSV data from query_base result
4488
+ */
4489
+ function extractQueryBase(result) {
4490
+ return extractDataFrame(result);
4491
+ }
4492
+ /**
4493
+ * Extract CSV data from query_diff result
4494
+ * Supports two result shapes:
4495
+ * 1. { diff: DataFrame } - joined diff result (QueryDiffJoinResultView)
4496
+ * 2. { base: DataFrame, current: DataFrame } - separate base/current (QueryDiffResultView)
4497
+ *
4498
+ * Display modes:
4499
+ * - "inline": Merged rows where same values shown as-is, differing values shown as "(base) (current)"
4500
+ * - "side_by_side": Single row per record with base__col, current__col columns
4501
+ *
4502
+ * Note: When base and current have different row counts (e.g., added/removed rows),
4503
+ * the merge is done positionally. Extra rows will show null for the missing environment.
4504
+ */
4505
+ function extractQueryDiff(result, options) {
4506
+ const typed = result;
4507
+ const displayMode = options?.displayMode ?? "inline";
4508
+ const primaryKeys = options?.primaryKeys ?? [];
4509
+ if (typed?.diff) return extractQueryDiffJoined(typed.diff, displayMode, primaryKeys);
4510
+ return extractQueryDiffSeparate(typed, displayMode);
4511
+ }
4512
+ /**
4513
+ * Extract CSV from joined diff DataFrame (QueryDiffJoinResultView)
4514
+ * The diff DataFrame has columns like: pk, col1, col2, in_a, in_b
4515
+ * where in_a/in_b indicate presence in base/current
4516
+ *
4517
+ * The DataFrame may have separate rows for base (in_a=true) and current (in_b=true)
4518
+ * records. This function groups them by primary key and merges into single output rows.
4519
+ *
4520
+ * Produces same layout as extractQueryDiffSeparate for consistency.
4521
+ */
4522
+ function extractQueryDiffJoined(diff, displayMode, primaryKeys) {
4523
+ if (!diff?.columns || !diff?.data) return null;
4524
+ const inAIndex = diff.columns.findIndex((col) => col.key.toLowerCase() === "in_a");
4525
+ const inBIndex = diff.columns.findIndex((col) => col.key.toLowerCase() === "in_b");
4526
+ const dataColumns = diff.columns.filter((col) => col.key.toLowerCase() !== "in_a" && col.key.toLowerCase() !== "in_b");
4527
+ const dataColumnNames = dataColumns.map((col) => col.name);
4528
+ const dataColumnIndices = dataColumns.map((col) => diff.columns.findIndex((c) => c.key === col.key));
4529
+ const pkIndices = primaryKeys.map((pk) => diff.columns.findIndex((col) => col.key === pk)).filter((idx) => idx >= 0);
4530
+ const extractRowValues = (rowData) => {
4531
+ return dataColumnIndices.map((colIndex) => rowData[colIndex]);
4532
+ };
4533
+ const getPrimaryKeyValue = (rowData) => {
4534
+ if (pkIndices.length === 0) return "";
4535
+ return pkIndices.map((idx) => String(rowData[idx] ?? "")).join("|||");
4536
+ };
4537
+ const groupedRows = /* @__PURE__ */ new Map();
4538
+ const rowOrder = [];
4539
+ diff.data.forEach((rowData, index) => {
4540
+ const inA = inAIndex >= 0 ? rowData[inAIndex] : true;
4541
+ const inB = inBIndex >= 0 ? rowData[inBIndex] : true;
4542
+ let pkValue = getPrimaryKeyValue(rowData);
4543
+ if (pkValue === "") pkValue = String(index);
4544
+ if (!groupedRows.has(pkValue)) {
4545
+ groupedRows.set(pkValue, {
4546
+ base: null,
4547
+ current: null
4548
+ });
4549
+ rowOrder.push(pkValue);
4550
+ }
4551
+ const group = groupedRows.get(pkValue);
4552
+ if (!group) return;
4553
+ const values = extractRowValues(rowData);
4554
+ if (inA) group.base = values;
4555
+ if (inB) group.current = values;
4556
+ });
4557
+ if (displayMode === "side_by_side") {
4558
+ const columns$1 = [];
4559
+ dataColumnNames.forEach((name) => {
4560
+ columns$1.push(`base__${name}`, `current__${name}`);
4561
+ });
4562
+ const rows$1 = [];
4563
+ for (const pkValue of rowOrder) {
4564
+ const group = groupedRows.get(pkValue);
4565
+ if (!group) continue;
4566
+ const baseValues = group.base;
4567
+ const currentValues = group.current;
4568
+ const row = [];
4569
+ dataColumnNames.forEach((_$1, colIndex) => {
4570
+ row.push(baseValues ? baseValues[colIndex] : null);
4571
+ row.push(currentValues ? currentValues[colIndex] : null);
4572
+ });
4573
+ rows$1.push(row);
4574
+ }
4575
+ return {
4576
+ columns: columns$1,
4577
+ rows: rows$1
4578
+ };
4579
+ }
4580
+ const columns = [...dataColumnNames];
4581
+ const rows = [];
4582
+ for (const pkValue of rowOrder) {
4583
+ const group = groupedRows.get(pkValue);
4584
+ if (!group) continue;
4585
+ const baseValues = group.base;
4586
+ const currentValues = group.current;
4587
+ const row = [];
4588
+ dataColumnNames.forEach((_$1, colIndex) => {
4589
+ const baseVal = baseValues ? baseValues[colIndex] : null;
4590
+ const currentVal = currentValues ? currentValues[colIndex] : null;
4591
+ row.push(formatInlineDiffCell(baseVal, currentVal));
4592
+ });
4593
+ rows.push(row);
4594
+ }
4595
+ return {
4596
+ columns,
4597
+ rows
4598
+ };
4599
+ }
4600
+ /**
4601
+ * Extract CSV from separate base/current DataFrames (QueryDiffResultView)
4602
+ */
4603
+ function extractQueryDiffSeparate(typed, displayMode) {
4604
+ const df = typed?.current || typed?.base;
4605
+ if (!df) return null;
4606
+ if (!typed?.base || !typed?.current) return extractDataFrame(df);
4607
+ const columnNames = typed.current.columns.map((c) => c.name);
4608
+ if (displayMode === "side_by_side") {
4609
+ const columns$1 = [];
4610
+ columnNames.forEach((name) => {
4611
+ columns$1.push(`base__${name}`, `current__${name}`);
4612
+ });
4613
+ const rows$1 = [];
4614
+ const maxRows$1 = Math.max(typed.base.data.length, typed.current.data.length);
4615
+ for (let i = 0; i < maxRows$1; i++) {
4616
+ const row = [];
4617
+ const baseRow = i < typed.base.data.length ? typed.base.data[i] : null;
4618
+ const currentRow = i < typed.current.data.length ? typed.current.data[i] : null;
4619
+ columnNames.forEach((_$1, colIndex) => {
4620
+ row.push(baseRow ? baseRow[colIndex] : null);
4621
+ row.push(currentRow ? currentRow[colIndex] : null);
4622
+ });
4623
+ rows$1.push(row);
4624
+ }
4625
+ return {
4626
+ columns: columns$1,
4627
+ rows: rows$1
4628
+ };
4629
+ }
4630
+ const columns = [...columnNames];
4631
+ const rows = [];
4632
+ const maxRows = Math.max(typed.base.data.length, typed.current.data.length);
4633
+ for (let i = 0; i < maxRows; i++) {
4634
+ const baseRow = i < typed.base.data.length ? typed.base.data[i] : null;
4635
+ const currentRow = i < typed.current.data.length ? typed.current.data[i] : null;
4636
+ const row = [];
4637
+ columnNames.forEach((_$1, colIndex) => {
4638
+ const baseVal = baseRow ? baseRow[colIndex] : null;
4639
+ const currentVal = currentRow ? currentRow[colIndex] : null;
4640
+ row.push(formatInlineDiffCell(baseVal, currentVal));
4641
+ });
4642
+ rows.push(row);
4643
+ }
4644
+ return {
4645
+ columns,
4646
+ rows
4647
+ };
4648
+ }
4649
+ /**
4650
+ * Extract CSV data from profile_diff result
4651
+ */
4652
+ function extractProfileDiff(result) {
4653
+ const typed = result;
4654
+ const df = typed?.current || typed?.base;
4655
+ if (!df) return null;
4656
+ if (typed?.base && typed?.current) {
4657
+ const columns = ["_source", ...typed.current.columns.map((c) => c.name)];
4658
+ const rows = [];
4659
+ typed.base.data.forEach((row) => {
4660
+ rows.push(["base", ...row]);
4661
+ });
4662
+ typed.current.data.forEach((row) => {
4663
+ rows.push(["current", ...row]);
4664
+ });
4665
+ return {
4666
+ columns,
4667
+ rows
4668
+ };
4669
+ }
4670
+ return extractDataFrame(df);
4671
+ }
4672
+ /**
4673
+ * Extract CSV data from row_count_diff result
4674
+ */
4675
+ function extractRowCountDiff(result) {
4676
+ const typed = result;
4677
+ if (!typed || typeof typed !== "object") return null;
4678
+ const columns = [
4679
+ "node",
4680
+ "base_count",
4681
+ "current_count",
4682
+ "diff",
4683
+ "diff_percent"
4684
+ ];
4685
+ const rows = [];
4686
+ for (const [nodeName, counts] of Object.entries(typed)) if (counts && typeof counts === "object") {
4687
+ const base = counts.base;
4688
+ const current = counts.curr;
4689
+ const diff = base != null && current != null ? current - base : null;
4690
+ const diffPercent = base && diff !== null ? (diff / base * 100).toFixed(2) + "%" : null;
4691
+ rows.push([
4692
+ nodeName,
4693
+ base,
4694
+ current,
4695
+ diff,
4696
+ diffPercent
4697
+ ]);
4698
+ }
4699
+ return {
4700
+ columns,
4701
+ rows
4702
+ };
4703
+ }
4704
+ /**
4705
+ * Extract CSV data from value_diff result
4706
+ */
4707
+ function extractValueDiff(result) {
4708
+ const typed = result;
4709
+ if (!typed?.data) return null;
4710
+ return extractDataFrame(typed.data);
4711
+ }
4712
+ /**
4713
+ * Extract CSV data from value_diff_detail result
4714
+ */
4715
+ function extractValueDiffDetail(result) {
4716
+ return extractDataFrame(result);
4717
+ }
4718
+ /**
4719
+ * Extract CSV data from top_k_diff result
4720
+ */
4721
+ function extractTopKDiff(result) {
4722
+ const typed = result;
4723
+ const hasBaseValues = !!typed?.base?.values;
4724
+ const hasCurrentValues = !!typed?.current?.values;
4725
+ if (!hasBaseValues && !hasCurrentValues) return null;
4726
+ const columns = [
4727
+ "_source",
4728
+ "value",
4729
+ "count"
4730
+ ];
4731
+ const rows = [];
4732
+ if (typed?.base?.values) typed.base.values.forEach((value, index) => {
4733
+ rows.push([
4734
+ "base",
4735
+ value,
4736
+ typed.base.counts[index]
4737
+ ]);
4738
+ });
4739
+ if (typed?.current?.values) typed.current.values.forEach((value, index) => {
4740
+ rows.push([
4741
+ "current",
4742
+ value,
4743
+ typed.current.counts[index]
4744
+ ]);
4745
+ });
4746
+ return {
4747
+ columns,
4748
+ rows
4749
+ };
4750
+ }
4751
+ /**
4752
+ * Map of run types to their extractor functions
4753
+ * Some extractors accept options (like query_diff for displayMode)
4754
+ */
4755
+ const extractors = {
4756
+ query: extractQuery,
4757
+ query_base: extractQueryBase,
4758
+ query_diff: extractQueryDiff,
4759
+ profile: extractProfileDiff,
4760
+ profile_diff: extractProfileDiff,
4761
+ row_count: extractRowCountDiff,
4762
+ row_count_diff: extractRowCountDiff,
4763
+ value_diff: extractValueDiff,
4764
+ value_diff_detail: extractValueDiffDetail,
4765
+ top_k_diff: extractTopKDiff
4766
+ };
4767
+ /**
4768
+ * Extract CSV data from a run result
4769
+ * @param runType - The type of run (query, query_diff, etc.)
4770
+ * @param result - The run result data
4771
+ * @param options - Optional export options (e.g., displayMode for query_diff)
4772
+ * @returns CSVData or null if the run type doesn't support CSV export
4773
+ */
4774
+ function extractCSVData(runType, result, options) {
4775
+ const extractor = extractors[runType];
4776
+ if (!extractor) return null;
4777
+ try {
4778
+ return extractor(result, options);
4779
+ } catch (error) {
4780
+ console.error(`Failed to extract CSV data for run type "${runType}":`, error);
4781
+ return null;
4782
+ }
4783
+ }
4784
+ /**
4785
+ * Check if a run type supports CSV export
4786
+ */
4787
+ function supportsCSVExport(runType) {
4788
+ return runType in extractors;
4789
+ }
4790
+
4791
+ //#endregion
4792
+ //#region recce-source/js/src/lib/csv/index.ts
4793
+ /**
4794
+ * CSV export utilities
4795
+ */
4796
+ /**
4797
+ * Trigger browser download of CSV file
4798
+ */
4799
+ function downloadCSV(content, filename) {
4800
+ saveAs(new Blob([content], { type: "text/csv;charset=utf-8" }), filename);
4801
+ }
4802
+ /**
4803
+ * Copy CSV content to clipboard
4804
+ * Uses modern Clipboard API with fallback for older browsers
4805
+ */
4806
+ async function copyCSVToClipboard(content) {
4807
+ if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
4808
+ await navigator.clipboard.writeText(content);
4809
+ return;
4810
+ }
4811
+ if (typeof document === "undefined") throw new Error("Clipboard API not available in this environment");
4812
+ const textarea = document.createElement("textarea");
4813
+ textarea.value = content;
4814
+ textarea.style.position = "fixed";
4815
+ textarea.style.opacity = "0";
4816
+ textarea.setAttribute("readonly", "");
4817
+ document.body.appendChild(textarea);
4818
+ textarea.focus();
4819
+ textarea.select();
4820
+ try {
4821
+ if (!document.execCommand("copy")) throw new Error("execCommand('copy') failed");
4822
+ } finally {
4823
+ document.body.removeChild(textarea);
4824
+ }
4825
+ }
4826
+ /**
4827
+ * Generate timestamp string for filenames
4828
+ * Format: YYYYMMDD-HHmmss
4829
+ */
4830
+ function generateTimestamp() {
4831
+ const now = /* @__PURE__ */ new Date();
4832
+ return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
4833
+ }
4834
+ /**
4835
+ * Generate context-aware CSV filename
4836
+ */
4837
+ function generateCSVFilename(runType, params) {
4838
+ const timestamp = generateTimestamp();
4839
+ const type = runType.replace(/_/g, "-");
4840
+ let nodeName;
4841
+ if (params?.node_names && Array.isArray(params.node_names) && params.node_names.length === 1) nodeName = String(params.node_names[0]);
4842
+ else if (params?.model && typeof params.model === "string") nodeName = params.model;
4843
+ if (nodeName) {
4844
+ nodeName = nodeName.replace(/[^a-zA-Z0-9_.-]/g, "-").toLowerCase();
4845
+ return `${type}-${nodeName}-${timestamp}.csv`;
4846
+ }
4847
+ return `${type}-result-${timestamp}.csv`;
4848
+ }
4849
+
4850
+ //#endregion
4851
+ //#region recce-source/js/src/lib/hooks/useCSVExport.ts
4852
+ /**
4853
+ * Hook for CSV export functionality
4854
+ */
4855
+ function useCSVExport({ run, viewOptions }) {
4856
+ const canExportCSV = useMemo(() => {
4857
+ if (!run?.type || !run?.result) return false;
4858
+ return supportsCSVExport(run.type);
4859
+ }, [run?.type, run?.result]);
4860
+ const getCSVContent = useCallback(() => {
4861
+ if (!run?.type || !run?.result) return null;
4862
+ const exportOptions = {
4863
+ displayMode: viewOptions?.display_mode,
4864
+ primaryKeys: (run?.params)?.primary_keys
4865
+ };
4866
+ const csvData = extractCSVData(run.type, run.result, exportOptions);
4867
+ if (!csvData) return null;
4868
+ return toCSV(csvData.columns, csvData.rows);
4869
+ }, [
4870
+ run?.type,
4871
+ run?.result,
4872
+ run?.params,
4873
+ viewOptions
4874
+ ]);
4875
+ return {
4876
+ canExportCSV,
4877
+ copyAsCSV: useCallback(async () => {
4878
+ const content = getCSVContent();
4879
+ if (!content) {
4880
+ toaster.create({
4881
+ title: "Export failed",
4882
+ description: "Unable to extract data for CSV export",
4883
+ type: "error",
4884
+ duration: 3e3
4885
+ });
4886
+ return;
4887
+ }
4888
+ try {
4889
+ await copyCSVToClipboard(content);
4890
+ toaster.create({
4891
+ title: "Copied to clipboard",
4892
+ description: "CSV data copied successfully",
4893
+ type: "success",
4894
+ duration: 2e3
4895
+ });
4896
+ } catch (error) {
4897
+ console.error("Failed to copy CSV to clipboard:", error);
4898
+ toaster.create({
4899
+ title: "Copy failed",
4900
+ description: "Failed to copy to clipboard",
4901
+ type: "error",
4902
+ duration: 3e3
4903
+ });
4904
+ }
4905
+ }, [getCSVContent]),
4906
+ downloadAsCSV: useCallback(() => {
4907
+ const content = getCSVContent();
4908
+ if (!content) {
4909
+ toaster.create({
4910
+ title: "Export failed",
4911
+ description: "Unable to extract data for CSV export",
4912
+ type: "error",
4913
+ duration: 3e3
4914
+ });
4915
+ return;
4916
+ }
4917
+ try {
4918
+ const filename = generateCSVFilename(run?.type ?? "", run?.params);
4919
+ downloadCSV(content, filename);
4920
+ toaster.create({
4921
+ title: "Downloaded",
4922
+ description: filename,
4923
+ type: "success",
4924
+ duration: 3e3
4925
+ });
4926
+ } catch (error) {
4927
+ console.error("Failed to download CSV file:", error);
4928
+ toaster.create({
4929
+ title: "Download failed",
4930
+ description: "Failed to download CSV file",
4931
+ type: "error",
4932
+ duration: 3e3
4933
+ });
4934
+ }
4935
+ }, [getCSVContent, run])
4936
+ };
4937
+ }
4938
+
4433
4939
  //#endregion
4434
4940
  //#region recce-source/js/src/components/query/SqlEditor.tsx
4435
4941
  function SqlEditor({ value, onChange, onRun, onRunBase, onRunDiff, label, CustomEditor, options = {}, manifestData, schemas, ...props }) {
@@ -4824,12 +5330,71 @@ const SingleEnvironmentSetupNotification = ({ runType }) => {
4824
5330
  default: return /* @__PURE__ */ jsx(Fragment$1, {});
4825
5331
  }
4826
5332
  };
4827
- const RunResultShareMenu = ({ disableCopyToClipboard, onCopyToClipboard, onMouseEnter, onMouseLeave }) => {
5333
+ const RunResultExportMenu = ({ run, viewOptions, disableExport, onCopyAsImage, onMouseEnter, onMouseLeave }) => {
5334
+ const [anchorEl, setAnchorEl] = useState(null);
5335
+ const open = Boolean(anchorEl);
5336
+ const { canExportCSV, copyAsCSV, downloadAsCSV } = useCSVExport({
5337
+ run,
5338
+ viewOptions
5339
+ });
5340
+ const handleClick = (event) => {
5341
+ setAnchorEl(event.currentTarget);
5342
+ };
5343
+ const handleClose = () => {
5344
+ setAnchorEl(null);
5345
+ };
5346
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
5347
+ size: "small",
5348
+ variant: "outlined",
5349
+ color: "neutral",
5350
+ onClick: handleClick,
5351
+ endIcon: /* @__PURE__ */ jsx(PiCaretDown, {}),
5352
+ sx: { textTransform: "none" },
5353
+ children: "Export"
5354
+ }), /* @__PURE__ */ jsxs(Menu, {
5355
+ anchorEl,
5356
+ open,
5357
+ onClose: handleClose,
5358
+ children: [
5359
+ /* @__PURE__ */ jsxs(MenuItem, {
5360
+ onClick: async () => {
5361
+ await onCopyAsImage();
5362
+ handleClose();
5363
+ },
5364
+ onMouseEnter,
5365
+ onMouseLeave,
5366
+ disabled: disableExport,
5367
+ children: [/* @__PURE__ */ jsx(ListItemIcon, { children: /* @__PURE__ */ jsx(PiImage, {}) }), /* @__PURE__ */ jsx(ListItemText, { children: "Copy as Image" })]
5368
+ }),
5369
+ /* @__PURE__ */ jsxs(MenuItem, {
5370
+ onClick: async () => {
5371
+ await copyAsCSV();
5372
+ handleClose();
5373
+ },
5374
+ disabled: disableExport || !canExportCSV,
5375
+ children: [/* @__PURE__ */ jsx(ListItemIcon, { children: /* @__PURE__ */ jsx(PiTable, {}) }), /* @__PURE__ */ jsx(ListItemText, { children: "Copy as CSV" })]
5376
+ }),
5377
+ /* @__PURE__ */ jsxs(MenuItem, {
5378
+ onClick: () => {
5379
+ downloadAsCSV();
5380
+ handleClose();
5381
+ },
5382
+ disabled: disableExport || !canExportCSV,
5383
+ children: [/* @__PURE__ */ jsx(ListItemIcon, { children: /* @__PURE__ */ jsx(PiDownloadSimple, {}) }), /* @__PURE__ */ jsx(ListItemText, { children: "Download as CSV" })]
5384
+ })
5385
+ ]
5386
+ })] });
5387
+ };
5388
+ const RunResultShareMenu = ({ run, viewOptions, disableCopyToClipboard, onCopyToClipboard, onMouseEnter, onMouseLeave }) => {
4828
5389
  const { authed } = useRecceInstanceContext();
4829
5390
  const { handleShareClick } = useRecceShareStateContext();
4830
5391
  const [showModal, setShowModal] = useState(false);
4831
5392
  const [anchorEl, setAnchorEl] = useState(null);
4832
5393
  const open = Boolean(anchorEl);
5394
+ const { canExportCSV, copyAsCSV, downloadAsCSV } = useCSVExport({
5395
+ run,
5396
+ viewOptions
5397
+ });
4833
5398
  const handleClick = (event) => {
4834
5399
  setAnchorEl(event.currentTarget);
4835
5400
  };
@@ -4859,7 +5424,23 @@ const RunResultShareMenu = ({ disableCopyToClipboard, onCopyToClipboard, onMouse
4859
5424
  onMouseEnter,
4860
5425
  onMouseLeave,
4861
5426
  disabled: disableCopyToClipboard,
4862
- children: [/* @__PURE__ */ jsx(ListItemIcon, { children: /* @__PURE__ */ jsx(PiCopy, {}) }), /* @__PURE__ */ jsx(ListItemText, { children: "Copy to Clipboard" })]
5427
+ children: [/* @__PURE__ */ jsx(ListItemIcon, { children: /* @__PURE__ */ jsx(PiImage, {}) }), /* @__PURE__ */ jsx(ListItemText, { children: "Copy as Image" })]
5428
+ }),
5429
+ /* @__PURE__ */ jsxs(MenuItem, {
5430
+ onClick: async () => {
5431
+ await copyAsCSV();
5432
+ handleClose();
5433
+ },
5434
+ disabled: disableCopyToClipboard || !canExportCSV,
5435
+ children: [/* @__PURE__ */ jsx(ListItemIcon, { children: /* @__PURE__ */ jsx(PiTable, {}) }), /* @__PURE__ */ jsx(ListItemText, { children: "Copy as CSV" })]
5436
+ }),
5437
+ /* @__PURE__ */ jsxs(MenuItem, {
5438
+ onClick: () => {
5439
+ downloadAsCSV();
5440
+ handleClose();
5441
+ },
5442
+ disabled: disableCopyToClipboard || !canExportCSV,
5443
+ children: [/* @__PURE__ */ jsx(ListItemIcon, { children: /* @__PURE__ */ jsx(PiDownloadSimple, {}) }), /* @__PURE__ */ jsx(ListItemText, { children: "Download as CSV" })]
4863
5444
  }),
4864
5445
  /* @__PURE__ */ jsx(Divider, {}),
4865
5446
  authed ? /* @__PURE__ */ jsxs(MenuItem, {
@@ -4957,18 +5538,16 @@ const PrivateLoadableRunView = ({ runId, onClose, isSingleEnvironment }) => {
4957
5538
  sx: { textTransform: "none" },
4958
5539
  children: "Rerun"
4959
5540
  }),
4960
- featureToggles.disableShare ? /* @__PURE__ */ jsx(Button, {
4961
- variant: "outlined",
4962
- color: "neutral",
4963
- disabled: !runId || !run?.result || !!error || tabValue !== "result",
5541
+ featureToggles.disableShare ? /* @__PURE__ */ jsx(RunResultExportMenu, {
5542
+ run,
5543
+ viewOptions,
5544
+ disableExport: !runId || !run?.result || !!error || tabValue !== "result",
5545
+ onCopyAsImage: onCopyToClipboard,
4964
5546
  onMouseEnter,
4965
- onMouseLeave,
4966
- size: "small",
4967
- onClick: onCopyToClipboard,
4968
- startIcon: /* @__PURE__ */ jsx(PiCopy, {}),
4969
- sx: { textTransform: "none" },
4970
- children: "Copy to Clipboard"
5547
+ onMouseLeave
4971
5548
  }) : /* @__PURE__ */ jsx(RunResultShareMenu, {
5549
+ run,
5550
+ viewOptions,
4972
5551
  disableCopyToClipboard,
4973
5552
  onCopyToClipboard: async () => {
4974
5553
  await onCopyToClipboard();
@@ -11766,4 +12345,4 @@ function NavBar() {
11766
12345
 
11767
12346
  //#endregion
11768
12347
  export { NodeSqlView as A, GraphEdge as B, NodeView as C, SqlEditor_default as D, RunStatusAndDate as E, HistoryToggle as F, MuiProvider as G, HSplit as H, GraphNode as I, mui_provider_default as K, ModelRowCount as L, CodeEditor_default as M, SchemaView as N, QueryForm as O, LineageViewTopBar as P, ResourceTypeTag as R, SetupConnectionBanner as S, RunView as T, VSplit as U, GraphColumnNode as V, ErrorBoundary$1 as W, CheckList as _, IdleTimeoutBadge as a, LineagePage as b, ChangeSummary as c, CheckEmptyState as d, CheckDetail as f, CheckBreadcrumb as g, CheckDescription as h, DisplayModeToggle as i, DiffEditor_default as j, EnvInfo as k, RunList as l, LineageDiffView as m, TopBar as n, SummaryView as o, SchemaDiffView as p, RecceVersionBadge as r, SchemaSummary as s, NavBar as t, RunPage as u, QueryPage as v, RunResultPane as w, LineageView as x, SetupConnectionGuide as y, RowCountDiffTag as z };
11769
- //# sourceMappingURL=components-B6oaPB5f.mjs.map
12348
+ //# sourceMappingURL=components-jh6r4tQn.mjs.map