diff --git a/packages/compass-indexes/src/components/indexes/indexes.spec.tsx b/packages/compass-indexes/src/components/indexes/indexes.spec.tsx index 559bb5662ee..e26ed4d4f7d 100644 --- a/packages/compass-indexes/src/components/indexes/indexes.spec.tsx +++ b/packages/compass-indexes/src/components/indexes/indexes.spec.tsx @@ -203,7 +203,7 @@ describe('Indexes Component', function () { value: 1, }, ], - status: 'inprogress', + status: 'creating', buildProgress: 0, }, ], @@ -217,7 +217,7 @@ describe('Indexes Component', function () { const indexStatusField = within(indexesList).getAllByTestId( 'indexes-status-field' )[1]; - expect(indexStatusField).to.contain.text('In Progress'); + expect(indexStatusField).to.contain.text('Creating'); const dropIndexButton = within(indexesList).queryByTestId( 'index-actions-delete-action' 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 deleted file mode 100644 index f589c89d2dc..00000000000 --- a/packages/compass-indexes/src/components/regular-indexes-table/in-progress-index-actions.spec.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { - cleanup, - render, - screen, - userEvent, -} from '@mongodb-js/testing-library-compass'; -import { expect } from 'chai'; -import { spy } from 'sinon'; -import type { SinonSpy } from 'sinon'; - -import InProgressIndexActions from './in-progress-index-actions'; - -describe('IndexActions Component', function () { - let onDeleteSpy: SinonSpy; - - before(cleanup); - afterEach(cleanup); - beforeEach(function () { - onDeleteSpy = spy(); - }); - - it('does not render the delete button for an in progress index that is still in progress', function () { - render( - - ); - - const button = screen.queryByTestId('index-actions-delete-action'); - expect(button).to.not.exist; - }); - - it('renders delete button for an in progress index that has failed', function () { - render( - - ); - - const button = screen.getByTestId('index-actions-delete-action'); - expect(button).to.exist; - expect(button.getAttribute('aria-label')).to.equal( - 'Drop Index artist_id_index' - ); - expect(onDeleteSpy.callCount).to.equal(0); - userEvent.click(button); - expect(onDeleteSpy.callCount).to.equal(1); - }); -}); 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 deleted file mode 100644 index 8cc5d533661..00000000000 --- a/packages/compass-indexes/src/components/regular-indexes-table/in-progress-index-actions.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import type { GroupedItemAction } 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; -}; - -type IndexAction = 'delete'; - -const IndexActions: React.FunctionComponent = ({ - index, - onDeleteFailedIndexClick, -}) => { - const indexActions: GroupedItemAction[] = useMemo(() => { - const actions: GroupedItemAction[] = []; - - // you can only drop regular indexes or failed inprogress indexes - if (index.status === 'failed') { - actions.push({ - action: 'delete', - label: `Drop Index ${index.name}`, - icon: 'Trash', - }); - } - - return actions; - }, [index]); - - const onAction = useCallback( - (action: IndexAction) => { - if (action === 'delete') { - onDeleteFailedIndexClick(index.name); - } - }, - [onDeleteFailedIndexClick, index] - ); - - return ( -
- - data-testid="index-actions" - actions={indexActions} - onAction={onAction} - /> -
- ); -}; - -export default IndexActions; diff --git a/packages/compass-indexes/src/components/regular-indexes-table/index-actions.spec.tsx b/packages/compass-indexes/src/components/regular-indexes-table/index-actions.spec.tsx new file mode 100644 index 00000000000..ab41bca33aa --- /dev/null +++ b/packages/compass-indexes/src/components/regular-indexes-table/index-actions.spec.tsx @@ -0,0 +1,398 @@ +import React from 'react'; +import { + cleanup, + render, + screen, + within, + userEvent, +} from '@mongodb-js/testing-library-compass'; +import { expect } from 'chai'; +import sinon from 'sinon'; + +import IndexActions from './index-actions'; +import type { InProgressIndex } from '../../modules/regular-indexes'; +import { mockRegularIndex } from '../../../test/helpers'; + +describe('IndexActions Component', function () { + let onDeleteIndexClick: sinon.SinonSpy; + let onDeleteFailedIndexClick: sinon.SinonSpy; + let onHideIndexClick: sinon.SinonSpy; + let onUnhideIndexClick: sinon.SinonSpy; + + beforeEach(function () { + onDeleteIndexClick = sinon.spy(); + onDeleteFailedIndexClick = sinon.spy(); + onHideIndexClick = sinon.spy(); + onUnhideIndexClick = sinon.spy(); + }); + + afterEach(cleanup); + + describe('Critical Boundary Cases', function () { + describe('buildProgress transition boundaries', function () { + it('boundary: buildProgress = 0 (exactly) shows ready actions', function () { + const exactlyZeroIndex = mockRegularIndex({ + name: 'exactly_zero', + buildProgress: 0, + }); + + render( + + ); + + // Should show ready actions, not building UI + expect(screen.queryByTestId('index-building-spinner')).to.not.exist; + expect(screen.getByTestId('index-actions')).to.exist; + expect(screen.getByLabelText('Drop Index exactly_zero')).to.exist; + expect(screen.getByLabelText('Hide Index exactly_zero')).to.exist; + }); + + it('boundary: buildProgress = 0.000001 (just above 0) shows building UI', function () { + const barelyBuildingIndex = mockRegularIndex({ + name: 'barely_building', + buildProgress: 0.000001, + }); + + render( + + ); + + // Should show building UI even for tiny progress + expect(screen.getByTestId('index-building-spinner')).to.exist; + expect(screen.getByText('Building… 0%')).to.exist; // Math.trunc rounds down + expect(screen.getByLabelText('Cancel Index barely_building')).to.exist; + }); + + it('boundary: buildProgress = 0.999999 (just below 1) shows building UI', function () { + const almostCompleteIndex = mockRegularIndex({ + name: 'almost_complete', + buildProgress: 0.999999, + }); + + render( + + ); + + // Should still show building UI + expect(screen.getByTestId('index-building-spinner')).to.exist; + expect(screen.getByText('Building… 99%')).to.exist; // Math.trunc rounds down + expect(screen.getByLabelText('Cancel Index almost_complete')).to.exist; + }); + + it('boundary: buildProgress = 1 (exactly) shows ready actions', function () { + const exactlyOneIndex = mockRegularIndex({ + name: 'exactly_one', + buildProgress: 1, + }); + + render( + + ); + + // Should show ready actions, not building UI + expect(screen.queryByTestId('index-building-spinner')).to.not.exist; + expect(screen.getByTestId('index-actions')).to.exist; + expect(screen.getByLabelText('Drop Index exactly_one')).to.exist; + expect(screen.getByLabelText('Hide Index exactly_one')).to.exist; + }); + }); + + describe('Permission handling (currentOp unavailable)', function () { + it('handles missing currentOp permission correctly (buildProgress defaults to 0)', function () { + // This simulates what happens when currentOp permission is not available + // The index-detail-helper defaults buildProgress to 0 + const noPermissionIndex = mockRegularIndex({ + name: 'no_permission_index', + buildProgress: 0, // This is what gets set when currentOp permission is unavailable + }); + + render( + + ); + + // Should treat as ready index with full actions + expect(screen.queryByTestId('index-building-spinner')).to.not.exist; + expect(screen.getByTestId('index-actions')).to.exist; + expect(screen.getByLabelText('Drop Index no_permission_index')).to + .exist; + expect(screen.getByLabelText('Hide Index no_permission_index')).to + .exist; + }); + }); + + describe('Extreme buildProgress values', function () { + it('handles negative buildProgress as ready', function () { + const negativeIndex = mockRegularIndex({ + name: 'negative_progress', + buildProgress: -0.1, + }); + + render( + + ); + + // Should treat as ready (negative should not show building UI) + expect(screen.queryByTestId('index-building-spinner')).to.not.exist; + expect(screen.getByTestId('index-actions')).to.exist; + }); + + it('handles buildProgress > 1 as ready', function () { + const overOneIndex = mockRegularIndex({ + name: 'over_one', + buildProgress: 1.1, + }); + + render( + + ); + + // Should treat as ready (>1 should not show building UI) + expect(screen.queryByTestId('index-building-spinner')).to.not.exist; + expect(screen.getByTestId('index-actions')).to.exist; + }); + }); + }); + + describe('Building State Tests', function () { + const buildingIndexes = [ + { buildProgress: 0.1, expectedPercent: '10' }, + { buildProgress: 0.25, expectedPercent: '25' }, + { buildProgress: 0.5, expectedPercent: '50' }, + { buildProgress: 0.75, expectedPercent: '75' }, + { buildProgress: 0.99, expectedPercent: '99' }, + ]; + + buildingIndexes.forEach(({ buildProgress, expectedPercent }) => { + it(`shows building UI for buildProgress ${buildProgress} (${expectedPercent}%)`, function () { + const buildingIndex = mockRegularIndex({ + name: 'building_index', + buildProgress, + }); + + render( + + ); + + // Should show building UI + const buildingSpinner = screen.getByTestId('index-building-spinner'); + expect(buildingSpinner).to.exist; + + // Should show progress percentage + expect(screen.getByText(`Building… ${expectedPercent}%`)).to.exist; + + // Should show cancel button (destructive) + const cancelButton = screen.getByLabelText( + 'Cancel Index building_index' + ); + expect(cancelButton).to.exist; + + // Should NOT show hide/unhide actions for building indexes + expect(screen.queryByLabelText('Hide Index building_index')).to.not + .exist; + expect(screen.queryByLabelText('Unhide Index building_index')).to.not + .exist; + }); + }); + }); + + describe('In-Progress Index States', function () { + describe('Creating State', function () { + const creatingIndex: InProgressIndex = { + id: 'creating-index-id', + name: 'creating_index', + status: 'creating', + buildProgress: 0, + fields: [{ field: 'test', value: 1 }], + }; + + it('shows no actions for creating index', function () { + render( + + ); + + // For creating indexes, there should be no actions group rendered at all + // since there are no actions available + expect(screen.queryByTestId('index-actions')).to.not.exist; + + // Also should not show building spinner for creating state + expect(screen.queryByTestId('index-building-spinner')).to.not.exist; + }); + }); + + describe('Failed State', function () { + const failedIndex: InProgressIndex = { + id: 'failed-index-id', + name: 'failed_index', + status: 'failed', + error: 'Index creation failed', + buildProgress: 0, + fields: [{ field: 'test', value: 1 }], + }; + + it('shows delete action for failed index', function () { + render( + + ); + + const actionsGroup = screen.getByTestId('index-actions'); + const deleteButton = within(actionsGroup).getByLabelText( + 'Drop Index failed_index' + ); + expect(deleteButton).to.exist; + }); + + it('calls onDeleteFailedIndexClick when failed index delete is clicked', function () { + render( + + ); + + const deleteButton = screen.getByLabelText('Drop Index failed_index'); + userEvent.click(deleteButton); + + expect(onDeleteFailedIndexClick).to.have.been.calledOnceWith( + 'failed_index' + ); + }); + }); + }); + + describe('Server Version Compatibility', function () { + const testIndex = mockRegularIndex({ + name: 'version_test', + buildProgress: 0, + }); + + const versionTestCases = [ + { version: '4.3.9', shouldHaveHide: false }, + { version: '4.4.0', shouldHaveHide: true }, + { version: '4.4.1', shouldHaveHide: true }, + { version: '5.0.0', shouldHaveHide: true }, + { version: '6.0.0', shouldHaveHide: true }, + { version: 'invalid-version', shouldHaveHide: true }, // Invalid versions default to true + ]; + + versionTestCases.forEach(({ version, shouldHaveHide }) => { + it(`${ + shouldHaveHide ? 'shows' : 'hides' + } hide action for server version ${version}`, function () { + render( + + ); + + const actionsGroup = screen.getByTestId('index-actions'); + + if (shouldHaveHide) { + const hideButton = within(actionsGroup).getByLabelText( + 'Hide Index version_test' + ); + expect(hideButton).to.exist; + } else { + expect(() => + within(actionsGroup).getByLabelText('Hide Index version_test') + ).to.throw; + } + }); + }); + }); + + describe('Action Event Handling', function () { + const testIndex = mockRegularIndex({ + name: 'test_actions', + buildProgress: 0, + }); + + it('calls onDeleteIndexClick for regular index delete', function () { + render( + + ); + + const deleteButton = screen.getByLabelText('Drop Index test_actions'); + userEvent.click(deleteButton); + + expect(onDeleteIndexClick).to.have.been.calledOnceWith('test_actions'); + }); + + it('calls onDeleteIndexClick for building index cancel (regular index)', function () { + const buildingIndex = mockRegularIndex({ + name: 'test_actions', + buildProgress: 0.5, + }); + + render( + + ); + + const cancelButton = screen.getByLabelText('Cancel Index test_actions'); + userEvent.click(cancelButton); + + expect(onDeleteIndexClick).to.have.been.calledOnceWith('test_actions'); + }); + }); +}); diff --git a/packages/compass-indexes/src/components/regular-indexes-table/index-actions.tsx b/packages/compass-indexes/src/components/regular-indexes-table/index-actions.tsx new file mode 100644 index 00000000000..b5cc5a2d1cb --- /dev/null +++ b/packages/compass-indexes/src/components/regular-indexes-table/index-actions.tsx @@ -0,0 +1,220 @@ +import semver from 'semver'; +import React, { useCallback, useMemo } from 'react'; +import type { GroupedItemAction } from '@mongodb-js/compass-components'; +import { + css, + ItemActionGroup, + SpinLoader, + Body, + spacing, +} from '@mongodb-js/compass-components'; +import type { + RegularIndex, + InProgressIndex, +} from '../../modules/regular-indexes'; + +const styles = css({ + // Align actions with the end of the table + justifyContent: 'flex-end', +}); + +const buildProgressStyles = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + gap: spacing[200], +}); + +// Union type for all possible index types +type IndexForActions = RegularIndex | InProgressIndex; + +type IndexActionsProps = { + index: IndexForActions; + serverVersion?: string; // Optional for in-progress indexes + onDeleteIndexClick?: (name: string) => void; + onDeleteFailedIndexClick?: (name: string) => void; + onHideIndexClick?: (name: string) => void; + onUnhideIndexClick?: (name: string) => void; +}; + +type IndexAction = 'delete' | 'hide' | 'unhide'; + +const MIN_HIDE_INDEX_SERVER_VERSION = '4.4.0'; + +// Helper: Check if server supports hide/unhide +const serverSupportsHideIndex = (serverVersion: string): boolean => { + try { + return semver.gte(serverVersion, MIN_HIDE_INDEX_SERVER_VERSION); + } catch { + return true; + } +}; + +// Helper: Determine if index is a regular index +const isRegularIndex = (index: IndexForActions): index is RegularIndex => { + return 'buildProgress' in index && 'type' in index; +}; + +// Helper: Determine if index is in-progress +const isInProgressIndex = ( + index: IndexForActions +): index is InProgressIndex => { + return 'status' in index && !('type' in index); +}; + +// Helper: Get build progress from any index type +const getBuildProgress = (index: IndexForActions): number => { + if (isRegularIndex(index)) { + return index.buildProgress; + } + if (isInProgressIndex(index)) { + return index.buildProgress || 0; + } + return 0; +}; + +// Helper: Determine if index is currently building +const isIndexBuilding = (index: IndexForActions): boolean => { + const progress = getBuildProgress(index); + return progress > 0 && progress < 1; +}; + +// Helper: Determine if index can be deleted +const canDeleteIndex = (index: IndexForActions): boolean => { + if (isInProgressIndex(index)) { + // In-progress indexes can only be deleted if failed + return index.status === 'failed'; + } + + // Regular indexes can always be deleted (except _id_ which is filtered out at table level) + return true; +}; + +// Helper: Determine if index can be hidden/unhidden +const canToggleVisibility = ( + index: IndexForActions, + serverVersion?: string +): boolean => { + if (!serverVersion || !isRegularIndex(index)) { + return false; + } + + // Only completed regular indexes can be hidden/unhidden + return !isIndexBuilding(index) && serverSupportsHideIndex(serverVersion); +}; + +// Helper: Build actions array based on index state +const buildIndexActions = ( + index: IndexForActions, + serverVersion?: string +): GroupedItemAction[] => { + const actions: GroupedItemAction[] = []; + + if (isIndexBuilding(index)) { + // Building indexes can only be cancelled + actions.push({ + action: 'delete', + label: `Cancel Index ${index.name}`, + icon: 'XWithCircle', + variant: 'destructive', + }); + } else { + // Completed or failed indexes + + // Add hide/unhide for completed regular indexes + if (canToggleVisibility(index, serverVersion) && isRegularIndex(index)) { + 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', + } + ); + } + + // Add delete action if applicable + if (canDeleteIndex(index)) { + actions.push({ + action: 'delete', + label: `Drop Index ${index.name}`, + icon: 'Trash', + }); + } + } + + return actions; +}; + +const IndexActions: React.FunctionComponent = ({ + index, + serverVersion, + onDeleteIndexClick, + onDeleteFailedIndexClick, + onHideIndexClick, + onUnhideIndexClick, +}) => { + const indexActions = useMemo( + () => buildIndexActions(index, serverVersion), + [index, serverVersion] + ); + + const onAction = useCallback( + (action: IndexAction) => { + if (action === 'delete') { + if (isInProgressIndex(index) && index.status === 'failed') { + onDeleteFailedIndexClick?.(index.name); + } else { + onDeleteIndexClick?.(index.name); + } + } else if (action === 'hide') { + onHideIndexClick?.(index.name); + } else if (action === 'unhide') { + onUnhideIndexClick?.(index.name); + } + }, + [ + index, + onDeleteIndexClick, + onDeleteFailedIndexClick, + onHideIndexClick, + onUnhideIndexClick, + ] + ); + + const buildProgress = getBuildProgress(index); + + // Show build progress UI for building indexes + if (isIndexBuilding(index)) { + return ( +
+ Building… {Math.trunc(buildProgress * 100)}% + + + data-testid="index-actions" + actions={indexActions} + onAction={onAction} + /> +
+ ); + } + + // Standard actions layout for completed/failed indexes + return ( + + data-testid="index-actions" + className={styles} + actions={indexActions} + onAction={onAction} + /> + ); +}; + +export default IndexActions; 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 deleted file mode 100644 index 3b58e726bc3..00000000000 --- a/packages/compass-indexes/src/components/regular-indexes-table/regular-index-actions.spec.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import React from 'react'; -import { - cleanup, - render, - screen, - userEvent, -} from '@mongodb-js/testing-library-compass'; -import { expect } from 'chai'; -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; - let onHideIndexSpy: SinonSpy; - let onUnhideIndexSpy: SinonSpy; - - before(cleanup); - afterEach(cleanup); - beforeEach(function () { - onDeleteSpy = spy(); - onHideIndexSpy = spy(); - 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( - - ); - - const button = screen.getByTestId('index-actions-delete-action'); - expect(button).to.exist; - expect(button.getAttribute('aria-label')).to.equal( - 'Drop Index artist_id_index' - ); - expect(onDeleteSpy.callCount).to.equal(0); - userEvent.click(button); - expect(onDeleteSpy.callCount).to.equal(1); - }); - - context( - 'when server version is >= 4.4.0 and the index is a regular index', - function () { - it('renders hide index button when index is not hidden', function () { - render( - - ); - - const button = screen.getByTestId('index-actions-hide-action'); - expect(button).to.exist; - expect(button.getAttribute('aria-label')).to.equal( - 'Hide Index artist_id_index' - ); - expect(onHideIndexSpy.callCount).to.equal(0); - userEvent.click(button); - expect(onHideIndexSpy.callCount).to.equal(1); - }); - - it('renders unhide index button when index is hidden', function () { - render( - - ); - const button = screen.getByTestId('index-actions-unhide-action'); - expect(button).to.exist; - expect(button.getAttribute('aria-label')).to.equal( - 'Unhide Index artist_id_index' - ); - expect(onUnhideIndexSpy.callCount).to.equal(0); - userEvent.click(button); - expect(onUnhideIndexSpy.callCount).to.equal(1); - }); - } - ); - - context( - 'when server version is < 4.4.0 and the index is a regular index', - function () { - it('will not render hide index button', function () { - render( - - ); - expect(() => screen.getByTestId('index-actions-hide-action')).to.throw; - }); - } - ); -}); diff --git a/packages/compass-indexes/src/components/regular-indexes-table/regular-index-actions.tsx b/packages/compass-indexes/src/components/regular-indexes-table/regular-index-actions.tsx deleted file mode 100644 index 8f50640a208..00000000000 --- a/packages/compass-indexes/src/components/regular-indexes-table/regular-index-actions.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import semver from 'semver'; -import React, { useCallback, useMemo } from 'react'; -import type { GroupedItemAction } from '@mongodb-js/compass-components'; -import { - css, - ItemActionGroup, - SpinLoader, - Body, - spacing, -} from '@mongodb-js/compass-components'; -import type { RegularIndex } from '../../modules/regular-indexes'; - -const styles = css({ - // Align actions with the end of the table - justifyContent: 'flex-end', -}); - -const buildProgressStyles = css({ - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - gap: spacing[200], -}); - -type IndexActionsProps = { - index: RegularIndex; - serverVersion: string; - onDeleteIndexClick: (name: string) => void; - onHideIndexClick: (name: string) => void; - onUnhideIndexClick: (name: string) => void; -}; - -type IndexAction = 'delete' | 'hide' | 'unhide'; - -const MIN_HIDE_INDEX_SERVER_VERSION = '4.4.0'; - -const serverSupportsHideIndex = (serverVersion: string) => { - try { - return semver.gte(serverVersion, MIN_HIDE_INDEX_SERVER_VERSION); - } catch { - return true; - } -}; - -const IndexActions: React.FunctionComponent = ({ - index, - serverVersion, - onDeleteIndexClick, - onHideIndexClick, - onUnhideIndexClick, -}) => { - const indexActions: GroupedItemAction[] = useMemo(() => { - const actions: GroupedItemAction[] = []; - const buildProgress = index.buildProgress; - const isBuilding = buildProgress > 0 && buildProgress < 1; - - 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', - }); - } - - return actions; - }, [index.name, index.extra?.hidden, index.buildProgress, serverVersion]); - - const onAction = useCallback( - (action: IndexAction) => { - if (action === 'delete') { - onDeleteIndexClick(index.name); - } else if (action === 'hide') { - onHideIndexClick(index.name); - } else if (action === 'unhide') { - onUnhideIndexClick(index.name); - } - }, - [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" - className={styles} - actions={indexActions} - onAction={onAction} - /> - ); -}; - -export default IndexActions; 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 63ac2278aa5..e97c9ba4c76 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 @@ -121,7 +121,7 @@ const inProgressIndexes: InProgressIndex[] = [ value: -1, }, ], - status: 'inprogress', + status: 'creating', buildProgress: 0, }, { @@ -133,7 +133,7 @@ const inProgressIndexes: InProgressIndex[] = [ value: 'text', }, ], - status: 'inprogress', + status: 'creating', error: 'this is an error', buildProgress: 0, }, @@ -274,7 +274,7 @@ describe('RegularIndexesTable Component', function () { expect(() => within(indexRow).getByTestId('index-actions-hide-action')).to .throw; - if (index.status === 'inprogress') { + if (index.status === 'creating') { expect(() => within(indexRow).getByTestId('index-actions-delete-action') ).to.throw; diff --git a/packages/compass-indexes/src/components/regular-indexes-table/regular-indexes-table.tsx b/packages/compass-indexes/src/components/regular-indexes-table/regular-indexes-table.tsx index 565d122d0c6..5400668417c 100644 --- a/packages/compass-indexes/src/components/regular-indexes-table/regular-indexes-table.tsx +++ b/packages/compass-indexes/src/components/regular-indexes-table/regular-indexes-table.tsx @@ -16,8 +16,7 @@ import SizeField from './size-field'; import UsageField from './usage-field'; import PropertyField from './property-field'; import StatusField from './status-field'; -import RegularIndexActions from './regular-index-actions'; -import InProgressIndexActions from './in-progress-index-actions'; +import IndexActions from './index-actions'; import { IndexesTable } from '../indexes-table'; import { @@ -122,7 +121,7 @@ function mergedIndexFieldValue( } if (field === 'status') { - return 'ready'; + return determineRegularIndexStatus(index); } return index[field]; @@ -282,6 +281,21 @@ function mergeIndexes( type CommonIndexInfo = Omit; +/** + * Determines the display status for a regular index based on its build progress + */ +function determineRegularIndexStatus( + index: RegularIndex +): 'inprogress' | 'ready' { + // Build progress determines building vs ready + if (index.buildProgress > 0 && index.buildProgress < 1) { + return 'inprogress'; + } + + // Default to ready for completed indexes (buildProgress = 0 or 1) + return 'ready'; +} + function getInProgressIndexInfo( index: MappedInProgressIndex, { @@ -301,10 +315,10 @@ function getInProgressIndexInfo( properties: null, status: , actions: ( - + /> ), }; } @@ -321,7 +335,7 @@ function getRollingIndexInfo(index: MappedRollingIndex): CommonIndexInfo { // TODO(COMPASS-7589): add properties for rolling indexes properties: null, status: , - actions: null, + actions: null, // Rolling indexes don't have actions }; } @@ -339,6 +353,8 @@ function getRegularIndexInfo( onDeleteIndexClick: (indexName: string) => void; } ): CommonIndexInfo { + const status = determineRegularIndexStatus(index); + return { id: index.name, name: index.name, @@ -355,23 +371,15 @@ function getRegularIndexInfo( properties={index.properties} /> ), - status: ( - 0 && index.buildProgress < 1 - ? 'inprogress' - : 'ready' - } - /> - ), + status: , actions: index.name !== '_id_' && ( - + /> ), }; } diff --git a/packages/compass-indexes/src/components/regular-indexes-table/status-field.tsx b/packages/compass-indexes/src/components/regular-indexes-table/status-field.tsx index 0fd5c75e1c1..fd8187c88dc 100644 --- a/packages/compass-indexes/src/components/regular-indexes-table/status-field.tsx +++ b/packages/compass-indexes/src/components/regular-indexes-table/status-field.tsx @@ -53,7 +53,7 @@ const BadgeWithTooltip: React.FunctionComponent<{ }; type StatusFieldProps = { - status: InProgressIndex['status'] | 'ready' | 'building'; + status: InProgressIndex['status'] | 'ready' | 'building' | 'inprogress'; error?: InProgressIndex['error']; }; @@ -87,6 +87,12 @@ const StatusField: React.FunctionComponent = ({ )} + {status === 'creating' && ( + + Creating + + )} + {status === 'failed' && ( & export type InProgressIndex = Pick & { id: string; - status: 'inprogress' | 'failed'; + status: 'creating' | 'failed'; error?: string; buildProgress: number; }; @@ -82,7 +82,7 @@ export const prepareInProgressIndex = ( id, // TODO(COMPASS-8335): we need the type because it shows in the table // TODO(COMPASS-8335): the table can also use cardinality - status: 'inprogress', + status: 'creating', fields: inProgressIndexFields, name: inProgressIndexName, buildProgress: 0, diff --git a/packages/compass-indexes/test/fixtures/regular-indexes.ts b/packages/compass-indexes/test/fixtures/regular-indexes.ts index 2e4218d2d10..dc880db665d 100644 --- a/packages/compass-indexes/test/fixtures/regular-indexes.ts +++ b/packages/compass-indexes/test/fixtures/regular-indexes.ts @@ -254,7 +254,7 @@ export const inProgressIndexes: InProgressIndex[] = [ name: 'AAAA', //version: 2, fields: [], - status: 'inprogress', + status: 'creating', buildProgress: 0, }, { @@ -266,7 +266,7 @@ export const inProgressIndexes: InProgressIndex[] = [ value: 1, }, ], - status: 'inprogress', + status: 'creating', buildProgress: 0, }, ];