Skip to content

Commit 0268356

Browse files
authored
feat: diagram creation progress COMPASS-9521 (#7638)
1 parent f766973 commit 0268356

File tree

13 files changed

+445
-66
lines changed

13 files changed

+445
-66
lines changed

package-lock.json

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"@leafygreen-ui/polymorphic": "^3.1.0",
6565
"@leafygreen-ui/popover": "^14.3.1",
6666
"@leafygreen-ui/portal": "^7.1.0",
67+
"@leafygreen-ui/progress-bar": "^1.0.7",
6768
"@leafygreen-ui/radio-box-group": "^15.0.10",
6869
"@leafygreen-ui/radio-group": "^13.0.10",
6970
"@leafygreen-ui/search-input": "^6.1.1",

packages/compass-components/src/components/leafygreen.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { Menu, MenuSeparator, MenuItem } from '@leafygreen-ui/menu';
2424
export type { MenuItemProps } from '@leafygreen-ui/menu';
2525
import { InfoSprinkle } from '@leafygreen-ui/info-sprinkle';
26+
import { ProgressBar } from '@leafygreen-ui/progress-bar';
2627

2728
// If a leafygreen Menu (and therefore MenuItems) makes its way into a <form>,
2829
// clicking on a menu item will submit that form. This is because it uses a button
@@ -191,6 +192,7 @@ export {
191192
Combobox,
192193
ComboboxGroup,
193194
ComboboxOption,
195+
ProgressBar,
194196
};
195197

196198
export * as Avatar from '@leafygreen-ui/avatar';

packages/compass-components/src/components/loader.tsx

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { palette } from '@leafygreen-ui/palette';
33
import { spacing } from '@leafygreen-ui/tokens';
44
import { css, cx, keyframes } from '@leafygreen-ui/emotion';
55
import { useDarkMode } from '../hooks/use-theme';
6-
import { Subtitle, Button } from './leafygreen';
6+
import { Subtitle, Button, ProgressBar } from './leafygreen';
7+
import type { ProgressBarProps } from '@leafygreen-ui/progress-bar';
78

8-
const containerStyles = css({
9+
const loaderContainerStyles = css({
910
display: 'flex',
1011
gap: spacing[200],
1112
flexDirection: 'column',
@@ -14,6 +15,18 @@ const containerStyles = css({
1415
maxWidth: spacing[1600] * 8,
1516
});
1617

18+
const progressContainerStyles = css({
19+
width: '100%',
20+
height: '100%',
21+
display: 'flex',
22+
flexDirection: 'column',
23+
justifyContent: 'center',
24+
alignItems: 'center',
25+
maxWidth: spacing[1600] * 8,
26+
gap: spacing[500],
27+
margin: '0 auto',
28+
});
29+
1730
const textStyles = css({
1831
color: palette.green.dark2,
1932
textAlign: 'center',
@@ -36,11 +49,16 @@ type SpinLoaderWithLabelProps = Omit<SpinLoaderProps, 'size' | 'title'> & {
3649
['data-testid']?: string;
3750
};
3851

39-
type CancelLoaderProps = Omit<SpinLoaderWithLabelProps, 'children'> & {
52+
type CancelActionProps = {
4053
onCancel(): void;
41-
cancelText: string;
54+
cancelText?: string;
4255
};
4356

57+
type CancelLoaderProps = Omit<SpinLoaderWithLabelProps, 'children'> &
58+
CancelActionProps;
59+
60+
type ProgressLoaderWithCancelProps = CancelActionProps & ProgressBarProps;
61+
4462
const shellLoaderSpin = keyframes`
4563
0% { transform: rotate(0deg); }
4664
100% { transform: rotate(360deg); }
@@ -94,7 +112,10 @@ function SpinLoaderWithLabel({
94112
const darkMode = useDarkMode(_darkMode);
95113

96114
return (
97-
<div className={cx(containerStyles, className)} data-testid={dataTestId}>
115+
<div
116+
className={cx(loaderContainerStyles, className)}
117+
data-testid={dataTestId}
118+
>
98119
<SpinLoader
99120
size={spacing[600]}
100121
darkMode={darkMode}
@@ -126,4 +147,29 @@ function CancelLoader({
126147
);
127148
}
128149

129-
export { SpinLoaderWithLabel, SpinLoader, CancelLoader };
150+
function ProgressLoaderWithCancel({
151+
cancelText = 'Cancel',
152+
className,
153+
onCancel,
154+
...props
155+
}: ProgressLoaderWithCancelProps): React.ReactElement {
156+
return (
157+
<div className={cx(progressContainerStyles, className)}>
158+
<ProgressBar {...props} />
159+
<Button
160+
variant="primaryOutline"
161+
onClick={onCancel}
162+
data-testid={`${props['data-testid'] ?? 'spin-loader'}-button`}
163+
>
164+
{cancelText}
165+
</Button>
166+
</div>
167+
);
168+
}
169+
170+
export {
171+
SpinLoaderWithLabel,
172+
SpinLoader,
173+
CancelLoader,
174+
ProgressLoaderWithCancel,
175+
};

packages/compass-components/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export {
4040
SpinLoader,
4141
SpinLoaderWithLabel,
4242
CancelLoader,
43+
ProgressLoaderWithCancel,
4344
} from './components/loader';
4445
import { ResizeHandle, ResizeDirection } from './components/resize-handle';
4546
import { Accordion } from './components/accordion';
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import React from 'react';
2+
import { expect } from 'chai';
3+
import { screen, waitFor } from '@mongodb-js/testing-library-compass';
4+
import AnalysisProgressStatus from './analysis-progress-status';
5+
import { renderWithStore, testConnections } from '../../test/setup-store';
6+
import {
7+
AnalysisProcessActionTypes,
8+
startAnalysis,
9+
} from '../store/analysis-process';
10+
import { createSandboxFromDefaultPreferences } from 'compass-preferences-model';
11+
12+
describe('AnalysisProgressStatus', () => {
13+
async function renderAnalysisProgressStatus({
14+
automaticallyInferRelations = false,
15+
} = {}) {
16+
const preferences = await createSandboxFromDefaultPreferences();
17+
const { store } = renderWithStore(<AnalysisProgressStatus />, {
18+
services: {
19+
preferences,
20+
},
21+
});
22+
void store.dispatch(
23+
startAnalysis(
24+
'My Diagram',
25+
testConnections[0].id,
26+
'testDB',
27+
['coll1', 'coll2', 'coll3'],
28+
{ automaticallyInferRelations }
29+
)
30+
);
31+
return store;
32+
}
33+
34+
it('Allows cancellation', async () => {
35+
const store = await renderAnalysisProgressStatus();
36+
expect(screen.getByText('Sampling collections…')).to.be.visible;
37+
expect(screen.getByText('Cancel')).to.be.visible;
38+
screen.getByText('Cancel').click();
39+
await waitFor(() => {
40+
expect(store.getState().analysisProgress.step).to.equal('IDLE');
41+
expect(store.getState().diagram).to.be.null;
42+
});
43+
});
44+
45+
describe('Keeps showing progress along the way', () => {
46+
it('Without relationship inferring', async () => {
47+
const store = await renderAnalysisProgressStatus({
48+
automaticallyInferRelations: false,
49+
});
50+
expect(screen.getByText('Sampling collections…')).to.be.visible;
51+
expect(screen.getByText('0/3')).to.be.visible;
52+
53+
// 2 out of 3 samples fetched, 1 analyzed
54+
store.dispatch({
55+
type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED,
56+
});
57+
store.dispatch({
58+
type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED,
59+
});
60+
store.dispatch({
61+
type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED,
62+
});
63+
64+
expect(screen.getByText('Sampling collections…')).to.be.visible;
65+
expect(screen.getByText('2/3')).to.be.visible;
66+
67+
// Last sample fetched
68+
store.dispatch({
69+
type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED,
70+
});
71+
72+
expect(screen.getByText('Analyzing collection schemas…')).to.be.visible;
73+
expect(screen.getByText('1/3')).to.be.visible;
74+
75+
// Finish analyzing
76+
store.dispatch({
77+
type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED,
78+
});
79+
store.dispatch({
80+
type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED,
81+
});
82+
83+
expect(screen.queryByText('Inferring relationships between collections…'))
84+
.not.to.exist;
85+
expect(screen.getByText('Preparing diagram…')).to.be.visible;
86+
});
87+
88+
it('With relationship inferring', async () => {
89+
const store = await renderAnalysisProgressStatus({
90+
automaticallyInferRelations: true,
91+
});
92+
expect(screen.getByText('Sampling collections…')).to.be.visible;
93+
expect(screen.getByText('0/3')).to.be.visible;
94+
95+
// Fetch and analyze all samples
96+
store.dispatch({
97+
type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED,
98+
});
99+
store.dispatch({
100+
type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED,
101+
});
102+
store.dispatch({
103+
type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED,
104+
});
105+
store.dispatch({
106+
type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED,
107+
});
108+
store.dispatch({
109+
type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED,
110+
});
111+
store.dispatch({
112+
type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED,
113+
});
114+
115+
expect(screen.getByText('Inferring relationships between collections…'))
116+
.to.be.visible;
117+
expect(screen.queryByText('0/3')).not.to.exist;
118+
119+
// Infer some relationships
120+
store.dispatch({
121+
type: AnalysisProcessActionTypes.NAMESPACE_RELATIONS_INFERRED,
122+
});
123+
store.dispatch({
124+
type: AnalysisProcessActionTypes.NAMESPACE_RELATIONS_INFERRED,
125+
});
126+
127+
expect(screen.getByText('Inferring relationships between collections…'))
128+
.to.be.visible;
129+
expect(screen.queryByText('2/3')).not.to.exist;
130+
131+
// Finish inferring
132+
store.dispatch({
133+
type: AnalysisProcessActionTypes.NAMESPACE_RELATIONS_INFERRED,
134+
});
135+
136+
expect(screen.getByText('Preparing diagram…')).to.be.visible;
137+
});
138+
});
139+
});

0 commit comments

Comments
 (0)