@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.
- package/dist/api.d.mts +1 -1
- package/dist/{components-DTLQ2djq.js → components-DfXnN1Hx.js} +592 -13
- package/dist/components-DfXnN1Hx.js.map +1 -0
- package/dist/{components-B6oaPB5f.mjs → components-jh6r4tQn.mjs} +593 -14
- package/dist/components-jh6r4tQn.mjs.map +1 -0
- package/dist/components.d.mts +1 -1
- package/dist/components.js +1 -1
- package/dist/components.mjs +1 -1
- package/dist/hooks.d.mts +1 -1
- package/dist/{index-CbF0x3kW.d.mts → index-B5bpmv0i.d.mts} +70 -70
- package/dist/{index-CbF0x3kW.d.mts.map → index-B5bpmv0i.d.mts.map} +1 -1
- package/dist/index-B9lSPJTi.d.ts.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/theme.d.mts +1 -1
- package/dist/types.d.mts +1 -1
- package/package.json +1 -1
- package/recce-source/docs/plans/2024-12-31-csv-download-design.md +121 -0
- package/recce-source/docs/plans/2024-12-31-csv-download-implementation.md +930 -0
- package/recce-source/js/src/components/run/RunResultPane.tsx +138 -14
- package/recce-source/js/src/lib/csv/extractors.test.ts +456 -0
- package/recce-source/js/src/lib/csv/extractors.ts +468 -0
- package/recce-source/js/src/lib/csv/format.test.ts +211 -0
- package/recce-source/js/src/lib/csv/format.ts +44 -0
- package/recce-source/js/src/lib/csv/index.test.ts +155 -0
- package/recce-source/js/src/lib/csv/index.ts +109 -0
- package/recce-source/js/src/lib/hooks/useCSVExport.ts +136 -0
- package/recce-source/recce/mcp_server.py +54 -30
- package/recce-source/recce/models/check.py +10 -2
- package/dist/components-B6oaPB5f.mjs.map +0 -1
- 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
|
|
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(
|
|
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(
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
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-
|
|
12348
|
+
//# sourceMappingURL=components-jh6r4tQn.mjs.map
|