diff --git a/packages/compass-indexes/src/components/indexes/indexes.spec.tsx b/packages/compass-indexes/src/components/indexes/indexes.spec.tsx index 370e79e0edd..559bb5662ee 100644 --- a/packages/compass-indexes/src/components/indexes/indexes.spec.tsx +++ b/packages/compass-indexes/src/components/indexes/indexes.spec.tsx @@ -190,6 +190,7 @@ describe('Indexes Component', function () { }, ], usageCount: 20, + buildProgress: 0, }, ], inProgressIndexes: [ @@ -203,6 +204,7 @@ describe('Indexes Component', function () { }, ], status: 'inprogress', + buildProgress: 0, }, ], error: undefined, @@ -245,6 +247,7 @@ describe('Indexes Component', function () { }, ], usageCount: 20, + buildProgress: 0, }, ], inProgressIndexes: [ @@ -259,6 +262,7 @@ describe('Indexes Component', function () { ], status: 'failed', error: 'Error message', + buildProgress: 0, }, ], error: undefined, diff --git a/packages/compass-indexes/src/components/regular-indexes-table/in-progress-index-actions.spec.tsx b/packages/compass-indexes/src/components/regular-indexes-table/in-progress-index-actions.spec.tsx index a9641c189de..f589c89d2dc 100644 --- a/packages/compass-indexes/src/components/regular-indexes-table/in-progress-index-actions.spec.tsx +++ b/packages/compass-indexes/src/components/regular-indexes-table/in-progress-index-actions.spec.tsx @@ -26,6 +26,7 @@ describe('IndexActions Component', function () { index={{ name: 'artist_id_index', status: 'inprogress', + buildProgress: 0, }} onDeleteFailedIndexClick={onDeleteSpy} /> @@ -41,6 +42,7 @@ describe('IndexActions Component', function () { index={{ name: 'artist_id_index', status: 'failed', + buildProgress: 0, }} onDeleteFailedIndexClick={onDeleteSpy} /> diff --git a/packages/compass-indexes/src/components/regular-indexes-table/in-progress-index-actions.tsx b/packages/compass-indexes/src/components/regular-indexes-table/in-progress-index-actions.tsx index 1596a1e541c..8cc5d533661 100644 --- a/packages/compass-indexes/src/components/regular-indexes-table/in-progress-index-actions.tsx +++ b/packages/compass-indexes/src/components/regular-indexes-table/in-progress-index-actions.tsx @@ -1,13 +1,21 @@ import React, { useCallback, useMemo } from 'react'; import type { GroupedItemAction } from '@mongodb-js/compass-components'; -import { ItemActionGroup } from '@mongodb-js/compass-components'; +import { ItemActionGroup, css, spacing } from '@mongodb-js/compass-components'; import type { InProgressIndex } from '../../modules/regular-indexes'; type Index = { name: string; status: InProgressIndex['status']; + buildProgress: number; }; +const indexActionsContainerStyles = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + gap: spacing[200], +}); + type IndexActionsProps = { index: Index; onDeleteFailedIndexClick: (name: string) => void; @@ -44,11 +52,13 @@ const IndexActions: React.FunctionComponent = ({ ); return ( - - data-testid="index-actions" - actions={indexActions} - onAction={onAction} - > +
+ + data-testid="index-actions" + actions={indexActions} + onAction={onAction} + /> +
); }; diff --git a/packages/compass-indexes/src/components/regular-indexes-table/regular-index-actions.spec.tsx b/packages/compass-indexes/src/components/regular-indexes-table/regular-index-actions.spec.tsx index f0606d5affa..7fe6983dc6f 100644 --- a/packages/compass-indexes/src/components/regular-indexes-table/regular-index-actions.spec.tsx +++ b/packages/compass-indexes/src/components/regular-indexes-table/regular-index-actions.spec.tsx @@ -10,6 +10,20 @@ import { spy } from 'sinon'; import type { SinonSpy } from 'sinon'; import RegularIndexActions from './regular-index-actions'; +import type { RegularIndex } from '../../modules/regular-indexes'; + +const commonIndexProperties: RegularIndex = { + name: 'artist_id_index', + type: 'regular', + cardinality: 'compound', + properties: [], + fields: [], + extra: {}, + size: 0, + relativeSize: 0, + usageCount: 0, + buildProgress: 0, +}; describe('IndexActions Component', function () { let onDeleteSpy: SinonSpy; @@ -24,10 +38,104 @@ describe('IndexActions Component', function () { onUnhideIndexSpy = spy(); }); + describe('build progress display', function () { + it('does not display progress percentage when buildProgress is 0', function () { + render( + + ); + + // Should not show building spinner or percentage + expect(() => screen.getByTestId('index-building-spinner')).to.throw( + /Unable to find/ + ); + expect(() => screen.getByText(/Building\.\.\. \d+%/)).to.throw( + /Unable to find/ + ); + }); + + it('displays progress percentage when buildProgress is 50% (0.5)', function () { + render( + + ); + + // Should show building spinner and percentage + const buildingSpinner = screen.getByTestId('index-building-spinner'); + expect(buildingSpinner).to.exist; + + const progressText = screen.getByText('Building... 50%'); + expect(progressText).to.exist; + }); + + it('does not display progress percentage when buildProgress is 100% (1.0)', function () { + render( + + ); + + // Should not show building spinner or percentage when complete + expect(() => screen.getByTestId('index-building-spinner')).to.throw; + expect(() => screen.getByText(/Building\.\.\. \d+%/)).to.throw; + }); + + it('displays cancel button when index is building', function () { + render( + + ); + + const cancelButton = screen.getByLabelText('Cancel Index building_index'); + expect(cancelButton).to.exist; + expect(onDeleteSpy.callCount).to.equal(0); + userEvent.click(cancelButton); + expect(onDeleteSpy.callCount).to.equal(1); + }); + }); + it('renders delete button for a regular index', function () { render( void; onHideIndexClick: (name: string) => void; @@ -44,33 +51,46 @@ const IndexActions: React.FunctionComponent = ({ }) => { const indexActions: GroupedItemAction[] = useMemo(() => { const actions: GroupedItemAction[] = []; + const buildProgress = index.buildProgress; + const isBuilding = buildProgress > 0 && buildProgress < 1; - if (serverSupportsHideIndex(serverVersion)) { - actions.push( - index.extra?.hidden - ? { - action: 'unhide', - label: `Unhide Index ${index.name}`, - tooltip: `Unhide Index`, - icon: 'Visibility', - } - : { - action: 'hide', - label: `Hide Index ${index.name}`, - tooltip: `Hide Index`, - icon: 'VisibilityOff', - } - ); - } + if (isBuilding) { + // partially built + actions.push({ + action: 'delete', + label: `Cancel Index ${index.name}`, + icon: 'XWithCircle', + variant: 'destructive', + }); + } else { + // completed + if (serverSupportsHideIndex(serverVersion)) { + actions.push( + index.extra?.hidden + ? { + action: 'unhide', + label: `Unhide Index ${index.name}`, + tooltip: `Unhide Index`, + icon: 'Visibility', + } + : { + action: 'hide', + label: `Hide Index ${index.name}`, + tooltip: `Hide Index`, + icon: 'VisibilityOff', + } + ); + } - actions.push({ - action: 'delete', - label: `Drop Index ${index.name}`, - icon: 'Trash', - }); + actions.push({ + action: 'delete', + label: `Drop Index ${index.name}`, + icon: 'Trash', + }); + } return actions; - }, [index, serverVersion]); + }, [index.name, index.extra?.hidden, index.buildProgress, serverVersion]); const onAction = useCallback( (action: IndexAction) => { @@ -85,6 +105,21 @@ const IndexActions: React.FunctionComponent = ({ [onDeleteIndexClick, onHideIndexClick, onUnhideIndexClick, index] ); + const buildProgress = index.buildProgress; + if (buildProgress > 0 && buildProgress < 1) { + return ( +
+ Building... {Math.trunc(buildProgress * 100)}% + + + data-testid="index-actions" + actions={indexActions} + onAction={onAction} + /> +
+ ); + } + return ( data-testid="index-actions" diff --git a/packages/compass-indexes/src/components/regular-indexes-table/regular-indexes-table.spec.tsx b/packages/compass-indexes/src/components/regular-indexes-table/regular-indexes-table.spec.tsx index dc98401ac5c..40937d9018c 100644 --- a/packages/compass-indexes/src/components/regular-indexes-table/regular-indexes-table.spec.tsx +++ b/packages/compass-indexes/src/components/regular-indexes-table/regular-indexes-table.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { cleanup, - render, + renderWithConnections, screen, within, userEvent, @@ -33,6 +33,7 @@ const indexes: RegularIndex[] = [ }, ], usageCount: 10, + buildProgress: 0, }, { ns: 'db.coll', @@ -56,6 +57,7 @@ const indexes: RegularIndex[] = [ }, ], usageCount: 15, + buildProgress: 0, }, { ns: 'db.coll', @@ -78,6 +80,7 @@ const indexes: RegularIndex[] = [ }, ], usageCount: 20, + buildProgress: 0, }, { ns: 'db.coll', @@ -100,6 +103,7 @@ const indexes: RegularIndex[] = [ }, ], usageCount: 25, + buildProgress: 0, }, ]; @@ -118,6 +122,7 @@ const inProgressIndexes: InProgressIndex[] = [ }, ], status: 'inprogress', + buildProgress: 0, }, { id: 'in-progress-2', @@ -130,6 +135,7 @@ const inProgressIndexes: InProgressIndex[] = [ ], status: 'inprogress', error: 'this is an error', + buildProgress: 0, }, ]; @@ -151,7 +157,7 @@ const rollingIndexes: RollingIndex[] = [ const renderIndexList = ( props: Partial> = {} ) => { - return render( + return renderWithConnections( index.indexName) ); + const inProgressIndexNames = new Set( + inProgressIndexes.map(({ name }) => name) + ); + + const regularIndexesByName = new Map( + indexes.map((index) => [index.name, index]) + ); const mappedIndexes: MappedRegularIndex[] = indexes // exclude partially-built indexes so that we don't include indexes that // only exist on the primary node and then duplicate those as rolling // builds in the same table - .filter((index) => !rollingIndexNames.has(index.name)) + .filter( + (index) => + !rollingIndexNames.has(index.name) && + !inProgressIndexNames.has(index.name) + ) .map((index) => { return { ...index, compassIndexType: 'regular-index' }; }); + // For in-progress indexes, merge in buildProgress from regular indexes if available const mappedInProgressIndexes: MappedInProgressIndex[] = inProgressIndexes.map((index) => { - return { ...index, compassIndexType: 'in-progress-index' }; + return { + ...index, + buildProgress: regularIndexesByName.get(index.name)?.buildProgress ?? 0, + compassIndexType: 'in-progress-index', + }; }); const mappedRollingIndexes: MappedRollingIndex[] = rollingIndexes.map( diff --git a/packages/compass-indexes/src/modules/regular-indexes.ts b/packages/compass-indexes/src/modules/regular-indexes.ts index 62d4cd5db53..c7aa94abaa8 100644 --- a/packages/compass-indexes/src/modules/regular-indexes.ts +++ b/packages/compass-indexes/src/modules/regular-indexes.ts @@ -36,12 +36,14 @@ export type RegularIndex = Partial & | 'size' | 'relativeSize' | 'usageCount' + | 'buildProgress' >; export type InProgressIndex = Pick & { id: string; status: 'inprogress' | 'failed'; error?: string; + buildProgress: number; }; export type RollingIndex = Partial & @@ -83,6 +85,7 @@ export const prepareInProgressIndex = ( status: 'inprogress', fields: inProgressIndexFields, name: inProgressIndexName, + buildProgress: 0, // TODO(COMPASS-8335): we never mapped properties and the table does have // room to display them }; diff --git a/packages/compass-indexes/test/fixtures/regular-indexes.ts b/packages/compass-indexes/test/fixtures/regular-indexes.ts index e29bc223a7c..2e4218d2d10 100644 --- a/packages/compass-indexes/test/fixtures/regular-indexes.ts +++ b/packages/compass-indexes/test/fixtures/regular-indexes.ts @@ -19,6 +19,7 @@ export const indexesList: IndexDefinition[] = [ ns: 'foo', fields: [], relativeSize: 1, + buildProgress: 0, }, { name: 'CCCC', @@ -37,6 +38,7 @@ export const indexesList: IndexDefinition[] = [ ns: 'foo', fields: [], relativeSize: 1, + buildProgress: 0, }, { name: 'AAAA', @@ -55,6 +57,7 @@ export const indexesList: IndexDefinition[] = [ ns: 'foo', fields: [], relativeSize: 1, + buildProgress: 0, }, { name: 'BBBB', @@ -73,6 +76,7 @@ export const indexesList: IndexDefinition[] = [ ns: 'foo', fields: [], relativeSize: 1, + buildProgress: 0, }, ]; @@ -96,6 +100,7 @@ export const defaultSortedIndexes: IndexDefinition[] = [ fields: [], relativeSize: 1, + buildProgress: 0, }, { ns: 'citibike.trips', @@ -126,6 +131,7 @@ export const defaultSortedIndexes: IndexDefinition[] = [ fields: [], relativeSize: 1, + buildProgress: 0, }, { ns: 'citibike.trips', @@ -143,6 +149,7 @@ export const defaultSortedIndexes: IndexDefinition[] = [ fields: [], relativeSize: 1, + buildProgress: 0, }, ]; @@ -167,6 +174,7 @@ export const usageSortedIndexes: IndexDefinition[] = [ fields: [], relativeSize: 1, + buildProgress: 0, }, { name: 'CCCC', @@ -185,6 +193,7 @@ export const usageSortedIndexes: IndexDefinition[] = [ fields: [], relativeSize: 1, + buildProgress: 0, }, { name: '_id_', @@ -203,6 +212,7 @@ export const usageSortedIndexes: IndexDefinition[] = [ fields: [], relativeSize: 1, + buildProgress: 0, }, { name: 'BBBB', @@ -234,6 +244,7 @@ export const usageSortedIndexes: IndexDefinition[] = [ fields: [], relativeSize: 1, + buildProgress: 0, }, ]; @@ -244,6 +255,7 @@ export const inProgressIndexes: InProgressIndex[] = [ //version: 2, fields: [], status: 'inprogress', + buildProgress: 0, }, { id: 'in-progress-2', @@ -255,5 +267,6 @@ export const inProgressIndexes: InProgressIndex[] = [ }, ], status: 'inprogress', + buildProgress: 0, }, ]; diff --git a/packages/compass-indexes/test/helpers.ts b/packages/compass-indexes/test/helpers.ts index 4857ca8dbee..d8170769feb 100644 --- a/packages/compass-indexes/test/helpers.ts +++ b/packages/compass-indexes/test/helpers.ts @@ -12,6 +12,8 @@ export function mockRegularIndex(info: Partial): RegularIndex { relativeSize: 0, cardinality: 'single', properties: [], + buildProgress: 0, + usageCount: 0, ...info, extra: { ...info.extra, diff --git a/packages/data-service/src/data-service.ts b/packages/data-service/src/data-service.ts index b0d9e87ad73..38d15076f8e 100644 --- a/packages/data-service/src/data-service.ts +++ b/packages/data-service/src/data-service.ts @@ -2134,15 +2134,62 @@ class DataServiceImpl extends WithLogContext implements DataService { } } + private async _indexProgress(ns: string): Promise> { + type IndexProgressResult = { + _id: string; + progress: number; + }; + + const currentOps: IndexProgressResult[] = await this._database( + 'admin', + 'META' + ) + .aggregate([ + { $currentOp: { allUsers: true, localOps: true } }, // get all ops + { + $match: { + ns, + progress: { $type: 'object' }, + 'command.createIndexes': { $exists: true }, + }, + }, // filter for createIndexes + { $unwind: '$command.indexes' }, // explode the "indexes" array for each createIndexes command + { + $group: { + _id: '$command.indexes.name', + progress: { + $first: { + $cond: { + if: { $gt: ['$progress.total', 0] }, + then: { $divide: ['$progress.done', '$progress.total'] }, + else: 0, + }, + }, + }, + }, + }, // group on index name + ]) + .toArray() + .then(undefined, () => []); + + const indexToProgress = Object.create(null); + for (const { _id, progress } of currentOps) { + indexToProgress[_id] = progress; + } + + return indexToProgress; + } + @op(mongoLogId(1_001_000_047)) async indexes( ns: string, options?: IndexInformationOptions ): Promise { - const [indexes, indexStats, indexSizes] = await Promise.all([ + const [indexes, indexStats, indexSizes, indexProgress] = await Promise.all([ this._collection(ns, 'CRUD').indexes(options) as Promise, this._indexStats(ns), this._indexSizes(ns), + this._indexProgress(ns), ]); const maxSize = Math.max(...Object.values(indexSizes)); @@ -2154,7 +2201,8 @@ class DataServiceImpl extends WithLogContext implements DataService { index, indexStats[name], indexSizes[name], - maxSize + maxSize, + indexProgress[name] ); }); } diff --git a/packages/data-service/src/index-detail-helper.ts b/packages/data-service/src/index-detail-helper.ts index 3264e0b3a49..fbe2c691f7a 100644 --- a/packages/data-service/src/index-detail-helper.ts +++ b/packages/data-service/src/index-detail-helper.ts @@ -35,6 +35,7 @@ export type IndexDefinition = { extra: Record>; size: IndexSize; relativeSize: number; + buildProgress: number; } & IndexStats; export function getIndexCardinality( @@ -120,7 +121,8 @@ export function createIndexDefinition( { name, key, v, ...extra }: IndexInfo, indexStats?: IndexStats, indexSize?: number, - maxSize?: number + maxSize?: number, + buildProgress?: number ): IndexDefinition { indexStats ??= { name, @@ -149,5 +151,6 @@ export function createIndexDefinition( properties: getIndexProperties(index), size: indexSize, relativeSize: (indexSize / maxSize) * 100, + buildProgress: buildProgress ?? 0, }; }