Skip to content

Commit be5501a

Browse files
authored
feat(compass-collection): Document Count Screen - Mock Data Generator CLOUDP-333856 (#7381)
* document count screen * feat(compass-collection): Document Count Screen - Mock Data Generator CLOUDP-333856 * include updated json package * improve byte formatting and document count handling
1 parent 8969fc7 commit be5501a

File tree

9 files changed

+293
-6
lines changed

9 files changed

+293
-6
lines changed

package-lock.json

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

packages/compass-collection/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"mongodb-collection-model": "^5.35.2",
6969
"mongodb-ns": "^3.0.1",
7070
"mongodb-schema": "^12.6.3",
71+
"numeral": "^2.0.6",
7172
"react": "^17.0.2",
7273
"react-redux": "^8.1.3",
7374
"redux": "^4.2.1",

packages/compass-collection/src/components/mock-data-generator-modal/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ export const StepButtonLabelMap = {
77
[MockDataGeneratorStep.PREVIEW_DATA]: 'Generate Script',
88
[MockDataGeneratorStep.GENERATE_DATA]: 'Done',
99
} as const;
10+
11+
export const DEFAULT_DOCUMENT_COUNT = 1000;
12+
export const MAX_DOCUMENT_COUNT = 100000;
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import {
2+
Body,
3+
css,
4+
palette,
5+
spacing,
6+
TextInput,
7+
} from '@mongodb-js/compass-components';
8+
import React, { useMemo } from 'react';
9+
import { connect } from 'react-redux';
10+
import type { CollectionState } from '../../modules/collection-tab';
11+
import type { SchemaAnalysisState } from '../../schema-analysis-types';
12+
import numeral from 'numeral';
13+
import { DEFAULT_DOCUMENT_COUNT, MAX_DOCUMENT_COUNT } from './constants';
14+
15+
const BYTE_PRECISION_THRESHOLD = 1000;
16+
17+
const titleStyles = css({
18+
fontWeight: 600,
19+
});
20+
21+
const descriptionStyles = css({
22+
color: palette.gray.dark1,
23+
fontStyle: 'italic',
24+
});
25+
26+
const inputContainerStyles = css({
27+
display: 'flex',
28+
flexDirection: 'row',
29+
gap: spacing[600],
30+
marginTop: spacing[200],
31+
});
32+
33+
const estimatedDiskSizeStyles = css({
34+
fontSize: '13px',
35+
marginTop: spacing[100],
36+
});
37+
38+
const boldStyles = css({
39+
fontWeight: 600,
40+
});
41+
42+
const formatBytes = (bytes: number) => {
43+
const precision = bytes <= BYTE_PRECISION_THRESHOLD ? '0' : '0.0';
44+
return numeral(bytes).format(precision + 'b');
45+
};
46+
47+
type ErrorState =
48+
| {
49+
state: 'error';
50+
message: string;
51+
}
52+
| {
53+
state: 'none';
54+
};
55+
56+
interface OwnProps {
57+
documentCount: number;
58+
onDocumentCountChange: (documentCount: number) => void;
59+
}
60+
61+
interface Props extends OwnProps {
62+
schemaAnalysisState: SchemaAnalysisState;
63+
}
64+
65+
const DocumentCountScreen = ({
66+
documentCount,
67+
onDocumentCountChange,
68+
schemaAnalysisState,
69+
}: Props) => {
70+
const estimatedDiskSize = useMemo(
71+
() =>
72+
schemaAnalysisState.status === 'complete' &&
73+
schemaAnalysisState.schemaMetadata.avgDocumentSize
74+
? formatBytes(
75+
schemaAnalysisState.schemaMetadata.avgDocumentSize * documentCount
76+
)
77+
: 'Not available',
78+
[schemaAnalysisState, documentCount]
79+
);
80+
81+
const isOutOfRange = documentCount < 1 || documentCount > MAX_DOCUMENT_COUNT;
82+
83+
const errorState: ErrorState = useMemo(() => {
84+
if (isOutOfRange) {
85+
return {
86+
state: 'error',
87+
message: `Document count must be between 1 and ${MAX_DOCUMENT_COUNT}`,
88+
};
89+
}
90+
return {
91+
state: 'none',
92+
};
93+
}, [isOutOfRange]);
94+
95+
const handleDocumentCountChange = (
96+
event: React.ChangeEvent<HTMLInputElement>
97+
) => {
98+
const value = parseInt(event.target.value, 10);
99+
if (!isNaN(value)) {
100+
onDocumentCountChange(value);
101+
}
102+
};
103+
104+
return schemaAnalysisState.status === 'complete' ? (
105+
<div>
106+
<Body className={titleStyles}>
107+
Specify Number of Documents to Generate
108+
</Body>
109+
<Body className={descriptionStyles}>
110+
Indicate the amount of documents you want to generate below.
111+
<br />
112+
Note: We have defaulted to {DEFAULT_DOCUMENT_COUNT}.
113+
</Body>
114+
<div className={inputContainerStyles}>
115+
<TextInput
116+
label="Documents to generate in current collection"
117+
type="number"
118+
value={documentCount.toString()}
119+
onChange={handleDocumentCountChange}
120+
min={1}
121+
max={MAX_DOCUMENT_COUNT}
122+
state={errorState.state}
123+
errorMessage={
124+
errorState.state === 'error' ? errorState.message : undefined
125+
}
126+
/>
127+
<div>
128+
<Body className={boldStyles}>Estimated Disk Size</Body>
129+
<Body className={estimatedDiskSizeStyles}>{estimatedDiskSize}</Body>
130+
</div>
131+
</div>
132+
</div>
133+
) : (
134+
// Not reachable since schema analysis must be finished before the modal can be opened
135+
<div>We are analyzing your collection.</div>
136+
);
137+
};
138+
139+
const mapStateToProps = (state: CollectionState) => ({
140+
schemaAnalysisState: state.schemaAnalysis,
141+
});
142+
143+
const ConnectedDocumentCountScreen = connect(
144+
mapStateToProps,
145+
{}
146+
)(DocumentCountScreen);
147+
148+
export default ConnectedDocumentCountScreen;

packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ const defaultSchemaAnalysisState: SchemaAnalysisState = {
3232
},
3333
arrayLengthMap: {},
3434
sampleDocument: { name: 'John' },
35-
schemaMetadata: { maxNestingDepth: 1, validationRules: null },
35+
schemaMetadata: {
36+
maxNestingDepth: 1,
37+
validationRules: null,
38+
avgDocumentSize: undefined,
39+
},
3640
};
3741

