diff --git a/packages/compass-schema-validation/src/components/validation-states.tsx b/packages/compass-schema-validation/src/components/validation-states.tsx
index 34e9fafb4db..b02d552529d 100644
--- a/packages/compass-schema-validation/src/components/validation-states.tsx
+++ b/packages/compass-schema-validation/src/components/validation-states.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import {
Banner,
+ BannerVariant,
Button,
ButtonVariant,
CancelLoader,
@@ -22,7 +23,9 @@ import {
clearRulesGenerationError,
generateValidationRules,
stopRulesGeneration,
-} from '../modules/validation';
+ type RulesGenerationError,
+} from '../modules/rules-generation';
+import { DISTINCT_FIELDS_ABORT_THRESHOLD } from '@mongodb-js/compass-schema';
const validationStatesStyles = css({
padding: spacing[400],
@@ -67,7 +70,7 @@ const DOC_UPGRADE_REVISION =
type ValidationStatesProps = {
isZeroState: boolean;
isRulesGenerationInProgress?: boolean;
- rulesGenerationError?: string;
+ rulesGenerationError?: RulesGenerationError;
isLoaded: boolean;
changeZeroState: (value: boolean) => void;
generateValidationRules: () => void;
@@ -144,6 +147,51 @@ const GeneratingScreen: React.FunctionComponent<{
);
};
+const RulesGenerationErrorBanner: React.FunctionComponent<{
+ error: RulesGenerationError;
+ onDismissError: () => void;
+}> = ({ error, onDismissError }) => {
+ if (error?.errorType === 'timeout') {
+ return (
+
+ );
+ }
+ if (error?.errorType === 'highComplexity') {
+ return (
+
+ The rules generation was aborted because the number of fields exceeds{' '}
+ {DISTINCT_FIELDS_ABORT_THRESHOLD}. Consider breaking up your data into
+ more collections with smaller documents, and using references to
+ consolidate the data you need.
+
+ Learn more
+
+
+ );
+ }
+
+ return (
+
+ );
+};
+
export function ValidationStates({
isZeroState,
isRulesGenerationInProgress,
@@ -173,10 +221,9 @@ export function ValidationStates({
data-testid="schema-validation-states"
>
{rulesGenerationError && (
-
)}
@@ -248,8 +295,8 @@ const mapStateToProps = (state: RootState) => ({
isZeroState: state.isZeroState,
isLoaded: state.isLoaded,
editMode: state.editMode,
- isRulesGenerationInProgress: state.validation.isRulesGenerationInProgress,
- rulesGenerationError: state.validation.rulesGenerationError,
+ isRulesGenerationInProgress: state.rulesGeneration.isInProgress,
+ rulesGenerationError: state.rulesGeneration.error,
});
/**
diff --git a/packages/compass-schema-validation/src/modules/index.ts b/packages/compass-schema-validation/src/modules/index.ts
index fc7a1d4b8ca..df460142e3e 100644
--- a/packages/compass-schema-validation/src/modules/index.ts
+++ b/packages/compass-schema-validation/src/modules/index.ts
@@ -31,6 +31,11 @@ import type AppRegistry from 'hadron-app-registry';
import type { Logger } from '@mongodb-js/compass-logging/provider';
import type { TrackFunction } from '@mongodb-js/compass-telemetry';
import { type WorkspacesService } from '@mongodb-js/compass-workspaces/provider';
+import type { RulesGenerationState } from './rules-generation';
+import {
+ INITIAL_STATE as RULES_GENERATION_STATE,
+ rulesGenerationReducer,
+} from './rules-generation';
/**
* Reset action constant.
@@ -44,6 +49,7 @@ export interface RootState {
namespace: NamespaceState;
serverVersion: ServerVersionState;
validation: ValidationState;
+ rulesGeneration: RulesGenerationState;
sampleDocuments: SampleDocumentState;
isZeroState: IsZeroStateState;
isLoaded: IsLoadedState;
@@ -92,6 +98,7 @@ export const INITIAL_STATE: RootState = {
namespace: NS_INITIAL_STATE,
serverVersion: SV_INITIAL_STATE,
validation: VALIDATION_STATE,
+ rulesGeneration: RULES_GENERATION_STATE,
sampleDocuments: SAMPLE_DOCUMENTS_STATE,
isZeroState: IS_ZERO_STATE,
isLoaded: IS_LOADED_STATE,
@@ -101,7 +108,7 @@ export const INITIAL_STATE: RootState = {
/**
* The reducer.
*/
-const appReducer = combineReducers({
+const appReducer = combineReducers({
namespace,
serverVersion,
validation,
@@ -109,6 +116,7 @@ const appReducer = combineReducers({
isZeroState,
isLoaded,
editMode,
+ rulesGeneration: rulesGenerationReducer,
});
/**
diff --git a/packages/compass-schema-validation/src/modules/rules-generation.ts b/packages/compass-schema-validation/src/modules/rules-generation.ts
new file mode 100644
index 00000000000..b8a3c67a2f1
--- /dev/null
+++ b/packages/compass-schema-validation/src/modules/rules-generation.ts
@@ -0,0 +1,223 @@
+import type { SchemaValidationThunkAction } from '.';
+import { zeroStateChanged } from './zero-state';
+import { enableEditRules } from './edit-mode';
+import { analyzeSchema } from '@mongodb-js/compass-schema';
+import type { MongoError } from 'mongodb';
+import type { Action, AnyAction, Reducer } from 'redux';
+import { validationLevelChanged, validatorChanged } from './validation';
+
+export function isAction(
+ action: AnyAction,
+ type: A['type']
+): action is A {
+ return action.type === type;
+}
+
+export type ValidationServerAction = 'error' | 'warn';
+export type ValidationLevel = 'off' | 'moderate' | 'strict';
+
+const ERROR_CODE_MAX_TIME_MS_EXPIRED = 50;
+
+const SAMPLE_SIZE = 1000;
+const ABORT_MESSAGE = 'Operation cancelled';
+
+export const enum RulesGenerationActions {
+ generationStarted = 'schema-validation/rules-generation/generationStarted',
+ generationFailed = 'schema-validation/rules-generation/generationFailed',
+ generationFinished = 'schema-validation/rules-generation/generationFinished',
+ generationErrorCleared = 'schema-validation/rules-generation/generationErrorCleared',
+}
+
+export type RulesGenerationStarted = {
+ type: RulesGenerationActions.generationStarted;
+};
+
+export type RulesGenerationFailed = {
+ type: RulesGenerationActions.generationFailed;
+ error: Error;
+};
+
+export type RulesGenerationErrorCleared = {
+ type: RulesGenerationActions.generationErrorCleared;
+};
+
+export type RulesGenerationFinished = {
+ type: RulesGenerationActions.generationFinished;
+};
+
+export type RulesGenerationError = {
+ errorMessage: string;
+ errorType: 'timeout' | 'highComplexity' | 'general';
+};
+
+export interface RulesGenerationState {
+ isInProgress: boolean;
+ error?: RulesGenerationError;
+}
+
+/**
+ * The initial state.
+ */
+export const INITIAL_STATE: RulesGenerationState = {
+ isInProgress: false,
+};
+
+function getErrorDetails(error: Error): RulesGenerationError {
+ const errorCode = (error as MongoError).code;
+ const errorMessage = error.message || 'Unknown error';
+ let errorType: RulesGenerationError['errorType'] = 'general';
+ if (errorCode === ERROR_CODE_MAX_TIME_MS_EXPIRED) {
+ errorType = 'timeout';
+ } else if (error.message.includes('Schema analysis aborted: Fields count')) {
+ errorType = 'highComplexity';
+ }
+
+ return {
+ errorType,
+ errorMessage,
+ };
+}
+
+/**
+ * Reducer function for handle state changes to status.
+ */
+export const rulesGenerationReducer: Reducer = (
+ state = INITIAL_STATE,
+ action
+) => {
+ if (
+ isAction(
+ action,
+ RulesGenerationActions.generationStarted
+ )
+ ) {
+ return {
+ ...state,
+ isInProgress: true,
+ error: undefined,
+ };
+ }
+
+ if (
+ isAction(
+ action,
+ RulesGenerationActions.generationFinished
+ )
+ ) {
+ return {
+ ...state,
+ isInProgress: false,
+ };
+ }
+
+ if (
+ isAction(
+ action,
+ RulesGenerationActions.generationFailed
+ )
+ ) {
+ return {
+ ...state,
+ isInProgress: false,
+ error: getErrorDetails(action.error),
+ };
+ }
+
+ if (
+ isAction(
+ action,
+ RulesGenerationActions.generationErrorCleared
+ )
+ ) {
+ return {
+ ...state,
+ error: undefined,
+ };
+ }
+
+ return state;
+};
+
+export const clearRulesGenerationError =
+ (): SchemaValidationThunkAction => {
+ return (dispatch) =>
+ dispatch({ type: RulesGenerationActions.generationErrorCleared });
+ };
+
+export const stopRulesGeneration = (): SchemaValidationThunkAction => {
+ return (dispatch, getState, { rulesGenerationAbortControllerRef }) => {
+ if (!rulesGenerationAbortControllerRef.current) return;
+ rulesGenerationAbortControllerRef.current?.abort(ABORT_MESSAGE);
+ };
+};
+
+/**
+ * Get $jsonSchema from schema analysis
+ * @returns
+ */
+export const generateValidationRules = (): SchemaValidationThunkAction<
+ Promise
+> => {
+ return async (
+ dispatch,
+ getState,
+ { dataService, logger, preferences, rulesGenerationAbortControllerRef }
+ ) => {
+ dispatch({ type: RulesGenerationActions.generationStarted });
+
+ rulesGenerationAbortControllerRef.current = new AbortController();
+ const abortSignal = rulesGenerationAbortControllerRef.current.signal;
+
+ const { namespace } = getState();
+ const { maxTimeMS } = preferences.getPreferences();
+
+ try {
+ const samplingOptions = {
+ query: {},
+ size: SAMPLE_SIZE,
+ fields: undefined,
+ };
+ const driverOptions = {
+ maxTimeMS,
+ };
+ const schemaAccessor = await analyzeSchema(
+ dataService,
+ abortSignal,
+ namespace.toString(),
+ samplingOptions,
+ driverOptions,
+ logger,
+ preferences
+ );
+ if (abortSignal?.aborted) {
+ throw new Error(ABORT_MESSAGE);
+ }
+
+ const jsonSchema = await schemaAccessor?.getMongoDBJsonSchema({
+ signal: abortSignal,
+ });
+ if (abortSignal?.aborted) {
+ throw new Error(ABORT_MESSAGE);
+ }
+ const validator = JSON.stringify(
+ { $jsonSchema: jsonSchema },
+ undefined,
+ 2
+ );
+ dispatch(validationLevelChanged('moderate'));
+ dispatch(validatorChanged(validator));
+ dispatch(enableEditRules());
+ dispatch({ type: RulesGenerationActions.generationFinished });
+ dispatch(zeroStateChanged(false));
+ } catch (error) {
+ if (abortSignal.aborted) {
+ dispatch({ type: RulesGenerationActions.generationFinished });
+ return;
+ }
+ dispatch({
+ type: RulesGenerationActions.generationFailed,
+ error,
+ });
+ }
+ };
+};
diff --git a/packages/compass-schema-validation/src/modules/validation.ts b/packages/compass-schema-validation/src/modules/validation.ts
index 2df1700199f..c2059e38ba7 100644
--- a/packages/compass-schema-validation/src/modules/validation.ts
+++ b/packages/compass-schema-validation/src/modules/validation.ts
@@ -7,15 +7,11 @@ import { zeroStateChanged } from './zero-state';
import { isLoadedChanged } from './is-loaded';
import { isEqual, pick } from 'lodash';
import type { ThunkDispatch } from 'redux-thunk';
-import { disableEditRules, enableEditRules } from './edit-mode';
-import { analyzeSchema } from '@mongodb-js/compass-schema';
+import { disableEditRules } from './edit-mode';
export type ValidationServerAction = 'error' | 'warn';
export type ValidationLevel = 'off' | 'moderate' | 'strict';
-const SAMPLE_SIZE = 1000;
-const ABORT_MESSAGE = 'Operation cancelled';
-
/**
* The module action prefix.
*/
@@ -87,31 +83,6 @@ interface SyntaxErrorOccurredAction {
syntaxError: null | { message: string };
}
-export const RULES_GENERATION_STARTED =
- `${PREFIX}/RULES_GENERATION_STARTED` as const;
-interface RulesGenerationStartedAction {
- type: typeof RULES_GENERATION_STARTED;
-}
-
-export const RULES_GENERATION_FAILED =
- `${PREFIX}/RULES_GENERATION_FAILED` as const;
-interface RulesGenerationFailedAction {
- type: typeof RULES_GENERATION_FAILED;
- message: string;
-}
-
-export const RULES_GENERATION_CLEAR_ERROR =
- `${PREFIX}/RULES_GENERATION_CLEAR_ERROR` as const;
-interface RulesGenerationClearErrorAction {
- type: typeof RULES_GENERATION_CLEAR_ERROR;
-}
-
-export const RULES_GENERATION_FINISHED =
- `${PREFIX}/RULES_GENERATION_FINISHED` as const;
-interface RulesGenerationFinishedAction {
- type: typeof RULES_GENERATION_FINISHED;
-}
-
export type ValidationAction =
| ValidatorChangedAction
| ValidationCanceledAction
@@ -119,11 +90,7 @@ export type ValidationAction =
| ValidationFetchedAction
| ValidationActionChangedAction
| ValidationLevelChangedAction
- | SyntaxErrorOccurredAction
- | RulesGenerationStartedAction
- | RulesGenerationFinishedAction
- | RulesGenerationFailedAction
- | RulesGenerationClearErrorAction;
+ | SyntaxErrorOccurredAction;
export interface Validation {
validator: string;
@@ -143,8 +110,6 @@ export interface ValidationState extends Validation {
syntaxError: null | { message: string };
error: null | { message: string };
prevValidation?: Validation;
- isRulesGenerationInProgress?: boolean;
- rulesGenerationError?: string;
}
/**
@@ -224,34 +189,6 @@ const setSyntaxError = (
syntaxError: action.syntaxError,
});
-const startRulesGeneration = (state: ValidationState): ValidationState => ({
- ...state,
- isRulesGenerationInProgress: true,
- rulesGenerationError: undefined,
-});
-
-const finishRulesGeneration = (state: ValidationState): ValidationState => ({
- ...state,
- isRulesGenerationInProgress: undefined,
- rulesGenerationError: undefined,
-});
-
-const markRulesGenerationFailure = (
- state: ValidationState,
- action: RulesGenerationFailedAction
-): ValidationState => ({
- ...state,
- isRulesGenerationInProgress: undefined,
- rulesGenerationError: action.message,
-});
-
-const unsetRulesGenerationError = (
- state: ValidationState
-): ValidationState => ({
- ...state,
- rulesGenerationError: undefined,
-});
-
/**
* Set validation.
*/
@@ -354,10 +291,6 @@ const MAPPINGS: {
[VALIDATION_ACTION_CHANGED]: changeValidationAction,
[VALIDATION_LEVEL_CHANGED]: changeValidationLevel,
[SYNTAX_ERROR_OCCURRED]: setSyntaxError,
- [RULES_GENERATION_STARTED]: startRulesGeneration,
- [RULES_GENERATION_FINISHED]: finishRulesGeneration,
- [RULES_GENERATION_FAILED]: markRulesGenerationFailure,
- [RULES_GENERATION_CLEAR_ERROR]: unsetRulesGenerationError,
};
/**
@@ -448,11 +381,6 @@ export const syntaxErrorOccurred = (
syntaxError,
});
-export const clearRulesGenerationError =
- (): RulesGenerationClearErrorAction => ({
- type: RULES_GENERATION_CLEAR_ERROR,
- });
-
export const fetchValidation = (namespace: {
database: string;
collection: string;
@@ -608,81 +536,3 @@ export const activateValidation = (): SchemaValidationThunkAction => {
dispatch(fetchValidation(namespace));
};
};
-
-export const stopRulesGeneration = (): SchemaValidationThunkAction => {
- return (dispatch, getState, { rulesGenerationAbortControllerRef }) => {
- if (!rulesGenerationAbortControllerRef.current) return;
- rulesGenerationAbortControllerRef.current?.abort(ABORT_MESSAGE);
- };
-};
-
-/**
- * Get $jsonSchema from schema analysis
- * @returns
- */
-export const generateValidationRules = (): SchemaValidationThunkAction<
- Promise
-> => {
- return async (
- dispatch,
- getState,
- { dataService, logger, preferences, rulesGenerationAbortControllerRef }
- ) => {
- dispatch({ type: RULES_GENERATION_STARTED });
-
- rulesGenerationAbortControllerRef.current = new AbortController();
- const abortSignal = rulesGenerationAbortControllerRef.current.signal;
-
- const { namespace } = getState();
- const { maxTimeMS } = preferences.getPreferences();
-
- try {
- const samplingOptions = {
- query: {},
- size: SAMPLE_SIZE,
- fields: undefined,
- };
- const driverOptions = {
- maxTimeMS,
- };
- const schemaAccessor = await analyzeSchema(
- dataService,
- abortSignal,
- namespace.toString(),
- samplingOptions,
- driverOptions,
- logger,
- preferences
- );
- if (abortSignal?.aborted) {
- throw new Error(ABORT_MESSAGE);
- }
-
- const jsonSchema = await schemaAccessor?.getMongoDBJsonSchema({
- signal: abortSignal,
- });
- if (abortSignal?.aborted) {
- throw new Error(ABORT_MESSAGE);
- }
- const validator = JSON.stringify(
- { $jsonSchema: jsonSchema },
- undefined,
- 2
- );
- dispatch(validationLevelChanged('moderate'));
- dispatch(validatorChanged(validator));
- dispatch(enableEditRules());
- dispatch({ type: RULES_GENERATION_FINISHED });
- dispatch(zeroStateChanged(false));
- } catch (error) {
- if (abortSignal.aborted) {
- dispatch({ type: RULES_GENERATION_FINISHED });
- return;
- }
- dispatch({
- type: RULES_GENERATION_FAILED,
- message: `Rules generation failed: ${(error as Error).message}`,
- });
- }
- };
-};
diff --git a/packages/compass-schema/src/components/schema-toolbar.tsx b/packages/compass-schema/src/components/schema-toolbar.tsx
index e7d888bc9a7..0fca62f0ef2 100644
--- a/packages/compass-schema/src/components/schema-toolbar.tsx
+++ b/packages/compass-schema/src/components/schema-toolbar.tsx
@@ -21,8 +21,8 @@ import {
type SchemaAnalysisError,
analysisErrorDismissed,
} from '../stores/schema-analysis-reducer';
-import type { RootState } from '../stores/store';
import { DISTINCT_FIELDS_ABORT_THRESHOLD } from '../modules/schema-analysis';
+import type { RootState } from '../stores/store';
import { openExportSchema } from '../stores/schema-export-reducer';
const schemaToolbarStyles = css({