Skip to content

Commit 3dc8abd

Browse files
committed
feat(compass-indexes): add index build progress COMPASS-9495
1 parent 1162b73 commit 3dc8abd

File tree

6 files changed

+431
-34
lines changed

6 files changed

+431
-34
lines changed

packages/compass-indexes/src/components/regular-indexes-table/regular-index-actions.tsx

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,39 @@
11
import semver from 'semver';
22
import React, { useCallback, useMemo } from 'react';
33
import type { GroupedItemAction } from '@mongodb-js/compass-components';
4-
import { css, ItemActionGroup } from '@mongodb-js/compass-components';
4+
import {
5+
css,
6+
ItemActionGroup,
7+
SpinLoader,
8+
spacing,
9+
Body,
10+
} from '@mongodb-js/compass-components';
511

612
const styles = css({
713
// Align actions with the end of the table
814
justifyContent: 'flex-end',
915
});
1016

17+
const combinedContainerStyles = css({
18+
display: 'flex',
19+
alignItems: 'center',
20+
gap: spacing[200],
21+
minWidth: spacing[800],
22+
justifyContent: 'flex-end',
23+
});
24+
25+
const progressTextStyles = css({
26+
fontSize: '12px',
27+
fontWeight: 'normal',
28+
});
29+
1130
type Index = {
1231
name: string;
1332
extra?: {
1433
hidden?: boolean;
1534
};
35+
status?: 'inprogress' | 'ready' | 'failed';
36+
progressPercentage?: number;
1637
};
1738

