Skip to content

feat(data-modeling): open diagram COMPASS-9546 #7127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ if (!window.document.queryCommandSupported) {
globalThis.EventTarget = window.EventTarget;
globalThis.CustomEvent = window.CustomEvent;
globalThis.Event = window.Event;
globalThis.Blob = window.Blob;
globalThis.File = window.File;
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import FileInput, {
FileInputBackendProvider,
createElectronFileInputBackend,
} from './file-input';
} from './file-picker-dialog';

describe('FileInput', function () {
let spy;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,17 @@ export function createElectronFileInputBackend<ElectronWindow>(
};
}

function FileInput({
/**
* This component is not intended to work in a browser environment. It is designed
* to be used in environments like Electron where you have access to nodes fs module
* to read/write files.
*
* Always use `FileSelector` component, only use `FilePickerDialog` if you absolutely
* know what you're doing
*
* @deprecated
*/
function FilePickerDialog({
autoOpen = false,
id,
label,
Expand Down Expand Up @@ -553,4 +563,4 @@ function FileInput({
);
}

export default FileInput;
export default FilePickerDialog;
43 changes: 43 additions & 0 deletions packages/compass-components/src/components/file-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { type InputHTMLAttributes, useRef } from 'react';

type FileSelectorTriggerProps = {
onClick: () => void;
};

type FileSelectorProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
'onChange' | 'onSelect' | 'type' | 'style' | 'ref'
> & {
trigger: (props: FileSelectorTriggerProps) => React.ReactElement;
onSelect: (files: File[]) => void;
};

export function FileSelector({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily completely your problem to solve, but the fact that we now expose two file inputs from compass-components is very confusing. In the spirit of trying to make code universal, maybe we should rename the other one to make it clear that it's electron only and mark it as deprecated with a JSDoc annotation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming it makes sense, but why deprecation?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Universal code should be the default almost always, everything else should be an exception, we call this out often in a lot of places, but something that highlights component as "do not use unless you're really truly sure about it" seems to be a useful mechanism, @deprecated is just a tool to achieve that if people just pick up components automatically based on import suggestions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in ad151ea. let me know if it looks good, especially the name :)

trigger,
onSelect,
...props
}: FileSelectorProps) {
const inputRef = useRef<HTMLInputElement>(null);

const onFilesChanged = React.useCallback(
(evt: React.ChangeEvent<HTMLInputElement>) => {
onSelect(Array.from(evt.currentTarget.files ?? []));
},
[onSelect]
);

return (
<>
<input
{...props}
ref={inputRef}
type="file"
onChange={onFilesChanged}
style={{ display: 'none' }}
/>
{trigger({
onClick: () => inputRef.current?.click(),
})}
</>
);
}
9 changes: 5 additions & 4 deletions packages/compass-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import type {
ElectronFileDialogOptions,
ElectronShowFileDialogProvider,
FileInputBackend,
} from './components/file-input';
import FileInput, {
} from './components/file-picker-dialog';
import FilePickerDialog, {
createElectronFileInputBackend,
createJSDomFileInputDummyBackend,
FileInputBackendProvider,
} from './components/file-input';
} from './components/file-picker-dialog';
import { OptionsToggle } from './components/options-toggle';
import {
ErrorSummary,
Expand Down Expand Up @@ -118,7 +118,7 @@ export {
CollapsibleFieldSet,
ConfirmationModal,
ErrorSummary,
FileInput,
FilePickerDialog,
FileInputBackendProvider,
IndexIcon,
OptionsToggle,
Expand Down Expand Up @@ -219,3 +219,4 @@ export {
export { SelectList } from './components/select-list';
export { ParagraphSkeleton } from '@leafygreen-ui/skeleton-loader';
export { InsightsChip } from './components/insights-chip';
export { FileSelector } from './components/file-selector';
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { FileInput as CompassFileInput } from '@mongodb-js/compass-components';
import { FilePickerDialog } from '@mongodb-js/compass-components';

type FileInputProps = {
label: string;
Expand All @@ -24,7 +24,7 @@ export function FileInput({
);

return (
<CompassFileInput
<FilePickerDialog
disabled={disabled}
label={label}
onChange={onChangeFiles}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
useDarkMode,
} from '@mongodb-js/compass-components';
import { DiagramListContext } from './saved-diagrams-list';
import { ImportDiagramButton } from './import-diagram-button';

const containerStyles = css({
padding: spacing[400],
Expand All @@ -27,16 +28,19 @@ const containerStyles = css({
const titleStyles = css({
gridArea: 'title',
});
const createDiagramContainerStyles = css({
const diagramActionsStyles = css({
gridArea: 'createDiagram',
display: 'flex',
justifyContent: 'flex-end',
gap: spacing[200],
});
const searchInputStyles = css({
gridArea: 'searchInput',
});
const sortControlsStyles = css({
gridArea: 'sortControls',
display: 'flex',
justifyContent: 'flex-end',
});

const toolbarTitleLightStyles = css({ color: palette.gray.dark1 });
Expand All @@ -48,6 +52,7 @@ export const DiagramListToolbar = () => {
onCreateDiagram,
sortControls,
searchTerm,
onImportDiagram,
} = useContext(DiagramListContext);
const darkMode = useDarkMode();

Expand All @@ -61,7 +66,12 @@ export const DiagramListToolbar = () => {
>
Open an existing diagram:
</Subtitle>
<div className={createDiagramContainerStyles}>
<div className={diagramActionsStyles}>
<ImportDiagramButton
leftGlyph={<Icon glyph="Import" />}
size="small"
onImportDiagram={onImportDiagram}
/>
<Button
onClick={onCreateDiagram}
variant="primary"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { Button, FileSelector } from '@mongodb-js/compass-components';

type importDiagramButtonProps = Omit<
React.ComponentProps<typeof Button>,
'onClick'
> & {
onImportDiagram: (file: File) => void;
};

export const ImportDiagramButton = ({
onImportDiagram,
...buttonProps
}: importDiagramButtonProps) => {
return (
<FileSelector
id="import-diagram-file-input"
data-testid="import-diagram-file-input"
multiple={false}
accept=".compass"
onSelect={(files) => {
if (files.length === 0) {
return;
}
onImportDiagram(files[0]);
}}
trigger={({ onClick }) => (
<Button {...buttonProps} onClick={onClick}>
Import Diagram
</Button>
)}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
deleteDiagram,
selectCurrentModel,
openDiagram,
openDiagramFromFile,
renameDiagram,
} from '../store/diagram';
import type { MongoDBDataModelDescription } from '../services/data-model-storage';
Expand All @@ -27,6 +28,7 @@ import FlexibilityIcon from './icons/flexibility';
import { CARD_HEIGHT, CARD_WIDTH, DiagramCard } from './diagram-card';
import { DiagramListToolbar } from './diagram-list-toolbar';
import toNS from 'mongodb-ns';
import { ImportDiagramButton } from './import-diagram-button';

const sortBy = [
{
Expand All @@ -49,13 +51,17 @@ const rowStyles = css({

export const DiagramListContext = React.createContext<{
onSearchDiagrams: (search: string) => void;
onImportDiagram: (file: File) => void;
onCreateDiagram: () => void;
sortControls: React.ReactElement | null;
searchTerm: string;
}>({
onSearchDiagrams: () => {
/** */
},
onImportDiagram: () => {
/** */
},
onCreateDiagram: () => {
/** */
},
Expand All @@ -67,6 +73,11 @@ const subTitleStyles = css({
maxWidth: '750px',
});

const diagramActionsStyles = css({
display: 'flex',
gap: spacing[200],
});

const featuresListStyles = css({
display: 'flex',
flexDirection: 'row',
Expand Down Expand Up @@ -132,7 +143,8 @@ const FeaturesList: React.FunctionComponent<{ features: Feature[] }> = ({

const DiagramListEmptyContent: React.FunctionComponent<{
onCreateDiagramClick: () => void;
}> = ({ onCreateDiagramClick }) => {
onImportDiagramClick: (file: File) => void;
}> = ({ onCreateDiagramClick, onImportDiagramClick }) => {
return (
<WorkspaceContainer>
<EmptyContent
Expand All @@ -153,13 +165,16 @@ const DiagramListEmptyContent: React.FunctionComponent<{
}
subTitleClassName={subTitleStyles}
callToAction={
<Button
onClick={onCreateDiagramClick}
variant="primary"
data-testid="create-diagram-button"
>
Generate diagram
</Button>
<div className={diagramActionsStyles}>
<ImportDiagramButton onImportDiagram={onImportDiagramClick} />
<Button
onClick={onCreateDiagramClick}
variant="primary"
data-testid="create-diagram-button"
>
Generate diagram
</Button>
</div>
}
></EmptyContent>
</WorkspaceContainer>
Expand All @@ -171,11 +186,13 @@ export const SavedDiagramsList: React.FunctionComponent<{
onOpenDiagramClick: (diagram: MongoDBDataModelDescription) => void;
onDiagramDeleteClick: (id: string) => void;
onDiagramRenameClick: (id: string) => void;
onImportDiagramClick: (file: File) => void;
}> = ({
onCreateDiagramClick,
onOpenDiagramClick,
onDiagramRenameClick,
onDiagramDeleteClick,
onImportDiagramClick,
}) => {
const { items, status } = useDataModelSavedItems();
const decoratedItems = useMemo<
Expand Down Expand Up @@ -214,7 +231,10 @@ export const SavedDiagramsList: React.FunctionComponent<{
}
if (items.length === 0) {
return (
<DiagramListEmptyContent onCreateDiagramClick={onCreateDiagramClick} />
<DiagramListEmptyContent
onCreateDiagramClick={onCreateDiagramClick}
onImportDiagramClick={onImportDiagramClick}
/>
);
}

Expand All @@ -225,6 +245,7 @@ export const SavedDiagramsList: React.FunctionComponent<{
searchTerm: search,
onCreateDiagram: onCreateDiagramClick,
onSearchDiagrams: setSearch,
onImportDiagram: onImportDiagramClick,
}}
>
<WorkspaceContainer>
Expand Down Expand Up @@ -264,4 +285,5 @@ export default connect(null, {
onOpenDiagramClick: openDiagram,
onDiagramDeleteClick: deleteDiagram,
onDiagramRenameClick: renameDiagram,
onImportDiagramClick: openDiagramFromFile,
})(SavedDiagramsList);
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,19 @@ const EditSchemaVariants = z.discriminatedUnion('type', [
]);

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

export type Edit = z.output<typeof EditSchema>;
export type SetModelEdit = Extract<
z.output<typeof EditSchema>,
{ type: 'SetModel' }
>;

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

Expand Down Expand Up @@ -100,15 +111,7 @@ export const MongoDBDataModelDescriptionSchema = z.object({
* anything that would require re-fetching data associated with the diagram
*/
connectionId: z.string().nullable(),

// Ensure first item exists and is 'SetModel'
edits: z
.array(EditSchema)
.nonempty()
.refine((edits) => edits[0]?.type === 'SetModel', {
message: "First edit must be of type 'SetModel'",
}),

edits: EditListSchema,
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
Expand Down
Loading
Loading