Skip to content

Commit f623c03

Browse files
committed
feat: validation rules generation COMPASS-8859
1 parent 721f272 commit f623c03

File tree

4 files changed

+191
-21
lines changed

4 files changed

+191
-21
lines changed

packages/compass-schema-validation/src/components/validation-states.tsx

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
Banner,
44
Button,
55
ButtonVariant,
6+
CancelLoader,
67
EmptyContent,
8+
ErrorSummary,
79
Link,
810
WarningSummary,
911
css,
@@ -16,14 +18,27 @@ import type { RootState } from '../modules';
1618
import ValidationEditor from './validation-editor';
1719
import { SampleDocuments } from './sample-documents';
1820
import { ZeroGraphic } from './zero-graphic';
21+
import {
22+
clearRulesGenerationError,
23+
generateValidationRules,
24+
} from '../modules/validation';
1925

20-
const validationStatesStyles = css({ padding: spacing[3] });
26+
const validationStatesStyles = css({
27+
padding: spacing[400],
28+
height: '100%',
29+
});
2130
const contentContainerStyles = css({ height: '100%' });
2231
const zeroStateButtonsStyles = css({
2332
display: 'flex',
2433
gap: spacing[400],
2534
});
2635

36+
const loaderStyles = css({
37+
height: '100%',
38+
display: 'flex',
39+
justifyContent: 'center',
40+
});
41+
2742
/**
2843
* Warnings for the banner.
2944
*/
@@ -50,8 +65,12 @@ const DOC_UPGRADE_REVISION =
5065