1839
type IndexActionsProps = {
@@ -44,30 +65,42 @@ const IndexActions: React.FunctionComponent<IndexActionsProps> = ({
4465
}) => {
4566
const indexActions: GroupedItemAction<IndexAction>[] = useMemo(() => {
4667
const actions: GroupedItemAction<IndexAction>[] = [];
68+
const progressPercentage = index.progressPercentage ?? 0;
4769

48-
if (serverSupportsHideIndex(serverVersion)) {
49-
actions.push(
50-
index.extra?.hidden
51-
? {
52-
action: 'unhide',
53-
label: `Unhide Index ${index.name}`,
54-
tooltip: `Unhide Index`,
55-
icon: 'Visibility',
56-
}
57-
: {
58-
action: 'hide',
59-
label: `Hide Index ${index.name}`,
60-
tooltip: `Hide Index`,
61-
icon: 'VisibilityOff',
62-
}
63-
);
64-
}
70+
if (progressPercentage < 100 && progressPercentage > 0) {
71+
// partially built
72+
actions.push({
73+
action: 'delete',
74+
label: `Cancel Index ${index.name}`,
75+
icon: 'XWithCircle',
76+
variant: 'destructive',
77+
});
78+
} else {
79+
// completed
80+
if (serverSupportsHideIndex(serverVersion)) {
81+
actions.push(
82+
index.extra?.hidden
83+
? {
84+
action: 'unhide',
85+
label: `Unhide Index ${index.name}`,
86+
tooltip: `Unhide Index`,
87+
icon: 'Visibility',
88+
}
89+
: {
90+
action: 'hide',
91+
label: `Hide Index ${index.name}`,
92+
tooltip: `Hide Index`,
93+
icon: 'VisibilityOff',
94+
}
95+
);
96+
}
6597

66-
actions.push({
67-
action: 'delete',
68-
label: `Drop Index ${index.name}`,
69-
icon: 'Trash',
70-
});
98+
actions.push({
99+
action: 'delete',
100+
label: `Drop Index ${index.name}`,
101+
icon: 'Trash',
102+
});
103+
}
71104

72105
return actions;
73106
}, [index, serverVersion]);
@@ -85,6 +118,30 @@ const IndexActions: React.FunctionComponent<IndexActionsProps> = ({
85118
[onDeleteIndexClick, onHideIndexClick, onUnhideIndexClick, index]
86119
);
87120

121+
const progressPercentage = index.progressPercentage ?? 0;
122+
if (
123+
index.status !== 'failed' &&
124+
progressPercentage < 100 &&
125+
progressPercentage > 0
126+
) {
127+
const progressText = `${Math.round(progressPercentage)}%`;
128+
129+
return (
130+
<div
131+
className={combinedContainerStyles}
132+
data-testid="index-building-spinner"
133+
>
134+
<Body className={progressTextStyles}>Building... {progressText}</Body>
135+
<SpinLoader size={16} title="Index build in progress" />
136+
<ItemActionGroup<IndexAction>
137+
data-testid="index-actions"
138+
actions={indexActions}
139+
onAction={onAction}
140+
/>
141+
</div>
142+
);
143+
}
144+
88145
return (
89146
<ItemActionGroup<IndexAction>
90147
data-testid="index-actions"

packages/compass-indexes/src/components/regular-indexes-table/regular-indexes-table.tsx

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
} from '@mongodb-js/compass-components';
1111

1212
import type { RootState } from '../../modules';
13+
import { useIndexProgress } from '../../hooks/use-index-progress';
1314

1415
import TypeField from './type-field';
1516
import SizeField from './size-field';
@@ -248,12 +249,16 @@ function mergeIndexes(
248249
const rollingIndexNames = new Set(
249250
rollingIndexes.map((index) => index.indexName)
250251
);
252+
const inProgressIndexNames = new Set(
253+
inProgressIndexes.map(({ name }) => name)
254+
);
251255

252256
const mappedIndexes: MappedRegularIndex[] = indexes
253257
// exclude partially-built indexes so that we don't include indexes that
254258
// only exist on the primary node and then duplicate those as rolling
255259
// builds in the same table
256260
.filter((index) => !rollingIndexNames.has(index.name))
261+
.filter((index) => !inProgressIndexNames.has(index.name))
257262
.map((index) => {
258263
return { ...index, compassIndexType: 'regular-index' };
259264
});
@@ -286,10 +291,44 @@ function getInProgressIndexInfo(
286291
index: MappedInProgressIndex,
287292
{
288293
onDeleteFailedIndexClick,
294+
onDeleteIndexClick,
295+
serverVersion,
289296
}: {
290297
onDeleteFailedIndexClick: (indexName: string) => void;
298+
onDeleteIndexClick: (indexName: string) => void;
299+
serverVersion: string;
291300
}
292301
): CommonIndexInfo {
302+
// Use progress directly from Redux state
303+
const progressToUse = index.progressPercentage ?? 0;
304+
305+
// Show spinner and progress only if:
306+
// 1. Index is not failed (status !== 'failed')
307+
// 2. Index has meaningful progress (> 0% and < 100%)
308+
let actionsComponent;
309+
if (index.status !== 'failed' && progressToUse > 0 && progressToUse < 100) {
310+
actionsComponent = (
311+
<RegularIndexActions
312+
index={{
313+
name: index.name,
314+
progressPercentage: progressToUse,
315+
}}
316+
serverVersion={serverVersion}
317+
onDeleteIndexClick={onDeleteIndexClick}
318+
onHideIndexClick={() => {}} // No-op for building indexes
319+
onUnhideIndexClick={() => {}} // No-op for building indexes
320+
/>
321+
);
322+
} else {
323+
// For failed or pending indexes, use the InProgressIndexActions
324+
actionsComponent = (
325+
<InProgressIndexActions
326+
index={index}
327+
onDeleteFailedIndexClick={onDeleteFailedIndexClick}
328+
/>
329+
);
330+
}
331+
293332
return {
294333
id: index.id,
295334
name: index.name,
@@ -300,12 +339,7 @@ function getInProgressIndexInfo(
300339
// TODO(COMPASS-8335): add properties for in-progress indexes
301340
properties: null,
302341
status: <StatusField status={index.status} error={index.error} />,
303-
actions: (
304-
<InProgressIndexActions
305-
index={index}
306-
onDeleteFailedIndexClick={onDeleteFailedIndexClick}
307-
></InProgressIndexActions>
308-
),
342+
actions: actionsComponent,
309343
};
310344
}
311345

@@ -387,6 +421,9 @@ export const RegularIndexesTable: React.FunctionComponent<
387421
}) => {
388422
const tabId = useWorkspaceTabId();
389423

424+
// Use our custom hook to handle index progress tracking
425+
useIndexProgress(inProgressIndexes);
426+
390427
useEffect(() => {
391428
onRegularIndexesOpened(tabId);
392429
return () => {
@@ -407,6 +444,8 @@ export const RegularIndexesTable: React.FunctionComponent<
407444
if (index.compassIndexType === 'in-progress-index') {
408445
indexData = getInProgressIndexInfo(index, {
409446
onDeleteFailedIndexClick,
447+
onDeleteIndexClick,
448+
serverVersion,
410449
});
411450
} else if (index.compassIndexType === 'rolling-index') {
412451
indexData = getRollingIndexInfo(index);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { useEffect, useRef } from 'react';
2+
import { useDispatch } from 'react-redux';
3+
import type { InProgressIndex } from '../modules/regular-indexes';
4+
import { getIndexesProgress } from '../modules/regular-indexes';
5+
import type { IndexesThunkDispatch } from '../modules';
6+
7+
/** 1 seconds polling interval */
8+
const INDEX_INIT_PROGRESS_POLLING_INTERVAL_MS = 1 * 1000;
9+
/** 10 seconds polling interval */
10+
const INDEX_PROGRESS_POLLING_INTERVAL_MS = 10 * 1000;
11+
12+
/**
13+
* Custom hook to manage index build progress tracking
14+
* This hook automatically starts/stops progress polling for indexes that are:
15+
* 1. Being created (status: 'inprogress') - monitors for when build starts
16+
* 2. Currently building (progressPercentage > 0% and < 100%) - tracks build progress
17+
*/
18+
export function useIndexProgress(inProgressIndexes: InProgressIndex[]) {
19+
const dispatch = useDispatch<IndexesThunkDispatch>();
20+
const timeoutRef = useRef<number | undefined>(undefined);
21+
const checksRef = useRef<number>(0);
22+
23+
useEffect(() => {
24+
const indexesToTrack = inProgressIndexes.filter((index) => {
25+
const notFailed = index.status !== 'failed';
26+
const inProgress = index.status === 'inprogress';
27+
const progressPercentage = index.progressPercentage ?? 0;
28+
const stillPartiallyBuilt =
29+
progressPercentage > 0 && progressPercentage < 100;
30+
31+
const shouldTrack = notFailed && (inProgress || stillPartiallyBuilt);
32+
33+
return shouldTrack;
34+
});
35+
36+
clearTimeout(timeoutRef.current);
37+
timeoutRef.current = undefined;
38+
39+
if (indexesToTrack.length) {
40+
checksRef.current = 0;
41+
42+
const updateIndexProgress = () => {
43+
checksRef.current += 1;
44+
void dispatch(getIndexesProgress(indexesToTrack)).finally(() => {
45+
if (timeoutRef.current) {
46+
// After the first 3 checks, slow down the poller
47+
setTimeout(
48+
updateIndexProgress,
49+
checksRef.current < 3
50+
? INDEX_INIT_PROGRESS_POLLING_INTERVAL_MS
51+
: INDEX_PROGRESS_POLLING_INTERVAL_MS
52+
);
53+
}
54+
});
55+
};
56+
57+
if (!timeoutRef.current) updateIndexProgress();
58+
}
59+
60+
return () => {
61+
clearTimeout(timeoutRef.current);
62+
timeoutRef.current = undefined;
63+
};
64+
}, [inProgressIndexes, dispatch]);
65+
}

packages/compass-indexes/src/modules/regular-indexes.spec.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
unhideIndex,
1010
startPollingRegularIndexes,
1111
stopPollingRegularIndexes,
12+
trackIndexProgress,
13+
indexCreationStarted,
1214
} from './regular-indexes';
1315
import {
1416
indexesList,
@@ -547,4 +549,84 @@ describe('regular-indexes module', function () {
547549
expect(collection.fetch.callCount).to.equal(0);
548550
});
549551
});
552+
553+
describe('#trackIndexProgress action', function () {
554+
it('updates progress when currentOp returns matching index operation', async function () {
555+
const currentOpResponse = {
556+
inprog: [
557+
{
558+
command: {
559+
createIndexes: 'trips',
560+
indexes: [{ name: 'test_index_1' }],
561+
},
562+
progress: { done: 50, total: 100 },
563+
ns: 'citibike.trips', // Use the same namespace as the test setup
564+
},
565+
],
566+
};
567+
568+
const store = setupStore(
569+
{},
570+
{
571+
currentOp: () => Promise.resolve(currentOpResponse),
572+
}
573+
);
574+
575+
// Add an in-progress index to track
576+
const mockIndex = {
577+
id: 'test-index-id',
578+
name: 'test_index_1',
579+
fields: [{ field: 'field1', value: 1 }],
580+
status: 'inprogress' as const,
581+
progressPercentage: 0,
582+
};
583+
584+
store.dispatch(indexCreationStarted(mockIndex));
585+
586+
// Track progress
587+
await store.dispatch(trackIndexProgress('test-index-id', 'test_index_1'));
588+
589+
const state = store.getState();
590+
const inProgressIndex = state.regularIndexes.inProgressIndexes.find(
591+
(index) => index.id === 'test-index-id'
592+
);
593+
594+
expect(inProgressIndex?.progressPercentage).to.equal(50);
595+
});
596+
597+
it('does not update progress when no matching index operation is found', async function () {
598+
const currentOpResponse = {
599+
inprog: [], // No operations in progress
600+
};
601+
602+
const store = setupStore(
603+
{},
604+
{
605+
currentOp: () => Promise.resolve(currentOpResponse),
606+
}
607+
);
608+
609+
// Add an in-progress index to track
610+
const mockIndex = {
611+
id: 'test-index-id',
612+
name: 'test_index_1',
613+
fields: [{ field: 'field1', value: 1 }],
614+
status: 'inprogress' as const,
615+
progressPercentage: 0,
616+
};
617+
618+
store.dispatch(indexCreationStarted(mockIndex));
619+
620+
// Track progress
621+
await store.dispatch(trackIndexProgress('test-index-id', 'test_index_1'));
622+
623+
const state = store.getState();
624+
const inProgressIndex = state.regularIndexes.inProgressIndexes.find(
625+
(index) => index.id === 'test-index-id'
626+
);
627+
628+
// Progress should remain unchanged
629+
expect(inProgressIndex?.progressPercentage).to.equal(0);
630+
});
631+
});
550632
});

0 commit comments

Comments
 (0)