Skip to content

Commit d03488e

Browse files
authored
feat(query-bar, collection, explain-plan): replace explain plan tab with explain button COMPASS-6827 (#4465)
* feat(query-bar, collection, explain-plan): replace explain plan tab with explain button * chore(query-bar): update label to match design * chore(query-bar): hide cue if explain button was clicked
1 parent 129d5d5 commit d03488e

File tree

18 files changed

+517
-99
lines changed

18 files changed

+517
-99
lines changed

package-lock.json

Lines changed: 25 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-aggregations/src/modules/explain.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ export const explainAggregation = (): PipelineBuilderThunkAction<void> => {
77
const pipeline = getPipelineFromBuilderState(getState(), pipelineBuilder);
88
const {
99
collationString: { value: collation },
10+
maxTimeMS,
1011
} = getState();
1112

1213
dispatch(
1314
localAppRegistryEmit('open-explain-plan-modal', {
14-
aggregation: { pipeline, collation },
15+
aggregation: {
16+
pipeline,
17+
collation,
18+
maxTimeMS,
19+
},
1520
})
1621
);
1722
};

packages/compass-collection/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,18 @@
5959
"@mongodb-js/compass-components": "^1.9.0",
6060
"@mongodb-js/compass-logging": "^1.1.6",
6161
"bson": "^5.2.0",
62-
"hadron-app-registry": "^9.0.7",
63-
"hadron-ipc": "^3.1.3",
62+
"compass-preferences-model": "^2.8.0",
63+
"hadron-app-registry": "^9.0.6",
64+
"hadron-ipc": "^3.1.2",
6465
"react": "^17.0.2"
6566
},
6667
"dependencies": {
6768
"@mongodb-js/compass-components": "^1.9.0",
6869
"@mongodb-js/compass-logging": "^1.1.6",
6970
"bson": "^5.2.0",
70-
"hadron-app-registry": "^9.0.7",
71-
"hadron-ipc": "^3.1.3"
71+
"compass-preferences-model": "^2.8.0",
72+
"hadron-app-registry": "^9.0.6",
73+
"hadron-ipc": "^3.1.2"
7274
},
7375
"devDependencies": {
7476
"@mongodb-js/eslint-config-compass": "^1.0.6",

packages/compass-collection/src/components/collection/collection.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type AppRegistry from 'hadron-app-registry';
22
import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging';
33
import type { Document } from 'mongodb';
4-
import React, { useCallback, useEffect } from 'react';
4+
import React, { useCallback, useEffect, useMemo } from 'react';
55
import { TabNavBar, css } from '@mongodb-js/compass-components';
6-
6+
import { usePreference } from 'compass-preferences-model';
77
import CollectionHeader from '../collection-header';
88
import { getCollectionStatsInitialState } from '../../modules/stats';
99
import type { CollectionStatsMap } from '../../modules/stats';
@@ -77,13 +77,26 @@ const Collection: React.FunctionComponent<CollectionProps> = ({
7777
sourceName,
7878
activeSubTab,
7979
id,
80-
tabs,
81-
views,
80+
tabs: _tabs,
81+
views: _views,
8282
localAppRegistry,
8383
globalAppRegistry,
8484
changeActiveSubTab,
8585
scopedModals,
8686
}: CollectionProps) => {
87+
const newExplainPlan = usePreference('newExplainPlan', React);
88+
const explainPlanTabId = _tabs.indexOf('Explain Plan');
89+
const tabs = useMemo(() => {
90+
return _tabs.filter((_, id) => {
91+
return !newExplainPlan || id !== explainPlanTabId;
92+
});
93+
}, [newExplainPlan, explainPlanTabId, _tabs]);
94+
const views = useMemo(() => {
95+
return _views.filter((_, id) => {
96+
return !newExplainPlan || id !== explainPlanTabId;
97+
});
98+
}, [newExplainPlan, explainPlanTabId, _views]);
99+
87100
const activeSubTabName =
88101
tabs && tabs.length > 0
89102
? trackingIdForTabName(tabs[activeSubTab] || 'Unknown')

packages/compass-components/src/components/more-options-toggle.tsx

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ import { mergeProps } from '../utils/merge-props';
88

99
const optionContainerStyles = css({
1010
textAlign: 'center',
11-
minWidth: spacing[4] * 5,
1211
});
1312

1413
const optionsButtonStyles = css({
1514
// Reset button styles.
1615
backgroundColor: 'transparent',
1716
border: 'none',
18-
19-
padding: `${spacing[1]}px ${spacing[2]}px`,
17+
paddingTop: spacing[1],
18+
paddingBottom: spacing[1],
19+
paddingLeft: spacing[2],
20+
paddingRight: spacing[2],
2021
});
2122

2223
const optionStyles = css({
@@ -25,6 +26,8 @@ const optionStyles = css({
2526
});
2627

2728
type MoreOptionsToggleProps = {
29+
label?: (expanded: boolean) => string;
30+
'aria-label'?: (expanded: boolean) => string;
2831
'aria-controls': string;
2932
'data-testid'?: string;
3033
isExpanded: boolean;
@@ -40,20 +43,27 @@ export const MoreOptionsToggle: React.FunctionComponent<
4043
id,
4144
'data-testid': dataTestId,
4245
onToggleOptions,
46+
label = (expanded) => {
47+
return expanded ? 'Fewer Options' : 'More Options';
48+
},
49+
'aria-label': ariaLabel,
4350
}) => {
4451
const optionsIcon = useMemo(
4552
() => (isExpanded ? 'CaretDown' : 'CaretRight'),
4653
[isExpanded]
4754
);
48-
const optionsLabel = useMemo(
49-
() => (isExpanded ? 'Fewer Options' : 'More Options'),
50-
[isExpanded]
51-
);
55+
const optionsLabel = label(isExpanded);
56+
const labelStyle = useMemo(() => {
57+
const maxLabelLength = Math.max(label(true).length, label(false).length);
58+
return {
59+
// Maximum char length of the more / less label + icon size + button padding
60+
width: `calc(${maxLabelLength}ch + ${spacing[3]}px + ${spacing[2]}px)`,
61+
};
62+
}, [label]);
63+
const optionsAriaLabel = ariaLabel?.(isExpanded) ?? optionsLabel;
5264
const focusRingProps = useFocusRing();
5365
const buttonProps = mergeProps(
54-
{
55-
className: optionsButtonStyles,
56-
},
66+
{ className: optionsButtonStyles },
5767
focusRingProps
5868
// We cast here so that the `as` prop of link can be properly typed.
5969
) as Partial<React.ComponentType<React.ComponentProps<typeof Link>>>;
@@ -62,9 +72,9 @@ export const MoreOptionsToggle: React.FunctionComponent<
6272
}, [onToggleOptions]);
6373

6474
return (
65-
<div className={optionContainerStyles}>
75+
<div className={optionContainerStyles} style={labelStyle}>
6676
<Link
67-
aria-label={optionsLabel}
77+
aria-label={optionsAriaLabel}
6878
aria-expanded={isExpanded}
6979
aria-controls={ariaControls}
7080
as="button"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
3+
/**
4+
* React useState, but the value is persisted in Web API compatible Storage and
5+
* initial state is picked up from Storage if exists
6+
*
7+
* @param id storage key
8+
* @param initialValue initial value to use if one is not available
9+
* @param storage
10+
* @returns
11+
*/
12+
function usePersistedState<S>(
13+
id: string,
14+
initialValue: S | (() => S),
15+
storage: Storage = globalThis.localStorage
16+
) {
17+
const idRef = useRef(id);
18+
const storageRef = useRef(storage);
19+
const [state, setState] = useState<S>(() => {
20+
const initialStored = storageRef.current.getItem(idRef.current);
21+
if (initialStored) {
22+
try {
23+
return JSON.parse(initialStored);
24+
} catch (e) {
25+
throw new Error(
26+
'Failed to parse stored value, usePersistedState only supports serializeable values. ' +
27+
(e as Error).message
28+
);
29+
}
30+
}
31+
if (typeof initialValue === 'function') {
32+
return (initialValue as () => S)();
33+
}
34+
return initialValue;
35+
});
36+
useEffect(() => {
37+
storageRef.current.setItem(idRef.current, JSON.stringify(state));
38+
}, [state]);
39+
return [state, setState] as const;
40+
}
41+
42+
export { usePersistedState };

packages/compass-components/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,4 @@ export {
165165
} from './hooks/use-hotkeys';
166166
export { rafraf } from './utils/rafraf';
167167
export { ComboboxWithCustomOption } from './components/combobox-with-custom-option';
168+
export { usePersistedState } from './hooks/use-persisted-state';

packages/compass-crud/src/components/crud-toolbar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ const CrudToolbar: React.FunctionComponent<CrudToolbarProps> = ({
178178
buttonLabel="Find"
179179
onApply={onApplyClicked}
180180
onReset={onResetClicked}
181+
showExplainButton
181182
/>
182183
)}
183184
</div>

packages/compass-explain-plan/src/components/explain-plan-modal.spec.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ describe('ExplainPlanModal', function () {
3232
});
3333

3434
it('should render ready state', function () {
35-
render({
36-
status: 'ready',
37-
});
35+
render({ status: 'ready' });
3836
expect(screen.getByText('Query Performance Summary')).to.exist;
3937
});
4038
});

packages/compass-explain-plan/src/components/explain-plan-side-summary.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ const ExplainPlanSummaryStat = <T extends string | boolean | number>({
3232
formatter = defaultFormatter,
3333
label,
3434
definition,
35+
'data-testid': dataTestId,
3536
}: {
3637
as?: keyof ReactHTML;
3738
glyph?: ReactElement;
3839
value?: T;
3940
formatter?: (val: T) => string;
4041
label: ReactNode;
4142
definition: string;
43+
['data-testid']?: string;
4244
}): ReactElement | null => {
4345
return React.createElement(
4446
as,
@@ -54,6 +56,7 @@ const ExplainPlanSummaryStat = <T extends string | boolean | number>({
5456
definition={definition}
5557
className={statsTextStyles}
5658
tooltipProps={{ align: 'left', spacing: spacing[3] }}
59+
data-testid={dataTestId}
5760
>
5861
{typeof value === 'undefined' ? (
5962
label
@@ -139,7 +142,10 @@ export const ExplainPlanSummary: React.FunctionComponent<
139142
}, [indexType]);
140143

141144
return (
142-
<KeylineCard className={summaryCardStyles}>
145+
<KeylineCard
146+
className={summaryCardStyles}
147+
data-testid="explain-plan-summary"
148+
>
143149
<Subtitle
144150
className={cx(
145151
summaryHeadingStyles,
@@ -161,6 +167,7 @@ export const ExplainPlanSummary: React.FunctionComponent<
161167
</svg>
162168
}
163169
value={docsReturned}
170+
data-testid="docsReturned"
164171
label="documents returned"
165172
definition="Number of documents returned by the query."
166173
></ExplainPlanSummaryStat>
@@ -176,6 +183,7 @@ export const ExplainPlanSummary: React.FunctionComponent<
176183
</svg>
177184
}
178185
value={docsExamined}
186+
data-testid="docsExamined"
179187
label="documents examined"
180188
definition="Number of documents examined during query execution. When an index covers a query, this value is 0."
181189
></ExplainPlanSummaryStat>
@@ -190,6 +198,7 @@ export const ExplainPlanSummary: React.FunctionComponent<
190198
</svg>
191199
}
192200
value={executionTimeMs}
201+
data-testid="executionTimeMs"
193202
formatter={(val) => `${String(val)}\xa0ms`}
194203
label="execution time"
195204
definition="Total time in milliseconds for query plan selection and query execution."
@@ -207,6 +216,7 @@ export const ExplainPlanSummary: React.FunctionComponent<
207216
</svg>
208217
}
209218
value={sortedInMemory}
219+
data-testid="sortedInMemory"
210220
formatter={(val) => (val ? 'Is' : 'Is not')}
211221
label="sorted in memory"
212222
definition="Indicates whether the sort operation occurred in system memory. In-memory sorts perform better than on-disk sorts."
@@ -223,6 +233,7 @@ export const ExplainPlanSummary: React.FunctionComponent<
223233
</svg>
224234
}
225235
value={indexKeysExamined}
236+
data-testid="indexKeysExamined"
226237
label="index keys examined"
227238
definition="Number of indexes examined to fulfill the query."
228239
></ExplainPlanSummaryStat>

0 commit comments

Comments
 (0)