Skip to content

Commit 44cc2ed

Browse files
authored
fix(compass-query-bar): add a maxTimeMS over 5min warning to CompassWeb COMPASS-9023 (#7380)
1 parent c345081 commit 44cc2ed

File tree

7 files changed

+209
-75
lines changed

7 files changed

+209
-75
lines changed

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

Lines changed: 75 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';
@@ -58,16 +59,48 @@ const PipelineCollation: React.FunctionComponent<PipelineCollationProps> = ({
5859
maxTimeMSValue,
5960
maxTimeMSChanged,
6061
}) => {
62+
const maxTimeMSEnvLimit = usePreference('maxTimeMSEnvLimit');
63+
6164
const onMaxTimeMSChanged = useCallback(
6265
(evt: React.ChangeEvent<HTMLInputElement>) => {
6366
if (maxTimeMSChanged) {
64-
maxTimeMSChanged(parseInt(evt.currentTarget.value, 10));
67+
const parsed = Number(evt.currentTarget.value);
68+
const newValue = Number.isNaN(parsed) ? 0 : parsed;
69+
70+
// When environment limit is set (> 0), enforce it
71+
if (maxTimeMSEnvLimit && newValue > maxTimeMSEnvLimit) {
72+
maxTimeMSChanged(maxTimeMSEnvLimit);
73+
} else {
74+
maxTimeMSChanged(newValue);
75+
}
6576
}
6677
},
67-
[maxTimeMSChanged]
78+
[maxTimeMSChanged, maxTimeMSEnvLimit]
6879
);
80+
6981
const maxTimeMSLimit = usePreference('maxTimeMS');
7082

83+
// Determine the effective max limit when environment limit is set (> 0)
84+
const effectiveMaxLimit = useMemo(() => {
85+
if (maxTimeMSEnvLimit) {
86+
return maxTimeMSLimit
87+
? Math.min(maxTimeMSLimit, maxTimeMSEnvLimit)
88+
: maxTimeMSEnvLimit;
89+
}
90+
return maxTimeMSLimit;
91+
}, [maxTimeMSEnvLimit, maxTimeMSLimit]);
92+
93+
// Check if value exceeds the environment limit (when limit > 0)
94+
const exceedsLimit = Boolean(
95+
useMemo(() => {
96+
return (
97+
maxTimeMSEnvLimit &&
98+
maxTimeMSValue &&
99+
maxTimeMSValue >= maxTimeMSEnvLimit
100+
);
101+
}, [maxTimeMSEnvLimit, maxTimeMSValue])
102+
);
103+
71104
return (
72105
<div
73106
className={pipelineOptionsContainerStyles}
@@ -107,27 +140,45 @@ const PipelineCollation: React.FunctionComponent<PipelineCollationProps> = ({
107140
>
108141
Max Time MS
109142
</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-
/>
143+
<Tooltip
144+
enabled={exceedsLimit}
145+
open={exceedsLimit}
146+
trigger={({
147+
children,
148+
...triggerProps
149+
}: React.HTMLProps<HTMLDivElement>) => (
150+
<div {...triggerProps}>
151+
<TextInput
152+
aria-labelledby={maxTimeMSLabelId}
153+
id={maxTimeMSInputId}
154+
data-testid="max-time-ms"
155+
className={inputStyles}
156+
placeholder={`${Math.min(
157+
DEFAULT_MAX_TIME_MS,
158+
effectiveMaxLimit || Infinity
159+
)}`}
160+
type="number"
161+
min="0"
162+
max={effectiveMaxLimit}
163+
sizeVariant="small"
164+
value={`${maxTimeMSValue ?? ''}`}
165+
state={
166+
(maxTimeMSValue &&
167+
effectiveMaxLimit &&
168+
maxTimeMSValue > effectiveMaxLimit) ||
169+
exceedsLimit
170+
? 'error'
171+
: 'none'
172+
}
173+
onChange={onMaxTimeMSChanged}
174+
/>
175+
{children}
176+
</div>
177+
)}
178+
>
179+
Operations longer than 5 minutes are not supported in the web
180+
environment
181+
</Tooltip>
131182
</div>
132183
);
133184
};

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

Lines changed: 13 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+
maxTimeMSEnvLimit?: number;
108110
};
109111

110112
/**
@@ -1062,6 +1064,17 @@ export const storedUserPreferencesProps: Required<{
10621064
validator: z.boolean().default(true),
10631065
type: 'boolean',
10641066
},
1067+
maxTimeMSEnvLimit: {
1068+
ui: true,
1069+
cli: true,
1070+
global: true,
1071+
description: {
1072+
short:
1073+
'Maximum time limit for operations in environment (milliseconds). Set to 0 for no limit.',
1074+
},
1075+
validator: z.number().min(0).default(0),
1076+
type: 'number',
1077+
},
10651078

10661079
...allFeatureFlagsProps,
10671080
};

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

Lines changed: 69 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',
@@ -153,6 +155,22 @@ const QueryOption: React.FunctionComponent<QueryOptionProps> = ({
153155
}
154156
}, [track, name, connectionInfoRef]);
155157

158+
// MaxTimeMS warning tooltip logic
159+
const maxTimeMSEnvLimit = usePreference('maxTimeMSEnvLimit');
160+
const numericValue = useMemo(() => {
161+
if (!value) return 0;
162+
const parsed = Number(value);
163+
return Number.isNaN(parsed) ? 0 : parsed;
164+
}, [value]);
165+
166+
const exceedsMaxTimeMSLimit = useMemo(() => {
167+
return (
168+
name === 'maxTimeMS' &&
169+
maxTimeMSEnvLimit && // 0 is falsy, so no limit when 0
170+
numericValue >= maxTimeMSEnvLimit
171+
);
172+
}, [name, maxTimeMSEnvLimit, numericValue]);
173+
156174
return (
157175
<div
158176
className={cx(
@@ -200,30 +218,56 @@ const QueryOption: React.FunctionComponent<QueryOptionProps> = ({
200218
/>
201219
) : (
202220
<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-
)}
221+
{({ props }) => {
222+
const textInput = (
223+
<TextInput
224+
aria-labelledby={`query-bar-option-input-${name}-label`}
225+
id={id}
226+
data-testid={`query-bar-option-${name}-input`}
227+
className={cx(
228+
darkMode
229+
? numericTextInputDarkStyles
230+
: numericTextInputLightStyles,
231+
hasError && optionInputWithErrorStyles
232+
)}
233+
type="text"
234+
sizeVariant="small"
235+
state={hasError ? 'error' : 'none'}
236+
value={value}
237+
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
238+
onValueChange(evt.currentTarget.value)
239+
}
240+
onBlur={onBlurEditor}
241+
placeholder={placeholder as string}
242+
disabled={disabled}
243+
{...props}
244+
/>
245+
);
246+
247+
// Wrap maxTimeMS field with tooltip in web environment when exceeding limit
248+
if (exceedsMaxTimeMSLimit) {
249+
return (
250+
<Tooltip
251+
enabled={true}
252+
open={true}
253+
trigger={({
254+
children,
255+
...triggerProps
256+
}: React.HTMLProps<HTMLDivElement>) => (
257+
<div {...triggerProps}>
258+
{textInput}
259+
{children}
260+
</div>
261+
)}
262+
>
263+
Operations longer than 5 minutes are not supported in the
264+
web environment
265+
</Tooltip>
266+
);
267+
}
268+
269+
return textInput;
270+
}}
227271
</WithOptionDefinitionTextInputProps>
228272
)}
229273
</div>

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,26 @@ export const OPTION_DEFINITION: {
7070
link: 'https://docs.mongodb.com/manual/reference/method/cursor.maxTimeMS/',
7171
extraTextInputProps() {
7272
const preferenceMaxTimeMS = usePreference('maxTimeMS');
73-
const props: { max?: number; placeholder?: string } = {
74-
max: preferenceMaxTimeMS,
73+
const maxTimeMSEnvLimit = usePreference('maxTimeMSEnvLimit');
74+
75+
// Determine the effective max limit when environment limit is set (> 0)
76+
const effectiveMaxLimit = maxTimeMSEnvLimit
77+
? preferenceMaxTimeMS
78+
? Math.min(preferenceMaxTimeMS, maxTimeMSEnvLimit)
79+
: maxTimeMSEnvLimit
80+
: preferenceMaxTimeMS;
81+
82+
const props: {
83+
max?: number;
84+
placeholder?: string;
85+
} = {
86+
max: effectiveMaxLimit,
7587
};
76-
if (preferenceMaxTimeMS !== undefined && preferenceMaxTimeMS < 60000) {
77-
props.placeholder = String(preferenceMaxTimeMS);
88+
89+
if (effectiveMaxLimit && effectiveMaxLimit < 60000) {
90+
props.placeholder = `${+effectiveMaxLimit}`;
7891
}
92+
7993
return props;
8094
},
8195
},

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

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type QueryBarState = {
5252

5353
export const INITIAL_STATE: QueryBarState = {
5454
isReadonlyConnection: false,
55-
fields: mapQueryToFormFields({}, DEFAULT_FIELD_VALUES),
55+
fields: mapQueryToFormFields({ maxTimeMSEnvLimit: 0 }, DEFAULT_FIELD_VALUES),
5656
expanded: false,
5757
serverVersion: '3.6.0',
5858
lastAppliedQuery: { source: null, query: {} },
@@ -103,6 +103,7 @@ export const changeField = (
103103
return (dispatch, getState, { preferences }) => {
104104
const parsedValue = validateField(name, stringValue, {
105105
maxTimeMS: preferences.getPreferences().maxTimeMS ?? undefined,
106+
maxTimeMSEnvLimit: preferences.getPreferences().maxTimeMSEnvLimit,
106107
});
107108
const isValid = parsedValue !== false;
108109
dispatch({
@@ -162,7 +163,7 @@ export const resetQuery = (
162163
return false;
163164
}
164165
const fields = mapQueryToFormFields(
165-
{ maxTimeMS: preferences.getPreferences().maxTimeMS },
166+
preferences.getPreferences(),
166167
DEFAULT_FIELD_VALUES
167168
);
168169
dispatch({ type: QueryBarActions.ResetQuery, fields, source });
@@ -179,10 +180,7 @@ export const setQuery = (
179180
query: BaseQuery
180181
): QueryBarThunkAction<void, SetQueryAction> => {
181182
return (dispatch, getState, { preferences }) => {
182-
const fields = mapQueryToFormFields(
183-
{ maxTimeMS: preferences.getPreferences().maxTimeMS },
184-
query
185-
);
183+
const fields = mapQueryToFormFields(preferences.getPreferences(), query);
186184
dispatch({ type: QueryBarActions.SetQuery, fields });
187185
};
188186
};
@@ -238,14 +236,11 @@ export const applyFromHistory = (
238236
}
239237
return acc;
240238
}, {});
241-
const fields = mapQueryToFormFields(
242-
{ maxTimeMS: preferences.getPreferences().maxTimeMS },
243-
{
244-
...DEFAULT_FIELD_VALUES,
245-
...query,
246-
...currentQuery,
247-
}
248-
);
239+
const fields = mapQueryToFormFields(preferences.getPreferences(), {
240+
...DEFAULT_FIELD_VALUES,
241+
...query,
242+
...currentQuery,
243+
});
249244
dispatch({
250245
type: QueryBarActions.ApplyFromHistory,
251246
fields,

0 commit comments

Comments
 (0)