From 8db037eb48e3ac33aa236117f6e117f4a62100c5 Mon Sep 17 00:00:00 2001 From: Basit Chonka Date: Wed, 16 Jul 2025 14:09:51 +0200 Subject: [PATCH 01/13] add import file button --- .../src/components/diagram-list-toolbar.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/compass-data-modeling/src/components/diagram-list-toolbar.tsx b/packages/compass-data-modeling/src/components/diagram-list-toolbar.tsx index 3ae92c0b71b..cab02cca99a 100644 --- a/packages/compass-data-modeling/src/components/diagram-list-toolbar.tsx +++ b/packages/compass-data-modeling/src/components/diagram-list-toolbar.tsx @@ -27,16 +27,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 }); @@ -61,7 +64,15 @@ export const DiagramListToolbar = () => { > Open an existing diagram: -
+
+ + { + if (files.length === 0) { + return; + } + onImportDiagram(files[0]); + }} + trigger={({ onClick }) => ( + + )} + /> - )} + } + size="small" + onImportDiagram={onImportDiagram} /> + )} + /> + ); +}; diff --git a/packages/compass-data-modeling/src/components/saved-diagrams-list.tsx b/packages/compass-data-modeling/src/components/saved-diagrams-list.tsx index 438b7aea602..4a4bf24274b 100644 --- a/packages/compass-data-modeling/src/components/saved-diagrams-list.tsx +++ b/packages/compass-data-modeling/src/components/saved-diagrams-list.tsx @@ -28,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 { OpenDiagramButton } from './open-diagram-button'; const sortBy = [ { @@ -72,6 +73,11 @@ const subTitleStyles = css({ maxWidth: '750px', }); +const diagramActionsStyles = css({ + display: 'flex', + gap: spacing[200], +}); + const featuresListStyles = css({ display: 'flex', flexDirection: 'row', @@ -137,7 +143,8 @@ const FeaturesList: React.FunctionComponent<{ features: Feature[] }> = ({ const DiagramListEmptyContent: React.FunctionComponent<{ onCreateDiagramClick: () => void; -}> = ({ onCreateDiagramClick }) => { + onImportDiagramClick: (file: File) => void; +}> = ({ onCreateDiagramClick, onImportDiagramClick }) => { return ( - Generate diagram - +
+ + +
} >
@@ -219,7 +229,10 @@ export const SavedDiagramsList: React.FunctionComponent<{ } if (items.length === 0) { return ( - + ); } diff --git a/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts b/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts index 2958ac98e59..317c9a52bde 100644 --- a/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts +++ b/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts @@ -1,5 +1,9 @@ import { expect } from 'chai'; -import { getDownloadDiagramContent } from './open-and-download-diagram'; +import { + getDownloadDiagramContent, + getDiagramName, + getDiagramContentsFromFile, +} from './open-and-download-diagram'; import FlightDiagram from '../../test/fixtures/flights-diagram.json'; describe('open-and-download-diagram', function () { @@ -21,4 +25,135 @@ describe('open-and-download-diagram', function () { ); expect(decodedEdits).to.deep.equal(FlightDiagram.edits); }); + + it('should return the correct diagram name', function () { + const existingNames = ['Flights', 'Berlin Public Transport']; + + expect( + getDiagramName(existingNames, 'Airbnb'), + 'should return the expected name when it does not exist' + ).to.equal('Airbnb'); + + expect( + getDiagramName(existingNames, 'Flights'), + 'should return the next expected name when it exists' + ).to.equal('Flights (1)'); + + existingNames.push('Flights (1)'); + + expect( + getDiagramName(existingNames, 'Flights'), + 'should return the next expected name when multiple versions exist' + ).to.equal('Flights (2)'); + }); + + context('getDiagramContentsFromFile', function () { + const makeFile = ( + content: string, + fileName: string, + type: string = 'application/json' + ) => { + const blob = new Blob([content], { type }); + return new File([blob], fileName, { type }); + }; + const errorUsecases = [ + { + title: 'should throw an error for a file with invalid JSON', + file: makeFile('invalid content', 'invalid.txt', 'text/plain'), + expected: 'Failed to parse diagram file', + }, + { + title: + 'should throw an error if content.version and content.type is not valid', + file: makeFile( + JSON.stringify({ version: 0, type: 'something' }), + 'file.json', + 'application/json' + ), + expected: 'Unsupported diagram file format', + }, + { + title: 'should throw if name or edits are missing', + file: makeFile( + JSON.stringify({ version: 1, type: 'Compass Data Modeling Diagram' }), + 'file.json', + 'application/json' + ), + expected: 'Diagram file is missing required fields', + }, + { + title: 'should throw if name or edits is not a string', + file: makeFile( + JSON.stringify({ + version: 1, + type: 'Compass Data Modeling Diagram', + name: 'Test diagram', + edits: [], + }), + 'file.json', + 'application/json' + ), + expected: 'Diagram file is missing required fields', + }, + { + title: 'should throw if edits is invalid base64', + file: makeFile( + JSON.stringify({ + version: 1, + type: 'Compass Data Modeling Diagram', + name: 'Test Diagram', + edits: 'something', + }), + 'file.json', + 'application/json' + ), + expected: 'Failed to parse diagram file', + }, + { + title: 'should throw if edits is valid base64 but not valid schema', + file: makeFile( + JSON.stringify({ + version: 1, + type: 'Compass Data Modeling Diagram', + name: 'Test Diagram', + edits: Buffer.from( + JSON.stringify([{ type: 'NonExistent' }]) + ).toString('base64'), + }), + 'file.json', + 'application/json' + ), + expected: 'Failed to parse diagram file', + }, + ]; + for (const { title, file, expected } of errorUsecases) { + it(title, async function () { + try { + await getDiagramContentsFromFile(file); + expect.fail('Expected an error to be thrown'); + } catch (error) { + expect(error.message).to.contain(expected); + } + }); + } + + it('should return the correct diagram contents from a valid file', async function () { + const file = makeFile( + JSON.stringify({ + version: 1, + type: 'Compass Data Modeling Diagram', + name: 'Test Diagram', + edits: Buffer.from(JSON.stringify(FlightDiagram.edits)).toString( + 'base64' + ), + }), + 'diagram.json', + 'application/json' + ); + + const { name, edits } = await getDiagramContentsFromFile(file); + expect(name).to.equal('Test Diagram'); + expect(edits).to.deep.equal(FlightDiagram.edits); + }); + }); }); diff --git a/packages/compass-data-modeling/src/services/open-and-download-diagram.ts b/packages/compass-data-modeling/src/services/open-and-download-diagram.ts index a3e4771559d..98f706d5863 100644 --- a/packages/compass-data-modeling/src/services/open-and-download-diagram.ts +++ b/packages/compass-data-modeling/src/services/open-and-download-diagram.ts @@ -1,4 +1,4 @@ -import type { Edit } from './data-model-storage'; +import { EditSchema, type Edit } from './data-model-storage'; import { downloadFile } from './export-diagram'; const kCurrentVersion = 1; @@ -25,3 +25,61 @@ export function getDownloadDiagramContent(name: string, edits: Edit[]) { edits: Buffer.from(JSON.stringify(edits)).toString('base64'), }; } + +export async function getDiagramContentsFromFile( + file: File +): Promise<{ name: string; edits: Edit[] }> { + const reader = new FileReader(); + return new Promise((resolve, reject) => { + reader.onload = (event) => { + const content = event.target?.result; + if (typeof content !== 'string') { + return reject(new Error('Invalid file contents')); + } + try { + const parsedContent = JSON.parse(content); + + if ( + parsedContent.version !== kCurrentVersion && + parsedContent.type !== kFileTypeDescription + ) { + return reject(new Error('Unsupported diagram file format')); + } + + const { name, edits } = parsedContent; + + if (!name || !edits || typeof edits !== 'string') { + return reject(new Error('Diagram file is missing required fields')); + } + + const parsedEdits = JSON.parse( + Buffer.from(edits, 'base64').toString('utf-8') + ); + // Ensure that edits validate using EditSchema + const validEdits = EditSchema.array().parse(parsedEdits); + return resolve({ name: parsedContent.name, edits: validEdits }); + } catch (error) { + reject( + new Error(`Failed to parse diagram file: ${(error as Error).message}`) + ); + } + }; + reader.onerror = (error) => { + reject(error.target?.error || new Error('File read error')); + }; + reader.readAsText(file); + }); +} + +export function getDiagramName( + existingNames: string[], + expectedName: string +): string { + let name = expectedName; + let index = 1; + while (existingNames.includes(name)) { + name = `${expectedName} (${index})`; + index += 1; + } + return name; +} diff --git a/packages/compass-data-modeling/src/store/diagram.ts b/packages/compass-data-modeling/src/store/diagram.ts index 4f6e60f7ec2..bd95f7400fb 100644 --- a/packages/compass-data-modeling/src/store/diagram.ts +++ b/packages/compass-data-modeling/src/store/diagram.ts @@ -2,7 +2,6 @@ import type { Reducer } from 'redux'; import { UUID } from 'bson'; import { isAction } from './util'; import { - EditSchema, validateEdit, type Edit, type MongoDBDataModelDescription, @@ -16,7 +15,11 @@ import { showConfirmation, showPrompt, } from '@mongodb-js/compass-components'; -import { downloadDiagram } from '../services/open-and-download-diagram'; +import { + downloadDiagram, + getDiagramContentsFromFile, + getDiagramName, +} from '../services/open-and-download-diagram'; function isNonEmptyArray(arr: T[]): arr is [T, ...T[]] { return Array.isArray(arr) && arr.length > 0; @@ -358,56 +361,6 @@ export function renameDiagram( }; } -// TODO: Move to its service. -async function getDiagramContentsFromFile( - file: File -): Promise<{ name: string; edits: Edit[] }> { - const reader = new FileReader(); - return new Promise((resolve, reject) => { - reader.onload = (event) => { - const content = event.target?.result; - if (typeof content !== 'string') { - return reject(new Error('Invalid file contents')); - } - try { - const parsedContent = JSON.parse(content); - - if ( - parsedContent.version !== 1 && - parsedContent.type !== 'Compass Data Modeling Diagram' - ) { - return reject(new Error('Unsupported diagram file format')); - } - - const edits = JSON.parse( - Buffer.from(parsedContent.edits, 'base64').toString('utf-8') - ); - // Ensure that edits validate using EditSchema - const validEdits = EditSchema.array().parse(edits); - return resolve({ name: parsedContent.name, edits: validEdits }); - } catch (error) { - reject( - new Error(`Failed to parse diagram file: ${(error as Error).message}`) - ); - } - }; - reader.onerror = (error) => { - reject(error.target?.error || new Error('File read error')); - }; - reader.readAsText(file); - }); -} - -function getDiagramName(existingNames: string[], expectedName: string): string { - let name = expectedName; - let index = 1; - while (existingNames.includes(name)) { - name = `${expectedName} - ${index}`; - index += 1; - } - return name; -} - export function openDiagramFromFile( file: File ): DataModelingThunkAction, OpenDiagramAction> { @@ -427,11 +380,8 @@ export function openDiagramFromFile( updatedAt: new Date().toISOString(), edits, }; - const saved = await dataModelStorage.save(diagram); - if (!saved) { - throw new Error('Failed to save the diagram'); - } dispatch(openDiagram(diagram)); + void dataModelStorage.save(diagram); } catch (error) { openToast('data-modeling-file-read-error', { variant: 'warning', From bd331ea783da4cccc26904b80f084f5e2f9c33b5 Mon Sep 17 00:00:00 2001 From: Basit Chonka Date: Mon, 21 Jul 2025 16:04:33 +0200 Subject: [PATCH 04/13] e2e tests --- .../src/components/open-diagram-button.tsx | 1 + .../compass-e2e-tests/helpers/selectors.ts | 10 +++++-- .../tests/data-modeling-tab.test.ts | 27 ++++++++++++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/compass-data-modeling/src/components/open-diagram-button.tsx b/packages/compass-data-modeling/src/components/open-diagram-button.tsx index 1b3a09aaca3..02593ff5b19 100644 --- a/packages/compass-data-modeling/src/components/open-diagram-button.tsx +++ b/packages/compass-data-modeling/src/components/open-diagram-button.tsx @@ -15,6 +15,7 @@ export const OpenDiagramButton = ({ return ( { diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index 97e015f35e2..e38aa8e7eaf 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -1430,6 +1430,7 @@ export const AutoUpdateReleaseNotesLink = // Data Modeling export const SidebarDataModelingTab = `${Sidebar} [aria-label="Data Modeling"]`; +export const OpenDataModelInput = '[data-testid="open-diagram-file-input"]'; export const CreateNewDataModelButton = '[data-testid="create-diagram-button"]'; export const CreateDataModelModal = '[data-testid="new-diagram-modal"]'; export const CreateDataModelConfirmButton = `${CreateDataModelModal} [data-testid="new-diagram-confirm-button"]`; @@ -1455,8 +1456,13 @@ export const DataModelExportPngOption = `${DataModelExportModal} input[aria-labe export const DataModelExportJsonOption = `${DataModelExportModal} input[aria-label="JSON"]`; export const DataModelExportModalConfirmButton = '[data-testid="export-button"]'; -export const DataModelsListItem = (diagramName: string) => - `[data-testid="saved-diagram-card"][data-diagram-name="${diagramName}"]`; +export const DataModelsListItem = (diagramName?: string) => { + const diagramListSelector = `[data-testid="saved-diagram-card"]`; + if (diagramName) { + return `${diagramListSelector}[data-diagram-name="${diagramName}"]`; + } + return diagramListSelector; +}; export const DataModelsListItemActions = (diagramName: string) => `${DataModelsListItem(diagramName)} [aria-label="Show actions"]`; export const DataModelsListItemDeleteButton = `[data-action="delete"]`; diff --git a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts index a4417da0c6c..41cd65f43b5 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -386,7 +386,7 @@ describe('Data Modeling tab', function () { expect(text).to.include('String string'.toLowerCase()); }); - it('downloads the data model', async function () { + it('downloads the data model and opens it', async function () { const dataModelName = 'Test Export Model - Save-Open'; exportFileName = `${dataModelName}.compass`; await setupDiagram(browser, { @@ -426,5 +426,30 @@ describe('Data Modeling tab', function () { expect(edits).to.be.an('array').of.length(2); expect(edits[0].type).to.equal('SetModel'); expect(edits[1].type).to.equal('MoveCollection'); + + // Open the saved diagram + await browser.closeWorkspaceTabs(); + await browser.navigateToDataModeling(); + + await browser.selectFile(Selectors.OpenDataModelInput, filePath); + await browser.$(Selectors.DataModelEditor).waitForDisplayed(); + const savedNodes = await getDiagramNodes(browser); + + expect(savedNodes).to.have.lengthOf(2); + expect(savedNodes[0].id).to.equal('test.testCollection-one'); + expect(savedNodes[1].id).to.equal('test.testCollection-two'); + + // Ensure that two diagrams exist (with correct incremental name) + await browser.closeWorkspaceTabs(); + await browser.navigateToDataModeling(); + + const cardsSelector = Selectors.DataModelsListItem(); + await browser.waitForAnimations(cardsSelector); + const titles = await browser + .$$(cardsSelector) + .map((element) => element.getAttribute('data-diagram-name')); + expect(titles).to.include(dataModelName); + // The second one is the one we just opened + expect(titles).to.include(`${dataModelName} (1)`); }); }); From 8a87bcc101d7adc4039f8caa03d1c2953bad009c Mon Sep 17 00:00:00 2001 From: Basit Chonka Date: Mon, 21 Jul 2025 16:17:18 +0200 Subject: [PATCH 05/13] clean up --- .../services/open-and-download-diagram.spec.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts b/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts index 317c9a52bde..566d19c3902 100644 --- a/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts +++ b/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts @@ -67,8 +67,7 @@ describe('open-and-download-diagram', function () { 'should throw an error if content.version and content.type is not valid', file: makeFile( JSON.stringify({ version: 0, type: 'something' }), - 'file.json', - 'application/json' + 'file.json' ), expected: 'Unsupported diagram file format', }, @@ -76,8 +75,7 @@ describe('open-and-download-diagram', function () { title: 'should throw if name or edits are missing', file: makeFile( JSON.stringify({ version: 1, type: 'Compass Data Modeling Diagram' }), - 'file.json', - 'application/json' + 'file.json' ), expected: 'Diagram file is missing required fields', }, @@ -90,8 +88,7 @@ describe('open-and-download-diagram', function () { name: 'Test diagram', edits: [], }), - 'file.json', - 'application/json' + 'file.json' ), expected: 'Diagram file is missing required fields', }, @@ -104,8 +101,7 @@ describe('open-and-download-diagram', function () { name: 'Test Diagram', edits: 'something', }), - 'file.json', - 'application/json' + 'file.json' ), expected: 'Failed to parse diagram file', }, @@ -120,8 +116,7 @@ describe('open-and-download-diagram', function () { JSON.stringify([{ type: 'NonExistent' }]) ).toString('base64'), }), - 'file.json', - 'application/json' + 'file.json' ), expected: 'Failed to parse diagram file', }, @@ -132,7 +127,7 @@ describe('open-and-download-diagram', function () { await getDiagramContentsFromFile(file); expect.fail('Expected an error to be thrown'); } catch (error) { - expect(error.message).to.contain(expected); + expect((error as Error).message).to.contain(expected); } }); } From f64c626027cf187950d7e69fde89e2e537edcea0 Mon Sep 17 00:00:00 2001 From: Basit Chonka Date: Mon, 21 Jul 2025 16:49:52 +0200 Subject: [PATCH 06/13] make ts happy --- .../src/services/data-model-storage.ts | 20 ++++++++------- .../open-and-download-diagram.spec.ts | 25 ++++++++++++++++++- .../src/services/open-and-download-diagram.ts | 24 ++++++++++++------ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/packages/compass-data-modeling/src/services/data-model-storage.ts b/packages/compass-data-modeling/src/services/data-model-storage.ts index 7134c94c6d6..f3f4d9e1790 100644 --- a/packages/compass-data-modeling/src/services/data-model-storage.ts +++ b/packages/compass-data-modeling/src/services/data-model-storage.ts @@ -63,8 +63,18 @@ 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; +export type SetModelEdit = z.output & { + type: 'SetModel'; +}; export const validateEdit = ( edit: unknown @@ -94,15 +104,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(), }); diff --git a/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts b/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts index 566d19c3902..1fb61769cfa 100644 --- a/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts +++ b/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts @@ -118,7 +118,30 @@ describe('open-and-download-diagram', function () { }), 'file.json' ), - expected: 'Failed to parse diagram file', + expected: 'Failed to parse diagram file: Invalid diagram data.', + }, + { + title: 'should throw if first edit is not SetModel', + file: makeFile( + JSON.stringify({ + version: 1, + type: 'Compass Data Modeling Diagram', + name: 'Test Diagram', + edits: Buffer.from( + JSON.stringify([ + { + type: 'MoveCollection', + ns: 'test', + newPosition: [0, 0], + id: '123e4567-e89b-12d3-a456-426614174000', + timestamp: new Date().toISOString(), + }, + ]) + ).toString('base64'), + }), + 'file.json' + ), + expected: 'Failed to parse diagram file: Invalid diagram data.', }, ]; for (const { title, file, expected } of errorUsecases) { diff --git a/packages/compass-data-modeling/src/services/open-and-download-diagram.ts b/packages/compass-data-modeling/src/services/open-and-download-diagram.ts index 98f706d5863..ff43700a1ce 100644 --- a/packages/compass-data-modeling/src/services/open-and-download-diagram.ts +++ b/packages/compass-data-modeling/src/services/open-and-download-diagram.ts @@ -1,5 +1,7 @@ -import { EditSchema, type Edit } from './data-model-storage'; +import type { SetModelEdit } from './data-model-storage'; +import { EditListSchema, type Edit } from './data-model-storage'; import { downloadFile } from './export-diagram'; +import { z } from '@mongodb-js/compass-user-data'; const kCurrentVersion = 1; const kFileTypeDescription = 'Compass Data Modeling Diagram'; @@ -28,7 +30,7 @@ export function getDownloadDiagramContent(name: string, edits: Edit[]) { export async function getDiagramContentsFromFile( file: File -): Promise<{ name: string; edits: Edit[] }> { +): Promise<{ name: string; edits: [SetModelEdit, ...Edit[]] }> { const reader = new FileReader(); return new Promise((resolve, reject) => { reader.onload = (event) => { @@ -55,13 +57,19 @@ export async function getDiagramContentsFromFile( const parsedEdits = JSON.parse( Buffer.from(edits, 'base64').toString('utf-8') ); - // Ensure that edits validate using EditSchema - const validEdits = EditSchema.array().parse(parsedEdits); - return resolve({ name: parsedContent.name, edits: validEdits }); + // Ensure that edits validate using EditListSchema (SetModel must be first) + const validEdits = EditListSchema.parse(parsedEdits); + + return resolve({ + name: parsedContent.name, + edits: [validEdits[0] as SetModelEdit, ...validEdits.slice(1)], + }); } catch (error) { - reject( - new Error(`Failed to parse diagram file: ${(error as Error).message}`) - ); + const message = + error instanceof z.ZodError + ? 'Failed to parse diagram file: Invalid diagram data.' + : `Failed to parse diagram file: ${(error as Error).message}`; + reject(new Error(message)); } }; reader.onerror = (error) => { From 92ba8c2cfbaf476640b05e03897e132381392904 Mon Sep 17 00:00:00 2001 From: Basit Chonka Date: Mon, 21 Jul 2025 17:05:54 +0200 Subject: [PATCH 07/13] bot review --- .../src/services/open-and-download-diagram.spec.ts | 12 ++++++++++-- .../src/services/open-and-download-diagram.ts | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts b/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts index 1fb61769cfa..462fa10f7f3 100644 --- a/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts +++ b/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts @@ -64,9 +64,17 @@ describe('open-and-download-diagram', function () { }, { title: - 'should throw an error if content.version and content.type is not valid', + 'should throw an error if content.version is not the current version', file: makeFile( - JSON.stringify({ version: 0, type: 'something' }), + JSON.stringify({ version: 0, type: 'Compass Data Modeling Diagram' }), + 'file.json' + ), + expected: 'Unsupported diagram file format', + }, + { + title: 'should throw an error if content.type is not the current type', + file: makeFile( + JSON.stringify({ version: 1, type: 'Compass Data Modeling' }), 'file.json' ), expected: 'Unsupported diagram file format', diff --git a/packages/compass-data-modeling/src/services/open-and-download-diagram.ts b/packages/compass-data-modeling/src/services/open-and-download-diagram.ts index ff43700a1ce..f76e409ba26 100644 --- a/packages/compass-data-modeling/src/services/open-and-download-diagram.ts +++ b/packages/compass-data-modeling/src/services/open-and-download-diagram.ts @@ -42,7 +42,7 @@ export async function getDiagramContentsFromFile( const parsedContent = JSON.parse(content); if ( - parsedContent.version !== kCurrentVersion && + parsedContent.version !== kCurrentVersion || parsedContent.type !== kFileTypeDescription ) { return reject(new Error('Unsupported diagram file format')); From bea3e3dc70951e6719e1a285a09e4cf095c56a63 Mon Sep 17 00:00:00 2001 From: Basit Chonka Date: Mon, 21 Jul 2025 17:14:37 +0200 Subject: [PATCH 08/13] use existing diagram --- .../open-and-download-diagram.spec.ts | 2 +- .../test/fixtures/flights-diagram.json | 303 ------------------ 2 files changed, 1 insertion(+), 304 deletions(-) delete mode 100644 packages/compass-data-modeling/test/fixtures/flights-diagram.json diff --git a/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts b/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts index 462fa10f7f3..3060e3b15f8 100644 --- a/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts +++ b/packages/compass-data-modeling/src/services/open-and-download-diagram.spec.ts @@ -4,7 +4,7 @@ import { getDiagramName, getDiagramContentsFromFile, } from './open-and-download-diagram'; -import FlightDiagram from '../../test/fixtures/flights-diagram.json'; +import FlightDiagram from '../../test/fixtures/data-model-with-relationships.json'; describe('open-and-download-diagram', function () { it('should return correct content to download', function () { diff --git a/packages/compass-data-modeling/test/fixtures/flights-diagram.json b/packages/compass-data-modeling/test/fixtures/flights-diagram.json deleted file mode 100644 index 2d0c1a6908b..00000000000 --- a/packages/compass-data-modeling/test/fixtures/flights-diagram.json +++ /dev/null @@ -1,303 +0,0 @@ -{ - "id": "26fea481-14a0-40de-aa8e-b3ef22afcf1b", - "connectionId": "108acc00-4d7b-4f56-be19-05c7288da71a", - "name": "Flights and countries", - "edits": [ - { - "id": "5e16572a-6978-4669-8103-e1f087b412cd", - "timestamp": "2025-06-20T06:35:26.773Z", - "type": "SetModel", - "model": { - "collections": [ - { - "ns": "flights.airlines", - "jsonSchema": { - "bsonType": "object", - "required": [ - "_id", - "active", - "airline", - "alias", - "base", - "country", - "iata", - "icao", - "name" - ], - "properties": { - "_id": { - "bsonType": "objectId" - }, - "active": { - "bsonType": "string" - }, - "airline": { - "bsonType": "int" - }, - "alias": { - "bsonType": ["string", "int"] - }, - "alliance": { - "bsonType": "string" - }, - "base": { - "bsonType": "string" - }, - "country": { - "bsonType": "string" - }, - "iata": { - "bsonType": "string" - }, - "icao": { - "bsonType": "string" - }, - "name": { - "bsonType": "string" - } - } - }, - "indexes": [], - "displayPosition": [144.04516098441445, 226.78180342288712] - }, - { - "ns": "flights.airports", - "jsonSchema": { - "bsonType": "object", - "required": [ - "_id", - "Altitude", - "Country", - "IATA", - "ICAO", - "Latitude", - "Longitude", - "Name" - ], - "properties": { - "_id": { - "bsonType": "int" - }, - "Altitude": { - "bsonType": "int" - }, - "City": { - "bsonType": "string" - }, - "Country": { - "bsonType": "string" - }, - "IATA": { - "bsonType": "string" - }, - "ICAO": { - "bsonType": "string" - }, - "Latitude": { - "bsonType": "double" - }, - "Longitude": { - "bsonType": "double" - }, - "Name": { - "bsonType": "string" - } - } - }, - "indexes": [], - "displayPosition": [157.74741328703078, 614.6105002761217] - }, - { - "ns": "flights.airports_coordinates_for_schema", - "jsonSchema": { - "bsonType": "object", - "required": ["_id", "coordinates", "Country", "Name"], - "properties": { - "_id": { - "bsonType": "int" - }, - "coordinates": { - "bsonType": "array", - "items": { - "bsonType": "double" - } - }, - "Country": { - "bsonType": "string" - }, - "Name": { - "bsonType": "string" - } - } - }, - "indexes": [], - "displayPosition": [611.3592580503537, 238.3680626820135] - }, - { - "ns": "flights.countries", - "jsonSchema": { - "bsonType": "object", - "required": ["_id", "iso_code", "name"], - "properties": { - "_id": { - "bsonType": "objectId" - }, - "dafif_code": { - "bsonType": "string" - }, - "iso_code": { - "bsonType": "string" - }, - "name": { - "bsonType": "string" - } - } - }, - "indexes": [], - "displayPosition": [156.9088146439409, 808.1350158017262] - }, - { - "ns": "flights.planes", - "jsonSchema": { - "bsonType": "object", - "required": ["_id", "IATA", "ICAO", "name"], - "properties": { - "_id": { - "bsonType": "objectId" - }, - "IATA": { - "bsonType": "string" - }, - "ICAO": { - "bsonType": "string" - }, - "name": { - "bsonType": "string" - } - } - }, - "indexes": [], - "displayPosition": [479.9432289278143, 650.1759375929954] - }, - { - "ns": "flights.routes", - "jsonSchema": { - "bsonType": "object", - "required": [ - "_id", - "airline", - "airline_id", - "destination_airport", - "destination_airport_id", - "equipment", - "source_airport", - "source_airport_id", - "stops" - ], - "properties": { - "_id": { - "bsonType": "objectId" - }, - "airline": { - "bsonType": "string" - }, - "airline_id": { - "bsonType": "string" - }, - "codeshare": { - "bsonType": "string" - }, - "destination_airport": { - "bsonType": "string" - }, - "destination_airport_id": { - "bsonType": "string" - }, - "equipment": { - "bsonType": "string" - }, - "source_airport": { - "bsonType": "string" - }, - "source_airport_id": { - "bsonType": "string" - }, - "stops": { - "bsonType": "int" - } - } - }, - "indexes": [], - "displayPosition": [853.3477815091105, 168.4596944341812] - } - ], - "relationships": [] - } - }, - { - "id": "cfba18e8-ffe6-4222-9c60-e063a31303b4", - "timestamp": "2025-06-20T06:36:04.745Z", - "type": "AddRelationship", - "relationship": { - "id": "6f776467-4c98-476b-9b71-1f8a724e6c2c", - "relationship": [ - { - "ns": "flights.airlines", - "cardinality": 1, - "fields": ["country"] - }, - { - "ns": "flights.countries", - "cardinality": 1, - "fields": ["name"] - } - ], - "isInferred": false - } - }, - { - "id": "74383587-5f0a-4b43-8eba-b810cc058c5b", - "timestamp": "2025-06-20T06:36:32.785Z", - "type": "AddRelationship", - "relationship": { - "id": "204b1fc0-601f-4d62-bba3-38fade71e049", - "relationship": [ - { - "ns": "flights.countries", - "cardinality": 1, - "fields": ["name"] - }, - { - "ns": "flights.airports", - "cardinality": 1, - "fields": ["Country"] - } - ], - "isInferred": false - } - }, - { - "type": "MoveCollection", - "ns": "flights.airports_coordinates_for_schema", - "newPosition": [477.4524339160448, 212.09520478224402], - "id": "538f3bcd-6f0c-49fd-9fc7-ffe7cf5a4718", - "timestamp": "2025-07-16T14:34:20.394Z" - }, - { - "type": "MoveCollection", - "ns": "flights.countries", - "newPosition": [506.93108279248247, 415.73717039549217], - "id": "5fb8b8af-4687-4521-8ed7-832156c12c22", - "timestamp": "2025-07-16T14:34:24.792Z" - }, - { - "type": "MoveCollection", - "ns": "flights.planes", - "newPosition": [548.5916640852764, 701.8741418473805], - "id": "7acf2862-5c29-40f4-8133-e976ae85b20d", - "timestamp": "2025-07-16T14:34:25.872Z" - } - ], - "createdAt": "2025-06-20T06:35:26.773Z", - "updatedAt": "2025-07-16T14:34:25.872Z" -} From 9db4b8e0f7e991e725065d666349699036622d83 Mon Sep 17 00:00:00 2001 From: Basit Chonka Date: Tue, 22 Jul 2025 10:50:39 +0200 Subject: [PATCH 09/13] change text to import --- .../src/components/diagram-list-toolbar.tsx | 4 +-- ...m-button.tsx => import-diagram-button.tsx} | 12 ++++----- .../src/components/saved-diagrams-list.tsx | 4 +-- .../src/services/data-model-storage.ts | 7 ++--- .../open-and-download-diagram.spec.ts | 27 +++++++------------ .../compass-e2e-tests/helpers/selectors.ts | 2 +- .../tests/data-modeling-tab.test.ts | 2 +- 7 files changed, 25 insertions(+), 33 deletions(-) rename packages/compass-data-modeling/src/components/{open-diagram-button.tsx => import-diagram-button.tsx} (72%) diff --git a/packages/compass-data-modeling/src/components/diagram-list-toolbar.tsx b/packages/compass-data-modeling/src/components/diagram-list-toolbar.tsx index 2916157976a..db353a4a599 100644 --- a/packages/compass-data-modeling/src/components/diagram-list-toolbar.tsx +++ b/packages/compass-data-modeling/src/components/diagram-list-toolbar.tsx @@ -11,7 +11,7 @@ import { useDarkMode, } from '@mongodb-js/compass-components'; import { DiagramListContext } from './saved-diagrams-list'; -import { OpenDiagramButton } from './open-diagram-button'; +import { ImportDiagramButton } from './import-diagram-button'; const containerStyles = css({ padding: spacing[400], @@ -67,7 +67,7 @@ export const DiagramListToolbar = () => { Open an existing diagram:
- } size="small" onImportDiagram={onImportDiagram} diff --git a/packages/compass-data-modeling/src/components/open-diagram-button.tsx b/packages/compass-data-modeling/src/components/import-diagram-button.tsx similarity index 72% rename from packages/compass-data-modeling/src/components/open-diagram-button.tsx rename to packages/compass-data-modeling/src/components/import-diagram-button.tsx index 02593ff5b19..6632569b9da 100644 --- a/packages/compass-data-modeling/src/components/open-diagram-button.tsx +++ b/packages/compass-data-modeling/src/components/import-diagram-button.tsx @@ -1,21 +1,21 @@ import React from 'react'; import { Button, FileSelector } from '@mongodb-js/compass-components'; -type OpenDiagramButtonProps = Omit< +type importDiagramButtonProps = Omit< React.ComponentProps, 'onClick' > & { onImportDiagram: (file: File) => void; }; -export const OpenDiagramButton = ({ +export const ImportDiagramButton = ({ onImportDiagram, ...buttonProps -}: OpenDiagramButtonProps) => { +}: importDiagramButtonProps) => { return ( { @@ -26,7 +26,7 @@ export const OpenDiagramButton = ({ }} trigger={({ onClick }) => ( )} /> diff --git a/packages/compass-data-modeling/src/components/saved-diagrams-list.tsx b/packages/compass-data-modeling/src/components/saved-diagrams-list.tsx index 89dbdbb7a05..108c7928b58 100644 --- a/packages/compass-data-modeling/src/components/saved-diagrams-list.tsx +++ b/packages/compass-data-modeling/src/components/saved-diagrams-list.tsx @@ -28,7 +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 { OpenDiagramButton } from './open-diagram-button'; +import { ImportDiagramButton } from './import-diagram-button'; const sortBy = [ { @@ -166,7 +166,7 @@ const DiagramListEmptyContent: React.FunctionComponent<{ subTitleClassName={subTitleStyles} callToAction={
- +