5166
type ValidationStatesProps = {
5267
isZeroState: boolean;
68+
isRulesGenerationInProgress?: boolean;
69+
rulesGenerationError?: string;
5370
isLoaded: boolean;
5471
changeZeroState: (value: boolean) => void;
72+
generateValidationRules: () => void;
73+
clearRulesGenerationError: () => void;
5574
editMode: {
5675
collectionTimeSeries?: boolean;
5776
collectionReadOnly?: boolean;
@@ -108,10 +127,29 @@ function ValidationBanners({
108127
return null;
109128
}
110129

130+
const GeneratingScreen: React.FunctionComponent<{
131+
onCancelClicked: () => void;
132+
}> = ({ onCancelClicked }) => {
133+
return (
134+
<div className={loaderStyles}>
135+
<CancelLoader
136+
data-testid="generating-rules"
137+
progressText="Generating rules"
138+
cancelText="Stop"
139+
onCancel={onCancelClicked}
140+
/>
141+
</div>
142+
);
143+
};
144+
111145
export function ValidationStates({
112146
isZeroState,
147+
isRulesGenerationInProgress,
148+
rulesGenerationError,
113149
isLoaded,
114150
changeZeroState,
151+
generateValidationRules,
152+
clearRulesGenerationError,
115153
editMode,
116154
}: ValidationStatesProps) {
117155
const { readOnly, enableExportSchema } = usePreferences([
@@ -131,10 +169,17 @@ export function ValidationStates({
131169
className={validationStatesStyles}
132170
data-testid="schema-validation-states"
133171
>
172+
{rulesGenerationError && (
173+
<ErrorSummary
174+
errors={rulesGenerationError}
175+
dismissible={true}
176+
onDismiss={clearRulesGenerationError}
177+
/>
178+
)}
134179
<ValidationBanners editMode={editMode} />
135180
{isLoaded && (
136181
<>
137-
{isZeroState ? (
182+
{isZeroState && !isRulesGenerationInProgress && (
138183
<EmptyContent
139184
icon={ZeroGraphic}
140185
title="Create validation rules"
@@ -145,7 +190,7 @@ export function ValidationStates({
145190
<Button
146191
data-testid="generate-rules-button"
147192
disabled={!isEditable}
148-
onClick={() => changeZeroState(false)}
193+
onClick={generateValidationRules}
149194
variant={ButtonVariant.Primary}
150195
size="small"
151196
>
@@ -169,7 +214,11 @@ export function ValidationStates({
169214
</Link>
170215
}
171216
/>
172-
) : (
217+
)}
218+
{isZeroState && isRulesGenerationInProgress && (
219+
<GeneratingScreen onCancelClicked={() => ({})} />
220+
)}
221+
{!isZeroState && (
173222
<div className={contentContainerStyles}>
174223
<ValidationEditor isEditable={isEditable} />
175224
<SampleDocuments />
@@ -192,11 +241,15 @@ const mapStateToProps = (state: RootState) => ({
192241
isZeroState: state.isZeroState,
193242
isLoaded: state.isLoaded,
194243
editMode: state.editMode,
244+
isRulesGenerationInProgress: state.validation.isRulesGenerationInProgress,
245+
rulesGenerationError: state.validation.rulesGenerationError,
195246
});
196247

197248
/**
198249
* Connect the redux store to the component (dispatch).
199250
*/
200251
export default connect(mapStateToProps, {
201252
changeZeroState,
253+
generateValidationRules,
254+
clearRulesGenerationError,
202255
})(ValidationStates);

packages/compass-schema-validation/src/modules/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export type SchemaValidationExtraArgs = {
7777
workspaces: WorkspacesService;
7878
logger: Logger;
7979
track: TrackFunction;
80+
rulesGenerationAbortControllerRef: { current?: AbortController };
8081
};
8182

8283
export type SchemaValidationThunkAction<

packages/compass-schema-validation/src/modules/validation.ts

Lines changed: 124 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import { zeroStateChanged } from './zero-state';
77
import { isLoadedChanged } from './is-loaded';
88
import { isEqual, pick } from 'lodash';
99
import type { ThunkDispatch } from 'redux-thunk';
10-
import { disableEditRules } from './edit-mode';
10+
import { disableEditRules, enableEditRules } from './edit-mode';
1111
import { analyzeSchema } from '@mongodb-js/compass-schema-analysis';
1212

1313
export type ValidationServerAction = 'error' | 'warn';
1414
export type ValidationLevel = 'off' | 'moderate' | 'strict';
1515

16+
const SAMPLE_SIZE = 1000;
17+
1618
/**
1719
* The module action prefix.
1820
*/
@@ -93,8 +95,20 @@ interface RulesGenerationStartedAction {
9395
export const RULES_GENERATION_FAILED =
9496
`${PREFIX}/RULES_GENERATION_FAILED` as const;
9597
interface RulesGenerationFailedAction {
96-
type: typeof RULES_GENERATION_STARTED;
97-
syntaxError: null | { message: string };
98+
type: typeof RULES_GENERATION_FAILED;
99+
message: string;
100+
}
101+
102+
export const RULES_GENERATION_CLEAR_ERROR =
103+
`${PREFIX}/RULES_GENERATION_CLEAR_ERROR` as const;
104+
interface RulesGenerationClearErrorAction {
105+
type: typeof RULES_GENERATION_CLEAR_ERROR;
106+
}
107+
108+
export const RULES_GENERATION_FINISHED =
109+
`${PREFIX}/RULES_GENERATION_FINISHED` as const;
110+
interface RulesGenerationFinishedAction {
111+
type: typeof RULES_GENERATION_FINISHED;
98112
}
99113

100114
export type ValidationAction =
@@ -104,7 +118,11 @@ export type ValidationAction =
104118
| ValidationFetchedAction
105119
| ValidationActionChangedAction
106120
| ValidationLevelChangedAction
107-
| SyntaxErrorOccurredAction;
121+
| SyntaxErrorOccurredAction
122+
| RulesGenerationStartedAction
123+
| RulesGenerationFinishedAction
124+
| RulesGenerationFailedAction
125+
| RulesGenerationClearErrorAction;
108126

109127
export interface Validation {
110128
validator: string;
@@ -124,8 +142,8 @@ export interface ValidationState extends Validation {
124142
syntaxError: null | { message: string };
125143
error: null | { message: string };
126144
prevValidation?: Validation;
127-
rulesGenerationStatus?: 'in-progress' | 'failed';
128-
rulesGenerationErrorMessage?: string;
145+
isRulesGenerationInProgress?: boolean;
146+
rulesGenerationError?: string;
129147
}
130148

131149
/**
@@ -205,6 +223,34 @@ const setSyntaxError = (
205223
syntaxError: action.syntaxError,
206224
});
207225

226+
const startRulesGeneration = (state: ValidationState): ValidationState => ({
227+
...state,
228+
isRulesGenerationInProgress: true,
229+
rulesGenerationError: undefined,
230+
});
231+
232+
const finishRulesGeneration = (state: ValidationState): ValidationState => ({
233+
...state,
234+
isRulesGenerationInProgress: undefined,
235+
rulesGenerationError: undefined,
236+
});
237+
238+
const markRulesGenerationFailure = (
239+
state: ValidationState,
240+
action: RulesGenerationFailedAction
241+
): ValidationState => ({
242+
...state,
243+
isRulesGenerationInProgress: undefined,
244+
rulesGenerationError: action.message,
245+
});
246+
247+
const unsetRulesGenerationError = (
248+
state: ValidationState
249+
): ValidationState => ({
250+
...state,
251+
rulesGenerationError: undefined,
252+
});
253+
208254
/**
209255
* Set validation.
210256
*/
@@ -307,6 +353,10 @@ const MAPPINGS: {
307353
[VALIDATION_ACTION_CHANGED]: changeValidationAction,
308354
[VALIDATION_LEVEL_CHANGED]: changeValidationLevel,
309355
[SYNTAX_ERROR_OCCURRED]: setSyntaxError,
356+
[RULES_GENERATION_STARTED]: startRulesGeneration,
357+
[RULES_GENERATION_FINISHED]: finishRulesGeneration,
358+
[RULES_GENERATION_FAILED]: markRulesGenerationFailure,
359+
[RULES_GENERATION_CLEAR_ERROR]: unsetRulesGenerationError,
310360
};
311361

312362
/**
@@ -397,6 +447,11 @@ export const syntaxErrorOccurred = (
397447
syntaxError,
398448
});
399449

450+
export const clearRulesGenerationError =
451+
(): RulesGenerationClearErrorAction => ({
452+
type: RULES_GENERATION_CLEAR_ERROR,
453+
});
454+
400455
export const fetchValidation = (namespace: {
401456
database: string;
402457
collection: string;
@@ -553,39 +608,92 @@ export const activateValidation = (): SchemaValidationThunkAction<void> => {
553608
};
554609
};
555610

611+
export const stopRulesGeneration = (): SchemaValidationThunkAction<void> => {
612+
return (
613+
dispatch,
614+
getState,
615+
{ rulesGenerationAbortControllerRef, connectionInfoRef, track }
616+
) => {
617+
if (!rulesGenerationAbortControllerRef.current) return;
618+
// const analysisTime =
619+
// Date.now() - (getState().schemaAnalysis.analysisStartTime ?? 0);
620+
// track(
621+
// 'Schema Analysis Cancelled',
622+
// {
623+
// analysis_time_ms: analysisTime,
624+
// with_filter: Object.entries(query.filter ?? {}).length > 0,
625+
// },
626+
// connectionInfoRef.current
627+
// );
628+
629+
rulesGenerationAbortControllerRef.current?.abort('Analysis cancelled');
630+
};
631+
};
632+
556633
/**
557634
* Get $jsonSchema from schema analysis
558635
* @returns
559636
*/
560637
export const generateValidationRules = (): SchemaValidationThunkAction<
561638
Promise<void>
562639
> => {
563-
return async (dispatch, getState, { dataService, logger, preferences }) => {
640+
return async (
641+
dispatch,
642+
getState,
643+
{ dataService, logger, preferences, rulesGenerationAbortControllerRef }
644+
) => {
564645
dispatch({ type: RULES_GENERATION_STARTED });
646+
console.log('START');
647+
648+
rulesGenerationAbortControllerRef.current = new AbortController();
649+
const abortSignal = rulesGenerationAbortControllerRef.current.signal;
650+
651+
const { namespace } = getState();
652+
const { maxTimeMS } = preferences.getPreferences();
565653

566654
try {
567-
///// TODO
568-
const samplingOptions = {};
569-
const driverOptions = {};
570-
const abortSignal = new AbortSignal();
571-
const namespace = '';
655+
const samplingOptions = {
656+
query: {},
657+
size: SAMPLE_SIZE,
658+
fields: undefined,
659+
};
660+
const driverOptions = {
661+
maxTimeMS,
662+
};
663+
console.log('ANALYZING');
572664
const schemaAccessor = await analyzeSchema(
573665
dataService,
574666
abortSignal,
575-
namespace,
667+
namespace.toString(),
576668
samplingOptions,
577669
driverOptions,
578670
logger,
579671
preferences
580672
);
673+
if (abortSignal?.aborted) {
674+
throw new Error(abortSignal?.reason || new Error('Operation aborted'));
675+
}
581676

582-
const jsonSchema = await schemaAccessor?.getMongoDBJsonSchema();
677+
console.log('CONVERTING');
678+
const jsonSchema = await schemaAccessor?.getMongoDBJsonSchema({
679+
signal: abortSignal,
680+
});
681+
if (abortSignal?.aborted) {
682+
throw new Error(abortSignal?.reason || new Error('Operation aborted'));
683+
}
684+
console.log('STRINGIFYING');
583685
const validator = JSON.stringify(jsonSchema, undefined, 2);
584-
686+
console.log('DONE');
585687
dispatch(validationLevelChanged('moderate'));
586688
dispatch(validatorChanged(validator));
689+
dispatch(enableEditRules());
690+
dispatch({ type: RULES_GENERATION_FINISHED });
691+
dispatch(zeroStateChanged(false));
587692
} catch (error) {
588-
dispatch({ type: RULES_GENERATION_FAILED });
693+
dispatch({
694+
type: RULES_GENERATION_FAILED,
695+
message: `Rules generation failed: ${(error as Error).message}`,
696+
});
589697
}
590698
};
591699
};

packages/compass-schema-validation/src/stores/store.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,21 @@ export function configureStore(
4545
| 'connectionInfoRef'
4646
>
4747
) {
48+
const rulesGenerationAbortControllerRef = {
49+
current: undefined,
50+
};
4851
return createStore(
4952
reducer,
5053
{
5154
...INITIAL_STATE,
5255
...state,
5356
},
54-
applyMiddleware(thunk.withExtraArgument(services))
57+
applyMiddleware(
58+
thunk.withExtraArgument({
59+
...services,
60+
rulesGenerationAbortControllerRef,
61+
})
62+
)
5563
);
5664
}
5765

0 commit comments

Comments
 (0)