Skip to content

Commit a48c7fa

Browse files
committed
move rulesGeneration into separate module + align errors
1 parent 4d326f7 commit a48c7fa

File tree

5 files changed

+290
-162
lines changed

5 files changed

+290
-162
lines changed

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

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import {
33
Banner,
4+
BannerVariant,
45
Button,
56
ButtonVariant,
67
CancelLoader,
@@ -22,7 +23,9 @@ import {
2223
clearRulesGenerationError,
2324
generateValidationRules,
2425
stopRulesGeneration,
25-
} from '../modules/validation';
26+
type RulesGenerationError,
27+
} from '../modules/rules-generation';
28+
import { DISTINCT_FIELDS_ABORT_THRESHOLD } from '@mongodb-js/compass-schema';
2629

2730
const validationStatesStyles = css({
2831
padding: spacing[400],
@@ -67,7 +70,7 @@ const DOC_UPGRADE_REVISION =
6770
type ValidationStatesProps = {
6871
isZeroState: boolean;
6972
isRulesGenerationInProgress?: boolean;
70-
rulesGenerationError?: string;
73+
rulesGenerationError?: RulesGenerationError;
7174
isLoaded: boolean;
7275
changeZeroState: (value: boolean) => void;
7376
generateValidationRules: () => void;
@@ -144,6 +147,51 @@ const GeneratingScreen: React.FunctionComponent<{
144147
);
145148
};
146149

150+
const RulesGenerationErrorBanner: React.FunctionComponent<{
151+
error: RulesGenerationError;
152+
onDismissError: () => void;
153+
}> = ({ error, onDismissError }) => {
154+
if (error?.errorType === 'timeout') {
155+
return (
156+
<WarningSummary
157+
data-testid="rules-generation-timeout-message"
158+
warnings={[
159+
'Operation exceeded time limit. Please try increasing the maxTimeMS for the query in the filter options.',
160+
]}
161+
dismissible={true}
162+
onClose={onDismissError}
163+
/>
164+
);
165+
}
166+
if (error?.errorType === 'highComplexity') {
167+
return (
168+
<Banner
169+
variant={BannerVariant.Danger}
170+
data-testid="rules-generation-complexity-abort-message"
171+
dismissible={true}
172+
onClose={onDismissError}
173+
>
174+
The rules generation was aborted because the number of fields exceeds{' '}
175+
{DISTINCT_FIELDS_ABORT_THRESHOLD}. Consider breaking up your data into
176+
more collections with smaller documents, and using references to
177+
consolidate the data you need.&nbsp;
178+
<Link href="https://www.mongodb.com/docs/manual/data-modeling/design-antipatterns/bloated-documents/">
179+
Learn more
180+
</Link>
181+
</Banner>
182+
);
183+
}
184+
185+
return (
186+
<ErrorSummary
187+
data-testid="rules-generation-error-message"
188+
errors={[`Error occured during rules generation: ${error.errorMessage}`]}
189+
dismissible={true}
190+
onClose={onDismissError}
191+
/>
192+
);
193+
};
194+
147195
export function ValidationStates({
148196
isZeroState,
149197
isRulesGenerationInProgress,
@@ -173,10 +221,9 @@ export function ValidationStates({
173221
data-testid="schema-validation-states"
174222
>
175223
{rulesGenerationError && (
176-
<ErrorSummary
177-
errors={rulesGenerationError}
178-
dismissible={true}
179-
onClose={clearRulesGenerationError}
224+
<RulesGenerationErrorBanner
225+
error={rulesGenerationError}
226+
onDismissError={clearRulesGenerationError}
180227
/>
181228
)}
182229
<ValidationBanners editMode={editMode} />
@@ -248,8 +295,8 @@ const mapStateToProps = (state: RootState) => ({
248295
isZeroState: state.isZeroState,
249296
isLoaded: state.isLoaded,
250297
editMode: state.editMode,
251-
isRulesGenerationInProgress: state.validation.isRulesGenerationInProgress,
252-
rulesGenerationError: state.validation.rulesGenerationError,
298+
isRulesGenerationInProgress: state.rulesGeneration.isInProgress,
299+
rulesGenerationError: state.rulesGeneration.error,
253300
});
254301

255302
/**

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ import type AppRegistry from 'hadron-app-registry';
3131
import type { Logger } from '@mongodb-js/compass-logging/provider';
3232
import type { TrackFunction } from '@mongodb-js/compass-telemetry';
3333
import { type WorkspacesService } from '@mongodb-js/compass-workspaces/provider';
34+
import type { RulesGenerationState } from './rules-generation';
35+
import {
36+
INITIAL_STATE as RULES_GENERATION_STATE,
37+
rulesGenerationReducer,
38+
} from './rules-generation';
3439

3540
/**
3641
* Reset action constant.
@@ -44,6 +49,7 @@ export interface RootState {
4449
namespace: NamespaceState;
4550
serverVersion: ServerVersionState;
4651
validation: ValidationState;
52+
rulesGeneration: RulesGenerationState;
4753
sampleDocuments: SampleDocumentState;
4854
isZeroState: IsZeroStateState;
4955
isLoaded: IsLoadedState;
@@ -92,6 +98,7 @@ export const INITIAL_STATE: RootState = {
9298
namespace: NS_INITIAL_STATE,
9399
serverVersion: SV_INITIAL_STATE,
94100
validation: VALIDATION_STATE,
101+
rulesGeneration: RULES_GENERATION_STATE,
95102
sampleDocuments: SAMPLE_DOCUMENTS_STATE,
96103
isZeroState: IS_ZERO_STATE,
97104
isLoaded: IS_LOADED_STATE,
@@ -101,14 +108,15 @@ export const INITIAL_STATE: RootState = {
101108
/**
102109
* The reducer.
103110
*/
104-
const appReducer = combineReducers<RootState, RootAction>({
111+
const appReducer = combineReducers<RootState, AnyAction>({
105112
namespace,
106113
serverVersion,
107114
validation,
108115
sampleDocuments,
109116
isZeroState,
110117
isLoaded,
111118
editMode,
119+
rulesGeneration: rulesGenerationReducer,
112120
});
113121

114122
/**
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import type { SchemaValidationThunkAction } from '.';
2+
import { zeroStateChanged } from './zero-state';
3+
import { enableEditRules } from './edit-mode';
4+
import { analyzeSchema } from '@mongodb-js/compass-schema';
5+
import type { MongoError } from 'mongodb';
6+
import type { Action, AnyAction, Reducer } from 'redux';
7+
import { validationLevelChanged, validatorChanged } from './validation';
8+
9+
export function isAction<A extends AnyAction>(
10+
action: AnyAction,
11+
type: A['type']
12+
): action is A {
13+
return action.type === type;
14+
}
15+
16+
export type ValidationServerAction = 'error' | 'warn';
17+
export type ValidationLevel = 'off' | 'moderate' | 'strict';
18+
19+
const ERROR_CODE_MAX_TIME_MS_EXPIRED = 50;
20+
21+
const SAMPLE_SIZE = 1000;
22+
const ABORT_MESSAGE = 'Operation cancelled';
23+
24+
export const enum RulesGenerationActions {
25+
generationStarted = 'schema-validation/rules-generation/generationStarted',
26+
generationFailed = 'schema-validation/rules-generation/generationFailed',
27+
generationFinished = 'schema-validation/rules-generation/generationFinished',
28+
generationErrorCleared = 'schema-validation/rules-generation/generationErrorCleared',
29+
}
30+
31+
export type RulesGenerationStarted = {
32+
type: RulesGenerationActions.generationStarted;
33+
};
34+
35+
export type RulesGenerationFailed = {
36+
type: RulesGenerationActions.generationFailed;
37+
error: Error;
38+
};
39+
40+
export type RulesGenerationErrorCleared = {
41+
type: RulesGenerationActions.generationErrorCleared;
42+
};
43+
44+
export type RulesGenerationFinished = {
45+
type: RulesGenerationActions.generationFinished;
46+
};
47+
48+
export type RulesGenerationError = {
49+
errorMessage: string;
50+
errorType: 'timeout' | 'highComplexity' | 'general';
51+
};
52+
53+
export interface RulesGenerationState {
54+
isInProgress: boolean;
55+
error?: RulesGenerationError;
56+
}
57+
58+
/**
59+
* The initial state.
60+
*/
61+
export const INITIAL_STATE: RulesGenerationState = {
62+
isInProgress: false,
63+
};
64+
65+
function getErrorDetails(error: Error): RulesGenerationError {
66+
const errorCode = (error as MongoError).code;
67+
const errorMessage = error.message || 'Unknown error';
68+
let errorType: RulesGenerationError['errorType'] = 'general';
69+
if (errorCode === ERROR_CODE_MAX_TIME_MS_EXPIRED) {
70+
errorType = 'timeout';
71+
} else if (error.message.includes('Schema analysis aborted: Fields count')) {
72+
errorType = 'highComplexity';
73+
}
74+
75+
return {
76+
errorType,
77+
errorMessage,
78+
};
79+
}
80+
81+
/**
82+
* Reducer function for handle state changes to status.
83+
*/
84+
export const rulesGenerationReducer: Reducer<RulesGenerationState, Action> = (
85+
state = INITIAL_STATE,
86+
action
87+
) => {
88+
if (
89+
isAction<RulesGenerationStarted>(
90+
action,
91+
RulesGenerationActions.generationStarted
92+
)
93+
) {
94+
return {
95+
...state,
96+
isInProgress: true,
97+
error: undefined,
98+
};
99+
}
100+
101+
if (
102+
isAction<RulesGenerationFinished>(
103+
action,
104+
RulesGenerationActions.generationFinished
105+
)
106+
) {
107+
return {
108+
...state,
109+
isInProgress: false,
110+
};
111+
}
112+
113+
if (
114+
isAction<RulesGenerationFailed>(
115+
action,
116+
RulesGenerationActions.generationFailed
117+
)
118+
) {
119+
return {
120+
...state,
121+
isInProgress: false,
122+
error: getErrorDetails(action.error),
123+
};
124+
}
125+
126+
if (
127+
isAction<RulesGenerationErrorCleared>(
128+
action,
129+
RulesGenerationActions.generationErrorCleared
130+
)
131+
) {
132+
return {
133+
...state,
134+
error: undefined,
135+
};
136+
}
137+
138+
return state;
139+
};
140+
141+
export const clearRulesGenerationError =
142+
(): SchemaValidationThunkAction<RulesGenerationErrorCleared> => {
143+
return (dispatch) =>
144+
dispatch({ type: RulesGenerationActions.generationErrorCleared });
145+
};
146+
147+
export const stopRulesGeneration = (): SchemaValidationThunkAction<void> => {
148+
return (dispatch, getState, { rulesGenerationAbortControllerRef }) => {
149+
if (!rulesGenerationAbortControllerRef.current) return;
150+
rulesGenerationAbortControllerRef.current?.abort(ABORT_MESSAGE);
151+
};
152+
};
153+
154+
/**
155+
* Get $jsonSchema from schema analysis
156+
* @returns
157+
*/
158+
export const generateValidationRules = (): SchemaValidationThunkAction<
159+
Promise<void>
160+
> => {
161+
return async (
162+
dispatch,
163+
getState,
164+
{ dataService, logger, preferences, rulesGenerationAbortControllerRef }
165+
) => {
166+
dispatch({ type: RulesGenerationActions.generationStarted });
167+
168+
rulesGenerationAbortControllerRef.current = new AbortController();
169+
const abortSignal = rulesGenerationAbortControllerRef.current.signal;
170+
171+
const { namespace } = getState();
172+
const { maxTimeMS } = preferences.getPreferences();
173+
174+
try {
175+
const samplingOptions = {
176+
query: {},
177+
size: SAMPLE_SIZE,
178+
fields: undefined,
179+
};
180+
const driverOptions = {
181+
maxTimeMS,
182+
};
183+
const schemaAccessor = await analyzeSchema(
184+
dataService,
185+
abortSignal,
186+
namespace.toString(),
187+
samplingOptions,
188+
driverOptions,
189+
logger,
190+
preferences
191+
);
192+
if (abortSignal?.aborted) {
193+
throw new Error(ABORT_MESSAGE);
194+
}
195+
196+
const jsonSchema = await schemaAccessor?.getMongoDBJsonSchema({
197+
signal: abortSignal,
198+
});
199+
if (abortSignal?.aborted) {
200+
throw new Error(ABORT_MESSAGE);
201+
}
202+
const validator = JSON.stringify(
203+
{ $jsonSchema: jsonSchema },
204+
undefined,
205+
2
206+
);
207+
dispatch(validationLevelChanged('moderate'));
208+
dispatch(validatorChanged(validator));
209+
dispatch(enableEditRules());
210+
dispatch({ type: RulesGenerationActions.generationFinished });
211+
dispatch(zeroStateChanged(false));
212+
} catch (error) {
213+
if (abortSignal.aborted) {
214+
dispatch({ type: RulesGenerationActions.generationFinished });
215+
return;
216+
}
217+
dispatch({
218+
type: RulesGenerationActions.generationFailed,
219+
error,
220+
});
221+
}
222+
};
223+
};

0 commit comments

Comments
 (0)