diff --git a/packages/compass-data-modeling/src/components/diagram-editor.spec.tsx b/packages/compass-data-modeling/src/components/diagram-editor.spec.tsx index 67df93d6391..8236028254d 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.spec.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.spec.tsx @@ -1,9 +1,12 @@ import React from 'react'; import { expect } from 'chai'; import { + createDefaultConnectionInfo, createPluginTestHelpers, screen, + userEvent, waitFor, + within, } from '@mongodb-js/testing-library-compass'; import DiagramEditor from './diagram-editor'; import type { DataModelingStore } from '../../test/setup-store'; @@ -17,6 +20,13 @@ import { DiagramProvider } from '@mongodb-js/diagramming'; import { DataModelingWorkspaceTab } from '..'; import { openDiagram } from '../store/diagram'; import { DrawerAnchor } from '@mongodb-js/compass-components'; +import { type AnalysisOptions, startAnalysis } from '../store/analysis-process'; +import type { DataService } from '@mongodb-js/compass-connections/provider'; + +const mockConnections = [ + { ...createDefaultConnectionInfo(), id: 'connection1' }, + { ...createDefaultConnectionInfo(), id: 'connection2' }, +]; const storageItems: MongoDBDataModelDescription[] = [ { @@ -108,30 +118,41 @@ const mockDiagramming = { }, }; -const renderDiagramEditor = ({ - items = storageItems, - renderedItem = items[0], -}: { - items?: MongoDBDataModelDescription[]; - renderedItem?: MongoDBDataModelDescription; -} = {}) => { +const renderDiagramEditor = async ({ + existingDiagram, + newDiagram, +}: + | { + existingDiagram: MongoDBDataModelDescription; + newDiagram?: never; + } + | { + newDiagram: { + name: string; + database: string; + connectionId: string; + collections: string[]; + analysisOptions: AnalysisOptions; + }; + existingDiagram?: never; + }) => { const mockDataModelStorage = { status: 'READY', error: null, - items, + items: storageItems, save: () => { return Promise.resolve(false); }, delete: () => { return Promise.resolve(false); }, - loadAll: () => Promise.resolve(items), + loadAll: () => Promise.resolve(storageItems), load: (id: string) => { - return Promise.resolve(items.find((x) => x.id === id) ?? null); + return Promise.resolve(storageItems.find((x) => x.id === id) ?? null); }, }; - const { renderWithConnections } = createPluginTestHelpers( + const { renderWithActiveConnection } = createPluginTestHelpers( DataModelingWorkspaceTab.provider.withMockServices({ services: { dataModelStorage: mockDataModelStorage, @@ -139,18 +160,45 @@ const renderDiagramEditor = ({ }), { namespace: 'foo.bar', - } as any + } ); const { plugin: { store }, - } = renderWithConnections( + } = await renderWithActiveConnection( - + , + mockConnections[0], + { + connections: mockConnections, + connectFn: () => { + return { + sample: () => + Promise.resolve([ + { + _id: 'doc1', + }, + { + _id: 'doc2', + }, + ]), + } as unknown as DataService; + }, + } ); - store.dispatch(openDiagram(renderedItem)); + if (existingDiagram) store.dispatch(openDiagram(existingDiagram)); + if (newDiagram) + store.dispatch( + startAnalysis( + newDiagram.name, + newDiagram.connectionId, + newDiagram.database, + newDiagram.collections, + newDiagram.analysisOptions + ) + ); return { store }; }; @@ -168,8 +216,8 @@ describe('DiagramEditor', function () { context('with existing diagram', function () { beforeEach(async function () { - const result = renderDiagramEditor({ - renderedItem: storageItems[0], + const result = await renderDiagramEditor({ + existingDiagram: storageItems[0], }); store = result.store; @@ -179,6 +227,10 @@ describe('DiagramEditor', function () { }); }); + it('does not show the banner', function () { + expect(screen.queryByText('Worried about your data?')).not.to.exist; + }); + it('does not change the position of the nodes', function () { const state = store.getState(); @@ -200,4 +252,40 @@ describe('DiagramEditor', function () { ); }); }); + + context('with a new diagram', function () { + beforeEach(async function () { + const result = await renderDiagramEditor({ + newDiagram: { + name: 'New Diagram', + database: 'test', + connectionId: 'connection1', + collections: ['collection1', 'collection2'], + analysisOptions: { + automaticallyInferRelations: false, + }, + }, + }); + store = result.store; + + // wait till the editor is loaded + await waitFor(() => { + expect(screen.getByTestId('model-preview')).to.be.visible; + }); + }); + + it('shows the banner', function () { + expect(screen.getByText('Questions about your data?')).to.be.visible; + }); + + it('banner can be closed', function () { + const closeBtn = within(screen.getByTestId('data-info-banner')).getByRole( + 'button', + { name: 'Close Message' } + ); + expect(closeBtn).to.be.visible; + userEvent.click(closeBtn); + expect(screen.queryByText('Questions about your data?')).not.to.exist; + }); + }); }); diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index 4f193b78e2d..bc9607d8ca2 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -47,6 +47,7 @@ import { getHighlightedFields, relationshipToDiagramEdge, } from '../utils/nodes-and-edges'; +import toNS from 'mongodb-ns'; const loadingContainerStyles = css({ width: '100%', @@ -57,7 +58,7 @@ const loaderStyles = css({ margin: '0 auto', }); -const bannerStyles = css({ +const errorBannerStyles = css({ margin: spacing[200], '& > div': { display: 'flex', @@ -65,6 +66,17 @@ const bannerStyles = css({ }, }); +const dataInfoBannerStyles = css({ + margin: spacing[400], + position: 'absolute', + zIndex: 100, + + h4: { + marginTop: 0, + marginBottom: 0, + }, +}); + const bannerButtonStyles = css({ marginLeft: 'auto', }); @@ -73,7 +85,7 @@ const ErrorBannerWithRetry: React.FunctionComponent<{ onRetryClick: () => void; }> = ({ children, onRetryClick }) => { return ( - + {children} ['selectedItems']; const DiagramContent: React.FunctionComponent<{ diagramLabel: string; + database: string | null; + isNewlyCreatedDiagram?: boolean; model: StaticModel | null; isInRelationshipDrawingMode: boolean; editErrors?: string[]; @@ -134,6 +148,8 @@ const DiagramContent: React.FunctionComponent<{ onRelationshipDrawn: () => void; }> = ({ diagramLabel, + database, + isNewlyCreatedDiagram, model, isInRelationshipDrawingMode, newCollection, @@ -151,6 +167,9 @@ const DiagramContent: React.FunctionComponent<{ const diagram = useRef(useDiagram()); const { openDrawer } = useDrawerActions(); const { isDrawerOpen } = useDrawerState(); + const [showDataInfoBanner, setshowDataInfoBanner] = useState( + isNewlyCreatedDiagram ?? false + ); const setDiagramContainerRef = useCallback((ref: HTMLDivElement | null) => { if (ref) { @@ -307,6 +326,20 @@ const DiagramContent: React.FunctionComponent<{ data-testid="diagram-editor-container" > + {showDataInfoBanner && ( + setshowDataInfoBanner(false)} + className={dataInfoBannerStyles} + data-testid="data-info-banner" + > + Questions about your data? + This diagram was generated based on a sample of documents from{' '} + {database ?? 'a database'}. Changes made to the diagram will not + impact your data + + )} { const { diagram } = state; + const model = diagram ? selectCurrentModelFromState(state) : null; return { - model: diagram ? selectCurrentModelFromState(state) : null, + model, diagramLabel: diagram?.name || 'Schema Preview', selectedItems: state.diagram?.selectedItems ?? null, newCollection: diagram?.draftCollection, + isNewlyCreatedDiagram: diagram?.isNewlyCreated, + database: model?.collections[0]?.ns + ? toNS(model.collections[0].ns).database + : null, // TODO(COMPASS-9718): use diagram.database }; }, { diff --git a/packages/compass-data-modeling/src/store/diagram.ts b/packages/compass-data-modeling/src/store/diagram.ts index 9be07203139..fd41d43b3d7 100644 --- a/packages/compass-data-modeling/src/store/diagram.ts +++ b/packages/compass-data-modeling/src/store/diagram.ts @@ -61,6 +61,7 @@ export type DiagramState = }; editErrors?: string[]; selectedItems: SelectedItems | null; + isNewlyCreated: boolean; draftCollection?: string; }) | null; // null when no diagram is currently open @@ -159,6 +160,7 @@ export const diagramReducer: Reducer = ( prev.shift(); // Remove the first item, which is initial SetModel and there's no previous edit for it. return { ...action.diagram, + isNewlyCreated: false, edits: { prev, current, @@ -171,6 +173,7 @@ export const diagramReducer: Reducer = ( if (isAction(action, AnalysisProcessActionTypes.ANALYSIS_FINISHED)) { return { id: new UUID().toString(), + isNewlyCreated: true, name: action.name, connectionId: action.connectionId, createdAt: new Date().toISOString(), diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index fc9eb088d73..5da7673a9f1 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -1501,6 +1501,7 @@ export const DataModelCollectionRelationshipItemEdit = `[aria-label="Edit relati export const DataModelCollectionRelationshipItemDelete = `[aria-label="Delete relationship"]`; export const DataModelCollectionSidebarItemDelete = `[aria-label="Delete collection"]`; export const DataModelCollectionSidebarItemDeleteButton = `[data-action="delete"]`; +export const DataModelInfoBannerCloseBtn = `[data-testid="data-info-banner"] [aria-label="Close Message"]`; // Side drawer export const SideDrawer = `[data-testid="${getDrawerIds().root}"]`; 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 91e51119cfc..bff9dd760f6 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -115,6 +115,11 @@ async function setupDiagram( // Wait for the diagram editor to load const dataModelEditor = browser.$(Selectors.DataModelEditor); await dataModelEditor.waitForDisplayed(); + + // Close the info banner to get it out of the way + const infoBannerCloseBtn = browser.$(Selectors.DataModelInfoBannerCloseBtn); + await infoBannerCloseBtn.waitForClickable(); + await browser.clickVisible(Selectors.DataModelInfoBannerCloseBtn); } async function selectCollectionOnTheDiagram(