Skip to content

Commit f3d01e0

Browse files
authored
feat(compass-data-modeling): add info banner COMPASS-9773 (#7345)
1 parent 55c1d2e commit f3d01e0

File tree

5 files changed

+155
-20
lines changed

5 files changed

+155
-20
lines changed

packages/compass-data-modeling/src/components/diagram-editor.spec.tsx

Lines changed: 105 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import React from 'react';
22
import { expect } from 'chai';
33
import {
4+
createDefaultConnectionInfo,
45
createPluginTestHelpers,
56
screen,
7+
userEvent,
68
waitFor,
9+
within,
710
} from '@mongodb-js/testing-library-compass';
811
import DiagramEditor from './diagram-editor';
912
import type { DataModelingStore } from '../../test/setup-store';
@@ -17,6 +20,13 @@ import { DiagramProvider } from '@mongodb-js/diagramming';
1720
import { DataModelingWorkspaceTab } from '..';
1821
import { openDiagram } from '../store/diagram';
1922
import { DrawerAnchor } from '@mongodb-js/compass-components';
23+
import { type AnalysisOptions, startAnalysis } from '../store/analysis-process';
24+
import type { DataService } from '@mongodb-js/compass-connections/provider';
25+
26+
const mockConnections = [
27+
{ ...createDefaultConnectionInfo(), id: 'connection1' },
28+
{ ...createDefaultConnectionInfo(), id: 'connection2' },
29+
];
2030

2131
const storageItems: MongoDBDataModelDescription[] = [
2232
{
@@ -108,49 +118,87 @@ const mockDiagramming = {
108118
},
109119
};
110120

111-
const renderDiagramEditor = ({
112-
items = storageItems,
113-
renderedItem = items[0],
114-
}: {
115-
items?: MongoDBDataModelDescription[];
116-
renderedItem?: MongoDBDataModelDescription;
117-
} = {}) => {
121+
const renderDiagramEditor = async ({
122+
existingDiagram,
123+
newDiagram,
124+
}:
125+
| {
126+
existingDiagram: MongoDBDataModelDescription;
127+
newDiagram?: never;
128+
}
129+
| {
130+
newDiagram: {
131+
name: string;
132+
database: string;
133+
connectionId: string;
134+
collections: string[];
135+
analysisOptions: AnalysisOptions;
136+
};
137+
existingDiagram?: never;
138+
}) => {
118139
const mockDataModelStorage = {
119140
status: 'READY',
120141
error: null,
121-
items,
142+
items: storageItems,
122143
save: () => {
123144
return Promise.resolve(false);
124145
},
125146
delete: () => {
126147
return Promise.resolve(false);
127148
},
128-
loadAll: () => Promise.resolve(items),
149+
loadAll: () => Promise.resolve(storageItems),
129150
load: (id: string) => {
130-
return Promise.resolve(items.find((x) => x.id === id) ?? null);
151+
return Promise.resolve(storageItems.find((x) => x.id === id) ?? null);
131152
},
132153
};
133154

134-
const { renderWithConnections } = createPluginTestHelpers(
155+
const { renderWithActiveConnection } = createPluginTestHelpers(
135156
DataModelingWorkspaceTab.provider.withMockServices({
136157
services: {
137158
dataModelStorage: mockDataModelStorage,
138159
},
139160
}),
140161
{
141162
namespace: 'foo.bar',
142-
} as any
163+
}
143164
);
144165
const {
145166
plugin: { store },
146-
} = renderWithConnections(
167+
} = await renderWithActiveConnection(
147168
<DrawerAnchor>
148169
<DiagramProvider fitView>
149170
<DiagramEditor />
150171
</DiagramProvider>
151-
</DrawerAnchor>
172+
</DrawerAnchor>,
173+
mockConnections[0],
174+
{
175+
connections: mockConnections,
176+
connectFn: () => {
177+
return {
178+
sample: () =>
179+
Promise.resolve([
180+
{
181+
_id: 'doc1',
182+
},
183+
{
184+
_id: 'doc2',
185+
},
186+
]),
187+
} as unknown as DataService;
188+
},
189+
}
152190
);
153-
store.dispatch(openDiagram(renderedItem));
191+
if (existingDiagram) store.dispatch(openDiagram(existingDiagram));
192+
if (newDiagram)
193+
store.dispatch(
194+
startAnalysis(
195+
newDiagram.name,
196+
newDiagram.connectionId,
197+
newDiagram.database,
198+
newDiagram.collections,
199+
newDiagram.analysisOptions
200+
)
201+
);
154202

155203
return { store };
156204
};
@@ -168,8 +216,8 @@ describe('DiagramEditor', function () {
168216

169217
context('with existing diagram', function () {
170218
beforeEach(async function () {
171-
const result = renderDiagramEditor({
172-
renderedItem: storageItems[0],
219+
const result = await renderDiagramEditor({
220+
existingDiagram: storageItems[0],
173221
});
174222
store = result.store;
175223

@@ -179,6 +227,10 @@ describe('DiagramEditor', function () {
179227
});
180228
});
181229

230+
it('does not show the banner', function () {
231+
expect(screen.queryByText('Worried about your data?')).not.to.exist;
232+
});
233+
182234
it('does not change the position of the nodes', function () {
183235
const state = store.getState();
184236

@@ -200,4 +252,40 @@ describe('DiagramEditor', function () {
200252
);
201253
});
202254
});
255+
256+
context('with a new diagram', function () {
257+
beforeEach(async function () {
258+
const result = await renderDiagramEditor({
259+
newDiagram: {
260+
name: 'New Diagram',
261+
database: 'test',
262+
connectionId: 'connection1',
263+
collections: ['collection1', 'collection2'],
264+
analysisOptions: {
265+
automaticallyInferRelations: false,
266+
},
267+
},
268+
});
269+
store = result.store;
270+
271+
// wait till the editor is loaded
272+
await waitFor(() => {
273+
expect(screen.getByTestId('model-preview')).to.be.visible;
274+
});
275+
});
276+
277+
it('shows the banner', function () {
278+
expect(screen.getByText('Questions about your data?')).to.be.visible;
279+
});
280+
281+
it('banner can be closed', function () {
282+
const closeBtn = within(screen.getByTestId('data-info-banner')).getByRole(
283+
'button',
284+
{ name: 'Close Message' }
285+
);
286+
expect(closeBtn).to.be.visible;
287+
userEvent.click(closeBtn);
288+
expect(screen.queryByText('Questions about your data?')).not.to.exist;
289+
});
290+
});
203291
});

packages/compass-data-modeling/src/components/diagram-editor.tsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
getHighlightedFields,
4848
relationshipToDiagramEdge,
4949
} from '../utils/nodes-and-edges';
50+
import toNS from 'mongodb-ns';
5051

5152
const loadingContainerStyles = css({
5253
width: '100%',
@@ -57,14 +58,25 @@ const loaderStyles = css({
5758
margin: '0 auto',
5859
});
5960

60-
const bannerStyles = css({
61+
const errorBannerStyles = css({
6162
margin: spacing[200],
6263
'& > div': {
6364
display: 'flex',
6465
alignItems: 'center',
6566
},
6667
});
6768

69+
const dataInfoBannerStyles = css({
70+
margin: spacing[400],
71+
position: 'absolute',
72+
zIndex: 100,
73+
74+
h4: {
75+
marginTop: 0,
76+
marginBottom: 0,
77+
},
78+
});
79+
6880
const bannerButtonStyles = css({
6981
marginLeft: 'auto',
7082
});
@@ -73,7 +85,7 @@ const ErrorBannerWithRetry: React.FunctionComponent<{
7385
onRetryClick: () => void;
7486
}> = ({ children, onRetryClick }) => {
7587
return (
76-
<Banner variant="danger" className={bannerStyles}>
88+
<Banner variant="danger" className={errorBannerStyles}>
7789
<div>{children}</div>
7890
<Button
7991
className={bannerButtonStyles}
@@ -113,6 +125,8 @@ type SelectedItems = NonNullable<DiagramState>['selectedItems'];
113125

114126
const DiagramContent: React.FunctionComponent<{
115127
diagramLabel: string;
128+
database: string | null;
129+
isNewlyCreatedDiagram?: boolean;
116130
model: StaticModel | null;
117131
isInRelationshipDrawingMode: boolean;
118132
editErrors?: string[];
@@ -134,6 +148,8 @@ const DiagramContent: React.FunctionComponent<{
134148
onRelationshipDrawn: () => void;
135149
}> = ({
136150
diagramLabel,
151+
database,
152+
isNewlyCreatedDiagram,
137153
model,
138154
isInRelationshipDrawingMode,
139155
newCollection,
@@ -151,6 +167,9 @@ const DiagramContent: React.FunctionComponent<{
151167
const diagram = useRef(useDiagram());
152168
const { openDrawer } = useDrawerActions();
153169
const { isDrawerOpen } = useDrawerState();
170+
const [showDataInfoBanner, setshowDataInfoBanner] = useState(
171+
isNewlyCreatedDiagram ?? false
172+
);
154173

155174
const setDiagramContainerRef = useCallback((ref: HTMLDivElement | null) => {
156175
if (ref) {
@@ -307,6 +326,20 @@ const DiagramContent: React.FunctionComponent<{
307326
data-testid="diagram-editor-container"
308327
>
309328
<div className={modelPreviewStyles} data-testid="model-preview">
329+
{showDataInfoBanner && (
330+
<Banner
331+
variant="info"
332+
dismissible
333+
onClose={() => setshowDataInfoBanner(false)}
334+
className={dataInfoBannerStyles}
335+
data-testid="data-info-banner"
336+
>
337+
<h4>Questions about your data?</h4>
338+
This diagram was generated based on a sample of documents from{' '}
339+
{database ?? 'a database'}. Changes made to the diagram will not
340+
impact your data
341+
</Banner>
342+
)}
310343
<Diagram
311344
isDarkMode={isDarkMode}
312345
title={diagramLabel}
@@ -331,11 +364,16 @@ const DiagramContent: React.FunctionComponent<{
331364
const ConnectedDiagramContent = connect(
332365
(state: DataModelingState) => {
333366
const { diagram } = state;
367+
const model = diagram ? selectCurrentModelFromState(state) : null;
334368
return {
335-
model: diagram ? selectCurrentModelFromState(state) : null,
369+
model,
336370
diagramLabel: diagram?.name || 'Schema Preview',
337371
selectedItems: state.diagram?.selectedItems ?? null,
338372
newCollection: diagram?.draftCollection,
373+
isNewlyCreatedDiagram: diagram?.isNewlyCreated,
374+
database: model?.collections[0]?.ns
375+
? toNS(model.collections[0].ns).database
376+
: null, // TODO(COMPASS-9718): use diagram.database
339377
};
340378
},
341379
{

packages/compass-data-modeling/src/store/diagram.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export type DiagramState =
6161
};
6262
editErrors?: string[];
6363
selectedItems: SelectedItems | null;
64+
isNewlyCreated: boolean;
6465
draftCollection?: string;
6566
})
6667
| null; // null when no diagram is currently open
@@ -159,6 +160,7 @@ export const diagramReducer: Reducer<DiagramState> = (
159160
prev.shift(); // Remove the first item, which is initial SetModel and there's no previous edit for it.
160161
return {
161162
...action.diagram,
163+
isNewlyCreated: false,
162164
edits: {
163165
prev,
164166
current,
@@ -171,6 +173,7 @@ export const diagramReducer: Reducer<DiagramState> = (
171173
if (isAction(action, AnalysisProcessActionTypes.ANALYSIS_FINISHED)) {
172174
return {
173175
id: new UUID().toString(),
176+
isNewlyCreated: true,
174177
name: action.name,
175178
connectionId: action.connectionId,
176179
createdAt: new Date().toISOString(),

packages/compass-e2e-tests/helpers/selectors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,6 +1501,7 @@ export const DataModelCollectionRelationshipItemEdit = `[aria-label="Edit relati
15011501
export const DataModelCollectionRelationshipItemDelete = `[aria-label="Delete relationship"]`;
15021502
export const DataModelCollectionSidebarItemDelete = `[aria-label="Delete collection"]`;
15031503
export const DataModelCollectionSidebarItemDeleteButton = `[data-action="delete"]`;
1504+
export const DataModelInfoBannerCloseBtn = `[data-testid="data-info-banner"] [aria-label="Close Message"]`;
15041505

15051506
// Side drawer
15061507
export const SideDrawer = `[data-testid="${getDrawerIds().root}"]`;

packages/compass-e2e-tests/tests/data-modeling-tab.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ async function setupDiagram(
115115
// Wait for the diagram editor to load
116116
const dataModelEditor = browser.$(Selectors.DataModelEditor);
117117
await dataModelEditor.waitForDisplayed();
118+
119+
// Close the info banner to get it out of the way
120+
const infoBannerCloseBtn = browser.$(Selectors.DataModelInfoBannerCloseBtn);
121+
await infoBannerCloseBtn.waitForClickable();
122+
await browser.clickVisible(Selectors.DataModelInfoBannerCloseBtn);
118123
}
119124

120125
async function selectCollectionOnTheDiagram(

0 commit comments

Comments
 (0)