Skip to content

Commit 68d00cf

Browse files
committed
abortable export
1 parent 42b9c8e commit 68d00cf

File tree

2 files changed

+70
-21
lines changed

2 files changed

+70
-21
lines changed

packages/compass-data-modeling/src/components/export-diagram-modal.tsx

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useState } from 'react';
1+
import React, { useCallback, useEffect, useRef, useState } from 'react';
22
import {
33
Button,
44
css,
@@ -13,6 +13,7 @@ import {
1313
RadioGroup,
1414
spacing,
1515
SpinLoader,
16+
useToast,
1617
} from '@mongodb-js/compass-components';
1718
import {
1819
closeExportModal,
@@ -24,6 +25,7 @@ import type { DataModelingState } from '../store/reducer';
2425
import type { StaticModel } from '../services/data-model-storage';
2526
import { exportToJson, exportToPng } from '../services/export-diagram';
2627
import { useDiagram } from '@mongodb-js/diagramming';
28+
import { isCancelError } from '@mongodb-js/compass-utils';
2729

2830
const nbsp = '\u00a0';
2931

@@ -64,25 +66,68 @@ const ExportDiagramModal = ({
6466
const [exportFormat, setExportFormat] = useState<'png' | 'json' | null>(null);
6567
const diagram = useDiagram();
6668
const [isExporting, setIsExporting] = useState(false);
69+
const abortControllerRef = useRef<AbortController | null>(null);
70+
const toast = useToast();
71+
useEffect(() => {
72+
const cleanup = () => {
73+
if (abortControllerRef.current) {
74+
abortControllerRef.current.abort();
75+
abortControllerRef.current = null;
76+
}
77+
};
78+
const abortController = new AbortController();
79+
if (isModalOpen) {
80+
abortControllerRef.current = abortController;
81+
} else {
82+
cleanup();
83+
}
84+
return cleanup;
85+
}, [isModalOpen]);
86+
87+
const onClose = useCallback(() => {
88+
setExportFormat(null);
89+
setIsExporting(false);
90+
abortControllerRef.current?.abort();
91+
abortControllerRef.current = null;
92+
onCloseClick();
93+
}, [onCloseClick]);
6794

6895
const onExport = useCallback(async () => {
69-
if (!exportFormat || !model) {
70-
return;
96+
try {
97+
if (!exportFormat || !model) {
98+
return;
99+
}
100+
setIsExporting(true);
101+
if (exportFormat === 'json') {
102+
exportToJson(diagramLabel, model);
103+
} else if (exportFormat === 'png') {
104+
await exportToPng(
105+
diagramLabel,
106+
diagram,
107+
abortControllerRef.current?.signal
108+
);
109+
}
110+
} catch (error) {
111+
if (isCancelError(error)) {
112+
return;
113+
}
114+
toast.pushToast({
115+
id: 'export-diagram-error',
116+
variant: 'warning',
117+
title: 'Export failed',
118+
description: `An error occurred while exporting the diagram: ${
119+
(error as Error).message
120+
}`,
121+
});
122+
} finally {
123+
onClose();
71124
}
72-
setIsExporting(true);
73-
if (exportFormat === 'json') {
74-
exportToJson(diagramLabel, model);
75-
} else if (exportFormat === 'png') {
76-
await exportToPng(diagramLabel, diagram);
77-
}
78-
onCloseClick();
79-
setIsExporting(false);
80-
}, [exportFormat, onCloseClick, model, diagram, diagramLabel]);
125+
}, [exportFormat, onClose, model, diagram, diagramLabel, toast]);
81126

82127
return (
83128
<Modal
84129
open={isModalOpen}
85-
setOpen={onCloseClick}
130+
setOpen={onClose}
86131
data-testid="export-diagram-modal"
87132
>
88133
<ModalHeader
@@ -140,11 +185,7 @@ const ExportDiagramModal = ({
140185
>
141186
Export
142187
</Button>
143-
<Button
144-
variant="default"
145-
onClick={onCloseClick}
146-
data-testid="cancel-button"
147-
>
188+
<Button variant="default" onClick={onClose} data-testid="cancel-button">
148189
Cancel
149190
</Button>
150191
</ModalFooter>

packages/compass-data-modeling/src/services/export-diagram.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { StaticModel } from './data-model-storage';
1616
import ReactDOM from 'react-dom';
1717
import { toPng } from 'html-to-image';
1818
import { rafraf, spacing } from '@mongodb-js/compass-components';
19+
import { raceWithAbort } from '@mongodb-js/compass-utils';
1920

2021
// TODO: Export these methods (and type) from the diagramming package
2122
type DiagramInstance = ReturnType<typeof useDiagram>;
@@ -67,7 +68,11 @@ function moveSvgDefsToViewportElement(
6768
markerDef.remove();
6869
}
6970

70-
export async function exportToPng(fileName: string, diagram: DiagramInstance) {
71+
export async function exportToPng(
72+
fileName: string,
73+
diagram: DiagramInstance,
74+
signal?: AbortSignal
75+
) {
7176
const container = document.createElement('div');
7277
container.setAttribute('data-testid', 'export-diagram-container');
7378
// Push it out of the viewport
@@ -76,7 +81,10 @@ export async function exportToPng(fileName: string, diagram: DiagramInstance) {
7681
container.style.left = '100vw';
7782
document.body.appendChild(container);
7883

79-
const dataUri = await getExportPngDataUri(container, diagram);
84+
const dataUri = await raceWithAbort(
85+
getExportPngDataUri(container, diagram),
86+
signal ?? new AbortController().signal
87+
);
8088
downloadFile(dataUri, fileName, () => {
8189
container.remove();
8290
});
@@ -110,7 +118,7 @@ export function getExportPngDataUri(
110118
'.react-flow__viewport'
111119
);
112120
if (!viewportElement) {
113-
throw new Error('Diagram element not found');
121+
return reject(new Error('Diagram element not found'));
114122
}
115123

116124
const bounds = getNodesBounds(nodes);

0 commit comments

Comments
 (0)