Skip to content

Commit 26a282f

Browse files
committed
COMPASS-9023(compass-query-bar): add a maxTimeMS over 5min warning to CompassWeb
1 parent ba8f9d4 commit 26a282f

File tree

7 files changed

+222
-62
lines changed

7 files changed

+222
-62
lines changed

packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.tsx

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useCallback, useMemo } from 'react';
22
import { connect } from 'react-redux';
33
import type { ConnectedProps } from 'react-redux';
44
import {
@@ -8,6 +8,7 @@ import {
88
spacing,
99
TextInput,
1010
palette,
11+
Tooltip,
1112
} from '@mongodb-js/compass-components';
1213

1314
import type { RootState } from '../../../modules';
@@ -51,23 +52,58 @@ const collationInputId = 'aggregations-collation-toolbar-input';
5152
const maxTimeMSLabelId = 'aggregations-max-time-ms-toolbar-input-label';
5253
const maxTimeMSInputId = 'aggregations-max-time-ms-toolbar-input';
5354

55+
// Data Explorer limits (5 minutes = 300,000ms)
56+
const WEB_MAX_TIME_MS_LIMIT = 300_000; // 5 minutes
57+
5458
const PipelineCollation: React.FunctionComponent<PipelineCollationProps> = ({
5559
collationValue,
5660
collationHasError,
5761
collationStringChanged,
5862
maxTimeMSValue,
5963
maxTimeMSChanged,
6064
}) => {
65+
const showMaxTimeMSWarning = Boolean(usePreference('showMaxTimeMSWarning'));
66+
6167
const onMaxTimeMSChanged = useCallback(
6268
(evt: React.ChangeEvent<HTMLInputElement>) => {
6369
if (maxTimeMSChanged) {
64-
maxTimeMSChanged(parseInt(evt.currentTarget.value, 10));
70+
const parsed = Number(evt.currentTarget.value);
71+
const newValue = Number.isNaN(parsed) ? 0 : parsed;
72+
73+
// When warning is enabled, enforce the hard limit
74+
if (showMaxTimeMSWarning && newValue > WEB_MAX_TIME_MS_LIMIT) {
75+
maxTimeMSChanged(WEB_MAX_TIME_MS_LIMIT);
76+
} else {
77+
maxTimeMSChanged(newValue);
78+
}
6579
}
6680
},
67-
[maxTimeMSChanged]
81+
[maxTimeMSChanged, showMaxTimeMSWarning]
6882
);
83+
6984
const maxTimeMSLimit = usePreference('maxTimeMS');
7085

86+
// Determine the effective max limit when warning is enabled
87+
const effectiveMaxLimit = useMemo(() => {
88+
if (showMaxTimeMSWarning) {
89+
return maxTimeMSLimit
90+
? Math.min(maxTimeMSLimit, WEB_MAX_TIME_MS_LIMIT)
91+
: WEB_MAX_TIME_MS_LIMIT;
92+
}
93+
return maxTimeMSLimit;
94+
}, [showMaxTimeMSWarning, maxTimeMSLimit]);
95+
96+
// Check if value exceeds the limit when warning is enabled
97+
const exceedsLimit = Boolean(
98+
useMemo(() => {
99+
return (
100+
showMaxTimeMSWarning &&
101+
maxTimeMSValue &&
102+
maxTimeMSValue >= WEB_MAX_TIME_MS_LIMIT
103+
);
104+
}, [showMaxTimeMSWarning, maxTimeMSValue])
105+
);
106+
71107
return (
72108
<div
73109
className={pipelineOptionsContainerStyles}
@@ -107,27 +143,45 @@ const PipelineCollation: React.FunctionComponent<PipelineCollationProps> = ({
107143
>
108144
Max Time MS
109145
</Label>
110-
<TextInput
111-
aria-labelledby={maxTimeMSLabelId}
112-
id={maxTimeMSInputId}
113-
data-testid="max-time-ms"
114-
className={inputStyles}
115-
placeholder={`${Math.min(
116-
DEFAULT_MAX_TIME_MS,
117-
maxTimeMSLimit || Infinity
118-
)}`}
119-
type="number"
120-
min="0"
121-
max={maxTimeMSLimit}
122-
sizeVariant="small"
123-
value={`${maxTimeMSValue ?? ''}`}
124-
state={
125-
maxTimeMSValue && maxTimeMSLimit && maxTimeMSValue > maxTimeMSLimit
126-
? 'error'
127-
: 'none'
128-
}
129-
onChange={onMaxTimeMSChanged}
130-
/>
146+
<Tooltip
147+
enabled={exceedsLimit}
148+
open={exceedsLimit}
149+
trigger={({
150+
children,
151+
...triggerProps
152+
}: React.HTMLProps<HTMLDivElement>) => (
153+
<div {...triggerProps}>
154+
<TextInput
155+
aria-labelledby={maxTimeMSLabelId}
156+
id={maxTimeMSInputId}
157+
data-testid="max-time-ms"
158+
className={inputStyles}
159+
placeholder={`${Math.min(
160+
DEFAULT_MAX_TIME_MS,
161+
effectiveMaxLimit || Infinity
162+
)}`}
163+
type="number"
164+
min="0"
165+
max={effectiveMaxLimit}
166+
sizeVariant="small"
167+
value={`${maxTimeMSValue ?? ''}`}
168+
state={
169+
(maxTimeMSValue &&
170+
effectiveMaxLimit &&
171+
maxTimeMSValue > effectiveMaxLimit) ||
172+
exceedsLimit
173+
? 'error'
174+
: 'none'
175+
}
176+
onChange={onMaxTimeMSChanged}
177+
/>
178+
{children}
179+
</div>
180+
)}
181+
>
182+
Operations longer than 5 minutes are not supported in the web
183+
environment
184+
</Tooltip>
131185
</div>
132186
);
133187
};

packages/compass-preferences-model/src/preferences-schema.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ export type UserConfigurablePreferences = PermanentFeatureFlags &
105105
enableProxySupport: boolean;
106106
proxy: string;
107107
inferNamespacesFromPrivileges?: boolean;
108+
// Features that are enabled by default in Date Explorer, but are disabled in Compass
109+
showMaxTimeMSWarning?: boolean;
108110
};
109111

110112
/**
@@ -1050,6 +1052,16 @@ export const storedUserPreferencesProps: Required<{
10501052
validator: z.boolean().default(true),
10511053
type: 'boolean',
10521054
},
1055+
showMaxTimeMSWarning: {
1056+
ui: true,
1057+
cli: true,
1058+
global: true,
1059+
description: {
1060+
short: 'Show Max Time MS over 5min Warning for Data Explorer',
1061+
},
1062+
validator: z.boolean().default(false),
1063+
type: 'boolean',
1064+
},
10531065

10541066
...allFeatureFlagsProps,
10551067
};

packages/compass-query-bar/src/components/query-option.tsx

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import React, { useCallback, useRef } from 'react';
1+
import React, { useCallback, useRef, useMemo } from 'react';
22
import {
33
Label,
44
TextInput,
5+
Tooltip,
56
css,
67
cx,
78
spacing,
@@ -20,6 +21,7 @@ import type { QueryProperty } from '../constants/query-properties';
2021
import type { RootState } from '../stores/query-bar-store';
2122
import { useTelemetry } from '@mongodb-js/compass-telemetry/provider';
2223
import { useConnectionInfoRef } from '@mongodb-js/compass-connections/provider';
24+
import { usePreference } from 'compass-preferences-model/provider';
2325

2426
const queryOptionStyles = css({
2527
display: 'flex',
@@ -79,6 +81,9 @@ export const documentEditorLabelContainerStyles = css(
7981
}
8082
);
8183

84+
// Data Explorer limits for maxTimeMS (5 minutes = 300,000ms)
85+
const WEB_MAX_TIME_MS_LIMIT = 300_000; // 5 minutes
86+
8287
type QueryBarProperty = Exclude<QueryProperty, 'update'>;
8388

8489
type QueryOptionProps = {
@@ -153,6 +158,24 @@ const QueryOption: React.FunctionComponent<QueryOptionProps> = ({
153158
}
154159
}, [track, name, connectionInfoRef]);
155160

161+
// MaxTimeMS warning tooltip logic
162+
const showMaxTimeMSWarning = Boolean(usePreference('showMaxTimeMSWarning'));
163+
const numericValue = useMemo(() => {
164+
if (!value) return 0;
165+
const parsed = Number(value);
166+
return Number.isNaN(parsed) ? 0 : parsed;
167+
}, [value]);
168+
169+
const exceedsMaxTimeMSLimit = Boolean(
170+
useMemo(() => {
171+
return (
172+
name === 'maxTimeMS' &&
173+
showMaxTimeMSWarning &&
174+
numericValue >= WEB_MAX_TIME_MS_LIMIT
175+
);
176+
}, [name, showMaxTimeMSWarning, numericValue])
177+
);
178+
156179
return (
157180
<div
158181
className={cx(
@@ -200,30 +223,56 @@ const QueryOption: React.FunctionComponent<QueryOptionProps> = ({
200223
/>
201224
) : (
202225
<WithOptionDefinitionTextInputProps definition={optionDefinition}>
203-
{({ props }) => (
204-
<TextInput
205-
aria-labelledby={`query-bar-option-input-${name}-label`}
206-
id={id}
207-
data-testid={`query-bar-option-${name}-input`}
208-
className={cx(
209-
darkMode
210-
? numericTextInputDarkStyles
211-
: numericTextInputLightStyles,
212-
hasError && optionInputWithErrorStyles
213-
)}
214-
type="text"
215-
sizeVariant="small"
216-
state={hasError ? 'error' : 'none'}
217-
value={value}
218-
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
219-
onValueChange(evt.currentTarget.value)
220-
}
221-
onBlur={onBlurEditor}
222-
placeholder={placeholder as string}
223-
disabled={disabled}
224-
{...props}
225-
/>
226-
)}
226+
{({ props }) => {
227+
const textInput = (
228+
<TextInput
229+
aria-labelledby={`query-bar-option-input-${name}-label`}
230+
id={id}
231+
data-testid={`query-bar-option-${name}-input`}
232+
className={cx(
233+
darkMode
234+
? numericTextInputDarkStyles
235+
: numericTextInputLightStyles,
236+
hasError && optionInputWithErrorStyles
237+
)}
238+
type="text"
239+
sizeVariant="small"
240+
state={hasError ? 'error' : 'none'}
241+
value={value}
242+
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
243+
onValueChange(evt.currentTarget.value)
244+
}
245+
onBlur={onBlurEditor}
246+
placeholder={placeholder as string}
247+
disabled={disabled}
248+
{...props}
249+
/>
250+
);
251+
252+
// Wrap maxTimeMS field with tooltip in web environment when exceeding limit
253+
if (exceedsMaxTimeMSLimit) {
254+
return (
255+
<Tooltip
256+
enabled={true}
257+
open={true}
258+
trigger={({
259+
children,
260+
...triggerProps
261+
}: React.HTMLProps<HTMLDivElement>) => (
262+
<div {...triggerProps}>
263+
{textInput}
264+
{children}
265+
</div>
266+
)}
267+
>
268+
Operations longer than 5 minutes are not supported in the
269+
web environment
270+
</Tooltip>
271+
);
272+
}
273+
274+
return textInput;
275+
}}
227276
</WithOptionDefinitionTextInputProps>
228277
)}
229278
</div>

packages/compass-query-bar/src/constants/query-option-definition.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export type QueryOptionOfTypeDocument = Exclude<
99
'maxTimeMS' | 'limit' | 'skip'
1010
>;
1111

12+
// Data Explorer limits (5 minutes = 300,000ms)
13+
const WEB_MAX_TIME_MS_LIMIT = 300_000; // 5 minutes
14+
1215
export const OPTION_DEFINITION: {
1316
[optionName in QueryOption]: {
1417
name: optionName;
@@ -70,12 +73,27 @@ export const OPTION_DEFINITION: {
7073
link: 'https://docs.mongodb.com/manual/reference/method/cursor.maxTimeMS/',
7174
extraTextInputProps() {
7275
const preferenceMaxTimeMS = usePreference('maxTimeMS');
73-
const props: { max?: number; placeholder?: string } = {
74-
max: preferenceMaxTimeMS,
76+
const showMaxTimeMSWarning =
77+
usePreference('showMaxTimeMSWarning') ?? false;
78+
79+
// Determine the effective max limit when warning is enabled
80+
const effectiveMaxLimit = showMaxTimeMSWarning
81+
? preferenceMaxTimeMS
82+
? Math.min(preferenceMaxTimeMS, WEB_MAX_TIME_MS_LIMIT)
83+
: WEB_MAX_TIME_MS_LIMIT
84+
: preferenceMaxTimeMS;
85+
86+
const props: {
87+
max?: number;
88+
placeholder?: string;
89+
} = {
90+
max: effectiveMaxLimit,
7591
};
76-
if (preferenceMaxTimeMS !== undefined && preferenceMaxTimeMS < 60000) {
77-
props.placeholder = String(preferenceMaxTimeMS);
92+
93+
if (effectiveMaxLimit !== undefined && effectiveMaxLimit < 60000) {
94+
props.placeholder = String(effectiveMaxLimit);
7895
}
96+
7997
return props;
8098
},
8199
},

packages/compass-query-bar/src/stores/query-bar-reducer.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ export const changeField = (
103103
return (dispatch, getState, { preferences }) => {
104104
const parsedValue = validateField(name, stringValue, {
105105
maxTimeMS: preferences.getPreferences().maxTimeMS ?? undefined,
106+
showMaxTimeMSWarning:
107+
Boolean(preferences.getPreferences().showMaxTimeMSWarning) ?? false,
106108
});
107109
const isValid = parsedValue !== false;
108110
dispatch({
@@ -162,7 +164,12 @@ export const resetQuery = (
162164
return false;
163165
}
164166
const fields = mapQueryToFormFields(
165-
{ maxTimeMS: preferences.getPreferences().maxTimeMS },
167+
{
168+
maxTimeMS: preferences.getPreferences().maxTimeMS,
169+
showMaxTimeMSWarning: Boolean(
170+
preferences.getPreferences().showMaxTimeMSWarning
171+
),
172+
},
166173
DEFAULT_FIELD_VALUES
167174
);
168175
dispatch({ type: QueryBarActions.ResetQuery, fields, source });

0 commit comments

Comments
 (0)