Skip to content

Commit 1c456ad

Browse files
mabaasitaddaleax
andauthored
feat(explain-aggregation): show index key in explain COMPASS-5857 (#3154)
Co-authored-by: Anna Henningsen <[email protected]>
1 parent 128ca79 commit 1c456ad

File tree

12 files changed

+279
-44
lines changed

12 files changed

+279
-44
lines changed
Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,96 @@
11
import React from 'react';
2-
import { Badge, BadgeVariant, Body } from '@mongodb-js/compass-components';
3-
import type { IndexInformation } from '@mongodb-js/explain-plan-helper';
2+
import {
3+
Badge,
4+
BadgeVariant,
5+
css,
6+
Icon,
7+
spacing,
8+
uiColors,
9+
Accordion,
10+
} from '@mongodb-js/compass-components';
11+
import type { ExplainIndex } from '../../modules/explain';
12+
import type { IndexDirection } from 'mongodb';
413

514
type ExplainIndexesProps = {
6-
indexes: IndexInformation[];
15+
indexes: ExplainIndex[];
716
};
817

18+
const IndexDirectionIcon = ({ direction }: { direction: IndexDirection }) => {
19+
return direction === 1 ? (
20+
<Icon glyph="ArrowUp" />
21+
) : direction === -1 ? (
22+
<Icon glyph="ArrowDown" />
23+
) : (
24+
<>({String(direction)})</>
25+
);
26+
};
27+
28+
const containerStyles = css({
29+
display: 'flex',
30+
flexDirection: 'column',
31+
gap: spacing[1],
32+
});
33+
34+
const accordianContainerStyles = css({
35+
marginTop: spacing[1],
36+
marginBottom: spacing[1],
37+
});
38+
39+
const accordianContentStyles = css({
40+
marginTop: spacing[1],
41+
'*:not(:last-child)': {
42+
marginRight: spacing[1],
43+
},
44+
});
45+
46+
const shardStyles = css({
47+
color: uiColors.gray.dark1,
48+
});
49+
950
export const ExplainIndexes: React.FunctionComponent<ExplainIndexesProps> = ({
1051
indexes,
1152
}) => {
12-
if (indexes.filter(({ index }) => index).length === 0) {
13-
return <Body weight="medium">No index available for this query.</Body>;
53+
if (indexes.length === 0) {
54+
return null;
1455
}
1556

1657
return (
17-
<div>
18-
{indexes.map((info, idx) => (
19-
<Badge key={idx} variant={BadgeVariant.LightGray}>
20-
{info.index} {info.shard && <>({info.shard})</>}
21-
</Badge>
22-
))}
58+
<div className={containerStyles}>
59+
{indexes.map(
60+
({ name, shard, key: indexKeys }: ExplainIndex, arrIndex) => {
61+
const title = shard ? (
62+
<>
63+
{name}&nbsp;
64+
<span className={shardStyles}>({shard})</span>
65+
</>
66+
) : (
67+
name
68+
);
69+
return (
70+
<div className={accordianContainerStyles} key={arrIndex}>
71+
<Accordion
72+
text={title}
73+
data-testid={`explain-index-button-${name}-${shard ?? ''}`}
74+
>
75+
<div
76+
className={accordianContentStyles}
77+
data-testid={`explain-index-content-${name}-${shard ?? ''}`}
78+
>
79+
{Object.entries(indexKeys).map(
80+
([keyName, direction], listIndex) => (
81+
<Badge variant={BadgeVariant.LightGray} key={listIndex}>
82+
{keyName}
83+
&nbsp;
84+
<IndexDirectionIcon direction={direction} />
85+
</Badge>
86+
)
87+
)}
88+
</div>
89+
</Accordion>
90+
</div>
91+
);
92+
}
93+
)}
2394
</div>
2495
);
2596
};

packages/compass-aggregations/src/components/pipeline-explain/explain-query-performance.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import React from 'react';
22
import { Body, Subtitle, css, spacing } from '@mongodb-js/compass-components';
3-
import type { IndexInformation } from '@mongodb-js/explain-plan-helper';
4-
3+
import type { ExplainIndex } from '../../modules/explain';
54
import { ExplainIndexes } from './explain-indexes';
65

76
type ExplainQueryPerformanceProps = {
87
executionTimeMillis: number;
98
nReturned: number;
10-
usedIndexes: IndexInformation[];
9+
indexes: ExplainIndex[];
1110
};
1211

1312
const containerStyles = css({
@@ -33,7 +32,7 @@ const statTitleStyles = css({
3332
});
3433

3534
export const ExplainQueryPerformance: React.FunctionComponent<ExplainQueryPerformanceProps> =
36-
({ nReturned, executionTimeMillis, usedIndexes }) => {
35+
({ nReturned, executionTimeMillis, indexes }) => {
3736
return (
3837
<div
3938
className={containerStyles}
@@ -49,16 +48,21 @@ export const ExplainQueryPerformance: React.FunctionComponent<ExplainQueryPerfor
4948
)}
5049
{executionTimeMillis > 0 && (
5150
<div className={statItemStyles}>
52-
<Body>Actual query execution time(ms):</Body>
51+
<Body>Actual query execution time (ms):</Body>
5352
<Body weight="medium">{executionTimeMillis}</Body>
5453
</div>
5554
)}
5655
<div className={statItemStyles}>
5756
<Body className={statTitleStyles}>
5857
Query used the following indexes:
5958
</Body>
60-
<ExplainIndexes indexes={usedIndexes} />
59+
<Body weight="medium">
60+
{indexes.length === 0
61+
? 'No index available for this query.'
62+
: indexes.length}
63+
</Body>
6164
</div>
65+
<ExplainIndexes indexes={indexes} />
6266
</div>
6367
</div>
6468
);

packages/compass-aggregations/src/components/pipeline-explain/explain-results.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const ExplainResults: React.FunctionComponent<ExplainResultsProps> = ({
5555
<ExplainQueryPerformance
5656
nReturned={stats.nReturned}
5757
executionTimeMillis={stats.executionTimeMillis}
58-
usedIndexes={stats.usedIndexes}
58+
indexes={stats.indexes}
5959
/>
6060
</div>
6161
)}

packages/compass-aggregations/src/components/pipeline-explain/index.spec.tsx

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe('PipelineExplain', function () {
8282
stats: {
8383
executionTimeMillis: 20,
8484
nReturned: 100,
85-
usedIndexes: [{ index: 'name', shard: 'shard1' }],
85+
indexes: [],
8686
},
8787
plan: {
8888
stages: [],
@@ -103,7 +103,70 @@ describe('PipelineExplain', function () {
103103
expect(within(summary).getByText(/actual query execution time/gi)).to.exist;
104104
expect(within(summary).getByText(/query used the following indexes/gi)).to
105105
.exist;
106+
expect(within(summary).getByText(/no index available for this query./gi)).to
107+
.exist;
106108

107109
expect(screen.getByTestId('pipeline-explain-footer-close-button')).to.exist;
108110
});
111+
112+
it('renders explain results - indexes', function () {
113+
renderPipelineExplain({
114+
explain: {
115+
stats: {
116+
executionTimeMillis: 20,
117+
nReturned: 100,
118+
indexes: [
119+
{
120+
name: 'compound_index',
121+
shard: 'shard1',
122+
key: { host_id: 1, location: '2dsphere' },
123+
},
124+
{
125+
name: 'compound_index',
126+
shard: 'shard2',
127+
key: { city_id: -1, title: 'text' },
128+
},
129+
],
130+
},
131+
plan: {
132+
stages: [],
133+
},
134+
},
135+
});
136+
const summary = screen.getByTestId('pipeline-explain-results-summary');
137+
138+
// Toggle first accordian
139+
userEvent.click(
140+
within(summary).getByTestId('explain-index-button-compound_index-shard1')
141+
);
142+
const indexContent1 = within(summary).getByTestId(
143+
'explain-index-content-compound_index-shard1'
144+
);
145+
expect(indexContent1).to.exist;
146+
147+
expect(within(indexContent1).getByText(/host_id/gi)).to.exist;
148+
expect(
149+
within(indexContent1).getByRole('img', {
150+
name: /arrow up icon/i, // host_id index direction 1
151+
})
152+
).to.exist;
153+
expect(within(indexContent1).getByText(/location \(2dsphere\)/i)).to.exist;
154+
155+
// Toggle second accordian
156+
userEvent.click(
157+
within(summary).getByTestId('explain-index-button-compound_index-shard2')
158+
);
159+
const indexContent2 = within(summary).getByTestId(
160+
'explain-index-content-compound_index-shard2'
161+
);
162+
expect(indexContent2).to.exist;
163+
164+
expect(within(indexContent2).getByText(/city_id/gi)).to.exist;
165+
expect(
166+
within(indexContent2).getByRole('img', {
167+
name: /arrow down icon/i, // city_id index direction -1
168+
})
169+
).to.exist;
170+
expect(within(indexContent2).getByText(/title \(text\)/i)).to.exist;
171+
});
109172
});

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

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging';
88
import type { RootState } from '.';
99
import { DEFAULT_MAX_TIME_MS } from '../constants';
1010
import { generateStage } from './stage';
11+
import type { IndexInfo } from './indexes';
1112

1213
const { log, mongoLogId } = createLoggerAndTelemetry(
1314
'COMPASS-AGGREGATIONS-UI'
@@ -44,12 +45,19 @@ export type Actions =
4445
| ExplainFailedAction
4546
| ExplainCancelledAction;
4647

48+
49+
export type ExplainIndex = {
50+
name: string;
51+
shard?: string;
52+
key: IndexInfo['key'];
53+
}
54+
4755
export type ExplainData = {
4856
plan: Document;
4957
stats?: {
5058
executionTimeMillis: number;
5159
nReturned: number;
52-
usedIndexes: IndexInformation[];
60+
indexes: ExplainIndex[];
5361
};
5462
};
5563

@@ -139,6 +147,7 @@ export const explainAggregation = (): ThunkAction<
139147
maxTimeMS,
140148
collation,
141149
dataService: { dataService },
150+
indexes: collectionIndexes,
142151
} = getState();
143152

144153
if (!dataService) {
@@ -182,10 +191,11 @@ export const explainAggregation = (): ThunkAction<
182191
executionTimeMillis,
183192
usedIndexes
184193
} = new ExplainPlan(rawExplain as any);
194+
const indexes = mapIndexesInformation(collectionIndexes, usedIndexes);
185195
const stats = {
186196
executionTimeMillis,
187197
nReturned,
188-
usedIndexes,
198+
indexes,
189199
}
190200
explain.stats = stats;
191201
} catch (e) {
@@ -237,4 +247,26 @@ const getExplainVerbosity = (
237247
: ExplainVerbosity.allPlansExecution;
238248
};
239249

250+
const mapIndexesInformation = function (
251+
collectionIndexes: IndexInfo[],
252+
explainIndexes: IndexInformation[]
253+
): ExplainIndex[] {
254+
return explainIndexes
255+
.filter(x => x.index)
256+
.map((explainIndex) => {
257+
const index = collectionIndexes.find(
258+
(collectionIndex) => collectionIndex.name === explainIndex.index
259+
);
260+
if (!index) {
261+
return null;
262+
}
263+
return {
264+
name: index.name,
265+
shard: explainIndex.shard,
266+
key: index.key,
267+
};
268+
})
269+
.filter(Boolean) as ExplainIndex[];
270+
}
271+
240272
export default reducer;

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ import workspace, {
132132
INITIAL_STATE as WORKSPACE_INITIAL_STATE
133133
} from './workspace';
134134
import aggregationWorkspaceId from './aggregation-workspace-id';
135+
import indexes, {
136+
INITIAL_STATE as INDEXES_INITIAL_STATE,
137+
} from './indexes';
135138

136139
import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging';
137140
const { track, debug } = createLoggerAndTelemetry('COMPASS-AGGREGATIONS-UI');
@@ -187,6 +190,7 @@ export const INITIAL_STATE = {
187190
countDocuments: COUNT_INITIAL_STATE,
188191
explain: EXPLAIN_INITIAL_STATE,
189192
isDataLake: DATALAKE_INITIAL_STATE,
193+
indexes: INDEXES_INITIAL_STATE,
190194
};
191195

192196
/**
@@ -269,6 +273,7 @@ const appReducer = combineReducers({
269273
aggregationWorkspaceId,
270274
explain,
271275
isDataLake,
276+
indexes,
272277
});
273278

274279
export type RootState = ReturnType<typeof appReducer>;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Reducer } from 'redux';
2+
import type { IndexDirection } from 'mongodb';
3+
4+
export enum ActionTypes {
5+
IndexesFetched = 'compass-aggregations/indexesFetched',
6+
}
7+
8+
export type IndexInfo = {
9+
ns: string;
10+
name: string;
11+
key: Record<string, IndexDirection>;
12+
extra: Record<string, any>;
13+
};
14+
15+
type IndexesFetchedAction = {
16+
type: ActionTypes.IndexesFetched;
17+
indexes: IndexInfo[];
18+
};
19+
20+
export type Actions = IndexesFetchedAction;
21+
22+
export type State = IndexInfo[];
23+
24+
export const INITIAL_STATE: State = [];
25+
26+
const reducer: Reducer<State, Actions> = (state = INITIAL_STATE, action) => {
27+
switch (action.type) {
28+
case ActionTypes.IndexesFetched:
29+
return action.indexes;
30+
default:
31+
return state;
32+
}
33+
};
34+
35+
export const indexesFetched = (indexes: IndexInfo[]): IndexesFetchedAction => ({
36+
type: ActionTypes.IndexesFetched,
37+
indexes,
38+
});
39+
40+
export default reducer;

0 commit comments

Comments
 (0)