@marimo-team/frontend 0.25.2-dev56 → 0.25.2-dev62
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/assets/ConnectedDataExplorerComponent-C5UW0p9I.js +1 -0
- package/dist/assets/JsonOutput-nqqzL-n0.js +11 -0
- package/dist/assets/{agent-panel-9FeJ9hAg.js → agent-panel-Bkcf1sED.js} +1 -1
- package/dist/assets/{cell-editor-BNIogYsY.js → cell-editor-Cmqe3QmM.js} +1 -1
- package/dist/assets/{column-preview-D2Um0Qe3.js → column-preview-DtrgJzVI.js} +1 -1
- package/dist/assets/{command-palette-ChCapZF9.js → command-palette-DUPyVGsq.js} +1 -1
- package/dist/assets/{edit-page-DIBiWEli.js → edit-page-DMsRAPsr.js} +4 -4
- package/dist/assets/{file-explorer-panel-CxBxvZDk.js → file-explorer-panel-BQi88S1o.js} +1 -1
- package/dist/assets/{hooks-5owWOOnf.js → hooks-BHoInJWN.js} +1 -1
- package/dist/assets/{index-D9fmFCuc.js → index-C968xQuz.js} +7 -7
- package/dist/assets/index-CF8Gfz1R.css +2 -0
- package/dist/assets/{layout-giNu7tsO.js → layout-CzNq5YtR.js} +3 -3
- package/dist/assets/{panels-DftjTXK1.js → panels-Dsf6yEsA.js} +1 -1
- package/dist/assets/{reveal-component-C72HbbmJ.js → reveal-component-CJxdWpbj.js} +1 -1
- package/dist/assets/{run-page-BOSkpSu_.js → run-page-DCu4YGwW.js} +1 -1
- package/dist/assets/{scratchpad-panel-C3gu3_1_.js → scratchpad-panel-BSlnM0TK.js} +1 -1
- package/dist/assets/{session-panel-CNK8FDu3.js → session-panel-BdN43Lzi.js} +1 -1
- package/dist/assets/spec-D9d9nT0z.js +1 -0
- package/dist/assets/{list-filter-CTg19675.js → table-B7KYyTxw.js} +1 -1
- package/dist/assets/{useNotebookActions-ByyEgzc-.js → useNotebookActions-BDD321Hs.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +1 -1
- package/src/components/data-table/TableTopBar.tsx +4 -0
- package/src/components/data-table/__tests__/export-actions.test.tsx +1111 -14
- package/src/components/data-table/__tests__/schemas.test.ts +57 -0
- package/src/components/data-table/data-table.tsx +3 -0
- package/src/components/data-table/export-actions.tsx +365 -81
- package/src/components/data-table/schemas.ts +90 -14
- package/src/plugins/impl/DataTablePlugin.tsx +23 -2
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +65 -8
- package/src/plugins/impl/data-frames/DataFramePlugin.tsx +15 -3
- package/src/stories/dataframe.stories.tsx +7 -0
- package/dist/assets/ConnectedDataExplorerComponent-Jv_dljLQ.js +0 -1
- package/dist/assets/JsonOutput-Dmq_45PS.js +0 -11
- package/dist/assets/index-BGdpHNhZ.css +0 -2
- package/dist/assets/spec-SOFn9xEK.js +0 -1
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
import { useAtomValue } from "jotai";
|
|
4
4
|
import {
|
|
5
5
|
AlertCircleIcon,
|
|
6
|
+
BracesIcon,
|
|
7
|
+
BrickWallIcon,
|
|
6
8
|
ChevronRightIcon,
|
|
7
9
|
ClipboardCopyIcon,
|
|
8
10
|
DownloadIcon,
|
|
11
|
+
FileTextIcon,
|
|
9
12
|
InfoIcon,
|
|
13
|
+
MapIcon,
|
|
14
|
+
TableIcon,
|
|
10
15
|
} from "lucide-react";
|
|
11
16
|
import React from "react";
|
|
12
17
|
import { downloadSizeLimitAtom } from "./download-policy/atoms";
|
|
@@ -14,6 +19,8 @@ import type {
|
|
|
14
19
|
DownloadAsArgs,
|
|
15
20
|
DownloadAsOptions,
|
|
16
21
|
DownloadFormat,
|
|
22
|
+
ExportMetadata,
|
|
23
|
+
GetExportMetadata,
|
|
17
24
|
} from "./schemas";
|
|
18
25
|
import { logNever } from "@/utils/assertNever";
|
|
19
26
|
import { cn } from "@/utils/cn";
|
|
@@ -47,6 +54,7 @@ const EXPORT_OPTIONS = [
|
|
|
47
54
|
label: "CSV",
|
|
48
55
|
format: "csv",
|
|
49
56
|
description: "Comma-separated values",
|
|
57
|
+
icon: TableIcon,
|
|
50
58
|
canDownload: true,
|
|
51
59
|
canCopy: true,
|
|
52
60
|
},
|
|
@@ -54,6 +62,7 @@ const EXPORT_OPTIONS = [
|
|
|
54
62
|
label: "TSV",
|
|
55
63
|
format: "tsv",
|
|
56
64
|
description: "Best for Excel and Google Sheets",
|
|
65
|
+
icon: TableIcon,
|
|
57
66
|
canDownload: true,
|
|
58
67
|
canCopy: true,
|
|
59
68
|
},
|
|
@@ -61,6 +70,7 @@ const EXPORT_OPTIONS = [
|
|
|
61
70
|
label: "JSON",
|
|
62
71
|
format: "json",
|
|
63
72
|
description: "Raw JSON data",
|
|
73
|
+
icon: BracesIcon,
|
|
64
74
|
canDownload: true,
|
|
65
75
|
canCopy: true,
|
|
66
76
|
},
|
|
@@ -68,13 +78,23 @@ const EXPORT_OPTIONS = [
|
|
|
68
78
|
label: "Parquet",
|
|
69
79
|
format: "parquet",
|
|
70
80
|
description: "Columnar binary format",
|
|
81
|
+
icon: BrickWallIcon,
|
|
71
82
|
canDownload: true,
|
|
72
83
|
canCopy: false,
|
|
73
84
|
},
|
|
85
|
+
{
|
|
86
|
+
label: "GeoJSON",
|
|
87
|
+
format: "geojson",
|
|
88
|
+
description: "Geometry in longitude and latitude",
|
|
89
|
+
icon: MapIcon,
|
|
90
|
+
canDownload: true,
|
|
91
|
+
canCopy: true,
|
|
92
|
+
},
|
|
74
93
|
{
|
|
75
94
|
label: "Markdown",
|
|
76
95
|
format: "markdown",
|
|
77
96
|
description: "Preserves hyperlinks and formatting",
|
|
97
|
+
icon: FileTextIcon,
|
|
78
98
|
canDownload: false,
|
|
79
99
|
canCopy: true,
|
|
80
100
|
},
|
|
@@ -82,6 +102,10 @@ const EXPORT_OPTIONS = [
|
|
|
82
102
|
|
|
83
103
|
type ExportFormat = (typeof EXPORT_OPTIONS)[number]["format"];
|
|
84
104
|
type CopyFormat = Exclude<ExportFormat, "parquet">;
|
|
105
|
+
type GeographicFormat = "parquet" | "geojson";
|
|
106
|
+
|
|
107
|
+
const isGeographic = (format: ExportFormat): format is GeographicFormat =>
|
|
108
|
+
format === "parquet" || format === "geojson";
|
|
85
109
|
type ExportAction =
|
|
86
110
|
| { destination: "download"; format: DownloadFormat }
|
|
87
111
|
| { destination: "copy"; format: CopyFormat };
|
|
@@ -95,10 +119,8 @@ type ExportFailure =
|
|
|
95
119
|
}
|
|
96
120
|
| {
|
|
97
121
|
kind: "missing-packages";
|
|
98
|
-
title: string;
|
|
99
122
|
description?: string | null;
|
|
100
123
|
packages: string[];
|
|
101
|
-
featureName: string;
|
|
102
124
|
action: ExportAction;
|
|
103
125
|
};
|
|
104
126
|
|
|
@@ -108,6 +130,7 @@ const COPY_SOURCE_FORMAT: Record<CopyFormat, DownloadFormat> = {
|
|
|
108
130
|
csv: "csv",
|
|
109
131
|
tsv: "tsv",
|
|
110
132
|
json: "json",
|
|
133
|
+
geojson: "geojson",
|
|
111
134
|
markdown: "json",
|
|
112
135
|
};
|
|
113
136
|
|
|
@@ -140,7 +163,7 @@ const DEFAULT_SETTINGS: ExportSettings = {
|
|
|
140
163
|
|
|
141
164
|
type ConfigurableFormat = keyof ExportSettings;
|
|
142
165
|
|
|
143
|
-
const isConfigurable = (format: ExportFormat): format is
|
|
166
|
+
const isConfigurable = (format: ExportFormat): format is keyof ExportSettings =>
|
|
144
167
|
format in DEFAULT_SETTINGS;
|
|
145
168
|
|
|
146
169
|
/**
|
|
@@ -182,6 +205,7 @@ function requestOptions(
|
|
|
182
205
|
}
|
|
183
206
|
break;
|
|
184
207
|
case "parquet":
|
|
208
|
+
case "geojson":
|
|
185
209
|
break;
|
|
186
210
|
default:
|
|
187
211
|
logNever(format);
|
|
@@ -212,6 +236,7 @@ function settingsSummary(
|
|
|
212
236
|
? "Escapes non-ASCII"
|
|
213
237
|
: "Keeps non-ASCII";
|
|
214
238
|
case "parquet":
|
|
239
|
+
case "geojson":
|
|
215
240
|
case "markdown":
|
|
216
241
|
return null;
|
|
217
242
|
default:
|
|
@@ -222,6 +247,8 @@ function settingsSummary(
|
|
|
222
247
|
|
|
223
248
|
export interface ExportActionProps {
|
|
224
249
|
downloadAs: DownloadAsArgs;
|
|
250
|
+
getExportMetadata?: GetExportMetadata;
|
|
251
|
+
metadataSource?: unknown;
|
|
225
252
|
// JSON-serialized size of the currently-rendered data. Used together with
|
|
226
253
|
// downloadSizeLimitAtom to disable the Export button when a host (e.g.,
|
|
227
254
|
// marimo-lsp inside VS Code) declares a download size cap. Null/undefined
|
|
@@ -230,8 +257,14 @@ export interface ExportActionProps {
|
|
|
230
257
|
sizeBytesIsLoading?: boolean;
|
|
231
258
|
}
|
|
232
259
|
|
|
233
|
-
const labelForFormat = (
|
|
234
|
-
|
|
260
|
+
const labelForFormat = (
|
|
261
|
+
format: ExportFormat | DownloadFormat,
|
|
262
|
+
hasGeometry = false,
|
|
263
|
+
): string =>
|
|
264
|
+
format === "parquet" && hasGeometry
|
|
265
|
+
? "GeoParquet"
|
|
266
|
+
: (EXPORT_OPTIONS.find((option) => option.format === format)?.label ??
|
|
267
|
+
format);
|
|
235
268
|
|
|
236
269
|
const failureTitleForAction = (action: ExportAction): string =>
|
|
237
270
|
action.destination === "download"
|
|
@@ -242,15 +275,62 @@ const failureDescription = (error: unknown): string =>
|
|
|
242
275
|
typeof error === "string" ? error : prettyError(error);
|
|
243
276
|
|
|
244
277
|
export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
278
|
+
const { getExportMetadata, metadataSource } = props;
|
|
245
279
|
const [exportDialogOpen, setExportDialogOpen] = React.useState(false);
|
|
246
280
|
const [failure, setFailure] = React.useState<ExportFailure | null>(null);
|
|
247
281
|
const [settings, setSettings] =
|
|
248
282
|
React.useState<ExportSettings>(DEFAULT_SETTINGS);
|
|
249
|
-
const [
|
|
250
|
-
|
|
283
|
+
const [metadata, setMetadata] = React.useState<ExportMetadata | null>(null);
|
|
284
|
+
const [metadataLoading, setMetadataLoading] = React.useState(false);
|
|
285
|
+
const [metadataError, setMetadataError] = React.useState<string | null>(null);
|
|
286
|
+
const [metadataRefresh, setMetadataRefresh] = React.useState(0);
|
|
287
|
+
const [geometryColumn, setGeometryColumn] = React.useState<string | null>(
|
|
288
|
+
null,
|
|
289
|
+
);
|
|
290
|
+
const hasGeometry = !!metadata?.geometry_columns.length;
|
|
291
|
+
const [expandedFormat, setExpandedFormat] = React.useState<
|
|
292
|
+
ConfigurableFormat | GeographicFormat | null
|
|
293
|
+
>(null);
|
|
251
294
|
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
252
295
|
const dialogRef = React.useRef<HTMLDivElement>(null);
|
|
253
296
|
const latestActionId = React.useRef(0);
|
|
297
|
+
React.useEffect(() => {
|
|
298
|
+
latestActionId.current += 1;
|
|
299
|
+
}, [metadataSource]);
|
|
300
|
+
React.useEffect(() => {
|
|
301
|
+
if (!exportDialogOpen || !getExportMetadata) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
let active = true;
|
|
305
|
+
setMetadata(null);
|
|
306
|
+
setMetadataLoading(true);
|
|
307
|
+
setMetadataError(null);
|
|
308
|
+
void getExportMetadata({}).then(
|
|
309
|
+
(result) => {
|
|
310
|
+
if (!active) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
setMetadata(result);
|
|
314
|
+
setGeometryColumn((current) =>
|
|
315
|
+
current !== null &&
|
|
316
|
+
result.geometry_columns.some((column) => column.name === current)
|
|
317
|
+
? current
|
|
318
|
+
: result.default_geometry_column,
|
|
319
|
+
);
|
|
320
|
+
setMetadataLoading(false);
|
|
321
|
+
},
|
|
322
|
+
(error: unknown) => {
|
|
323
|
+
if (!active) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
setMetadataError(failureDescription(error));
|
|
327
|
+
setMetadataLoading(false);
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
return () => {
|
|
331
|
+
active = false;
|
|
332
|
+
};
|
|
333
|
+
}, [exportDialogOpen, getExportMetadata, metadataSource, metadataRefresh]);
|
|
254
334
|
const policy = useAtomValue(downloadSizeLimitAtom);
|
|
255
335
|
const overLimit = !!(
|
|
256
336
|
policy &&
|
|
@@ -288,6 +368,9 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
288
368
|
setFailure(null);
|
|
289
369
|
setExpandedFormat(null);
|
|
290
370
|
setSettings(DEFAULT_SETTINGS);
|
|
371
|
+
setMetadata(null);
|
|
372
|
+
setMetadataError(null);
|
|
373
|
+
setGeometryColumn(null);
|
|
291
374
|
}
|
|
292
375
|
};
|
|
293
376
|
|
|
@@ -313,27 +396,33 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
313
396
|
filename: string;
|
|
314
397
|
} | null> => {
|
|
315
398
|
const options = requestOptions(settings, format, action.destination);
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
399
|
+
const request = {
|
|
400
|
+
format,
|
|
401
|
+
...(options ? { options } : {}),
|
|
402
|
+
...(isGeographic(format) && geometryColumn !== null
|
|
403
|
+
? { geometry_column: geometryColumn }
|
|
404
|
+
: {}),
|
|
405
|
+
};
|
|
406
|
+
const response = await props.downloadAs(request);
|
|
407
|
+
if (actionId !== latestActionId.current) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
319
410
|
|
|
320
411
|
if (response.missing_packages && response.missing_packages.length > 0) {
|
|
321
412
|
setActionFailure(actionId, {
|
|
322
413
|
kind: "missing-packages",
|
|
323
|
-
title: "Export failed",
|
|
324
414
|
packages: response.missing_packages,
|
|
325
|
-
featureName: `${labelForFormat(action.format)} export`,
|
|
326
415
|
description: response.error,
|
|
327
416
|
action,
|
|
328
417
|
});
|
|
329
418
|
return null;
|
|
330
419
|
}
|
|
331
420
|
|
|
332
|
-
if (response.error) {
|
|
421
|
+
if (response.error || response.code || !response.url) {
|
|
333
422
|
setActionFailure(actionId, {
|
|
334
423
|
kind: "error",
|
|
335
424
|
title: failureTitleForAction(action),
|
|
336
|
-
description: response.error,
|
|
425
|
+
description: response.error ?? "The export did not produce a file.",
|
|
337
426
|
action,
|
|
338
427
|
});
|
|
339
428
|
return null;
|
|
@@ -348,7 +437,7 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
348
437
|
const handleDownload = async (format: DownloadFormat) => {
|
|
349
438
|
const action: ExportAction = { destination: "download", format };
|
|
350
439
|
const actionId = beginAction();
|
|
351
|
-
const label = labelForFormat(format);
|
|
440
|
+
const label = labelForFormat(format, hasGeometry);
|
|
352
441
|
try {
|
|
353
442
|
const ok = await withLoadingToast(
|
|
354
443
|
`Preparing ${label} export...`,
|
|
@@ -357,6 +446,9 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
357
446
|
if (!result) {
|
|
358
447
|
return false;
|
|
359
448
|
}
|
|
449
|
+
if (actionId !== latestActionId.current) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
360
452
|
const rawName = (result.filename ?? "").trim();
|
|
361
453
|
const baseName = Filenames.withoutExtension(rawName) || "download";
|
|
362
454
|
const downloadName = `${baseName}.${format}`;
|
|
@@ -396,7 +488,7 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
396
488
|
actionId: number,
|
|
397
489
|
) => {
|
|
398
490
|
await withLoadingToast(
|
|
399
|
-
`Preparing ${labelForFormat(format)} for clipboard...`,
|
|
491
|
+
`Preparing ${labelForFormat(format, hasGeometry)} for clipboard...`,
|
|
400
492
|
async () => {
|
|
401
493
|
const sourceFormat = COPY_SOURCE_FORMAT[format];
|
|
402
494
|
const result = await resolveDownloadUrl(sourceFormat, action, actionId);
|
|
@@ -408,6 +500,7 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
408
500
|
switch (format) {
|
|
409
501
|
case "tsv":
|
|
410
502
|
case "csv":
|
|
503
|
+
case "geojson":
|
|
411
504
|
text = await fetchText(result.url);
|
|
412
505
|
break;
|
|
413
506
|
case "json": {
|
|
@@ -425,6 +518,9 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
425
518
|
return;
|
|
426
519
|
}
|
|
427
520
|
|
|
521
|
+
if (actionId !== latestActionId.current) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
428
524
|
await copyToClipboard(text);
|
|
429
525
|
toast({
|
|
430
526
|
title: "Copied to clipboard",
|
|
@@ -457,8 +553,61 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
457
553
|
}
|
|
458
554
|
};
|
|
459
555
|
|
|
556
|
+
const retryAfterInstall = async (action: ExportAction) => {
|
|
557
|
+
if (isGeographic(action.format) && getExportMetadata) {
|
|
558
|
+
const actionId = latestActionId.current;
|
|
559
|
+
let result: ExportMetadata;
|
|
560
|
+
try {
|
|
561
|
+
result = await getExportMetadata({});
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (actionId === latestActionId.current) {
|
|
564
|
+
setMetadataError(failureDescription(error));
|
|
565
|
+
}
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (actionId !== latestActionId.current) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
setMetadata(result);
|
|
572
|
+
setMetadataError(null);
|
|
573
|
+
if (result.formats[action.format]?.available === false) {
|
|
574
|
+
setFailure(null);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (
|
|
578
|
+
geometryColumn !== null &&
|
|
579
|
+
!result.geometry_columns.some(
|
|
580
|
+
(column) => column.name === geometryColumn,
|
|
581
|
+
)
|
|
582
|
+
) {
|
|
583
|
+
setGeometryColumn(result.default_geometry_column);
|
|
584
|
+
setActionFailure(actionId, {
|
|
585
|
+
kind: "error",
|
|
586
|
+
title: failureTitleForAction(action),
|
|
587
|
+
description:
|
|
588
|
+
"The selected geometry is no longer available. Choose another geometry to export.",
|
|
589
|
+
action,
|
|
590
|
+
});
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (result.geometry_columns.length > 0 && geometryColumn === null) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (
|
|
597
|
+
action.format === "geojson" &&
|
|
598
|
+
!result.geometry_columns.find(
|
|
599
|
+
(column) => column.name === geometryColumn,
|
|
600
|
+
)?.crs
|
|
601
|
+
) {
|
|
602
|
+
setFailure(null);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
retryAction(action);
|
|
607
|
+
};
|
|
608
|
+
|
|
460
609
|
const toggleExpanded = (format: ExportFormat) => {
|
|
461
|
-
if (!isConfigurable(format)) {
|
|
610
|
+
if (!isConfigurable(format) && !(isGeographic(format) && hasGeometry)) {
|
|
462
611
|
return;
|
|
463
612
|
}
|
|
464
613
|
setExpandedFormat((current) => (current === format ? null : format));
|
|
@@ -504,30 +653,80 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
504
653
|
aria-label="Export formats"
|
|
505
654
|
className="list-none overflow-hidden rounded-md border divide-y"
|
|
506
655
|
>
|
|
507
|
-
{EXPORT_OPTIONS.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
656
|
+
{EXPORT_OPTIONS.filter(
|
|
657
|
+
(option) => option.format !== "geojson" || hasGeometry,
|
|
658
|
+
).map((option) => {
|
|
659
|
+
const optionLabel = labelForFormat(option.format, hasGeometry);
|
|
660
|
+
const configurableFormat =
|
|
661
|
+
isConfigurable(option.format) ||
|
|
662
|
+
(isGeographic(option.format) && hasGeometry)
|
|
663
|
+
? option.format
|
|
664
|
+
: null;
|
|
511
665
|
const expanded =
|
|
512
666
|
configurableFormat !== null &&
|
|
513
667
|
expandedFormat === configurableFormat;
|
|
514
668
|
const panelId = `export-options-${option.format}`;
|
|
515
669
|
const rowFailure =
|
|
516
670
|
failure?.action.format === option.format ? failure : null;
|
|
517
|
-
const summary =
|
|
671
|
+
const summary =
|
|
672
|
+
isGeographic(option.format) && hasGeometry
|
|
673
|
+
? geometryColumn === ""
|
|
674
|
+
? "(unnamed geometry)"
|
|
675
|
+
: (geometryColumn ?? "Choose geometry")
|
|
676
|
+
: settingsSummary(settings, option.format);
|
|
677
|
+
const selectedGeometryIndex =
|
|
678
|
+
metadata?.geometry_columns.findIndex(
|
|
679
|
+
(column) => column.name === geometryColumn,
|
|
680
|
+
) ?? -1;
|
|
681
|
+
const eligibility = isGeographic(option.format)
|
|
682
|
+
? metadata?.formats[option.format]
|
|
683
|
+
: undefined;
|
|
684
|
+
const selectedGeometry =
|
|
685
|
+
metadata?.geometry_columns[selectedGeometryIndex];
|
|
686
|
+
const missingCrs =
|
|
687
|
+
option.format === "geojson" &&
|
|
688
|
+
selectedGeometry !== undefined &&
|
|
689
|
+
!selectedGeometry.crs;
|
|
690
|
+
const geometryReason = metadataError
|
|
691
|
+
? `Could not load geometry options: ${metadataError}`
|
|
692
|
+
: (eligibility?.reason ??
|
|
693
|
+
(hasGeometry && geometryColumn === null
|
|
694
|
+
? `Choose a primary geometry column to export ${optionLabel}.`
|
|
695
|
+
: missingCrs
|
|
696
|
+
? `Geometry column ${geometryColumn === "" ? "(unnamed geometry)" : geometryColumn} needs a CRS for GeoJSON export. Declare its source CRS in Python before exporting.`
|
|
697
|
+
: null));
|
|
698
|
+
const missingPackageFailure =
|
|
699
|
+
rowFailure?.kind === "missing-packages" ? rowFailure : null;
|
|
700
|
+
const missingPackages =
|
|
701
|
+
missingPackageFailure?.packages ??
|
|
702
|
+
(eligibility?.available === false
|
|
703
|
+
? eligibility.missing_packages
|
|
704
|
+
: []);
|
|
705
|
+
const geographicUnavailable =
|
|
706
|
+
isGeographic(option.format) &&
|
|
707
|
+
((!!getExportMetadata && !metadata) ||
|
|
708
|
+
metadataLoading ||
|
|
709
|
+
!!metadataError ||
|
|
710
|
+
eligibility?.available === false ||
|
|
711
|
+
missingCrs ||
|
|
712
|
+
(!!metadata?.geometry_columns.length &&
|
|
713
|
+
geometryColumn === null));
|
|
714
|
+
const copyUnavailable =
|
|
715
|
+
isGeographic(option.format) &&
|
|
716
|
+
(geographicUnavailable || missingPackages.length > 0);
|
|
518
717
|
return (
|
|
519
718
|
<li
|
|
520
719
|
key={option.format}
|
|
521
720
|
data-testid={`export-row-${option.format}`}
|
|
522
721
|
className={cn(expanded && "bg-muted/40")}
|
|
523
722
|
>
|
|
524
|
-
<div className="grid min-h-[60px] grid-cols-[
|
|
723
|
+
<div className="grid min-h-[60px] grid-cols-[28px_minmax(0,1fr)_auto] items-center gap-x-1 px-2.5 py-2 hover:bg-accent/50">
|
|
525
724
|
{configurableFormat ? (
|
|
526
725
|
<Button
|
|
527
726
|
type="button"
|
|
528
727
|
variant="ghost"
|
|
529
728
|
size="icon"
|
|
530
|
-
aria-label={`${
|
|
729
|
+
aria-label={`${optionLabel} options`}
|
|
531
730
|
aria-expanded={expanded}
|
|
532
731
|
aria-controls={panelId}
|
|
533
732
|
onClick={() => toggleExpanded(option.format)}
|
|
@@ -542,30 +741,39 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
542
741
|
) : (
|
|
543
742
|
<span aria-hidden={true} className="h-6 w-6" />
|
|
544
743
|
)}
|
|
545
|
-
<div className="
|
|
546
|
-
<
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
{summary}
|
|
744
|
+
<div className="flex min-w-0 items-center gap-3 pl-1">
|
|
745
|
+
<option.icon
|
|
746
|
+
aria-hidden={true}
|
|
747
|
+
data-testid={`export-format-icon-${option.format}`}
|
|
748
|
+
className="h-4 w-4 shrink-0 text-muted-foreground"
|
|
749
|
+
/>
|
|
750
|
+
<div className="grid min-w-0 gap-1">
|
|
751
|
+
<span className="flex min-w-0 items-baseline gap-x-2">
|
|
752
|
+
<span className="text-sm font-medium leading-tight">
|
|
753
|
+
{optionLabel}
|
|
556
754
|
</span>
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
755
|
+
{summary && (
|
|
756
|
+
<span
|
|
757
|
+
data-testid={`export-summary-${option.format}`}
|
|
758
|
+
className="min-w-0 truncate text-xs text-muted-foreground"
|
|
759
|
+
>
|
|
760
|
+
{summary}
|
|
761
|
+
</span>
|
|
762
|
+
)}
|
|
763
|
+
</span>
|
|
764
|
+
<span className="text-xs text-muted-foreground leading-tight">
|
|
765
|
+
{option.format === "parquet" && hasGeometry
|
|
766
|
+
? "Parquet with geometry and CRS metadata"
|
|
767
|
+
: option.description}
|
|
768
|
+
</span>
|
|
769
|
+
</div>
|
|
562
770
|
</div>
|
|
563
771
|
<div className="flex items-center gap-0.5">
|
|
564
772
|
<Tooltip
|
|
565
773
|
content={
|
|
566
774
|
option.canDownload
|
|
567
|
-
? `Download ${
|
|
568
|
-
: `${
|
|
775
|
+
? `Download ${optionLabel}`
|
|
776
|
+
: `${optionLabel} is available for copy only.`
|
|
569
777
|
}
|
|
570
778
|
>
|
|
571
779
|
<span className="inline-flex">
|
|
@@ -573,10 +781,16 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
573
781
|
type="button"
|
|
574
782
|
variant="ghost"
|
|
575
783
|
size="icon"
|
|
576
|
-
aria-label={`Download ${
|
|
577
|
-
disabled={
|
|
784
|
+
aria-label={`Download ${optionLabel}`}
|
|
785
|
+
disabled={
|
|
786
|
+
!option.canDownload ||
|
|
787
|
+
geographicUnavailable ||
|
|
788
|
+
missingPackages.length > 0
|
|
789
|
+
}
|
|
578
790
|
onClick={
|
|
579
|
-
option.canDownload
|
|
791
|
+
option.canDownload &&
|
|
792
|
+
!geographicUnavailable &&
|
|
793
|
+
missingPackages.length === 0
|
|
580
794
|
? () => {
|
|
581
795
|
void handleDownload(option.format);
|
|
582
796
|
}
|
|
@@ -590,8 +804,8 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
590
804
|
<Tooltip
|
|
591
805
|
content={
|
|
592
806
|
option.canCopy
|
|
593
|
-
? `Copy ${
|
|
594
|
-
: `${
|
|
807
|
+
? `Copy ${optionLabel}`
|
|
808
|
+
: `${optionLabel} is available for download only.`
|
|
595
809
|
}
|
|
596
810
|
>
|
|
597
811
|
<span className="inline-flex">
|
|
@@ -599,10 +813,10 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
599
813
|
type="button"
|
|
600
814
|
variant="ghost"
|
|
601
815
|
size="icon"
|
|
602
|
-
aria-label={`Copy ${
|
|
603
|
-
disabled={!option.canCopy}
|
|
816
|
+
aria-label={`Copy ${optionLabel}`}
|
|
817
|
+
disabled={!option.canCopy || copyUnavailable}
|
|
604
818
|
onClick={
|
|
605
|
-
option.canCopy
|
|
819
|
+
option.canCopy && !copyUnavailable
|
|
606
820
|
? () => {
|
|
607
821
|
void handleCopyAction(option.format);
|
|
608
822
|
}
|
|
@@ -620,42 +834,112 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
|
|
|
620
834
|
id={panelId}
|
|
621
835
|
className="grid grid-cols-2 gap-3 border-t bg-muted/50 px-3.5 py-3.5 pl-[52px]"
|
|
622
836
|
>
|
|
623
|
-
<legend className="sr-only">{
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
837
|
+
<legend className="sr-only">{optionLabel} options</legend>
|
|
838
|
+
{isGeographic(configurableFormat) && (
|
|
839
|
+
<p className="col-span-2 text-xs text-muted-foreground">
|
|
840
|
+
{configurableFormat === "geojson"
|
|
841
|
+
? "Reproject the selected geometry to longitude and latitude (WGS 84). Keep other geometry columns as complete WKT in their source coordinates."
|
|
842
|
+
: "Use the selected column as the primary geometry. Preserve all geometry columns and their coordinate reference systems (CRS)."}
|
|
843
|
+
</p>
|
|
844
|
+
)}
|
|
845
|
+
{isGeographic(configurableFormat) ? (
|
|
846
|
+
<SettingField
|
|
847
|
+
id="export-primary-geometry"
|
|
848
|
+
label="Primary geometry"
|
|
849
|
+
help={`Geometry column to use as the primary geometry in the ${optionLabel} file.`}
|
|
850
|
+
>
|
|
851
|
+
<NativeSelect
|
|
852
|
+
id="export-primary-geometry"
|
|
853
|
+
className="mb-0 w-full"
|
|
854
|
+
value={String(selectedGeometryIndex)}
|
|
855
|
+
onChange={(event) => {
|
|
856
|
+
const index = Number(event.target.value);
|
|
857
|
+
setGeometryColumn(
|
|
858
|
+
metadata?.geometry_columns[index]?.name ?? null,
|
|
859
|
+
);
|
|
860
|
+
}}
|
|
861
|
+
>
|
|
862
|
+
<option value="-1" disabled={true}>
|
|
863
|
+
Choose geometry
|
|
864
|
+
</option>
|
|
865
|
+
{metadata?.geometry_columns.map((column, index) => (
|
|
866
|
+
<option key={column.name} value={String(index)}>
|
|
867
|
+
{column.name || "(unnamed geometry)"}
|
|
868
|
+
</option>
|
|
869
|
+
))}
|
|
870
|
+
</NativeSelect>
|
|
871
|
+
</SettingField>
|
|
872
|
+
) : (
|
|
873
|
+
<ExportSettingsFields
|
|
874
|
+
format={configurableFormat}
|
|
875
|
+
settings={settings}
|
|
876
|
+
onChange={setSettings}
|
|
877
|
+
/>
|
|
878
|
+
)}
|
|
629
879
|
</fieldset>
|
|
630
880
|
)}
|
|
631
|
-
{
|
|
881
|
+
{isGeographic(option.format) &&
|
|
882
|
+
missingPackages.length === 0 &&
|
|
883
|
+
geometryReason && (
|
|
884
|
+
<div className="flex items-center justify-between gap-2 border-t px-3 py-2 pl-[42px] text-xs text-muted-foreground">
|
|
885
|
+
<span>{geometryReason}</span>
|
|
886
|
+
{metadataError && (
|
|
887
|
+
<Button
|
|
888
|
+
type="button"
|
|
889
|
+
size="xs"
|
|
890
|
+
variant="outline"
|
|
891
|
+
onClick={() =>
|
|
892
|
+
setMetadataRefresh((current) => current + 1)
|
|
893
|
+
}
|
|
894
|
+
>
|
|
895
|
+
Retry
|
|
896
|
+
</Button>
|
|
897
|
+
)}
|
|
898
|
+
</div>
|
|
899
|
+
)}
|
|
900
|
+
{missingPackages.length > 0 && (
|
|
632
901
|
<div className="border-t px-3 py-2 pl-[42px]">
|
|
633
|
-
<
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
)}
|
|
654
|
-
</AlertDescription>
|
|
655
|
-
</div>
|
|
656
|
-
</Alert>
|
|
902
|
+
<MissingPackagePrompt
|
|
903
|
+
packages={missingPackages}
|
|
904
|
+
featureName={`${optionLabel} export`}
|
|
905
|
+
description={
|
|
906
|
+
missingPackageFailure?.description ??
|
|
907
|
+
eligibility?.reason
|
|
908
|
+
}
|
|
909
|
+
onInstall={() => {
|
|
910
|
+
void retryAfterInstall(
|
|
911
|
+
missingPackageFailure?.action ?? {
|
|
912
|
+
destination: "download",
|
|
913
|
+
format:
|
|
914
|
+
option.format === "geojson"
|
|
915
|
+
? "geojson"
|
|
916
|
+
: "parquet",
|
|
917
|
+
},
|
|
918
|
+
);
|
|
919
|
+
}}
|
|
920
|
+
className="items-start"
|
|
921
|
+
/>
|
|
657
922
|
</div>
|
|
658
923
|
)}
|
|
924
|
+
{rowFailure?.kind === "error" &&
|
|
925
|
+
missingPackages.length === 0 && (
|
|
926
|
+
<div className="border-t px-3 py-2 pl-[42px]">
|
|
927
|
+
<Alert
|
|
928
|
+
variant="destructive"
|
|
929
|
+
className="p-3 has-[svg]:pl-9 [&>svg]:left-3 [&>svg]:top-3"
|
|
930
|
+
>
|
|
931
|
+
<AlertCircleIcon className="h-4 w-4" />
|
|
932
|
+
<div>
|
|
933
|
+
<AlertTitle className="text-sm">
|
|
934
|
+
{rowFailure.title}
|
|
935
|
+
</AlertTitle>
|
|
936
|
+
<AlertDescription className="text-xs">
|
|
937
|
+
{rowFailure.description}
|
|
938
|
+
</AlertDescription>
|
|
939
|
+
</div>
|
|
940
|
+
</Alert>
|
|
941
|
+
</div>
|
|
942
|
+
)}
|
|
659
943
|
</li>
|
|
660
944
|
);
|
|
661
945
|
})}
|