{error && (
@@ -61,6 +90,7 @@ function CreateIndexActions({
onClick={onCreateIndexClick}
variant="primary"
className={createIndexButtonStyles}
+ disabled={isCreateIndexButtonDisabled}
>
Create Index
@@ -68,4 +98,13 @@ function CreateIndexActions({
);
}
-export default CreateIndexActions;
+const mapState = ({ createIndex }: RootState) => {
+ const { fields, currentTab, indexSuggestions } = createIndex;
+ return {
+ fields,
+ currentTab,
+ indexSuggestions,
+ };
+};
+
+export default connect(mapState)(CreateIndexActions);
diff --git a/packages/compass-indexes/src/components/create-index-form/create-index-form.spec.tsx b/packages/compass-indexes/src/components/create-index-form/create-index-form.spec.tsx
index 507bea611c4..12e495ec2ef 100644
--- a/packages/compass-indexes/src/components/create-index-form/create-index-form.spec.tsx
+++ b/packages/compass-indexes/src/components/create-index-form/create-index-form.spec.tsx
@@ -5,10 +5,13 @@ import type { Field } from '../../modules/create-index';
import { expect } from 'chai';
import type { SinonSpy } from 'sinon';
+import { setupStore } from '../../../test/setup-store';
import sinon from 'sinon';
+import { Provider } from 'react-redux';
describe('CreateIndexForm', () => {
let onTabClickSpy: SinonSpy;
+ const store = setupStore();
beforeEach(function () {
onTabClickSpy = sinon.spy();
@@ -20,24 +23,26 @@ describe('CreateIndexForm', () => {
showIndexesGuidanceVariant?: boolean;
}) => {
render(
-
{}}
- onSelectFieldTypeClick={() => {}}
- onAddFieldClick={() => {}}
- onRemoveFieldClick={() => {}}
- onTabClick={onTabClickSpy}
- showIndexesGuidanceVariant={showIndexesGuidanceVariant || false}
- query={null}
- />
+
+ {}}
+ onSelectFieldTypeClick={() => {}}
+ onAddFieldClick={() => {}}
+ onRemoveFieldClick={() => {}}
+ onTabClick={onTabClickSpy}
+ showIndexesGuidanceVariant={showIndexesGuidanceVariant || false}
+ query={null}
+ />
+
);
};
diff --git a/packages/compass-indexes/src/components/create-index-form/create-index-form.tsx b/packages/compass-indexes/src/components/create-index-form/create-index-form.tsx
index c0cb3edc257..38b46e01153 100644
--- a/packages/compass-indexes/src/components/create-index-form/create-index-form.tsx
+++ b/packages/compass-indexes/src/components/create-index-form/create-index-form.tsx
@@ -21,7 +21,7 @@ import { usePreference } from 'compass-preferences-model/provider';
import IndexFlowSection from './index-flow-section';
import QueryFlowSection from './query-flow-section';
import toNS from 'mongodb-ns';
-import type { Document } from 'bson';
+import type { Document } from 'mongodb';
const createIndexModalFieldsStyles = css({
margin: `${spacing[600]}px 0 ${spacing[800]}px 0`,
diff --git a/packages/compass-indexes/src/components/create-index-form/index-flow-section.spec.tsx b/packages/compass-indexes/src/components/create-index-form/index-flow-section.spec.tsx
index 22574ea61ee..4132fadc4a8 100644
--- a/packages/compass-indexes/src/components/create-index-form/index-flow-section.spec.tsx
+++ b/packages/compass-indexes/src/components/create-index-form/index-flow-section.spec.tsx
@@ -3,8 +3,11 @@ import { render, screen } from '@mongodb-js/testing-library-compass';
import IndexFlowSection from './index-flow-section';
import { expect } from 'chai';
import type { Field } from '../../modules/create-index';
+import { Provider } from 'react-redux';
+import { setupStore } from '../../../test/setup-store';
describe('IndexFlowSection', () => {
+ const store = setupStore();
const renderComponent = ({
createIndexFieldsComponent,
fields,
@@ -13,12 +16,14 @@ describe('IndexFlowSection', () => {
fields?: Field[];
}) => {
render(
-
+
+
+
);
};
diff --git a/packages/compass-indexes/src/components/create-index-form/index-flow-section.tsx b/packages/compass-indexes/src/components/create-index-form/index-flow-section.tsx
index e367e3629d3..93c9bb957ce 100644
--- a/packages/compass-indexes/src/components/create-index-form/index-flow-section.tsx
+++ b/packages/compass-indexes/src/components/create-index-form/index-flow-section.tsx
@@ -12,9 +12,17 @@ import {
InfoSprinkle,
Tooltip,
} from '@mongodb-js/compass-components';
-import React, { useState, useCallback } from 'react';
-import type { Field } from '../../modules/create-index';
+import React, { useState, useCallback, useEffect } from 'react';
+import {
+ errorCleared,
+ errorEncountered,
+ type Field,
+} from '../../modules/create-index';
import MDBCodeViewer from './mdb-code-viewer';
+import { areAllFieldsFilledIn } from '../../utils/create-index-modal-validation';
+import { connect } from 'react-redux';
+import type { TrackFunction } from '@mongodb-js/compass-telemetry/provider';
+import { useTelemetry } from '@mongodb-js/compass-telemetry/provider';
const flexContainerStyles = css({
display: 'flex',
@@ -77,10 +85,13 @@ export type IndexFlowSectionProps = {
createIndexFieldsComponent: JSX.Element | null;
dbName: string;
collectionName: string;
+ onErrorEncountered: (error: string) => void;
+ onErrorCleared: () => void;
};
const generateCoveredQueries = (
- coveredQueriesArr: Array>
+ coveredQueriesArr: Array>,
+ track: TrackFunction
) => {
const rows = [];
for (let i = 0; i < coveredQueriesArr.length; i++) {
@@ -92,6 +103,15 @@ const generateCoveredQueries = (
);
}
+ if (rows.length === 0) {
+ // TODO: remove this in CLOUDP-320224
+ track('Error generating covered queries', {
+ context: 'Create Index Modal',
+ });
+ throw new Error(
+ 'Error generating covered query examples. Please try again later.'
+ );
+ }
return <>{rows}>;
};
@@ -148,20 +168,22 @@ const IndexFlowSection = ({
fields,
dbName,
collectionName,
+ onErrorEncountered,
+ onErrorCleared,
}: IndexFlowSectionProps) => {
const [isCodeEquivalentToggleChecked, setIsCodeEquivalentToggleChecked] =
useState(false);
-
- const areAllFieldsFilledIn = fields.every((field) => {
- return field.name && field.type;
- });
+ const [hasFieldChanges, setHasFieldChanges] = useState(false);
const hasUnsupportedQueryTypes = fields.some((field) => {
return field.type === '2dsphere' || field.type === 'text';
});
+ const track = useTelemetry();
const isCoveredQueriesButtonDisabled =
- !areAllFieldsFilledIn || hasUnsupportedQueryTypes;
+ !areAllFieldsFilledIn(fields) ||
+ hasUnsupportedQueryTypes ||
+ !hasFieldChanges;
const indexNameTypeMap = fields.reduce>(
(accumulator, currentValue) => {
@@ -188,12 +210,23 @@ const IndexFlowSection = ({
return { [field.name]: index + 1 };
});
- setCoveredQueriesObj({
- coveredQueries: generateCoveredQueries(coveredQueriesArr),
- optimalQueries: generateOptimalQueries(coveredQueriesArr),
- showCoveredQueries: true,
- });
- }, [fields]);
+ try {
+ setCoveredQueriesObj({
+ coveredQueries: generateCoveredQueries(coveredQueriesArr, track),
+ optimalQueries: generateOptimalQueries(coveredQueriesArr),
+ showCoveredQueries: true,
+ });
+ } catch (e) {
+ onErrorEncountered(e instanceof Error ? e.message : String(e));
+ }
+
+ setHasFieldChanges(false);
+ }, [fields, onErrorEncountered, track]);
+
+ useEffect(() => {
+ setHasFieldChanges(true);
+ onErrorCleared();
+ }, [fields, onErrorCleared]);
const { coveredQueries, optimalQueries, showCoveredQueries } =
coveredQueriesObj;
@@ -315,4 +348,13 @@ const IndexFlowSection = ({
);
};
-export default IndexFlowSection;
+const mapState = () => {
+ return {};
+};
+
+const mapDispatch = {
+ onErrorEncountered: errorEncountered,
+ onErrorCleared: errorCleared,
+};
+
+export default connect(mapState, mapDispatch)(IndexFlowSection);
diff --git a/packages/compass-indexes/src/components/create-index-form/query-flow-section.spec.tsx b/packages/compass-indexes/src/components/create-index-form/query-flow-section.spec.tsx
index acd4a9d1973..658f08ddbd1 100644
--- a/packages/compass-indexes/src/components/create-index-form/query-flow-section.spec.tsx
+++ b/packages/compass-indexes/src/components/create-index-form/query-flow-section.spec.tsx
@@ -67,7 +67,7 @@ describe('QueryFlowSection', () => {
type: ActionTypes.SuggestedIndexesFetched,
sampleDocs: [],
indexSuggestions: { a: 1, b: 2 },
- fetchingSuggestionsError: null,
+ error: null,
indexSuggestionsState: 'success',
});
});
diff --git a/packages/compass-indexes/src/components/create-index-form/query-flow-section.tsx b/packages/compass-indexes/src/components/create-index-form/query-flow-section.tsx
index 8e9e73e3845..0c304747fff 100644
--- a/packages/compass-indexes/src/components/create-index-form/query-flow-section.tsx
+++ b/packages/compass-indexes/src/components/create-index-form/query-flow-section.tsx
@@ -7,6 +7,7 @@ import {
useFocusRing,
ParagraphSkeleton,
} from '@mongodb-js/compass-components';
+import type { Document } from 'mongodb';
import React, { useMemo, useCallback } from 'react';
import { css, spacing } from '@mongodb-js/compass-components';
import {
@@ -21,7 +22,7 @@ import type {
SuggestedIndexFetchedProps,
} from '../../modules/create-index';
import { connect } from 'react-redux';
-import type { Document } from 'bson';
+import { parseFilter } from 'mongodb-query-parser';
const inputQueryContainerStyles = css({
display: 'flex',
@@ -81,6 +82,8 @@ const insightStyles = css({
display: 'flex',
alignItems: 'center',
gap: spacing[100],
+ marginBottom: spacing[200],
+ height: spacing[500],
});
const QueryFlowSection = ({
@@ -107,8 +110,14 @@ const QueryFlowSection = ({
initialQuery: Document | null;
}) => {
const [inputQuery, setInputQuery] = React.useState(
- JSON.stringify(initialQuery?.filter ?? {}, null, 2)
+ JSON.stringify(initialQuery ?? '', null, 2)
+ );
+ const [hasNewChanges, setHasNewChanges] = React.useState(
+ initialQuery !== null
);
+ const [isShowSuggestionsButtonDisabled, setIsShowSuggestionsButtonDisabled] =
+ React.useState(true);
+
const completer = useMemo(
() =>
createQueryAutocompleter({
@@ -133,10 +142,33 @@ const QueryFlowSection = ({
collectionName,
inputQuery: sanitizedInputQuery,
});
+
+ setHasNewChanges(false);
}, [inputQuery, dbName, collectionName, onSuggestedIndexButtonClick]);
+ const handleQueryInputChange = useCallback((text: string) => {
+ setInputQuery(text);
+ setHasNewChanges(true);
+ }, []);
+
const isFetchingIndexSuggestions = fetchingSuggestionsState === 'fetching';
+ // Validate query upon typing
+ useMemo(() => {
+ let _isShowSuggestionsButtonDisabled = !hasNewChanges;
+ try {
+ parseFilter(inputQuery);
+
+ if (!inputQuery.startsWith('{') || !inputQuery.endsWith('}')) {
+ _isShowSuggestionsButtonDisabled = true;
+ }
+ } catch (e) {
+ _isShowSuggestionsButtonDisabled = true;
+ } finally {
+ setIsShowSuggestionsButtonDisabled(_isShowSuggestionsButtonDisabled);
+ }
+ }, [hasNewChanges, inputQuery]);
+
return (
<>
{initialQuery && (
@@ -164,7 +196,7 @@ const QueryFlowSection = ({
copyable={false}
formattable={false}
text={inputQuery}
- onChangeText={(text) => setInputQuery(text)}
+ onChangeText={(text) => handleQueryInputChange(text)}
placeholder="Type a query: { field: 'value' }"
completer={completer}
className={codeEditorStyles}
@@ -176,6 +208,7 @@ const QueryFlowSection = ({
onClick={handleSuggestedIndexButtonClick}
className={suggestedIndexButtonStyles}
size="small"
+ disabled={isShowSuggestionsButtonDisabled}
>
Show suggested index
diff --git a/packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx b/packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx
index 976d5f6205f..88205465d46 100644
--- a/packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx
+++ b/packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx
@@ -29,7 +29,7 @@ import {
import { useConnectionInfoRef } from '@mongodb-js/compass-connections/provider';
import { usePreference } from 'compass-preferences-model/provider';
import CreateIndexModalHeader from './create-index-modal-header';
-import type { Document } from 'bson';
+import type { Document } from 'mongodb';
type CreateIndexModalProps = React.ComponentProps & {
isVisible: boolean;
@@ -121,6 +121,7 @@ function CreateIndexModal({
onErrorBannerCloseClick={onErrorBannerCloseClick}
onCreateIndexClick={onCreateIndexClick}
onCancelCreateIndexClick={onCancelCreateIndexClick}
+ showIndexesGuidanceVariant={showIndexesGuidanceVariant}
/>
diff --git a/packages/compass-indexes/src/modules/create-index.tsx b/packages/compass-indexes/src/modules/create-index.tsx
index c903ea780a5..1bfe5c17a96 100644
--- a/packages/compass-indexes/src/modules/create-index.tsx
+++ b/packages/compass-indexes/src/modules/create-index.tsx
@@ -1,5 +1,4 @@
import type { Document } from 'mongodb';
-import type { Document as BsonDocument } from 'bson';
import { EJSON, ObjectId } from 'bson';
import type { CreateIndexesOptions, IndexDirection } from 'mongodb';
import { isCollationValid } from 'mongodb-query-parser';
@@ -77,7 +76,7 @@ type ErrorClearedAction = {
export type CreateIndexOpenedAction = {
type: ActionTypes.CreateIndexOpened;
- query?: BsonDocument;
+ query?: Document;
};
type CreateIndexClosedAction = {
@@ -307,9 +306,6 @@ export type State = {
// state of the index suggestions
fetchingSuggestionsState: IndexSuggestionState;
- // error specific to fetching index suggestions
- fetchingSuggestionsError: string | null;
-
// index suggestions in a format such as {fieldName: 1}
indexSuggestions: Record | null;
@@ -317,7 +313,7 @@ export type State = {
sampleDocs: Array | null;
// base query to be used for query flow index creation
- query: BsonDocument | null;
+ query: Document | null;
};
export const INITIAL_STATE: State = {
@@ -328,7 +324,6 @@ export const INITIAL_STATE: State = {
options: INITIAL_OPTIONS_STATE,
currentTab: 'IndexFlow',
fetchingSuggestionsState: 'initial',
- fetchingSuggestionsError: null,
indexSuggestions: null,
sampleDocs: null,
query: null,
@@ -343,7 +338,7 @@ function getInitialState(): State {
//-------
-export const createIndexOpened = (query?: BsonDocument) => ({
+export const createIndexOpened = (query?: Document) => ({
type: ActionTypes.CreateIndexOpened,
query,
});
@@ -352,7 +347,7 @@ export const createIndexClosed = () => ({
type: ActionTypes.CreateIndexClosed,
});
-const errorEncountered = (error: string): ErrorEncounteredAction => ({
+export const errorEncountered = (error: string): ErrorEncounteredAction => ({
type: ActionTypes.ErrorEncountered,
error,
});
@@ -373,7 +368,7 @@ export type SuggestedIndexFetchedAction = {
type: ActionTypes.SuggestedIndexesFetched;
sampleDocs: Array;
indexSuggestions: { [key: string]: number } | null;
- fetchingSuggestionsError: string | null;
+ error: string | null;
indexSuggestionsState: IndexSuggestionState;
};
@@ -395,7 +390,7 @@ export const fetchIndexSuggestions = ({
Promise,
SuggestedIndexFetchedAction | SuggestedIndexesRequestedAction
> => {
- return async (dispatch, getState, { dataService }) => {
+ return async (dispatch, getState, { dataService, track }) => {
dispatch({
type: ActionTypes.SuggestedIndexesRequested,
});
@@ -416,6 +411,19 @@ export const fetchIndexSuggestions = ({
}
}
+ const throwError = (e?: unknown) => {
+ dispatch({
+ type: ActionTypes.SuggestedIndexesFetched,
+ sampleDocs: sampleDocuments || [],
+ indexSuggestions: null,
+ error:
+ e instanceof Error
+ ? 'Error parsing query. Please follow query structure. ' + e.message
+ : 'Error parsing query. Please follow query structure.',
+ indexSuggestionsState: 'error',
+ });
+ };
+
// Analyze namespace and fetch suggestions
try {
const analyzedNamespace = mql.analyzeNamespace(
@@ -428,18 +436,13 @@ export const fetchIndexSuggestions = ({
analyzedNamespace
);
const results = await mql.suggestIndex([query]);
- const indexSuggestions = results?.index || null;
-
- // TODO in CLOUDP-311787: add info banner and update the current error banner to take in fetchingSuggestionsError as well
- if (!indexSuggestions) {
- dispatch({
- type: ActionTypes.SuggestedIndexesFetched,
- sampleDocs: sampleDocuments,
- indexSuggestions,
- fetchingSuggestionsError:
- 'No suggested index found. Please choose "Start with an Index" at the top to continue.',
- indexSuggestionsState: 'error',
- });
+ const indexSuggestions = results?.index;
+
+ if (
+ !indexSuggestions ||
+ Object.keys(indexSuggestions as Record).length === 0
+ ) {
+ throwError();
return;
}
@@ -447,20 +450,13 @@ export const fetchIndexSuggestions = ({
type: ActionTypes.SuggestedIndexesFetched,
sampleDocs: sampleDocuments,
indexSuggestions,
- fetchingSuggestionsError: null,
+ error: null,
indexSuggestionsState: 'success',
});
} catch (e: unknown) {
- dispatch({
- type: ActionTypes.SuggestedIndexesFetched,
- sampleDocs: sampleDocuments,
- indexSuggestions: null,
- fetchingSuggestionsError:
- e instanceof Error
- ? 'Error parsing query. Please follow query structure. ' + e.message
- : 'Error parsing query. Please follow query structure.',
- indexSuggestionsState: 'error',
- });
+ // TODO: remove this in CLOUDP-320224
+ track('Error parsing query', { context: 'Create Index Modal' });
+ throwError(e);
}
};
};
@@ -496,6 +492,8 @@ export const createIndexFormSubmitted = (): IndexesThunkAction<
return (dispatch, getState, { track, preferences }) => {
// @experiment Early Journey Indexes Guidance & Awareness | Jira Epic: CLOUDP-239367
const currentTab = getState().createIndex.currentTab;
+ const isQueryFlow = currentTab === 'QueryFlow';
+ const indexSuggestions = getState().createIndex.indexSuggestions;
const { enableIndexesGuidanceExp, showIndexesGuidanceVariant } =
preferences.getPreferences();
@@ -511,6 +509,7 @@ export const createIndexFormSubmitted = (): IndexesThunkAction<
// Check for field errors.
if (
+ !isQueryFlow &&
getState().createIndex.fields.some(
(field: Field) => field.name === '' || field.type === ''
)
@@ -521,14 +520,22 @@ export const createIndexFormSubmitted = (): IndexesThunkAction<
const formIndexOptions = getState().createIndex.options;
- let spec: Record;
+ let spec: Record = {};
try {
- spec = Object.fromEntries(
- getState().createIndex.fields.map((field) => {
- return [field.name, fieldTypeToIndexDirection(field.type)];
- })
- );
+ if (isQueryFlow) {
+ // Gather from suggested index
+ if (indexSuggestions) {
+ spec = indexSuggestions;
+ }
+ } else {
+ // Gather from the index input fields
+ spec = Object.fromEntries(
+ getState().createIndex.fields.map((field) => {
+ return [field.name, fieldTypeToIndexDirection(field.type)];
+ })
+ );
+ }
} catch (e) {
dispatch(errorEncountered((e as any).message));
return;
@@ -768,7 +775,7 @@ const reducer: Reducer = (state = INITIAL_STATE, action) => {
return {
...state,
fetchingSuggestionsState: 'fetching',
- fetchingSuggestionsError: null,
+ error: null,
indexSuggestions: null,
};
}
@@ -782,7 +789,7 @@ const reducer: Reducer = (state = INITIAL_STATE, action) => {
return {
...state,
fetchingSuggestionsState: action.indexSuggestionsState,
- fetchingSuggestionsError: action.fetchingSuggestionsError,
+ error: action.error,
indexSuggestions: action.indexSuggestions,
sampleDocs: action.sampleDocs,
};
diff --git a/packages/compass-indexes/src/utils/create-index-modal-validation.ts b/packages/compass-indexes/src/utils/create-index-modal-validation.ts
new file mode 100644
index 00000000000..f90b614c859
--- /dev/null
+++ b/packages/compass-indexes/src/utils/create-index-modal-validation.ts
@@ -0,0 +1,5 @@
+import type { Field } from '../modules/create-index';
+
+export const areAllFieldsFilledIn = (fields: Field[]) => {
+ return fields.every((field) => field.name && field.type);
+};
diff --git a/packages/compass-telemetry/src/telemetry-events.ts b/packages/compass-telemetry/src/telemetry-events.ts
index 20133b20808..580f8f30a61 100644
--- a/packages/compass-telemetry/src/telemetry-events.ts
+++ b/packages/compass-telemetry/src/telemetry-events.ts
@@ -2694,6 +2694,20 @@ type CreateIndexButtonClickedEvent = CommonEvent<{
};
}>;
+type CreateIndexErrorParsingQueryEvent = CommonEvent<{
+ name: 'Error parsing query';
+ payload: {
+ context: CreateIndexModalContext;
+ };
+}>;
+
+type CreateIndexErrorGettingCoveredQueriesEvent = CommonEvent<{
+ name: 'Error generating covered queries';
+ payload: {
+ context: CreateIndexModalContext;
+ };
+}>;
+
type UUIDEncounteredEvent = CommonEvent<{
name: 'UUID Encountered';
payload: {
@@ -2825,4 +2839,6 @@ export type TelemetryEvent =
| TimeToFirstByteEvent
| ExperimentViewedEvent
| CreateIndexButtonClickedEvent
+ | CreateIndexErrorParsingQueryEvent
+ | CreateIndexErrorGettingCoveredQueriesEvent
| UUIDEncounteredEvent;