Skip to content

Commit d3e74db

Browse files
authored
feat(data-modeling): grid list and toolbar COMPASS-9311 (#6931)
* card list and toolbar * vgrid * fix tests * filter tests * pr feedback * npm check * add todo ticket
1 parent d567352 commit d3e74db

File tree

4 files changed

+395
-133
lines changed

4 files changed

+395
-133
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {
2+
Card,
3+
css,
4+
cx,
5+
ItemActionMenu,
6+
palette,
7+
spacing,
8+
Subtitle,
9+
useDarkMode,
10+
} from '@mongodb-js/compass-components';
11+
import type { MongoDBDataModelDescription } from '../services/data-model-storage';
12+
import React from 'react';
13+
14+
// Same as saved-queries-aggregations
15+
export const CARD_WIDTH = spacing[1600] * 4;
16+
export const CARD_HEIGHT = 218;
17+
18+
const diagramCardStyles = css({
19+
display: 'flex',
20+
flexDirection: 'column',
21+
overflow: 'hidden',
22+
});
23+
24+
const cardHeaderStyles = css({
25+
display: 'flex',
26+
gap: spacing[200],
27+
alignItems: 'flex-start',
28+
});
29+
const cardTitle = css({
30+
fontWeight: 'bold',
31+
height: spacing[600] * 2,
32+
marginBottom: spacing[400],
33+
whiteSpace: 'nowrap',
34+
textOverflow: 'ellipsis',
35+
overflow: 'hidden',
36+
});
37+
38+
const cardTitleDark = css({
39+
color: palette.green.light2,
40+
});
41+
const cardTitleLight = css({
42+
color: palette.green.dark2,
43+
});
44+
45+
export function DiagramCard({
46+
diagram,
47+
onOpen,
48+
onRename,
49+
onDelete,
50+
}: {
51+
diagram: MongoDBDataModelDescription;
52+
onOpen: (diagram: MongoDBDataModelDescription) => void;
53+
onRename: (id: string) => void;
54+
onDelete: (id: string) => void;
55+
}) {
56+
const darkmode = useDarkMode();
57+
return (
58+
<Card
59+
className={diagramCardStyles}
60+
contentStyle="clickable"
61+
onClick={() => onOpen(diagram)}
62+
data-testid="saved-diagram-card"
63+
data-diagram-name={diagram.name}
64+
title={diagram.name}
65+
>
66+
<div className={cardHeaderStyles}>
67+
<Subtitle
68+
as="div"
69+
className={cx(cardTitle, darkmode ? cardTitleDark : cardTitleLight)}
70+
title={diagram.name}
71+
>
72+
{diagram.name}
73+
</Subtitle>
74+
<ItemActionMenu
75+
isVisible
76+
actions={[
77+
{ action: 'rename', label: 'Rename' },
78+
{ action: 'delete', label: 'Delete' },
79+
]}
80+
onAction={(action) => {
81+
switch (action) {
82+
case 'rename':
83+
onRename(diagram.id);
84+
break;
85+
case 'delete':
86+
onDelete(diagram.id);
87+
break;
88+
default:
89+
break;
90+
}
91+
}}
92+
></ItemActionMenu>
93+
</div>
94+
{/* TODO(COMPASS-9398): Add lastModified and namespace to the card. */}
95+
</Card>
96+
);
97+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React, { useContext } from 'react';
2+
import {
3+
Button,
4+
css,
5+
cx,
6+
Icon,
7+
palette,
8+
SearchInput,
9+
spacing,
10+
Subtitle,
11+
useDarkMode,
12+
} from '@mongodb-js/compass-components';
13+
import { DiagramListContext } from './saved-diagrams-list';
14+
15+
const containerStyles = css({
16+
padding: spacing[400],
17+
display: 'grid',
18+
gridTemplateAreas: `
19+
'title createDiagram'
20+
'searchInput sortControls'
21+
`,
22+
columnGap: spacing[800],
23+
rowGap: spacing[200],
24+
gridTemplateColumns: '5fr',
25+
});
26+
27+
const titleStyles = css({
28+
gridArea: 'title',
29+
});
30+
const createDiagramContainerStyles = css({
31+
gridArea: 'createDiagram',
32+
display: 'flex',
33+
justifyContent: 'flex-end',
34+
});
35+
const searchInputStyles = css({
36+
gridArea: 'searchInput',
37+
});
38+
const sortControlsStyles = css({
39+
gridArea: 'sortControls',
40+
});
41+
42+
const toolbarTitleLightStyles = css({ color: palette.gray.dark1 });
43+
const toolbarTitleDarkStyles = css({ color: palette.gray.light1 });
44+
45+
export const DiagramListToolbar = () => {
46+
const {
47+
onSearchDiagrams: onSearch,
48+
onCreateDiagram,
49+
sortControls,
50+
searchTerm,
51+
} = useContext(DiagramListContext);
52+
const darkMode = useDarkMode();
53+
54+
return (
55+
<div className={containerStyles}>
56+
<Subtitle
57+
className={cx(
58+
titleStyles,
59+
darkMode ? toolbarTitleDarkStyles : toolbarTitleLightStyles
60+
)}
61+
>
62+
Open an existing diagram:
63+
</Subtitle>
64+
<div className={createDiagramContainerStyles}>
65+
<Button
66+
onClick={onCreateDiagram}
67+
variant="primary"
68+
size="small"
69+
data-testid="create-diagram-button"
70+
leftGlyph={<Icon glyph="Plus"></Icon>}
71+
>
72+
Generate new diagram
73+
</Button>
74+
</div>
75+
<SearchInput
76+
aria-label="Search diagrams"
77+
value={searchTerm}
78+
className={searchInputStyles}
79+
onChange={(e) => onSearch(e.target.value)}
80+
/>
81+
<div className={sortControlsStyles}>{sortControls}</div>
82+
</div>
83+
);
84+
};

packages/compass-data-modeling/src/components/saved-diagrams-list.spec.tsx

Lines changed: 90 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,44 +11,65 @@ import type { DataModelingStore } from '../../test/setup-store';
1111
import { DataModelStorageServiceProvider } from '../provider';
1212
import type { MongoDBDataModelDescription } from '../services/data-model-storage';
1313

14-
describe('SavedDiagramsList', function () {
15-
const renderSavedDiagramsList = ({
16-
loadAll = () => Promise.resolve([]),
17-
}: {
18-
loadAll?: () => Promise<MongoDBDataModelDescription[]>;
19-
} = {}) => {
20-
const mockDataModelStorage = {
21-
status: 'READY',
22-
error: null,
23-
items: [],
24-
save: () => {
25-
return Promise.resolve(false);
26-
},
27-
delete: () => {
28-
return Promise.resolve(false);
29-
},
30-
loadAll,
31-
load: () => {
32-
return Promise.resolve(null);
33-
},
34-
};
35-
return renderWithStore(
36-
<DataModelStorageServiceProvider storage={mockDataModelStorage}>
37-
<SavedDiagramsList />
38-
</DataModelStorageServiceProvider>,
39-
{
40-
services: {
41-
dataModelStorage: mockDataModelStorage,
42-
},
43-
}
44-
);
14+
const storageItems: MongoDBDataModelDescription[] = [
15+
{
16+
id: '1',
17+
name: 'One',
18+
edits: [],
19+
connectionId: null,
20+
},
21+
{
22+
id: '2',
23+
name: 'Two',
24+
edits: [],
25+
connectionId: null,
26+
},
27+
{
28+
id: '3',
29+
name: 'Three',
30+
edits: [],
31+
connectionId: null,
32+
},
33+
];
34+
35+
const renderSavedDiagramsList = ({
36+
items = storageItems,
37+
}: {
38+
items?: MongoDBDataModelDescription[];
39+
} = {}) => {
40+
const mockDataModelStorage = {
41+
status: 'READY',
42+
error: null,
43+
items,
44+
save: () => {
45+
return Promise.resolve(false);
46+
},
47+
delete: () => {
48+
return Promise.resolve(false);
49+
},
50+
loadAll: () => Promise.resolve(items),
51+
load: (id: string) => {
52+
return Promise.resolve(items.find((x) => x.id === id) ?? null);
53+
},
4554
};
55+
return renderWithStore(
56+
<DataModelStorageServiceProvider storage={mockDataModelStorage}>
57+
<SavedDiagramsList />
58+
</DataModelStorageServiceProvider>,
59+
{
60+
services: {
61+
dataModelStorage: mockDataModelStorage,
62+
},
63+
}
64+
);
65+
};
4666

67+
describe('SavedDiagramsList', function () {
4768
context('when there are no saved diagrams', function () {
4869
let store: DataModelingStore;
4970

5071
beforeEach(async function () {
51-
const result = renderSavedDiagramsList();
72+
const result = renderSavedDiagramsList({ items: [] });
5273
store = result.store;
5374

5475
// wait till the empty list is loaded
@@ -78,15 +99,7 @@ describe('SavedDiagramsList', function () {
7899
let store: DataModelingStore;
79100

80101
beforeEach(async function () {
81-
const result = renderSavedDiagramsList({
82-
loadAll: () =>
83-
Promise.resolve([
84-
{
85-
id: 'diagram-1',
86-
name: 'Diagram 1',
87-
} as MongoDBDataModelDescription,
88-
]),
89-
});
102+
const result = renderSavedDiagramsList();
90103
store = result.store;
91104

92105
// wait till the list is loaded
@@ -95,8 +108,12 @@ describe('SavedDiagramsList', function () {
95108
});
96109
});
97110

98-
it('shows the list of diagrams', function () {
99-
expect(screen.getByText('Diagram 1')).to.exist;
111+
it('shows the list of diagrams', async function () {
112+
await waitFor(() => {
113+
expect(screen.getByText('One')).to.exist;
114+
expect(screen.getByText('Two')).to.exist;
115+
expect(screen.getByText('Three')).to.exist;
116+
});
100117
});
101118

102119
it('allows to add another diagram', function () {
@@ -108,5 +125,35 @@ describe('SavedDiagramsList', function () {
108125
userEvent.click(createDiagramButton);
109126
expect(store.getState().generateDiagramWizard.inProgress).to.be.true;
110127
});
128+
129+
it('filters the list of diagrams', async function () {
130+
const searchInput = screen.getByPlaceholderText('Search');
131+
userEvent.type(searchInput, 'One');
132+
await waitFor(() => {
133+
expect(screen.queryByText('One')).to.exist;
134+
});
135+
136+
await waitFor(() => {
137+
expect(screen.queryByText('Two')).to.not.exist;
138+
expect(screen.queryByText('Three')).to.not.exist;
139+
});
140+
});
141+
142+
it('shows empty content when filter for a non-existent diagram', async function () {
143+
const searchInput = screen.getByPlaceholderText('Search');
144+
userEvent.type(searchInput, 'Hello');
145+
await waitFor(() => {
146+
expect(screen.queryByText('No results found.')).to.exist;
147+
expect(
148+
screen.queryByText("We can't find any diagram matching your search.")
149+
).to.exist;
150+
});
151+
152+
await waitFor(() => {
153+
expect(screen.queryByText('One')).to.not.exist;
154+
expect(screen.queryByText('Two')).to.not.exist;
155+
expect(screen.queryByText('Three')).to.not.exist;
156+
});
157+
});
111158
});
112159
});

0 commit comments

Comments
 (0)