Skip to content

Commit ef66a03

Browse files
authored
feat(data-modeling): open diagram COMPASS-9546 (#7127)
* add import file button * import diagram * open diagram back * e2e tests * clean up * make ts happy * bot review * use existing diagram * change text to import * cleaner error handling * better names for import * rename filinput * rephrase
1 parent b1e7351 commit ef66a03

File tree

23 files changed

+497
-352
lines changed

23 files changed

+497
-352
lines changed

configs/mocha-config-compass/register/jsdom-extra-mocks-register.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ if (!window.document.queryCommandSupported) {
4949
globalThis.EventTarget = window.EventTarget;
5050
globalThis.CustomEvent = window.CustomEvent;
5151
globalThis.Event = window.Event;
52+
globalThis.Blob = window.Blob;
53+
globalThis.File = window.File;

packages/compass-components/src/components/file-input.spec.tsx renamed to packages/compass-components/src/components/file-picker-dialog.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import FileInput, {
1515
FileInputBackendProvider,
1616
createElectronFileInputBackend,
17-
} from './file-input';
17+
} from './file-picker-dialog';
1818

1919
describe('FileInput', function () {
2020
let spy;

packages/compass-components/src/components/file-input.tsx renamed to packages/compass-components/src/components/file-picker-dialog.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,17 @@ export function createElectronFileInputBackend<ElectronWindow>(
311311
};
312312
}
313313

314-
function FileInput({
314+
/**
315+
* This component is not intended to work in a browser environment. It is designed
316+
* to be used in environments like Electron where you have access to nodes fs module
317+
* to read/write files.
318+
*
319+
* Always use `FileSelector` component, only use `FilePickerDialog` if you absolutely
320+
* know what you're doing
321+
*
322+
* @deprecated
323+
*/
324+
function FilePickerDialog({
315325
autoOpen = false,
316326
id,
317327
label,
@@ -553,4 +563,4 @@ function FileInput({
553563
);
554564
}
555565

556-
export default FileInput;
566+
export default FilePickerDialog;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React, { type InputHTMLAttributes, useRef } from 'react';
2+
3+
type FileSelectorTriggerProps = {
4+
onClick: () => void;
5+
};
6+
7+
type FileSelectorProps = Omit<
8+
InputHTMLAttributes<HTMLInputElement>,
9+
'onChange' | 'onSelect' | 'type' | 'style' | 'ref'
10+
> & {
11+
trigger: (props: FileSelectorTriggerProps) => React.ReactElement;
12+
onSelect: (files: File[]) => void;
13+
};
14+
15+
export function FileSelector({
16+
trigger,
17+
onSelect,
18+
...props
19+
}: FileSelectorProps) {
20+
const inputRef = useRef<HTMLInputElement>(null);
21+
22+
const onFilesChanged = React.useCallback(
23+
(evt: React.ChangeEvent<HTMLInputElement>) => {
24+
onSelect(Array.from(evt.currentTarget.files ?? []));
25+
},
26+
[onSelect]
27+
);
28+
29+
return (
30+
<>
31+
<input
32+
{...props}
33+
ref={inputRef}
34+
type="file"
35+
onChange={onFilesChanged}
36+
style={{ display: 'none' }}
37+
/>
38+
{trigger({
39+
onClick: () => inputRef.current?.click(),
40+
})}
41+
</>
42+
);
43+
}

packages/compass-components/src/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ import type {
1818
ElectronFileDialogOptions,
1919
ElectronShowFileDialogProvider,
2020
FileInputBackend,
21-
} from './components/file-input';
22-
import FileInput, {
21+
} from './components/file-picker-dialog';
22+
import FilePickerDialog, {
2323
createElectronFileInputBackend,
2424
createJSDomFileInputDummyBackend,
2525
FileInputBackendProvider,
26-
} from './components/file-input';
26+
} from './components/file-picker-dialog';
2727
import { OptionsToggle } from './components/options-toggle';
2828
import {
2929
ErrorSummary,
@@ -118,7 +118,7 @@ export {
118118
CollapsibleFieldSet,
119119
ConfirmationModal,
120120
ErrorSummary,
121-
FileInput,
121+
FilePickerDialog,
122122
FileInputBackendProvider,
123123
IndexIcon,
124124
OptionsToggle,
@@ -219,3 +219,4 @@ export {
219219
export { SelectList } from './components/select-list';
220220
export { ParagraphSkeleton } from '@leafygreen-ui/skeleton-loader';
221221
export { InsightsChip } from './components/insights-chip';
222+
export { FileSelector } from './components/file-selector';

packages/compass-connection-import-export/src/components/file-input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback } from 'react';
2-
import { FileInput as CompassFileInput } from '@mongodb-js/compass-components';
2+
import { FilePickerDialog } from '@mongodb-js/compass-components';
33

44
type FileInputProps = {
55
label: string;
@@ -24,7 +24,7 @@ export function FileInput({
2424
);
2525

2626
return (
27-
<CompassFileInput
27+
<FilePickerDialog
2828
disabled={disabled}
2929
label={label}
3030
onChange={onChangeFiles}

packages/compass-data-modeling/src/components/diagram-list-toolbar.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
useDarkMode,
1212
} from '@mongodb-js/compass-components';
1313
import { DiagramListContext } from './saved-diagrams-list';
14+
import { ImportDiagramButton } from './import-diagram-button';
1415

1516
const containerStyles = css({
1617
padding: spacing[400],
@@ -27,16 +28,19 @@ const containerStyles = css({
2728
const titleStyles = css({
2829
gridArea: 'title',
2930
});
30-
const createDiagramContainerStyles = css({
31+
const diagramActionsStyles = css({
3132
gridArea: 'createDiagram',
3233
display: 'flex',
3334
justifyContent: 'flex-end',
35+
gap: spacing[200],
3436
});
3537
const searchInputStyles = css({
3638
gridArea: 'searchInput',
3739
});
3840
const sortControlsStyles = css({
3941
gridArea: 'sortControls',
42+
display: 'flex',
43+
justifyContent: 'flex-end',
4044
});
4145

4246
const toolbarTitleLightStyles = css({ color: palette.gray.dark1 });
@@ -48,6 +52,7 @@ export const DiagramListToolbar = () => {
4852
onCreateDiagram,
4953
sortControls,
5054
searchTerm,
55+
onImportDiagram,
5156
} = useContext(DiagramListContext);
5257
const darkMode = useDarkMode();
5358

@@ -61,7 +66,12 @@ export const DiagramListToolbar = () => {
6166
>
6267
Open an existing diagram:
6368
</Subtitle>
64-
<div className={createDiagramContainerStyles}>
69+
<div className={diagramActionsStyles}>
70+
<ImportDiagramButton
71+
leftGlyph={<Icon glyph="Import" />}
72+
size="small"
73+
onImportDiagram={onImportDiagram}
74+
/>
6575
<Button
6676
onClick={onCreateDiagram}
6777
variant="primary"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import { Button, FileSelector } from '@mongodb-js/compass-components';
3+
4+
type importDiagramButtonProps = Omit<
5+
React.ComponentProps<typeof Button>,
6+
'onClick'
7+
> & {
8+
onImportDiagram: (file: File) => void;
9+
};
10+
11+
export const ImportDiagramButton = ({
12+
onImportDiagram,
13+
...buttonProps
14+
}: importDiagramButtonProps) => {
15+
return (
16+
<FileSelector
17+
id="import-diagram-file-input"
18+
data-testid="import-diagram-file-input"
19+
multiple={false}
20+
accept=".compass"
21+
onSelect={(files) => {
22+
if (files.length === 0) {
23+
return;
24+
}
25+
onImportDiagram(files[0]);
26+
}}
27+
trigger={({ onClick }) => (
28+
<Button {...buttonProps} onClick={onClick}>
29+
Import Diagram
30+
</Button>
31+
)}
32+
/>
33+
);
34+
};

packages/compass-data-modeling/src/components/saved-diagrams-list.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
deleteDiagram,
1919
selectCurrentModel,
2020
openDiagram,
21+
openDiagramFromFile,
2122
renameDiagram,
2223
} from '../store/diagram';
2324
import type { MongoDBDataModelDescription } from '../services/data-model-storage';
@@ -27,6 +28,7 @@ import FlexibilityIcon from './icons/flexibility';
2728
import { CARD_HEIGHT, CARD_WIDTH, DiagramCard } from './diagram-card';
2829
import { DiagramListToolbar } from './diagram-list-toolbar';
2930
import toNS from 'mongodb-ns';
31+
import { ImportDiagramButton } from './import-diagram-button';
3032

3133
const sortBy = [
3234
{
@@ -49,13 +51,17 @@ const rowStyles = css({
4951

5052
export const DiagramListContext = React.createContext<{
5153
onSearchDiagrams: (search: string) => void;
54+
onImportDiagram: (file: File) => void;
5255
onCreateDiagram: () => void;
5356
sortControls: React.ReactElement | null;
5457
searchTerm: string;
5558
}>({
5659
onSearchDiagrams: () => {
5760
/** */
5861
},
62+
onImportDiagram: () => {
63+
/** */
64+
},
5965
onCreateDiagram: () => {
6066
/** */
6167
},
@@ -67,6 +73,11 @@ const subTitleStyles = css({
6773
maxWidth: '750px',
6874
});
6975

76+
const diagramActionsStyles = css({
77+
display: 'flex',
78+
gap: spacing[200],
79+
});
80+
7081
const featuresListStyles = css({
7182
display: 'flex',
7283
flexDirection: 'row',
@@ -132,7 +143,8 @@ const FeaturesList: React.FunctionComponent<{ features: Feature[] }> = ({
132143

133144
const DiagramListEmptyContent: React.FunctionComponent<{
134145
onCreateDiagramClick: () => void;
135-
}> = ({ onCreateDiagramClick }) => {
146+
onImportDiagramClick: (file: File) => void;
147+
}> = ({ onCreateDiagramClick, onImportDiagramClick }) => {
136148
return (
137149
<WorkspaceContainer>
138150
<EmptyContent
@@ -153,13 +165,16 @@ const DiagramListEmptyContent: React.FunctionComponent<{
153165
}
154166
subTitleClassName={subTitleStyles}
155167
callToAction={
156-
<Button
157-
onClick={onCreateDiagramClick}
158-
variant="primary"
159-
data-testid="create-diagram-button"
160-
>
161-
Generate diagram
162-
</Button>
168+
<div className={diagramActionsStyles}>
169+
<ImportDiagramButton onImportDiagram={onImportDiagramClick} />
170+
<Button
171+
onClick={onCreateDiagramClick}
172+
variant="primary"
173+
data-testid="create-diagram-button"
174+
>
175+
Generate diagram
176+
</Button>
177+
</div>
163178
}
164179
></EmptyContent>
165180
</WorkspaceContainer>
@@ -171,11 +186,13 @@ export const SavedDiagramsList: React.FunctionComponent<{
171186
onOpenDiagramClick: (diagram: MongoDBDataModelDescription) => void;
172187
onDiagramDeleteClick: (id: string) => void;
173188
onDiagramRenameClick: (id: string) => void;
189+
onImportDiagramClick: (file: File) => void;
174190
}> = ({
175191
onCreateDiagramClick,
176192
onOpenDiagramClick,
177193
onDiagramRenameClick,
178194
onDiagramDeleteClick,
195+
onImportDiagramClick,
179196
}) => {
180197
const { items, status } = useDataModelSavedItems();
181198
const decoratedItems = useMemo<
@@ -214,7 +231,10 @@ export const SavedDiagramsList: React.FunctionComponent<{
214231
}
215232
if (items.length === 0) {
216233
return (
217-
<DiagramListEmptyContent onCreateDiagramClick={onCreateDiagramClick} />
234+
<DiagramListEmptyContent
235+
onCreateDiagramClick={onCreateDiagramClick}
236+
onImportDiagramClick={onImportDiagramClick}
237+
/>
218238
);
219239
}
220240

@@ -225,6 +245,7 @@ export const SavedDiagramsList: React.FunctionComponent<{
225245
searchTerm: search,
226246
onCreateDiagram: onCreateDiagramClick,
227247
onSearchDiagrams: setSearch,
248+
onImportDiagram: onImportDiagramClick,
228249
}}
229250
>
230251
<WorkspaceContainer>
@@ -264,4 +285,5 @@ export default connect(null, {
264285
onOpenDiagramClick: openDiagram,
265286
onDiagramDeleteClick: deleteDiagram,
266287
onDiagramRenameClick: renameDiagram,
288+
onImportDiagramClick: openDiagramFromFile,
267289
})(SavedDiagramsList);

packages/compass-data-modeling/src/services/data-model-storage.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,19 @@ const EditSchemaVariants = z.discriminatedUnion('type', [
6767
]);
6868

6969
export const EditSchema = z.intersection(EditSchemaBase, EditSchemaVariants);
70+
export const EditListSchema = z
71+
.array(EditSchema)
72+
.nonempty()
73+
// Ensure first item exists and is 'SetModel'
74+
.refine((edits) => edits[0]?.type === 'SetModel', {
75+
message: "First edit must be of type 'SetModel'",
76+
});
7077

7178
export type Edit = z.output<typeof EditSchema>;
79+
export type SetModelEdit = Extract<
80+
z.output<typeof EditSchema>,
81+
{ type: 'SetModel' }
82+
>;
7283

7384
export type EditAction = z.output<typeof EditSchemaVariants>;
7485

@@ -100,15 +111,7 @@ export const MongoDBDataModelDescriptionSchema = z.object({
100111
* anything that would require re-fetching data associated with the diagram
101112
*/
102113
connectionId: z.string().nullable(),
103-
104-
// Ensure first item exists and is 'SetModel'
105-
edits: z
106-
.array(EditSchema)
107-
.nonempty()
108-
.refine((edits) => edits[0]?.type === 'SetModel', {
109-
message: "First edit must be of type 'SetModel'",
110-
}),
111-
114+
edits: EditListSchema,
112115
createdAt: z.string().datetime(),
113116
updatedAt: z.string().datetime(),
114117
});

0 commit comments

Comments
 (0)