Skip to content

Commit 20cb662

Browse files
authored
feat(data-modeling): node selection and relationship editing store and actions COMPASS-9476 (#7118)
* feat(data-modeling): node selection and relationship editing store and actions * chore(data-modeling): add tests * fix(data-modeling): un-only the test and fix types * chore(data-modeling): remove sidebar state; connect form to the edits directly * chore(data-modeling): fix tests * chore(data-modeling): stricter check for diagram item selection * fix(data-modeling): do not use memoize for relationship filter * chore(data-modeling): add more tests
1 parent d8c7fb7 commit 20cb662

14 files changed

+1145
-110
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React from 'react';
2+
import { connect } from 'react-redux';
3+
import type { Relationship } from '../services/data-model-storage';
4+
import { Button, H3 } from '@mongodb-js/compass-components';
5+
import {
6+
createNewRelationship,
7+
deleteRelationship,
8+
getCurrentDiagramFromState,
9+
selectCurrentModel,
10+
selectRelationship,
11+
} from '../store/diagram';
12+
import type { DataModelingState } from '../store/reducer';
13+
14+
type CollectionDrawerContentProps = {
15+
namespace: string;
16+
relationships: Relationship[];
17+
shouldShowRelationshipEditingForm?: boolean;
18+
onCreateNewRelationshipClick: (namespace: string) => void;
19+
onEditRelationshipClick: (rId: string) => void;
20+
onDeleteRelationshipClick: (rId: string) => void;
21+
};
22+
23+
const CollectionDrawerContent: React.FunctionComponent<
24+
CollectionDrawerContentProps
25+
> = ({
26+
namespace,
27+
relationships,
28+
onCreateNewRelationshipClick,
29+
onEditRelationshipClick,
30+
onDeleteRelationshipClick,
31+
}) => {
32+
return (
33+
<>
34+
<H3>{namespace}</H3>
35+
<ul>
36+
{relationships.map((r) => {
37+
return (
38+
<li key={r.id} data-relationship-id={r.id}>
39+
{r.relationship[0].fields?.join('.')}&nbsp;-&gt;&nbsp;
40+
{r.relationship[1].fields?.join('.')}
41+
<Button
42+
onClick={() => {
43+
onEditRelationshipClick(r.id);
44+
}}
45+
>
46+
Edit
47+
</Button>
48+
<Button
49+
onClick={() => {
50+
onDeleteRelationshipClick(r.id);
51+
}}
52+
>
53+
Delete
54+
</Button>
55+
</li>
56+
);
57+
})}
58+
</ul>
59+
<Button
60+
onClick={() => {
61+
onCreateNewRelationshipClick(namespace);
62+
}}
63+
>
64+
Add relationship manually
65+
</Button>
66+
</>
67+
);
68+
};
69+
70+
export default connect(
71+
(state: DataModelingState, ownProps: { namespace: string }) => {
72+
return {
73+
relationships: selectCurrentModel(
74+
getCurrentDiagramFromState(state).edits
75+
).relationships.filter((r) => {
76+
const [local, foreign] = r.relationship;
77+
return (
78+
local.ns === ownProps.namespace || foreign.ns === ownProps.namespace
79+
);
80+
}),
81+
};
82+
},
83+
{
84+
onCreateNewRelationshipClick: createNewRelationship,
85+
onEditRelationshipClick: selectRelationship,
86+
onDeleteRelationshipClick: deleteRelationship,
87+
}
88+
)(CollectionDrawerContent);

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import NewDiagramFormModal from './new-diagram-form';
66
import type { DataModelingState } from '../store/reducer';
77
import { DiagramProvider } from '@mongodb-js/diagramming';
88
import DiagramEditorSidePanel from './diagram-editor-side-panel';
9-
type DataModelingPluginInitialProps = {
9+
10+
type DataModelingProps = {
1011
showList: boolean;
1112
};
1213

13-
const DataModeling: React.FunctionComponent<DataModelingPluginInitialProps> = ({
14+
const DataModeling: React.FunctionComponent<DataModelingProps> = ({
1415
showList,
1516
}) => {
1617
return (
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import React from 'react';
2+
import { expect } from 'chai';
3+
import {
4+
createPluginTestHelpers,
5+
screen,
6+
waitFor,
7+
userEvent,
8+
within,
9+
} from '@mongodb-js/testing-library-compass';
10+
import { DataModelingWorkspaceTab } from '../index';
11+
import DiagramEditorSidePanel from './diagram-editor-side-panel';
12+
import {
13+
getCurrentDiagramFromState,
14+
openDiagram,
15+
selectCollection,
16+
selectCurrentModel,
17+
selectRelationship,
18+
} from '../store/diagram';
19+
import dataModel from '../../test/fixtures/data-model-with-relationships.json';
20+
import type { MongoDBDataModelDescription } from '../services/data-model-storage';
21+
22+
async function comboboxSelectItem(
23+
label: string,
24+
value: string,
25+
visibleLabel = value
26+
) {
27+
userEvent.click(screen.getByRole('textbox', { name: label }));
28+
await waitFor(() => {
29+
screen.getByRole('option', { name: visibleLabel });
30+
});
31+
userEvent.click(screen.getByRole('option', { name: visibleLabel }));
32+
await waitFor(() => {
33+
expect(screen.getByRole('textbox', { name: label })).to.have.attribute(
34+
'value',
35+
value
36+
);
37+
});
38+
}
39+
40+
describe('DiagramEditorSidePanel', function () {
41+
function renderDrawer() {
42+
const { renderWithConnections } = createPluginTestHelpers(
43+
DataModelingWorkspaceTab.provider.withMockServices({})
44+
);
45+
const result = renderWithConnections(
46+
<DiagramEditorSidePanel></DiagramEditorSidePanel>
47+
);
48+
result.plugin.store.dispatch(
49+
openDiagram(dataModel as MongoDBDataModelDescription)
50+
);
51+
return result;
52+
}
53+
54+
it('should not render if no items are selected', function () {
55+
renderDrawer();
56+
expect(screen.queryByTestId('data-modeling-drawer')).to.eq(null);
57+
});
58+
59+
it('should render a collection context drawer when collection is clicked', function () {
60+
const result = renderDrawer();
61+
result.plugin.store.dispatch(selectCollection('flights.airlines'));
62+
expect(screen.getByText('flights.airlines')).to.be.visible;
63+
});
64+
65+
it('should render a relationship context drawer when relations is clicked', function () {
66+
const result = renderDrawer();
67+
result.plugin.store.dispatch(
68+
selectRelationship('204b1fc0-601f-4d62-bba3-38fade71e049')
69+
);
70+
expect(screen.getByText('Edit Relationship')).to.be.visible;
71+
expect(
72+
document.querySelector(
73+
'[data-relationship-id="204b1fc0-601f-4d62-bba3-38fade71e049"]'
74+
)
75+
).to.be.visible;
76+
});
77+
78+
it('should change the content of the drawer when selecting different items', function () {
79+
const result = renderDrawer();
80+
81+
result.plugin.store.dispatch(selectCollection('flights.airlines'));
82+
expect(screen.getByText('flights.airlines')).to.be.visible;
83+
84+
result.plugin.store.dispatch(
85+
selectCollection('flights.airports_coordinates_for_schema')
86+
);
87+
expect(screen.getByText('flights.airports_coordinates_for_schema')).to.be
88+
.visible;
89+
90+
result.plugin.store.dispatch(
91+
selectRelationship('204b1fc0-601f-4d62-bba3-38fade71e049')
92+
);
93+
expect(
94+
document.querySelector(
95+
'[data-relationship-id="204b1fc0-601f-4d62-bba3-38fade71e049"]'
96+
)
97+
).to.be.visible;
98+
99+
result.plugin.store.dispatch(
100+
selectRelationship('6f776467-4c98-476b-9b71-1f8a724e6c2c')
101+
);
102+
expect(
103+
document.querySelector(
104+
'[data-relationship-id="6f776467-4c98-476b-9b71-1f8a724e6c2c"]'
105+
)
106+
).to.be.visible;
107+
108+
result.plugin.store.dispatch(selectCollection('flights.planes'));
109+
expect(screen.getByText('flights.planes')).to.be.visible;
110+
});
111+
112+
it('should open and edit relationship starting from collection', async function () {
113+
const result = renderDrawer();
114+
result.plugin.store.dispatch(selectCollection('flights.countries'));
115+
116+
// Open relationshipt editing form
117+
const relationshipCard = document.querySelector<HTMLElement>(
118+
'[data-relationship-id="204b1fc0-601f-4d62-bba3-38fade71e049"]'
119+
);
120+
userEvent.click(
121+
within(relationshipCard!).getByRole('button', { name: 'Edit' })
122+
);
123+
expect(screen.getByText('Edit Relationship')).to.be.visible;
124+
125+
// Select new values
126+
await comboboxSelectItem('Local collection', 'planes');
127+
await comboboxSelectItem('Local field', 'name');
128+
await comboboxSelectItem('Foreign collection', 'countries');
129+
await comboboxSelectItem('Foreign field', 'iso_code');
130+
131+
// We should be testing through rendered UI but as it's really hard to make
132+
// diagram rendering in tests property, we are just validating the final
133+
// model here
134+
const modifiedRelationship = selectCurrentModel(
135+
getCurrentDiagramFromState(result.plugin.store.getState()).edits
136+
).relationships.find((r) => {
137+
return r.id === '204b1fc0-601f-4d62-bba3-38fade71e049';
138+
});
139+
140+
expect(modifiedRelationship)
141+
.to.have.property('relationship')
142+
.deep.eq([
143+
{
144+
ns: 'flights.planes',
145+
fields: ['name'],
146+
cardinality: 1,
147+
},
148+
{
149+
ns: 'flights.countries',
150+
fields: ['iso_code'],
151+
cardinality: 1,
152+
},
153+
]);
154+
});
155+
});
Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,20 @@
11
import React from 'react';
22
import { connect } from 'react-redux';
33
import type { DataModelingState } from '../store/reducer';
4-
import { closeSidePanel } from '../store/side-panel';
54
import {
65
Button,
76
css,
87
cx,
9-
Body,
10-
spacing,
118
palette,
129
useDarkMode,
1310
} from '@mongodb-js/compass-components';
11+
import CollectionDrawerContent from './collection-drawer-content';
12+
import RelationshipDrawerContent from './relationship-drawer-content';
13+
import { closeDrawer } from '../store/diagram';
1414

1515
const containerStyles = css({
1616
width: '400px',
1717
height: '100%',
18-
19-
display: 'flex',
20-
flexDirection: 'column',
21-
alignItems: 'center',
22-
justifyContent: 'center',
23-
gap: spacing[400],
2418
borderLeft: `1px solid ${palette.gray.light2}`,
2519
});
2620

@@ -29,21 +23,42 @@ const darkModeContainerStyles = css({
2923
});
3024

3125
type DiagramEditorSidePanelProps = {
32-
isOpen: boolean;
26+
selectedItems: { type: 'relationship' | 'collection'; id: string } | null;
3327
onClose: () => void;
3428
};
3529

3630
function DiagmramEditorSidePanel({
37-
isOpen,
31+
selectedItems,
3832
onClose,
3933
}: DiagramEditorSidePanelProps) {
4034
const isDarkMode = useDarkMode();
41-
if (!isOpen) {
35+
36+
if (!selectedItems) {
4237
return null;
4338
}
39+
40+
let content;
41+
42+
if (selectedItems.type === 'collection') {
43+
content = (
44+
<CollectionDrawerContent
45+
namespace={selectedItems.id}
46+
></CollectionDrawerContent>
47+
);
48+
} else if (selectedItems.type === 'relationship') {
49+
content = (
50+
<RelationshipDrawerContent
51+
relationshipId={selectedItems.id}
52+
></RelationshipDrawerContent>
53+
);
54+
}
55+
4456
return (
45-
<div className={cx(containerStyles, isDarkMode && darkModeContainerStyles)}>
46-
<Body>This feature is under development.</Body>
57+
<div
58+
className={cx(containerStyles, isDarkMode && darkModeContainerStyles)}
59+
data-testid="data-modeling-drawer"
60+
>
61+
{content}
4762
<Button onClick={onClose} variant="primary" size="small">
4863
Close Side Panel
4964
</Button>
@@ -53,12 +68,11 @@ function DiagmramEditorSidePanel({
5368

5469
export default connect(
5570
(state: DataModelingState) => {
56-
const { sidePanel } = state;
5771
return {
58-
isOpen: sidePanel.isOpen,
72+
selectedItems: state.diagram?.selectedItems ?? null,
5973
};
6074
},
6175
{
62-
onClose: closeSidePanel,
76+
onClose: closeDrawer,
6377
}
6478
)(DiagmramEditorSidePanel);

0 commit comments

Comments
 (0)