3842
describe('MockDataGeneratorModal', () => {
@@ -630,6 +634,85 @@ describe('MockDataGeneratorModal', () => {
630634
});
631635
});
632636

637+
describe('on the document count step', () => {
638+
it('displays the correct step title and description', async () => {
639+
await renderModal({ currentStep: MockDataGeneratorStep.DOCUMENT_COUNT });
640+
641+
expect(screen.getByText('Specify Number of Documents to Generate')).to
642+
.exist;
643+
644+
expect(
645+
screen.getByText(
646+
/Indicate the amount of documents you want to generate below./
647+
)
648+
).to.exist;
649+
expect(screen.getByText(/Note: We have defaulted to 1000./)).to.exist;
650+
});
651+
652+
it('displays the default document count when the user does not enter a document count', async () => {
653+
await renderModal({ currentStep: MockDataGeneratorStep.DOCUMENT_COUNT });
654+
655+
expect(
656+
screen.getByLabelText('Documents to generate in current collection')
657+
).to.have.value('1000');
658+
});
659+
660+
it('disables the Next button and shows an error message when the document count is greater than 100000', async () => {
661+
await renderModal({ currentStep: MockDataGeneratorStep.DOCUMENT_COUNT });
662+
663+
userEvent.type(
664+
screen.getByLabelText('Documents to generate in current collection'),
665+
'100001'
666+
);
667+
668+
expect(screen.getByText('Document count must be between 1 and 100000')).to
669+
.exist;
670+
expect(
671+
screen.getByTestId('next-step-button').getAttribute('aria-disabled')
672+
).to.equal('true');
673+
});
674+
675+
it('displays "Not available" when the avgDocumentSize is undefined', async () => {
676+
await renderModal({
677+
currentStep: MockDataGeneratorStep.DOCUMENT_COUNT,
678+
schemaAnalysis: {
679+
...defaultSchemaAnalysisState,
680+
schemaMetadata: {
681+
...defaultSchemaAnalysisState.schemaMetadata,
682+
avgDocumentSize: undefined,
683+
},
684+
},
685+
});
686+
687+
expect(screen.getByText('Estimated Disk Size')).to.exist;
688+
expect(screen.getByText('Not available')).to.exist;
689+
});
690+
691+
it('displays the correct estimated disk size when a valid document count is entered', async () => {
692+
await renderModal({
693+
currentStep: MockDataGeneratorStep.DOCUMENT_COUNT,
694+
schemaAnalysis: {
695+
...defaultSchemaAnalysisState,
696+
schemaMetadata: {
697+
...defaultSchemaAnalysisState.schemaMetadata,
698+
avgDocumentSize: 100, // 100 bytes
699+
},
700+
},
701+
});
702+
703+
expect(screen.getByText('Estimated Disk Size')).to.exist;
704+
const documentCountInput = screen.getByLabelText(
705+
'Documents to generate in current collection'
706+
);
707+
userEvent.clear(documentCountInput);
708+
userEvent.type(documentCountInput, '1000');
709+
expect(screen.getByText('100.0KB')).to.exist;
710+
userEvent.clear(documentCountInput);
711+
userEvent.type(documentCountInput, '2000');
712+
expect(screen.getByText('200.0KB')).to.exist;
713+
});
714+
});
715+
633716
describe('on the generate data step', () => {
634717
it('enables the Back button', async () => {
635718
await renderModal({

packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import {
1414
} from '@mongodb-js/compass-components';
1515

1616
import { type MockDataGeneratorState, MockDataGeneratorStep } from './types';
17-
import { StepButtonLabelMap } from './constants';
17+
import {
18+
DEFAULT_DOCUMENT_COUNT,
19+
MAX_DOCUMENT_COUNT,
20+
StepButtonLabelMap,
21+
} from './constants';
1822
import type { CollectionState } from '../../modules/collection-tab';
1923
import {
2024
mockDataGeneratorModalClosed,
@@ -26,6 +30,7 @@ import {
2630
import RawSchemaConfirmationScreen from './raw-schema-confirmation-screen';
2731
import FakerSchemaEditorScreen from './faker-schema-editor-screen';
2832
import ScriptScreen from './script-screen';
33+
import DocumentCountScreen from './document-count-screen';
2934

3035
const footerStyles = css`
3136
flex-direction: row;
@@ -66,6 +71,9 @@ const MockDataGeneratorModal = ({
6671
}: Props) => {
6772
const [isSchemaConfirmed, setIsSchemaConfirmed] =
6873
React.useState<boolean>(false);
74+
const [documentCount, setDocumentCount] = React.useState<number>(
75+
DEFAULT_DOCUMENT_COUNT
76+
);
6977

7078
const modalBodyContent = useMemo(() => {
7179
switch (currentStep) {
@@ -80,16 +88,32 @@ const MockDataGeneratorModal = ({
8088
/>
8189
);
8290
case MockDataGeneratorStep.DOCUMENT_COUNT:
83-
return <></>; // TODO: CLOUDP-333856
91+
return (
92+
<DocumentCountScreen
93+
documentCount={documentCount}
94+
onDocumentCountChange={setDocumentCount}
95+
/>
96+
);
8497
case MockDataGeneratorStep.PREVIEW_DATA:
8598
return <></>; // TODO: CLOUDP-333857
8699
case MockDataGeneratorStep.GENERATE_DATA:
87100
return <ScriptScreen />;
88101
}
89-
}, [currentStep, fakerSchemaGenerationState, isSchemaConfirmed]);
102+
}, [
103+
currentStep,
104+
fakerSchemaGenerationState,
105+
isSchemaConfirmed,
106+
documentCount,
107+
setDocumentCount,
108+
]);
90109

91110
const isNextButtonDisabled =
92-
currentStep === MockDataGeneratorStep.SCHEMA_EDITOR && !isSchemaConfirmed;
111+
(currentStep === MockDataGeneratorStep.SCHEMA_EDITOR &&
112+
!isSchemaConfirmed) ||
113+
(currentStep === MockDataGeneratorStep.DOCUMENT_COUNT &&
114+
documentCount < 1) ||
115+
(currentStep === MockDataGeneratorStep.DOCUMENT_COUNT &&
116+
documentCount > MAX_DOCUMENT_COUNT);
93117

94118
const handleNextClick = () => {
95119
if (currentStep === MockDataGeneratorStep.GENERATE_DATA) {

0 commit comments

Comments
 (0)