Skip to content

Commit 002fefc

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

File tree

6 files changed

+386
-10
lines changed

6 files changed

+386
-10
lines changed

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

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,90 @@ import InProgressIndexActions from './in-progress-index-actions';
1313

1414
describe('IndexActions Component', function () {
1515
let onDeleteSpy: SinonSpy;
16+
let onDeleteIndexSpy: SinonSpy;
1617

1718
before(cleanup);
1819
afterEach(cleanup);
1920
beforeEach(function () {
2021
onDeleteSpy = spy();
22+
onDeleteIndexSpy = spy();
2123
});
2224

23-
it('does not render the delete button for an in progress index that is still in progress', function () {
25+
it('renders a spinner and cancel action for an in progress index that is still in progress', function () {
2426
render(
2527
<InProgressIndexActions
2628
index={{
2729
name: 'artist_id_index',
2830
status: 'inprogress',
2931
}}
32+
onDeleteIndexClick={onDeleteIndexSpy}
3033
onDeleteFailedIndexClick={onDeleteSpy}
3134
/>
3235
);
3336

34-
const button = screen.queryByTestId('index-actions-delete-action');
35-
expect(button).to.not.exist;
37+
const spinner = screen.getByTestId('index-building-spinner');
38+
expect(spinner).to.exist;
39+
40+
// Should now have a cancel action for in-progress indexes
41+
const cancelButton = screen.getByTestId('index-actions-delete-action');
42+
expect(cancelButton).to.exist;
43+
expect(cancelButton.getAttribute('aria-label')).to.equal(
44+
'Cancel Index artist_id_index'
45+
);
46+
});
47+
48+
it('renders a spinner with initial progress percentage for an in progress index', function () {
49+
render(
50+
<InProgressIndexActions
51+
index={{
52+
name: 'artist_id_index',
53+
status: 'inprogress',
54+
progressPercentage: 75,
55+
}}
56+
onDeleteIndexClick={onDeleteIndexSpy}
57+
onDeleteFailedIndexClick={onDeleteSpy}
58+
/>
59+
);
60+
61+
const spinner = screen.getByTestId('index-building-spinner');
62+
expect(spinner).to.exist;
63+
// The component starts with the provided progress percentage
64+
expect(spinner).to.contain.text('75%');
65+
});
66+
67+
it('renders a spinner with default text when no progress percentage is available', function () {
68+
render(
69+
<InProgressIndexActions
70+
index={{
71+
name: 'artist_id_index',
72+
status: 'inprogress',
73+
}}
74+
onDeleteIndexClick={onDeleteIndexSpy}
75+
onDeleteFailedIndexClick={onDeleteSpy}
76+
/>
77+
);
78+
79+
const spinner = screen.getByTestId('index-building-spinner');
80+
expect(spinner).to.exist;
81+
});
82+
83+
it('calls onDeleteIndexClick when cancel button is clicked for in-progress index', function () {
84+
render(
85+
<InProgressIndexActions
86+
index={{
87+
name: 'artist_id_index',
88+
status: 'inprogress',
89+
}}
90+
onDeleteIndexClick={onDeleteIndexSpy}
91+
onDeleteFailedIndexClick={onDeleteSpy}
92+
/>
93+
);
94+
95+
const cancelButton = screen.getByTestId('index-actions-delete-action');
96+
expect(onDeleteIndexSpy.callCount).to.equal(0);
97+
userEvent.click(cancelButton);
98+
expect(onDeleteIndexSpy.callCount).to.equal(1);
99+
expect(onDeleteIndexSpy.getCall(0).args[0]).to.equal('artist_id_index');
36100
});
37101

38102
it('renders delete button for an in progress index that has failed', function () {
@@ -42,6 +106,7 @@ describe('IndexActions Component', function () {
42106
name: 'artist_id_index',
43107
status: 'failed',
44108
}}
109+
onDeleteIndexClick={onDeleteIndexSpy}
45110
onDeleteFailedIndexClick={onDeleteSpy}
46111
/>
47112
);

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

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,62 @@
11
import React, { useCallback, useMemo } from 'react';
22
import type { GroupedItemAction } from '@mongodb-js/compass-components';
3-
import { ItemActionGroup } from '@mongodb-js/compass-components';
3+
import {
4+
ItemActionGroup,
5+
SpinLoader,
6+
css,
7+
spacing,
8+
Body,
9+
} from '@mongodb-js/compass-components';
410
import type { InProgressIndex } from '../../modules/regular-indexes';
511

612
type Index = {
713
name: string;
814
status: InProgressIndex['status'];
15+
progressPercentage?: number;
916
};
1017

1118
type IndexActionsProps = {
1219
index: Index;
20+
onDeleteIndexClick: (name: string) => void;
1321
onDeleteFailedIndexClick: (name: string) => void;
1422
};
1523

1624
type IndexAction = 'delete';
1725

26+
const combinedContainerStyles = css({
27+
display: 'flex',
28+
alignItems: 'center',
29+
gap: spacing[200],
30+
minWidth: spacing[800],
31+
justifyContent: 'flex-end',
32+
});
33+
34+
const progressTextStyles = css({
35+
fontSize: '12px',
36+
fontWeight: 'normal',
37+
});
38+
1839
const IndexActions: React.FunctionComponent<IndexActionsProps> = ({
1940
index,
41+
onDeleteIndexClick,
2042
onDeleteFailedIndexClick,
2143
}) => {
2244
const indexActions: GroupedItemAction<IndexAction>[] = useMemo(() => {
2345
const actions: GroupedItemAction<IndexAction>[] = [];
2446

25-
// you can only drop regular indexes or failed inprogress indexes
47+
// you can drop failed inprogress indexes or cancel inprogress indexes
2648
if (index.status === 'failed') {
2749
actions.push({
2850
action: 'delete',
2951
label: `Drop Index ${index.name}`,
3052
icon: 'Trash',
3153
});
54+
} else if (index.status === 'inprogress') {
55+
actions.push({
56+
action: 'delete',
57+
label: `Cancel Index ${index.name}`,
58+
icon: 'XWithCircle',
59+
});
3260
}
3361

3462
return actions;
@@ -37,12 +65,40 @@ const IndexActions: React.FunctionComponent<IndexActionsProps> = ({
3765
const onAction = useCallback(
3866
(action: IndexAction) => {
3967
if (action === 'delete') {
40-
onDeleteFailedIndexClick(index.name);
68+
if (index.status === 'inprogress') {
69+
onDeleteIndexClick(index.name);
70+
} else {
71+
onDeleteFailedIndexClick(index.name);
72+
}
4173
}
4274
},
43-
[onDeleteFailedIndexClick, index]
75+
[onDeleteFailedIndexClick, onDeleteIndexClick, index]
4476
);
4577

78+
// Show spinner with progress percentage for in-progress indexes
79+
if (index.status === 'inprogress') {
80+
// Use provided progress percentage or show default text
81+
const currentProgress = index.progressPercentage ?? 0;
82+
const progressText =
83+
currentProgress > 0 ? `${Math.round(currentProgress)}%` : '';
84+
85+
return (
86+
<div
87+
className={combinedContainerStyles}
88+
data-testid="index-building-spinner"
89+
>
90+
<Body className={progressTextStyles}>{progressText}</Body>
91+
<SpinLoader size={16} title="Index build in progress" />
92+
<ItemActionGroup<IndexAction>
93+
data-testid="index-actions"
94+
actions={indexActions}
95+
onAction={onAction}
96+
/>
97+
</div>
98+
);
99+
}
100+
101+
// Show actions for failed indexes (delete action)
46102
return (
47103
<ItemActionGroup<IndexAction>
48104
data-testid="index-actions"

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

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useEffect } from 'react';
1+
import React, { useMemo, useEffect, useCallback, useRef } from 'react';
22
import { connect } from 'react-redux';
33
import { withPreferences } from 'compass-preferences-model/provider';
44
import { useWorkspaceTabId } from '@mongodb-js/compass-workspaces/provider';
@@ -27,6 +27,7 @@ import {
2727
unhideIndex,
2828
startPollingRegularIndexes,
2929
stopPollingRegularIndexes,
30+
trackIndexProgress,
3031
} from '../../modules/regular-indexes';
3132

3233
import type {
@@ -45,6 +46,7 @@ type RegularIndexesTableProps = {
4546
onUnhideIndexClick: (name: string) => void;
4647
onDeleteIndexClick: (name: string) => void;
4748
onDeleteFailedIndexClick: (name: string) => void;
49+
onTrackIndexProgress: (indexId: string, indexName: string) => Promise<void>;
4850
readOnly?: boolean;
4951
error?: string | null;
5052
onRegularIndexesOpened: (tabId: string) => void;
@@ -286,10 +288,15 @@ function getInProgressIndexInfo(
286288
index: MappedInProgressIndex,
287289
{
288290
onDeleteFailedIndexClick,
291+
onDeleteIndexClick,
289292
}: {
290293
onDeleteFailedIndexClick: (indexName: string) => void;
294+
onDeleteIndexClick: (indexName: string) => void;
291295
}
292296
): CommonIndexInfo {
297+
// Use progress directly from Redux state
298+
const progressToUse = index.progressPercentage;
299+
293300
return {
294301
id: index.id,
295302
name: index.name,
@@ -302,8 +309,9 @@ function getInProgressIndexInfo(
302309
status: <StatusField status={index.status} error={index.error} />,
303310
actions: (
304311
<InProgressIndexActions
305-
index={index}
312+
index={{ ...index, progressPercentage: progressToUse }}
306313
onDeleteFailedIndexClick={onDeleteFailedIndexClick}
314+
onDeleteIndexClick={onDeleteIndexClick}
307315
></InProgressIndexActions>
308316
),
309317
};
@@ -381,12 +389,74 @@ export const RegularIndexesTable: React.FunctionComponent<
381389
onUnhideIndexClick,
382390
onDeleteIndexClick,
383391
onDeleteFailedIndexClick,
392+
onTrackIndexProgress,
384393
onRegularIndexesOpened,
385394
onRegularIndexesClosed,
386395
error,
387396
}) => {
388397
const tabId = useWorkspaceTabId();
389398

399+
// Use Redux state for progress tracking instead of local state
400+
const intervalsRef = useRef<Record<string, NodeJS.Timeout>>({});
401+
402+
// Function to start progress tracking for an index
403+
const startProgressTracking = useCallback(
404+
(indexId: string, indexName: string) => {
405+
// Clear any existing interval for this index
406+
if (intervalsRef.current[indexId]) {
407+
clearInterval(intervalsRef.current[indexId]);
408+
}
409+
410+
// Start interval to check real progress every 2 seconds
411+
intervalsRef.current[indexId] = setInterval(() => {
412+
// Track real progress using the Redux action
413+
onTrackIndexProgress(indexId, indexName).catch(() => {
414+
// If real tracking fails, the error is already handled in the action
415+
// No fallback needed here since we want to rely on real progress only
416+
});
417+
}, 2000); // 2 second intervals for real progress checking
418+
},
419+
[onTrackIndexProgress]
420+
);
421+
422+
// Function to stop progress tracking for an index
423+
const stopProgressTracking = useCallback((indexId: string) => {
424+
if (intervalsRef.current[indexId]) {
425+
clearInterval(intervalsRef.current[indexId]);
426+
delete intervalsRef.current[indexId];
427+
}
428+
}, []);
429+
430+
// Manage progress tracking based on inProgressIndexes changes
431+
useEffect(() => {
432+
const currentInProgressIds = new Set(
433+
inProgressIndexes.map((index) => index.id)
434+
);
435+
const trackedIds = new Set(Object.keys(intervalsRef.current));
436+
437+
// Start tracking for new in-progress indexes
438+
inProgressIndexes.forEach((index) => {
439+
if (index.status === 'inprogress' && !trackedIds.has(index.id)) {
440+
startProgressTracking(index.id, index.name);
441+
}
442+
});
443+
444+
// Stop tracking for indexes that are no longer in progress
445+
trackedIds.forEach((indexId) => {
446+
if (!currentInProgressIds.has(indexId)) {
447+
stopProgressTracking(indexId);
448+
}
449+
});
450+
}, [inProgressIndexes, startProgressTracking, stopProgressTracking]);
451+
452+
// Cleanup intervals on unmount
453+
useEffect(() => {
454+
const currentIntervals = intervalsRef.current;
455+
return () => {
456+
Object.values(currentIntervals).forEach(clearInterval);
457+
};
458+
}, []);
459+
390460
useEffect(() => {
391461
onRegularIndexesOpened(tabId);
392462
return () => {
@@ -407,6 +477,8 @@ export const RegularIndexesTable: React.FunctionComponent<
407477
if (index.compassIndexType === 'in-progress-index') {
408478
indexData = getInProgressIndexInfo(index, {
409479
onDeleteFailedIndexClick,
480+
onDeleteIndexClick,
481+
// Remove currentProgress since we're using Redux state directly
410482
});
411483
} else if (index.compassIndexType === 'rolling-index') {
412484
indexData = getRollingIndexInfo(index);
@@ -479,6 +551,7 @@ const mapDispatch = {
479551
onUnhideIndexClick: unhideIndex,
480552
onRegularIndexesOpened: startPollingRegularIndexes,
481553
onRegularIndexesClosed: stopPollingRegularIndexes,
554+
onTrackIndexProgress: trackIndexProgress,
482555
};
483556

484557
export default connect(

0 commit comments

Comments
 (0)