diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.tsx index e70f37ab740..f568f484ee8 100644 --- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.tsx +++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { connect } from 'react-redux'; import type { ConnectedProps } from 'react-redux'; import { @@ -8,6 +8,7 @@ import { spacing, TextInput, palette, + Tooltip, } from '@mongodb-js/compass-components'; import type { RootState } from '../../../modules'; @@ -58,16 +59,48 @@ const PipelineCollation: React.FunctionComponent = ({ maxTimeMSValue, maxTimeMSChanged, }) => { + const maxTimeMSEnvLimit = usePreference('maxTimeMSEnvLimit'); + const onMaxTimeMSChanged = useCallback( (evt: React.ChangeEvent) => { if (maxTimeMSChanged) { - maxTimeMSChanged(parseInt(evt.currentTarget.value, 10)); + const parsed = Number(evt.currentTarget.value); + const newValue = Number.isNaN(parsed) ? 0 : parsed; + + // When environment limit is set (> 0), enforce it + if (maxTimeMSEnvLimit && newValue > maxTimeMSEnvLimit) { + maxTimeMSChanged(maxTimeMSEnvLimit); + } else { + maxTimeMSChanged(newValue); + } } }, - [maxTimeMSChanged] + [maxTimeMSChanged, maxTimeMSEnvLimit] ); + const maxTimeMSLimit = usePreference('maxTimeMS'); + // Determine the effective max limit when environment limit is set (> 0) + const effectiveMaxLimit = useMemo(() => { + if (maxTimeMSEnvLimit) { + return maxTimeMSLimit + ? Math.min(maxTimeMSLimit, maxTimeMSEnvLimit) + : maxTimeMSEnvLimit; + } + return maxTimeMSLimit; + }, [maxTimeMSEnvLimit, maxTimeMSLimit]); + + // Check if value exceeds the environment limit (when limit > 0) + const exceedsLimit = Boolean( + useMemo(() => { + return ( + maxTimeMSEnvLimit && + maxTimeMSValue && + maxTimeMSValue >= maxTimeMSEnvLimit + ); + }, [maxTimeMSEnvLimit, maxTimeMSValue]) + ); + return (
= ({ > Max Time MS - maxTimeMSLimit - ? 'error' - : 'none' - } - onChange={onMaxTimeMSChanged} - /> + ) => ( +
+ effectiveMaxLimit) || + exceedsLimit + ? 'error' + : 'none' + } + onChange={onMaxTimeMSChanged} + /> + {children} +
+ )} + > + Operations longer than 5 minutes are not supported in the web + environment +
); }; diff --git a/packages/compass-preferences-model/src/preferences-schema.tsx b/packages/compass-preferences-model/src/preferences-schema.tsx index 813edd6488b..799674538a6 100644 --- a/packages/compass-preferences-model/src/preferences-schema.tsx +++ b/packages/compass-preferences-model/src/preferences-schema.tsx @@ -105,6 +105,8 @@ export type UserConfigurablePreferences = PermanentFeatureFlags & enableProxySupport: boolean; proxy: string; inferNamespacesFromPrivileges?: boolean; + // Features that are enabled by default in Date Explorer, but are disabled in Compass + maxTimeMSEnvLimit?: number; }; /** @@ -1062,6 +1064,17 @@ export const storedUserPreferencesProps: Required<{ validator: z.boolean().default(true), type: 'boolean', }, + maxTimeMSEnvLimit: { + ui: true, + cli: true, + global: true, + description: { + short: + 'Maximum time limit for operations in environment (milliseconds). Set to 0 for no limit.', + }, + validator: z.number().min(0).default(0), + type: 'number', + }, ...allFeatureFlagsProps, }; diff --git a/packages/compass-query-bar/src/components/query-option.tsx b/packages/compass-query-bar/src/components/query-option.tsx index 026bfdfa3ad..27492118a43 100644 --- a/packages/compass-query-bar/src/components/query-option.tsx +++ b/packages/compass-query-bar/src/components/query-option.tsx @@ -1,7 +1,8 @@ -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useRef, useMemo } from 'react'; import { Label, TextInput, + Tooltip, css, cx, spacing, @@ -20,6 +21,7 @@ import type { QueryProperty } from '../constants/query-properties'; import type { RootState } from '../stores/query-bar-store'; import { useTelemetry } from '@mongodb-js/compass-telemetry/provider'; import { useConnectionInfoRef } from '@mongodb-js/compass-connections/provider'; +import { usePreference } from 'compass-preferences-model/provider'; const queryOptionStyles = css({ display: 'flex', @@ -153,6 +155,22 @@ const QueryOption: React.FunctionComponent = ({ } }, [track, name, connectionInfoRef]); + // MaxTimeMS warning tooltip logic + const maxTimeMSEnvLimit = usePreference('maxTimeMSEnvLimit'); + const numericValue = useMemo(() => { + if (!value) return 0; + const parsed = Number(value); + return Number.isNaN(parsed) ? 0 : parsed; + }, [value]); + + const exceedsMaxTimeMSLimit = useMemo(() => { + return ( + name === 'maxTimeMS' && + maxTimeMSEnvLimit && // 0 is falsy, so no limit when 0 + numericValue >= maxTimeMSEnvLimit + ); + }, [name, maxTimeMSEnvLimit, numericValue]); + return (
= ({ /> ) : ( - {({ props }) => ( - ) => - onValueChange(evt.currentTarget.value) - } - onBlur={onBlurEditor} - placeholder={placeholder as string} - disabled={disabled} - {...props} - /> - )} + {({ props }) => { + const textInput = ( + ) => + onValueChange(evt.currentTarget.value) + } + onBlur={onBlurEditor} + placeholder={placeholder as string} + disabled={disabled} + {...props} + /> + ); + + // Wrap maxTimeMS field with tooltip in web environment when exceeding limit + if (exceedsMaxTimeMSLimit) { + return ( + ) => ( +
+ {textInput} + {children} +
+ )} + > + Operations longer than 5 minutes are not supported in the + web environment +
+ ); + } + + return textInput; + }}
)}
diff --git a/packages/compass-query-bar/src/constants/query-option-definition.ts b/packages/compass-query-bar/src/constants/query-option-definition.ts index 76c9425ef3d..ad6ad7870c1 100644 --- a/packages/compass-query-bar/src/constants/query-option-definition.ts +++ b/packages/compass-query-bar/src/constants/query-option-definition.ts @@ -70,12 +70,26 @@ export const OPTION_DEFINITION: { link: 'https://docs.mongodb.com/manual/reference/method/cursor.maxTimeMS/', extraTextInputProps() { const preferenceMaxTimeMS = usePreference('maxTimeMS'); - const props: { max?: number; placeholder?: string } = { - max: preferenceMaxTimeMS, + const maxTimeMSEnvLimit = usePreference('maxTimeMSEnvLimit'); + + // Determine the effective max limit when environment limit is set (> 0) + const effectiveMaxLimit = maxTimeMSEnvLimit + ? preferenceMaxTimeMS + ? Math.min(preferenceMaxTimeMS, maxTimeMSEnvLimit) + : maxTimeMSEnvLimit + : preferenceMaxTimeMS; + + const props: { + max?: number; + placeholder?: string; + } = { + max: effectiveMaxLimit, }; - if (preferenceMaxTimeMS !== undefined && preferenceMaxTimeMS < 60000) { - props.placeholder = String(preferenceMaxTimeMS); + + if (effectiveMaxLimit && effectiveMaxLimit < 60000) { + props.placeholder = `${+effectiveMaxLimit}`; } + return props; }, }, diff --git a/packages/compass-query-bar/src/stores/query-bar-reducer.ts b/packages/compass-query-bar/src/stores/query-bar-reducer.ts index acd80141231..24691cf564f 100644 --- a/packages/compass-query-bar/src/stores/query-bar-reducer.ts +++ b/packages/compass-query-bar/src/stores/query-bar-reducer.ts @@ -52,7 +52,7 @@ type QueryBarState = { export const INITIAL_STATE: QueryBarState = { isReadonlyConnection: false, - fields: mapQueryToFormFields({}, DEFAULT_FIELD_VALUES), + fields: mapQueryToFormFields({ maxTimeMSEnvLimit: 0 }, DEFAULT_FIELD_VALUES), expanded: false, serverVersion: '3.6.0', lastAppliedQuery: { source: null, query: {} }, @@ -103,6 +103,7 @@ export const changeField = ( return (dispatch, getState, { preferences }) => { const parsedValue = validateField(name, stringValue, { maxTimeMS: preferences.getPreferences().maxTimeMS ?? undefined, + maxTimeMSEnvLimit: preferences.getPreferences().maxTimeMSEnvLimit, }); const isValid = parsedValue !== false; dispatch({ @@ -162,7 +163,7 @@ export const resetQuery = ( return false; } const fields = mapQueryToFormFields( - { maxTimeMS: preferences.getPreferences().maxTimeMS }, + preferences.getPreferences(), DEFAULT_FIELD_VALUES ); dispatch({ type: QueryBarActions.ResetQuery, fields, source }); @@ -179,10 +180,7 @@ export const setQuery = ( query: BaseQuery ): QueryBarThunkAction => { return (dispatch, getState, { preferences }) => { - const fields = mapQueryToFormFields( - { maxTimeMS: preferences.getPreferences().maxTimeMS }, - query - ); + const fields = mapQueryToFormFields(preferences.getPreferences(), query); dispatch({ type: QueryBarActions.SetQuery, fields }); }; }; @@ -238,14 +236,11 @@ export const applyFromHistory = ( } return acc; }, {}); - const fields = mapQueryToFormFields( - { maxTimeMS: preferences.getPreferences().maxTimeMS }, - { - ...DEFAULT_FIELD_VALUES, - ...query, - ...currentQuery, - } - ); + const fields = mapQueryToFormFields(preferences.getPreferences(), { + ...DEFAULT_FIELD_VALUES, + ...query, + ...currentQuery, + }); dispatch({ type: QueryBarActions.ApplyFromHistory, fields, diff --git a/packages/compass-query-bar/src/utils/query.ts b/packages/compass-query-bar/src/utils/query.ts index 91694a6289d..081a3cd6412 100644 --- a/packages/compass-query-bar/src/utils/query.ts +++ b/packages/compass-query-bar/src/utils/query.ts @@ -56,7 +56,7 @@ export function doesQueryHaveExtraOptionsSet(fields?: QueryFormFields) { export function parseQueryAttributesToFormFields( query: Record, - preferences: Pick + preferences: Pick ): QueryFormFields { return Object.fromEntries( Object.entries(query) @@ -81,7 +81,7 @@ export function parseQueryAttributesToFormFields( * Map query document to the query fields state only preserving valid values */ export function mapQueryToFormFields( - preferences: Pick, + preferences: Pick, query?: BaseQuery, onlyValid = true ): QueryFormFields { @@ -125,7 +125,10 @@ function isQueryProperty(field: string): field is QueryProperty { export function validateField( field: string, value: string, - { maxTimeMS: preferencesMaxTimeMS }: Pick + { + maxTimeMS: preferencesMaxTimeMS, + maxTimeMSEnvLimit, + }: Pick ) { const validated = validate(field, value); if (field === 'filter' && validated === '') { @@ -137,13 +140,24 @@ export function validateField( } // Additional validation for maxTimeMS to make sure that we are not over the - // upper bound set in preferences + // upper bound set in preferences or environment limits if (field === 'maxTimeMS') { + const maxTimeMS = Number(value); + + // When environment limit is set (> 0), enforce it + if ( + maxTimeMSEnvLimit && + !Number.isNaN(maxTimeMS) && + maxTimeMS > maxTimeMSEnvLimit + ) { + return false; + } + + // Standard preference validation if ( typeof preferencesMaxTimeMS !== 'undefined' && value && - Number(value) > - (preferencesMaxTimeMS ?? DEFAULT_FIELD_VALUES['maxTimeMS']) + maxTimeMS > (preferencesMaxTimeMS ?? DEFAULT_FIELD_VALUES['maxTimeMS']) ) { return false; } @@ -160,7 +174,7 @@ export function validateField( export function isQueryFieldsValid( fields: QueryFormFields, - preferences: Pick + preferences: Pick ) { return Object.entries(fields).every( ([key, value]) => validateField(key, value.string, preferences) !== false diff --git a/packages/compass-web/src/entrypoint.tsx b/packages/compass-web/src/entrypoint.tsx index bd862df26c9..083806962bc 100644 --- a/packages/compass-web/src/entrypoint.tsx +++ b/packages/compass-web/src/entrypoint.tsx @@ -382,7 +382,10 @@ const CompassWeb = ({ onLog, onDebug, }); - const preferencesAccess = useCompassWebPreferences(initialPreferences); + const preferencesAccess = useCompassWebPreferences({ + ...initialPreferences, + maxTimeMSEnvLimit: 300_000, // 5 minutes limit for Data Explorer + }); // TODO (COMPASS-9565): My Queries feature flag will be used to conditionally provide storage providers const initialWorkspaceRef = useRef(initialWorkspace); const initialWorkspaceTabsRef = useRef(