diff --git a/packages/compass-global-writes/src/components/index.tsx b/packages/compass-global-writes/src/components/index.tsx index 902342e1ce0..d7331022a17 100644 --- a/packages/compass-global-writes/src/components/index.tsx +++ b/packages/compass-global-writes/src/components/index.tsx @@ -16,6 +16,7 @@ import ShardKeyInvalid from './states/shard-key-invalid'; import ShardKeyMismatch from './states/shard-key-mismatch'; import ShardingError from './states/sharding-error'; import IncompleteShardingSetup from './states/incomplete-sharding-setup'; +import LoadingError from './states/loading-error'; const containerStyles = css({ display: 'flex', @@ -90,6 +91,10 @@ function ShardingStateView({ return ; } + if (shardingStatus === ShardingStatuses.LOADING_ERROR) { + return ; + } + return null; } diff --git a/packages/compass-global-writes/src/components/states/loading-error.spec.tsx b/packages/compass-global-writes/src/components/states/loading-error.spec.tsx new file mode 100644 index 00000000000..2ed220dd9bd --- /dev/null +++ b/packages/compass-global-writes/src/components/states/loading-error.spec.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { expect } from 'chai'; +import { screen } from '@mongodb-js/testing-library-compass'; +import { LoadingError } from './loading-error'; +import { renderWithStore } from '../../../tests/create-store'; + +const error = 'Test failure'; + +function renderWithProps( + props?: Partial> +) { + return renderWithStore(); +} + +describe('LoadingError', function () { + it('renders the error', async function () { + await renderWithProps(); + expect(screen.getByText(error)).to.exist; + }); +}); diff --git a/packages/compass-global-writes/src/components/states/loading-error.tsx b/packages/compass-global-writes/src/components/states/loading-error.tsx new file mode 100644 index 00000000000..8a508c38316 --- /dev/null +++ b/packages/compass-global-writes/src/components/states/loading-error.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { ErrorSummary } from '@mongodb-js/compass-components'; +import { connect } from 'react-redux'; +import { type RootState, ShardingStatuses } from '../../store/reducer'; +import { containerStyles } from '../common-styles'; + +interface LoadingErrorProps { + error: string; +} + +export function LoadingError({ error }: LoadingErrorProps) { + return ( +
+ +
+ ); +} + +export default connect((state: RootState) => { + if (state.status !== ShardingStatuses.LOADING_ERROR) { + throw new Error('Error not found in LoadingError'); + } + return { + error: state.loadingError, + }; +})(LoadingError); diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index 74a1ac056ec..19fb3bf21a6 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -73,6 +73,9 @@ function createStore({ hasShardKey = () => false, failsOnShardingRequest = () => false, failsOnShardZoneRequest = () => false, + failsToFetchClusterDetails = () => false, + failsToFetchDeploymentStatus = () => false, + failsToFetchShardKey = () => false, authenticatedFetchStub, }: | { @@ -81,6 +84,9 @@ function createStore({ hasShardKey?: () => boolean | AtlasShardKey; failsOnShardingRequest?: () => boolean; failsOnShardZoneRequest?: () => boolean; + failsToFetchClusterDetails?: () => boolean; + failsToFetchDeploymentStatus?: () => boolean; + failsToFetchShardKey?: () => boolean; authenticatedFetchStub?: never; } | { @@ -89,6 +95,9 @@ function createStore({ hasShardKey?: () => boolean | ShardKey; failsOnShardingRequest?: never; failsOnShardZoneRequest?: () => never; + failsToFetchClusterDetails?: never; + failsToFetchDeploymentStatus?: never; + failsToFetchShardKey?: () => boolean; authenticatedFetchStub?: () => void; } = {}): GlobalWritesStore { const atlasService = { @@ -98,6 +107,9 @@ function createStore({ } if (uri.includes('/clusters/')) { + if (failsToFetchClusterDetails()) { + return Promise.reject(new Error('Failed to fetch cluster details')); + } return createAuthFetchResponse({ ...clusterDetails, geoSharding: { @@ -108,6 +120,9 @@ function createStore({ } if (uri.includes('/deploymentStatus/')) { + if (failsToFetchDeploymentStatus()) { + return Promise.reject(new Error('Failed to fetch deployment status')); + } return createAuthFetchResponse({ automationStatus: { processes: hasShardingError() ? [failedShardingProcess] : [], @@ -130,6 +145,10 @@ function createStore({ }), automationAgentAwait: (_meta: unknown, type: string) => { if (type === 'getShardKey') { + if (failsToFetchShardKey()) { + return Promise.reject(new Error('Failed to fetch shardKey')); + } + const shardKey = hasShardKey(); return { response: @@ -188,6 +207,35 @@ describe('GlobalWritesStore Store', function () { }); context('scenarios', function () { + context('initial load fail', function () { + it('fails to fetch cluster details', async function () { + const store = createStore({ + failsToFetchClusterDetails: () => true, + }); + await waitFor(() => { + expect(store.getState().status).to.equal('LOADING_ERROR'); + }); + }); + + it('fails to fetch shard key', async function () { + const store = createStore({ + failsToFetchShardKey: () => true, + }); + await waitFor(() => { + expect(store.getState().status).to.equal('LOADING_ERROR'); + }); + }); + + it('fails to fetch deployment status', async function () { + const store = createStore({ + failsToFetchDeploymentStatus: () => true, + }); + await waitFor(() => { + expect(store.getState().status).to.equal('LOADING_ERROR'); + }); + }); + }); + it('not managed -> sharding -> valid shard key', async function () { let mockShardKey = false; let mockManagedNamespace = false; @@ -291,6 +339,52 @@ describe('GlobalWritesStore Store', function () { }); }); + context('pulling fail', function () { + it('sharding -> error (failed to fetch shard key)', async function () { + let mockFailure = false; + // initial state === sharding + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, + }); + const store = createStore({ + isNamespaceManaged: () => true, + failsToFetchShardKey: Sinon.fake(() => mockFailure), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING'); + }); + + // sharding ends with a request failure + mockFailure = true; + clock.tick(POLLING_INTERVAL); + await waitFor(() => { + expect(store.getState().status).to.equal('LOADING_ERROR'); + }); + }); + + it('sharding -> error (failed to fetch deployment status)', async function () { + let mockFailure = false; + // initial state === sharding + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, + }); + const store = createStore({ + isNamespaceManaged: () => true, + failsToFetchDeploymentStatus: Sinon.fake(() => mockFailure), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING'); + }); + + // sharding ends with a request failure + mockFailure = true; + clock.tick(POLLING_INTERVAL); + await waitFor(() => { + expect(store.getState().status).to.equal('LOADING_ERROR'); + }); + }); + }); + it('sharding -> cancelling request -> not managed', async function () { let mockManagedNamespace = true; confirmationStub.resolves(true); diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index 44a655f79d3..aa6904b0eee 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -46,6 +46,8 @@ enum GlobalWritesActionTypes { UnmanagingNamespaceStarted = 'global-writes/UnmanagingNamespaceStarted', UnmanagingNamespaceFinished = 'global-writes/UnmanagingNamespaceFinished', UnmanagingNamespaceErrored = 'global-writes/UnmanagingNamespaceErrored', + + LoadingFailed = 'global-writes/LoadingFailed', } type ManagedNamespaceFetchedAction = { @@ -53,6 +55,11 @@ type ManagedNamespaceFetchedAction = { managedNamespace?: ManagedNamespace; }; +type LoadingFailedAction = { + type: GlobalWritesActionTypes.LoadingFailed; + error: string; +}; + type NamespaceShardingErrorFetchedAction = { type: GlobalWritesActionTypes.NamespaceShardingErrorFetched; error: string; @@ -125,6 +132,11 @@ export enum ShardingStatuses { */ NOT_READY = 'NOT_READY', + /** + * The status could not be determined because loading failed + */ + LOADING_ERROR = 'LOADING_ERROR', + /** * Namespace is not geo-sharded. */ @@ -209,11 +221,19 @@ export type RootState = { managedNamespace?: ManagedNamespace; shardZones: ShardZoneData[]; } & ( + | { + status: ShardingStatuses.LOADING_ERROR; + shardKey?: ShardKey; + shardingError?: never; + pollingTimeout?: never; + loadingError: string; + } | { status: ShardingStatuses.NOT_READY; shardKey?: never; shardingError?: never; pollingTimeout?: never; + loadingError?: never; } | { status: @@ -225,6 +245,7 @@ export type RootState = { // and then unmanaged shardingError?: never; pollingTimeout?: never; + loadingError?: never; } | { status: ShardingStatuses.SHARDING; @@ -235,6 +256,7 @@ export type RootState = { shardKey?: ShardKey; shardingError?: never; pollingTimeout?: NodeJS.Timeout; + loadingError?: never; } | { status: @@ -244,6 +266,7 @@ export type RootState = { shardKey?: never; shardingError: string; pollingTimeout?: never; + loadingError?: never; } | { status: @@ -257,6 +280,7 @@ export type RootState = { shardKey: ShardKey; shardingError?: never; pollingTimeout?: never; + loadingError?: never; } ); @@ -616,6 +640,25 @@ const reducer: Reducer = (state = initialState, action) => { }; } + if ( + isAction( + action, + GlobalWritesActionTypes.LoadingFailed + ) && + (state.status === ShardingStatuses.NOT_READY || + state.status === ShardingStatuses.SHARDING) + ) { + if (state.pollingTimeout) { + throw new Error('Polling was not stopped'); + } + return { + ...state, + status: ShardingStatuses.LOADING_ERROR, + loadingError: action.error, + pollingTimeout: state.pollingTimeout, + }; + } + return state; }; @@ -644,16 +687,12 @@ export const fetchClusterShardingData = 'Error fetching cluster sharding data', (error as Error).message ); - openToast( - `global-writes-fetch-shard-info-error-${connectionInfoRef.current.id}-${namespace}`, - { - title: `Failed to fetch sharding information: ${ - (error as Error).message - }`, - dismissible: true, - timeout: 5000, - variant: 'important', - } + dispatch( + handleLoadingError({ + error: error as Error, + id: `global-writes-fetch-shard-info-error-${connectionInfoRef.current.id}-${namespace}`, + description: 'Failed to fetch sharding information', + }) ); } }; @@ -829,15 +868,48 @@ const stopPollingForShardKey = (): GlobalWritesThunkAction< }; }; +const handleLoadingError = ({ + error, + id, + description, +}: { + error: Error; + id: string; + description: string; +}): GlobalWritesThunkAction => { + return (dispatch, getState) => { + const { status } = getState(); + const isPolling = status === ShardingStatuses.SHARDING; + const isInitialLoad = status === ShardingStatuses.NOT_READY; + const errorMessage = `${description}: ${error.message}`; + if (isInitialLoad || isPolling) { + dispatch({ + type: GlobalWritesActionTypes.LoadingFailed, + error: errorMessage, + }); + return; + } + openToast(id, { + title: errorMessage, + dismissible: true, + timeout: 5000, + variant: 'important', + }); + }; +}; + export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< Promise, - NamespaceShardingErrorFetchedAction | NamespaceShardKeyFetchedAction + | NamespaceShardingErrorFetchedAction + | NamespaceShardKeyFetchedAction + | NextPollingTimeoutClearedAction > => { return async ( dispatch, getState, { atlasGlobalWritesService, logger, connectionInfoRef } ) => { + dispatch({ type: GlobalWritesActionTypes.NextPollingTimeoutCleared }); const { namespace, status, managedNamespace } = getState(); try { @@ -879,17 +951,15 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< logger.log.error( logger.mongoLogId(1_001_000_333), 'AtlasFetchError', - 'Error fetching shard key', + 'Error fetching shard key / deployment status', (error as Error).message ); - openToast( - `global-writes-fetch-shard-key-error-${connectionInfoRef.current.id}-${namespace}`, - { - title: `Failed to fetch shard key: ${(error as Error).message}`, - dismissible: true, - timeout: 5000, - variant: 'important', - } + dispatch( + handleLoadingError({ + error: error as Error, + id: `global-writes-fetch-shard-key-error-${connectionInfoRef.current.id}-${namespace}`, + description: 'Failed to fetch shard key or deployment status', + }) ); } };