From 4475f7260fec8ebbdaedab8c51313143d9dea7a3 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 7 Oct 2024 14:05:40 +0200 Subject: [PATCH 01/17] feat: cancel sharding --- .../src/components/index.tsx | 10 ++- .../src/components/states/sharding.tsx | 37 ++++++++- .../src/store/reducer.ts | 75 ++++++++++++++++++- 3 files changed, 117 insertions(+), 5 deletions(-) diff --git a/packages/compass-global-writes/src/components/index.tsx b/packages/compass-global-writes/src/components/index.tsx index 77aa286de46..f911a6a91b1 100644 --- a/packages/compass-global-writes/src/components/index.tsx +++ b/packages/compass-global-writes/src/components/index.tsx @@ -5,6 +5,7 @@ import { spacing, WorkspaceContainer, SpinLoaderWithLabel, + ConfirmationModalArea, } from '@mongodb-js/compass-components'; import type { RootState, ShardingStatus } from '../store/reducer'; import { ShardingStatuses } from '../store/reducer'; @@ -55,7 +56,10 @@ function ShardingStateView({ return ; } - if (shardingStatus === ShardingStatuses.SHARDING) { + if ( + shardingStatus === ShardingStatuses.SHARDING || + shardingStatus === ShardingStatuses.CANCEL_SHARDING + ) { return ; } @@ -73,7 +77,9 @@ export function GlobalWrites({ shardingStatus }: GlobalWritesProps) { return (
- + + +
); diff --git a/packages/compass-global-writes/src/components/states/sharding.tsx b/packages/compass-global-writes/src/components/states/sharding.tsx index f64e788a4ab..72ff02fb761 100644 --- a/packages/compass-global-writes/src/components/states/sharding.tsx +++ b/packages/compass-global-writes/src/components/states/sharding.tsx @@ -3,11 +3,18 @@ import { Banner, BannerVariant, Body, + Button, css, Link, spacing, + SpinLoader, } from '@mongodb-js/compass-components'; import { connect } from 'react-redux'; +import { + cancelSharding, + type RootState, + ShardingStatuses, +} from '../../store/reducer'; const nbsp = '\u00a0'; @@ -17,7 +24,15 @@ const containerStyles = css({ gap: spacing[400], }); -export function ShardingState() { +interface ShardingStateProps { + isCancellingSharding: boolean; + onCancelSharding: () => void; +} + +export function ShardingState({ + isCancellingSharding, + onCancelSharding, +}: ShardingStateProps) { return (
@@ -34,9 +49,27 @@ export function ShardingState() { hideExternalIcon > You can read more about Global Writes in our documentation. +
); } -export default connect()(ShardingState); +export default connect( + (state: RootState) => ({ + isCancellingSharding: state.status === ShardingStatuses.CANCEL_SHARDING, + }), + { + onCancelSharding: cancelSharding, + } +)(ShardingState); diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index 85981a039df..5aaea43a922 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -1,6 +1,6 @@ import type { Action, Reducer } from 'redux'; import type { GlobalWritesThunkAction } from '.'; -import { openToast } from '@mongodb-js/compass-components'; +import { openToast, showConfirmation } from '@mongodb-js/compass-components'; import type { ManagedNamespace } from '../services/atlas-global-writes-service'; export function isAction( @@ -30,6 +30,10 @@ enum GlobalWritesActionTypes { SubmittingForShardingFinished = 'global-writes/SubmittingForShardingFinished', SubmittingForShardingErrored = 'global-writes/SubmittingForShardingErrored', + CancellingShardingStarted = 'global-writes/CancellingShardingStarted', + CancellingShardingFinished = 'global-writes/CancellingShardingFinished', + CancellingShardingErrored = 'global-writes/CancellingShardingErrored', + UnmanagingNamespaceStarted = 'global-writes/UnmanagingNamespaceStarted', UnmanagingNamespaceFinished = 'global-writes/UnmanagingNamespaceFinished', UnmanagingNamespaceErrored = 'global-writes/UnmanagingNamespaceErrored', @@ -68,6 +72,19 @@ type SubmittingForShardingErroredAction = { type: GlobalWritesActionTypes.SubmittingForShardingErrored; }; +type CancellingShardingStartedAction = { + type: GlobalWritesActionTypes.CancellingShardingStarted; +}; + +type CancellingShardingFinishedAction = { + type: GlobalWritesActionTypes.CancellingShardingFinished; + managedNamespace?: ManagedNamespace; +}; + +type CancellingShardingErroredAction = { + type: GlobalWritesActionTypes.CancellingShardingErrored; +}; + type UnmanagingNamespaceStartedAction = { type: GlobalWritesActionTypes.UnmanagingNamespaceStarted; }; @@ -102,6 +119,12 @@ export enum ShardingStatuses { */ SHARDING = 'SHARDING', + /** + * State when user cancels the sharding and + * we are waiting for server to accept the request. + */ + CANCEL_SHARDING = 'CANCEL_SHARDING', + /** * Sharding failed. */ @@ -433,6 +456,56 @@ export const createShardKey = ( }; }; +export const cancelSharding = (): + | GlobalWritesThunkAction< + Promise, + | CancellingShardingStartedAction + | CancellingShardingFinishedAction + | CancellingShardingErroredAction + > + | undefined => { + return async (dispatch, getState, { atlasGlobalWritesService, logger }) => { + const confirmed = await showConfirmation({ + title: 'Confirmation', + description: 'Are you sure you want to cancel the sharding request?', + }); + + if (!confirmed) return; + + const { namespace } = getState(); + dispatch({ + type: GlobalWritesActionTypes.CancellingShardingStarted, + }); + + try { + await atlasGlobalWritesService.unmanageNamespace(namespace); + dispatch({ + type: GlobalWritesActionTypes.CancellingShardingFinished, + }); + } catch (error) { + logger.log.error( + logger.mongoLogId(1_001_000_331), + 'AtlasFetchError', + 'Error cancelling the sharding process', + { + error: (error as Error).message, + } + ); + openToast('global-writes-cancel-sharding-error', { + title: `Failed to cancel the sharding process: ${ + (error as Error).message + }`, + dismissible: true, + timeout: 5000, + variant: 'important', + }); + dispatch({ + type: GlobalWritesActionTypes.CancellingShardingErrored, + }); + } + }; +}; + const setNamespaceBeingSharded = ( managedNamespace?: ManagedNamespace ): GlobalWritesThunkAction => { From dc64754f9860ef80ad7c2a9258d7c6546ba4686c Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Tue, 8 Oct 2024 18:49:58 +0200 Subject: [PATCH 02/17] polling --- .../src/store/reducer.ts | 127 +++++++++++++++++- 1 file changed, 121 insertions(+), 6 deletions(-) diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index 5aaea43a922..b4730daab4a 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -3,6 +3,8 @@ import type { GlobalWritesThunkAction } from '.'; import { openToast, showConfirmation } from '@mongodb-js/compass-components'; import type { ManagedNamespace } from '../services/atlas-global-writes-service'; +export const POLLING_INTERVAL = 5000; + export function isAction( action: Action, type: A['type'] @@ -34,6 +36,8 @@ enum GlobalWritesActionTypes { CancellingShardingFinished = 'global-writes/CancellingShardingFinished', CancellingShardingErrored = 'global-writes/CancellingShardingErrored', + NextPollingTimeoutSet = 'global-writes/NextPollingTimeoutSet', + UnmanagingNamespaceStarted = 'global-writes/UnmanagingNamespaceStarted', UnmanagingNamespaceFinished = 'global-writes/UnmanagingNamespaceFinished', UnmanagingNamespaceErrored = 'global-writes/UnmanagingNamespaceErrored', @@ -85,6 +89,11 @@ type CancellingShardingErroredAction = { type: GlobalWritesActionTypes.CancellingShardingErrored; }; +type NextPollingTimeoutSetAction = { + type: GlobalWritesActionTypes.NextPollingTimeoutSet; + timeout: NodeJS.Timeout; +}; + type UnmanagingNamespaceStartedAction = { type: GlobalWritesActionTypes.UnmanagingNamespaceStarted; }; @@ -123,7 +132,7 @@ export enum ShardingStatuses { * State when user cancels the sharding and * we are waiting for server to accept the request. */ - CANCEL_SHARDING = 'CANCEL_SHARDING', + CANCELLING_SHARDING = 'CANCELLING_SHARDING', /** * Sharding failed. @@ -180,23 +189,36 @@ export type RootState = { status: ShardingStatuses.NOT_READY; shardKey?: never; shardingError?: never; + pollingTimeout?: never; } | { status: | ShardingStatuses.UNSHARDED | ShardingStatuses.SUBMITTING_FOR_SHARDING - | ShardingStatuses.SHARDING; + | ShardingStatuses.CANCELLING_SHARDING; /** * note: shardKey might exist even for unsharded. * if the collection was sharded previously and then unmanaged */ shardKey?: ShardKey; shardingError?: never; + pollingTimeout?: never; + } + | { + status: ShardingStatuses.SHARDING; + /** + * note: shardKey might exist + * if the collection was sharded previously and then unmanaged + */ + shardKey?: ShardKey; + shardingError?: never; + pollingTimeout?: NodeJS.Timeout; } | { status: ShardingStatuses.SHARDING_ERROR; shardKey?: never; shardingError: string; + pollingTimeout?: never; } | { status: @@ -206,6 +228,7 @@ export type RootState = { | ShardingStatuses.UNMANAGING_NAMESPACE; shardKey: ShardKey; shardingError?: never; + pollingTimeout?: never; } ); @@ -237,13 +260,15 @@ const reducer: Reducer = (state = initialState, action) => { action, GlobalWritesActionTypes.NamespaceShardingErrorFetched ) && - state.status === ShardingStatuses.NOT_READY + (state.status === ShardingStatuses.NOT_READY || + state.status === ShardingStatuses.SHARDING) ) { return { ...state, status: ShardingStatuses.SHARDING_ERROR, shardKey: undefined, shardingError: action.error, + pollingTimeout: undefined, }; } @@ -252,13 +277,15 @@ const reducer: Reducer = (state = initialState, action) => { action, GlobalWritesActionTypes.NamespaceShardKeyFetched ) && - state.status === ShardingStatuses.NOT_READY + (state.status === ShardingStatuses.NOT_READY || + state.status === ShardingStatuses.SHARDING) ) { return { ...state, status: getStatusFromShardKey(action.shardKey, state.managedNamespace), shardKey: action.shardKey, shardingError: undefined, + pollingTimeout: undefined, }; } @@ -301,6 +328,62 @@ const reducer: Reducer = (state = initialState, action) => { }; } + if ( + isAction( + action, + GlobalWritesActionTypes.NextPollingTimeoutSet + ) && + state.status === ShardingStatuses.SHARDING + ) { + return { + ...state, + pollingTimeout: action.timeout, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.CancellingShardingStarted + ) && + state.status === ShardingStatuses.SHARDING + ) { + return { + ...state, + status: ShardingStatuses.CANCELLING_SHARDING, + pollingTimeout: undefined, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.CancellingShardingErrored + ) && + state.status === ShardingStatuses.CANCELLING_SHARDING + ) { + return { + ...state, + status: ShardingStatuses.SHARDING, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.CancellingShardingFinished + ) && + (state.status === ShardingStatuses.CANCELLING_SHARDING || + state.status === ShardingStatuses.SHARDING_ERROR) + // the error might come before the cancel request was processed + ) { + return { + ...state, + status: ShardingStatuses.UNSHARDED, + shardingError: undefined, + }; + } + if ( isAction( action, @@ -472,7 +555,11 @@ export const cancelSharding = (): if (!confirmed) return; - const { namespace } = getState(); + const { namespace, status } = getState(); + + if (status === ShardingStatuses.SHARDING) { + stopPollingForShardKey(getState); + } dispatch({ type: GlobalWritesActionTypes.CancellingShardingStarted, }); @@ -514,9 +601,31 @@ const setNamespaceBeingSharded = ( type: GlobalWritesActionTypes.SubmittingForShardingFinished, managedNamespace, }); + dispatch(startPollingForShardKey()); }; }; +const startPollingForShardKey = (): GlobalWritesThunkAction< + void, + NextPollingTimeoutSetAction +> => { + return (dispatch) => { + const timeout = setTimeout(() => { + void dispatch(fetchNamespaceShardKey()); + }, POLLING_INTERVAL); + dispatch({ + type: GlobalWritesActionTypes.NextPollingTimeoutSet, + timeout, + }); + }; +}; + +const stopPollingForShardKey = (getState: () => RootState) => { + const { pollingTimeout } = getState(); + if (!pollingTimeout) return; + clearTimeout(pollingTimeout); +}; + export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< Promise, NamespaceShardingErrorFetchedAction | NamespaceShardKeyFetchedAction @@ -526,7 +635,7 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< getState, { atlasGlobalWritesService, logger, connectionInfoRef } ) => { - const { namespace } = getState(); + const { namespace, status } = getState(); try { const [shardingError, shardKey] = await Promise.all([ @@ -535,6 +644,9 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< ]); if (shardingError) { + if (status === ShardingStatuses.SHARDING) { + stopPollingForShardKey(getState); + } dispatch({ type: GlobalWritesActionTypes.NamespaceShardingErrorFetched, error: shardingError, @@ -547,6 +659,9 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< return; } + if (status === ShardingStatuses.SHARDING) { + stopPollingForShardKey(getState); + } dispatch({ type: GlobalWritesActionTypes.NamespaceShardKeyFetched, shardKey, From d48cc0d77dfec668df84373679c37d79fb15d74e Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 9 Oct 2024 14:08:47 +0200 Subject: [PATCH 03/17] .. --- .../src/components/states/sharding.spec.tsx | 38 ++++++++++++++++++- .../src/components/states/sharding.tsx | 11 ++---- .../services/atlas-global-writes-service.ts | 2 +- .../src/store/reducer.ts | 35 +++++++++++++++-- 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/packages/compass-global-writes/src/components/states/sharding.spec.tsx b/packages/compass-global-writes/src/components/states/sharding.spec.tsx index c3d8fb542be..69b767fe87e 100644 --- a/packages/compass-global-writes/src/components/states/sharding.spec.tsx +++ b/packages/compass-global-writes/src/components/states/sharding.spec.tsx @@ -1,13 +1,20 @@ import React from 'react'; import { expect } from 'chai'; -import { screen } from '@mongodb-js/testing-library-compass'; +import { screen, userEvent } from '@mongodb-js/testing-library-compass'; import { ShardingState } from './sharding'; import { renderWithStore } from '../../../tests/create-store'; +import Sinon from 'sinon'; function renderWithProps( props?: Partial> ) { - return renderWithStore(); + return renderWithStore( + {}} + isCancellingSharding={false} + {...props} + /> + ); } describe('Sharding', function () { @@ -15,4 +22,31 @@ describe('Sharding', function () { await renderWithProps(); expect(screen.getByRole('alert')).to.exist; }); + + it('sharding request can be cancelled', async function () { + const onCancelSharding = Sinon.spy(); + await renderWithProps({ + onCancelSharding, + }); + const btn = screen.getByRole('button', { name: 'Cancel Request' }); + expect(btn).to.be.visible; + + userEvent.click(btn); + + expect(onCancelSharding).to.have.been.calledOnce; + }); + + it('when cancelling is in progress, it cannot be triggered again', async function () { + const onCancelSharding = Sinon.spy(); + await renderWithProps({ + isCancellingSharding: true, + onCancelSharding, + }); + const btn = screen.getByTestId('cancel-sharding-btn'); + expect(btn.getAttribute('aria-disabled')).to.equal('true'); + + userEvent.click(btn); + + expect(onCancelSharding).not.to.have.been.calledOnce; + }); }); diff --git a/packages/compass-global-writes/src/components/states/sharding.tsx b/packages/compass-global-writes/src/components/states/sharding.tsx index 72ff02fb761..3cc12dff67f 100644 --- a/packages/compass-global-writes/src/components/states/sharding.tsx +++ b/packages/compass-global-writes/src/components/states/sharding.tsx @@ -7,7 +7,6 @@ import { css, Link, spacing, - SpinLoader, } from '@mongodb-js/compass-components'; import { connect } from 'react-redux'; import { @@ -50,13 +49,9 @@ export function ShardingState({ > You can read more about Global Writes in our documentation. @@ -67,7 +62,7 @@ export function ShardingState({ export default connect( (state: RootState) => ({ - isCancellingSharding: state.status === ShardingStatuses.CANCEL_SHARDING, + isCancellingSharding: state.status === ShardingStatuses.CANCELLING_SHARDING, }), { onCancelSharding: cancelSharding, diff --git a/packages/compass-global-writes/src/services/atlas-global-writes-service.ts b/packages/compass-global-writes/src/services/atlas-global-writes-service.ts index db41cb5f24f..1e0355544b9 100644 --- a/packages/compass-global-writes/src/services/atlas-global-writes-service.ts +++ b/packages/compass-global-writes/src/services/atlas-global-writes-service.ts @@ -42,7 +42,7 @@ export type ClusterDetailsApiResponse = { replicationSpecList: ReplicationItem[]; }; -type AutomationAgentProcess = { +export type AutomationAgentProcess = { statusType: string; workingOnShort: string; errorText: string; diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index b4730daab4a..c824909eddf 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -37,6 +37,7 @@ enum GlobalWritesActionTypes { CancellingShardingErrored = 'global-writes/CancellingShardingErrored', NextPollingTimeoutSet = 'global-writes/NextPollingTimeoutSet', + NextPollingTimeoutCleared = 'global-writes/NextPollingTimeoutCleared', UnmanagingNamespaceStarted = 'global-writes/UnmanagingNamespaceStarted', UnmanagingNamespaceFinished = 'global-writes/UnmanagingNamespaceFinished', @@ -94,6 +95,10 @@ type NextPollingTimeoutSetAction = { timeout: NodeJS.Timeout; }; +type NextPollingTimeoutClearedAction = { + type: GlobalWritesActionTypes.NextPollingTimeoutCleared; +}; + type UnmanagingNamespaceStartedAction = { type: GlobalWritesActionTypes.UnmanagingNamespaceStarted; }; @@ -263,12 +268,15 @@ const reducer: Reducer = (state = initialState, action) => { (state.status === ShardingStatuses.NOT_READY || state.status === ShardingStatuses.SHARDING) ) { + if (state.pollingTimeout) { + throw new Error('Polling was not stopped'); + } return { ...state, status: ShardingStatuses.SHARDING_ERROR, shardKey: undefined, shardingError: action.error, - pollingTimeout: undefined, + pollingTimeout: state.pollingTimeout, }; } @@ -280,12 +288,15 @@ const reducer: Reducer = (state = initialState, action) => { (state.status === ShardingStatuses.NOT_READY || state.status === ShardingStatuses.SHARDING) ) { + if (state.pollingTimeout) { + throw new Error('Polling was not stopped'); + } return { ...state, status: getStatusFromShardKey(action.shardKey, state.managedNamespace), shardKey: action.shardKey, shardingError: undefined, - pollingTimeout: undefined, + pollingTimeout: state.pollingTimeout, }; } @@ -341,6 +352,19 @@ const reducer: Reducer = (state = initialState, action) => { }; } + if ( + isAction( + action, + GlobalWritesActionTypes.NextPollingTimeoutCleared + ) && + state.status === ShardingStatuses.SHARDING + ) { + return { + ...state, + pollingTimeout: undefined, + }; + } + if ( isAction( action, @@ -348,10 +372,13 @@ const reducer: Reducer = (state = initialState, action) => { ) && state.status === ShardingStatuses.SHARDING ) { + if (state.pollingTimeout) { + throw new Error('Polling was not stopped'); + } return { ...state, status: ShardingStatuses.CANCELLING_SHARDING, - pollingTimeout: undefined, + pollingTimeout: state.pollingTimeout, }; } @@ -610,6 +637,7 @@ const startPollingForShardKey = (): GlobalWritesThunkAction< NextPollingTimeoutSetAction > => { return (dispatch) => { + console.log('START POLLING'); const timeout = setTimeout(() => { void dispatch(fetchNamespaceShardKey()); }, POLLING_INTERVAL); @@ -635,6 +663,7 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< getState, { atlasGlobalWritesService, logger, connectionInfoRef } ) => { + console.log('FETCHING KEY'); const { namespace, status } = getState(); try { From d22239602e9bdbfa7b4b431032a0a3d39dc7dbf7 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 9 Oct 2024 15:38:41 +0200 Subject: [PATCH 04/17] fixes --- .../src/components/index.tsx | 2 +- .../src/components/states/sharding.tsx | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/compass-global-writes/src/components/index.tsx b/packages/compass-global-writes/src/components/index.tsx index f911a6a91b1..12031c5656f 100644 --- a/packages/compass-global-writes/src/components/index.tsx +++ b/packages/compass-global-writes/src/components/index.tsx @@ -58,7 +58,7 @@ function ShardingStateView({ if ( shardingStatus === ShardingStatuses.SHARDING || - shardingStatus === ShardingStatuses.CANCEL_SHARDING + shardingStatus === ShardingStatuses.CANCELLING_SHARDING ) { return ; } diff --git a/packages/compass-global-writes/src/components/states/sharding.tsx b/packages/compass-global-writes/src/components/states/sharding.tsx index 3cc12dff67f..3bef1cf75d8 100644 --- a/packages/compass-global-writes/src/components/states/sharding.tsx +++ b/packages/compass-global-writes/src/components/states/sharding.tsx @@ -23,6 +23,11 @@ const containerStyles = css({ gap: spacing[400], }); +const btnStyles = css({ + float: 'right', + height: spacing[600], +}); + interface ShardingStateProps { isCancellingSharding: boolean; onCancelSharding: () => void; @@ -37,6 +42,14 @@ export function ShardingState({ Sharding your collection … {nbsp}this should not take too long. + Once your collection is sharded, this tab will show instructions on @@ -48,13 +61,6 @@ export function ShardingState({ hideExternalIcon > You can read more about Global Writes in our documentation. - ); From a9e4b4eccc618cbabdd24f557920459fdc2d7288 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Thu, 10 Oct 2024 13:12:23 +0200 Subject: [PATCH 05/17] rewrite tests --- .../src/store/index.spec.ts | 213 ++++++++++++------ .../src/store/reducer.ts | 38 ++-- 2 files changed, 171 insertions(+), 80 deletions(-) diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index 7aa4c56e6d6..729b81d4785 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -5,15 +5,18 @@ import { fetchClusterShardingData, createShardKey, type CreateShardKeyData, + TEST_POLLING_INTERVAL, } from './reducer'; import sinon from 'sinon'; import type { AutomationAgentDeploymentStatusApiResponse, + AutomationAgentProcess, ClusterDetailsApiResponse, ManagedNamespace, ShardZoneMapping, } from '../services/atlas-global-writes-service'; -import { waitFor } from '@mongodb-js/testing-library-compass'; +import { wait, waitFor } from '@mongodb-js/testing-library-compass'; +import Sinon from 'sinon'; const DB = 'test'; const COLL = 'coll'; @@ -28,22 +31,24 @@ const clusterDetails: ClusterDetailsApiResponse = { replicationSpecList: [], }; +const shardKeyData: CreateShardKeyData = { + customShardKey: 'secondary', + isCustomShardKeyHashed: true, + isShardKeyUnique: true, + numInitialChunks: 1, + presplitHashedZones: true, +}; + const managedNamespace: ManagedNamespace = { db: DB, collection: COLL, - customShardKey: 'secondary', - isCustomShardKeyHashed: false, - isShardKeyUnique: false, - numInitialChunks: null, - presplitHashedZones: false, + ...shardKeyData, }; -const shardKeyData: CreateShardKeyData = { - customShardKey: 'test', - isCustomShardKeyHashed: true, - isShardKeyUnique: false, - numInitialChunks: 1, - presplitHashedZones: true, +const failedShardingProcess: AutomationAgentProcess = { + statusType: 'ERROR', + workingOnShort: 'ShardingCollections', + errorText: `Failed to shard ${NS}`, }; function createAuthFetchResponse< @@ -57,7 +62,81 @@ function createAuthFetchResponse< }; } -function createStore(atlasService: any = {}): GlobalWritesStore { +function createStore({ + isNamespaceManaged = () => false, + hasShardingError = () => false, + hasShardKey = () => false, + failsOnShardingRequest = () => false, + authenticatedFetchStub, +}: + | { + isNamespaceManaged?: () => boolean; + hasShardingError?: () => boolean; + hasShardKey?: () => boolean; + failsOnShardingRequest?: () => boolean; + authenticatedFetchStub?: never; + } + | { + isNamespaceManaged?: never; + hasShardingError?: never; + hasShardKey?: () => boolean; + failsOnShardingRequest?: never; + authenticatedFetchStub?: () => void; + } = {}): GlobalWritesStore { + const atlasService = { + authenticatedFetch: (uri: string) => { + if (uri.includes(`/geoSharding`) && failsOnShardingRequest()) { + return Promise.reject(new Error('Failed to shard')); + } + + if (uri.includes('/clusters/')) { + return createAuthFetchResponse({ + ...clusterDetails, + geoSharding: { + ...clusterDetails.geoSharding, + managedNamespaces: isNamespaceManaged() ? [managedNamespace] : [], + }, + }); + } + + if (uri.includes('/deploymentStatus/')) { + return createAuthFetchResponse({ + automationStatus: { + processes: hasShardingError() ? [failedShardingProcess] : [], + }, + }); + } + + return createAuthFetchResponse({}); + }, + automationAgentRequest: (_meta: unknown, type: string) => ({ + _id: '123', + requestType: type, + }), + automationAgentAwait: (_meta: unknown, type: string) => { + if (type === 'getShardKey') { + return { + response: hasShardKey() + ? [ + { + key: { + location: 'range', + secondary: shardKeyData.isCustomShardKeyHashed + ? 'hashed' + : 'range', + }, + unique: true, + }, + ] + : [], + }; + } + }, + } as any; + + if (authenticatedFetchStub) + atlasService.authenticatedFetch = authenticatedFetchStub; + return setupStore( { namespace: NS, @@ -76,29 +155,59 @@ describe('GlobalWritesStore Store', function () { }); context('scenarios', function () { - it('not managed -> sharding', async function () { + it('not managed -> sharding -> success', async function () { + let mockShardKey = false; + const hasShardKey = Sinon.fake(() => mockShardKey); + // initial state === unsharded const store = createStore({ - authenticatedFetch: () => createAuthFetchResponse(clusterDetails), + hasShardKey, }); await store.dispatch(fetchClusterShardingData()); expect(store.getState().status).to.equal('UNSHARDED'); expect(store.getState().managedNamespace).to.equal(undefined); + // user requests sharding const promise = store.dispatch(createShardKey(shardKeyData)); expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); await promise; expect(store.getState().status).to.equal('SHARDING'); + + // sharding ends with a shardKey + mockShardKey = true; + await wait(TEST_POLLING_INTERVAL); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + }); }); - it('not managed -> failed sharding attempt', async function () { + it('not managed -> sharding -> sharding error', async function () { + let mockFailure = false; + const hasShardingError = Sinon.fake(() => mockFailure); + // initial state === unsharded const store = createStore({ - authenticatedFetch: (uri: string) => { - if (uri.includes('/geoSharding')) { - return Promise.reject(new Error('Failed to shard')); - } + hasShardingError, + }); + await store.dispatch(fetchClusterShardingData()); + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().managedNamespace).to.equal(undefined); - return createAuthFetchResponse(clusterDetails); - }, + // user requests sharding + const promise = store.dispatch(createShardKey(shardKeyData)); + expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); + await promise; + expect(store.getState().status).to.equal('SHARDING'); + + // sharding ends with an error + mockFailure = true; + await wait(TEST_POLLING_INTERVAL); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING_ERROR'); + }); + }); + + it('not managed -> failed sharding attempt', async function () { + const store = createStore({ + failsOnShardingRequest: () => true, }); await store.dispatch(fetchClusterShardingData()); expect(store.getState().status).to.equal('UNSHARDED'); @@ -110,48 +219,10 @@ describe('GlobalWritesStore Store', function () { expect(store.getState().status).to.equal('UNSHARDED'); }); - it('when the namespace is managed', async function () { + it('when the namespace is managed and has a valid shard key', async function () { const store = createStore({ - authenticatedFetch: (uri: string) => { - if (uri.includes('/clusters/')) { - return createAuthFetchResponse({ - ...clusterDetails, - geoSharding: { - ...clusterDetails.geoSharding, - managedNamespaces: [managedNamespace], - }, - }); - } - - if (uri.includes('/deploymentStatus/')) { - return createAuthFetchResponse({ - automationStatus: { - processes: [], - }, - }); - } - - return createAuthFetchResponse({}); - }, - automationAgentRequest: (_meta: unknown, type: string) => ({ - _id: '123', - requestType: type, - }), - automationAgentAwait: (_meta: unknown, type: string) => { - if (type === 'getShardKey') { - return { - response: [ - { - key: { - location: 'HASHED', - secondary: 'HASHED', - }, - unique: false, - }, - ], - }; - } - }, + isNamespaceManaged: () => true, + hasShardKey: () => true, }); await store.dispatch(fetchClusterShardingData()); await waitFor(() => { @@ -160,12 +231,24 @@ describe('GlobalWritesStore Store', function () { }); }); + it('when the namespace is managed but has a sharding error', async function () { + const store = createStore({ + isNamespaceManaged: () => true, + hasShardingError: () => true, + }); + await store.dispatch(fetchClusterShardingData()); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING_ERROR'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + }); + it('sends correct data to the server when creating a shard key', async function () { const alreadyManagedNamespaces = [ { db: 'test', collection: 'one', - customShardKey: 'test', + customShardKey: 'secondary', isCustomShardKeyHashed: true, isShardKeyUnique: false, numInitialChunks: 1, @@ -196,7 +279,7 @@ describe('GlobalWritesStore Store', function () { .resolves(); const store = createStore({ - authenticatedFetch: fetchStub, + authenticatedFetchStub: fetchStub, }); await store.dispatch(createShardKey(shardKeyData)); diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index c824909eddf..0e93c24ae63 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -3,7 +3,8 @@ import type { GlobalWritesThunkAction } from '.'; import { openToast, showConfirmation } from '@mongodb-js/compass-components'; import type { ManagedNamespace } from '../services/atlas-global-writes-service'; -export const POLLING_INTERVAL = 5000; +const POLLING_INTERVAL = 5000; +export const TEST_POLLING_INTERVAL = 1; export function isAction( action: Action, @@ -585,7 +586,7 @@ export const cancelSharding = (): const { namespace, status } = getState(); if (status === ShardingStatuses.SHARDING) { - stopPollingForShardKey(getState); + dispatch(stopPollingForShardKey()); } dispatch({ type: GlobalWritesActionTypes.CancellingShardingStarted, @@ -628,19 +629,21 @@ const setNamespaceBeingSharded = ( type: GlobalWritesActionTypes.SubmittingForShardingFinished, managedNamespace, }); - dispatch(startPollingForShardKey()); + dispatch(pollForShardKey()); }; }; -const startPollingForShardKey = (): GlobalWritesThunkAction< +const pollForShardKey = (): GlobalWritesThunkAction< void, NextPollingTimeoutSetAction > => { return (dispatch) => { - console.log('START POLLING'); - const timeout = setTimeout(() => { - void dispatch(fetchNamespaceShardKey()); - }, POLLING_INTERVAL); + const timeout = setTimeout( + () => { + void dispatch(fetchNamespaceShardKey()); + }, + process.env.NODE_ENV !== 'test' ? POLLING_INTERVAL : TEST_POLLING_INTERVAL + ); dispatch({ type: GlobalWritesActionTypes.NextPollingTimeoutSet, timeout, @@ -648,10 +651,16 @@ const startPollingForShardKey = (): GlobalWritesThunkAction< }; }; -const stopPollingForShardKey = (getState: () => RootState) => { - const { pollingTimeout } = getState(); - if (!pollingTimeout) return; - clearTimeout(pollingTimeout); +const stopPollingForShardKey = (): GlobalWritesThunkAction< + void, + NextPollingTimeoutClearedAction +> => { + return (dispatch, getState) => { + const { pollingTimeout } = getState(); + if (!pollingTimeout) return; + clearTimeout(pollingTimeout); + dispatch({ type: GlobalWritesActionTypes.NextPollingTimeoutCleared }); + }; }; export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< @@ -663,7 +672,6 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< getState, { atlasGlobalWritesService, logger, connectionInfoRef } ) => { - console.log('FETCHING KEY'); const { namespace, status } = getState(); try { @@ -674,7 +682,7 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< if (shardingError) { if (status === ShardingStatuses.SHARDING) { - stopPollingForShardKey(getState); + dispatch(stopPollingForShardKey()); } dispatch({ type: GlobalWritesActionTypes.NamespaceShardingErrorFetched, @@ -689,7 +697,7 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< } if (status === ShardingStatuses.SHARDING) { - stopPollingForShardKey(getState); + dispatch(stopPollingForShardKey()); } dispatch({ type: GlobalWritesActionTypes.NamespaceShardKeyFetched, From 09ce1525dae925d4f58b8a3829d53038d51aa6b9 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Thu, 10 Oct 2024 14:17:57 +0200 Subject: [PATCH 06/17] more tests & fix --- .../src/store/index.spec.ts | 79 +++++++++++++++++-- .../src/store/reducer.ts | 5 +- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index 729b81d4785..8e6d59e9b06 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -6,6 +6,7 @@ import { createShardKey, type CreateShardKeyData, TEST_POLLING_INTERVAL, + unmanageNamespace, } from './reducer'; import sinon from 'sinon'; import type { @@ -155,12 +156,11 @@ describe('GlobalWritesStore Store', function () { }); context('scenarios', function () { - it('not managed -> sharding -> success', async function () { + it('not managed -> sharding -> valid shard key', async function () { let mockShardKey = false; - const hasShardKey = Sinon.fake(() => mockShardKey); // initial state === unsharded const store = createStore({ - hasShardKey, + hasShardKey: Sinon.fake(() => mockShardKey), }); await store.dispatch(fetchClusterShardingData()); expect(store.getState().status).to.equal('UNSHARDED'); @@ -182,10 +182,9 @@ describe('GlobalWritesStore Store', function () { it('not managed -> sharding -> sharding error', async function () { let mockFailure = false; - const hasShardingError = Sinon.fake(() => mockFailure); // initial state === unsharded const store = createStore({ - hasShardingError, + hasShardingError: Sinon.fake(() => mockFailure), }); await store.dispatch(fetchClusterShardingData()); expect(store.getState().status).to.equal('UNSHARDED'); @@ -205,7 +204,7 @@ describe('GlobalWritesStore Store', function () { }); }); - it('not managed -> failed sharding attempt', async function () { + it('not managed -> not managed (failed sharding request)', async function () { const store = createStore({ failsOnShardingRequest: () => true, }); @@ -219,19 +218,83 @@ describe('GlobalWritesStore Store', function () { expect(store.getState().status).to.equal('UNSHARDED'); }); - it('when the namespace is managed and has a valid shard key', async function () { + it('sharding -> valid shard key', async function () { + let mockShardKey = false; + // initial state === sharding + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: Sinon.fake(() => mockShardKey), + }); + await store.dispatch(fetchClusterShardingData()); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + + // sharding ends with a shardKey + mockShardKey = true; + await wait(TEST_POLLING_INTERVAL); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + }); + }); + + it('valid shard key', async function () { + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: () => true, + }); + await store.dispatch(fetchClusterShardingData()); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + }); + + it('valid shard key -> not managed', async function () { + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: () => true, + }); + + // initial state === shard key correct + await store.dispatch(fetchClusterShardingData()); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + + // user asks to unmanage + const promise = store.dispatch(unmanageNamespace()); + expect(store.getState().status).to.equal('UNMANAGING_NAMESPACE'); + await promise; + expect(store.getState().status).to.equal('UNSHARDED'); + }); + + it('valid shard key -> valid shard key (failed unmanage attempt)', async function () { + let mockFailure = false; const store = createStore({ isNamespaceManaged: () => true, hasShardKey: () => true, + failsOnShardingRequest: Sinon.fake(() => mockFailure), }); + + // initial state === shard key correct await store.dispatch(fetchClusterShardingData()); await waitFor(() => { expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); expect(store.getState().managedNamespace).to.equal(managedNamespace); }); + + // user asks to unmanage + mockFailure = true; + const promise = store.dispatch(unmanageNamespace()); + expect(store.getState().status).to.equal('UNMANAGING_NAMESPACE'); + await promise; + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); }); - it('when the namespace is managed but has a sharding error', async function () { + it('sharding error', async function () { const store = createStore({ isNamespaceManaged: () => true, hasShardingError: () => true, diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index 0e93c24ae63..4129632942b 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -4,7 +4,7 @@ import { openToast, showConfirmation } from '@mongodb-js/compass-components'; import type { ManagedNamespace } from '../services/atlas-global-writes-service'; const POLLING_INTERVAL = 5000; -export const TEST_POLLING_INTERVAL = 1; +export const TEST_POLLING_INTERVAL = 100; export function isAction( action: Action, @@ -331,7 +331,8 @@ const reducer: Reducer = (state = initialState, action) => { action, GlobalWritesActionTypes.SubmittingForShardingFinished ) && - state.status === ShardingStatuses.SUBMITTING_FOR_SHARDING + (state.status === ShardingStatuses.SUBMITTING_FOR_SHARDING || + state.status === ShardingStatuses.NOT_READY) ) { return { ...state, From b26b93d72165d3b99c10d32b5522785cb3e952c1 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Thu, 10 Oct 2024 14:28:22 +0200 Subject: [PATCH 07/17] fix log id --- packages/compass-global-writes/src/store/reducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index 4129632942b..c348f2e4b56 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -600,7 +600,7 @@ export const cancelSharding = (): }); } catch (error) { logger.log.error( - logger.mongoLogId(1_001_000_331), + logger.mongoLogId(1_001_000_334), 'AtlasFetchError', 'Error cancelling the sharding process', { From 5dd0f2ed5d92bcfff078fe14a0aa1d732b063f72 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Thu, 10 Oct 2024 15:09:11 +0200 Subject: [PATCH 08/17] fix --- .../src/store/index.spec.ts | 37 +++++++++++++++++++ .../src/store/reducer.ts | 33 +++++++++++------ 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index 8e6d59e9b06..3351e7d727d 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -7,6 +7,7 @@ import { type CreateShardKeyData, TEST_POLLING_INTERVAL, unmanageNamespace, + cancelSharding, } from './reducer'; import sinon from 'sinon'; import type { @@ -18,6 +19,7 @@ import type { } from '../services/atlas-global-writes-service'; import { wait, waitFor } from '@mongodb-js/testing-library-compass'; import Sinon from 'sinon'; +import * as globalWritesReducer from './reducer'; const DB = 'test'; const COLL = 'coll'; @@ -149,6 +151,18 @@ function createStore({ } describe('GlobalWritesStore Store', function () { + let confirmationStub; + + beforeEach(() => { + confirmationStub = sinon + .stub(globalWritesReducer, 'showConfirmation') + .resolves(true); + }); + + afterEach(() => { + sinon.restore(); + }); + it('sets the initial state', function () { const store = createStore(); expect(store.getState().namespace).to.equal(NS); @@ -239,6 +253,29 @@ describe('GlobalWritesStore Store', function () { }); }); + it('sharding -> cancelling request -> not managed', async function () { + let mockManagedNamespace = true; + confirmationStub.resolves(true); + // initial state === sharding + const store = createStore({ + isNamespaceManaged: Sinon.fake(() => mockManagedNamespace), + }); + await store.dispatch(fetchClusterShardingData()); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING'); + expect(store.getState().pollingTimeout).not.to.be.undefined; + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + + // user cancels the sharding request + const promise = store.dispatch(cancelSharding()); + mockManagedNamespace = false; + await promise; + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().pollingTimeout).to.be.undefined; + expect(confirmationStub).to.have.been.called; + }); + it('valid shard key', async function () { const store = createStore({ isNamespaceManaged: () => true, diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index c348f2e4b56..f754683dba0 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -1,6 +1,9 @@ import type { Action, Reducer } from 'redux'; import type { GlobalWritesThunkAction } from '.'; -import { openToast, showConfirmation } from '@mongodb-js/compass-components'; +import { + openToast, + showConfirmation as showConfirmationModal, +} from '@mongodb-js/compass-components'; import type { ManagedNamespace } from '../services/atlas-global-writes-service'; const POLLING_INTERVAL = 5000; @@ -568,21 +571,25 @@ export const createShardKey = ( }; }; -export const cancelSharding = (): - | GlobalWritesThunkAction< - Promise, - | CancellingShardingStartedAction - | CancellingShardingFinishedAction - | CancellingShardingErroredAction - > - | undefined => { +// Exporting this for test only to stub it and set +// its value. This enables to test cancelSharding action. +export const showConfirmation = showConfirmationModal; + +export const cancelSharding = (): GlobalWritesThunkAction< + Promise, + | CancellingShardingStartedAction + | CancellingShardingFinishedAction + | CancellingShardingErroredAction +> => { return async (dispatch, getState, { atlasGlobalWritesService, logger }) => { const confirmed = await showConfirmation({ title: 'Confirmation', description: 'Are you sure you want to cancel the sharding request?', }); - if (!confirmed) return; + if (!confirmed) { + return; + } const { namespace, status } = getState(); @@ -638,13 +645,17 @@ const pollForShardKey = (): GlobalWritesThunkAction< void, NextPollingTimeoutSetAction > => { - return (dispatch) => { + return (dispatch, getState) => { + if (getState().pollingTimeout) { + return; + } const timeout = setTimeout( () => { void dispatch(fetchNamespaceShardKey()); }, process.env.NODE_ENV !== 'test' ? POLLING_INTERVAL : TEST_POLLING_INTERVAL ); + dispatch({ type: GlobalWritesActionTypes.NextPollingTimeoutSet, timeout, From 683c86cf3b6d8be5b44b8459a5b524f6e5e7b621 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Thu, 10 Oct 2024 15:48:43 +0200 Subject: [PATCH 09/17] . --- packages/compass-global-writes/src/store/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index 3351e7d727d..f18c05a5758 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -151,7 +151,7 @@ function createStore({ } describe('GlobalWritesStore Store', function () { - let confirmationStub; + let confirmationStub: Sinon.SinonStub; beforeEach(() => { confirmationStub = sinon From b89df89b986e03df697a5754b9c4e59c8078aa10 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Thu, 10 Oct 2024 16:55:24 +0200 Subject: [PATCH 10/17] remove extra calls --- .../src/store/index.spec.ts | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index f18c05a5758..adcdc2ecc43 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -2,7 +2,6 @@ import { expect } from 'chai'; import { type GlobalWritesStore } from '.'; import { setupStore } from '../../tests/create-store'; import { - fetchClusterShardingData, createShardKey, type CreateShardKeyData, TEST_POLLING_INTERVAL, @@ -176,9 +175,10 @@ describe('GlobalWritesStore Store', function () { const store = createStore({ hasShardKey: Sinon.fake(() => mockShardKey), }); - await store.dispatch(fetchClusterShardingData()); - expect(store.getState().status).to.equal('UNSHARDED'); - expect(store.getState().managedNamespace).to.equal(undefined); + await waitFor(() => { + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().managedNamespace).to.equal(undefined); + }); // user requests sharding const promise = store.dispatch(createShardKey(shardKeyData)); @@ -200,9 +200,10 @@ describe('GlobalWritesStore Store', function () { const store = createStore({ hasShardingError: Sinon.fake(() => mockFailure), }); - await store.dispatch(fetchClusterShardingData()); - expect(store.getState().status).to.equal('UNSHARDED'); - expect(store.getState().managedNamespace).to.equal(undefined); + await waitFor(() => { + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().managedNamespace).to.equal(undefined); + }); // user requests sharding const promise = store.dispatch(createShardKey(shardKeyData)); @@ -219,13 +220,16 @@ describe('GlobalWritesStore Store', function () { }); it('not managed -> not managed (failed sharding request)', async function () { + // initial state === not managed const store = createStore({ failsOnShardingRequest: () => true, }); - await store.dispatch(fetchClusterShardingData()); - expect(store.getState().status).to.equal('UNSHARDED'); - expect(store.getState().managedNamespace).to.equal(undefined); + await waitFor(() => { + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().managedNamespace).to.equal(undefined); + }); + // user tries to submit for sharding, but the request fails const promise = store.dispatch(createShardKey(shardKeyData)); expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); await promise; @@ -239,7 +243,6 @@ describe('GlobalWritesStore Store', function () { isNamespaceManaged: () => true, hasShardKey: Sinon.fake(() => mockShardKey), }); - await store.dispatch(fetchClusterShardingData()); await waitFor(() => { expect(store.getState().status).to.equal('SHARDING'); expect(store.getState().managedNamespace).to.equal(managedNamespace); @@ -260,7 +263,6 @@ describe('GlobalWritesStore Store', function () { const store = createStore({ isNamespaceManaged: Sinon.fake(() => mockManagedNamespace), }); - await store.dispatch(fetchClusterShardingData()); await waitFor(() => { expect(store.getState().status).to.equal('SHARDING'); expect(store.getState().pollingTimeout).not.to.be.undefined; @@ -281,7 +283,6 @@ describe('GlobalWritesStore Store', function () { isNamespaceManaged: () => true, hasShardKey: () => true, }); - await store.dispatch(fetchClusterShardingData()); await waitFor(() => { expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); expect(store.getState().managedNamespace).to.equal(managedNamespace); @@ -289,13 +290,11 @@ describe('GlobalWritesStore Store', function () { }); it('valid shard key -> not managed', async function () { + // initial state === shard key correct const store = createStore({ isNamespaceManaged: () => true, hasShardKey: () => true, }); - - // initial state === shard key correct - await store.dispatch(fetchClusterShardingData()); await waitFor(() => { expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); expect(store.getState().managedNamespace).to.equal(managedNamespace); @@ -309,6 +308,7 @@ describe('GlobalWritesStore Store', function () { }); it('valid shard key -> valid shard key (failed unmanage attempt)', async function () { + // initial state === shard key correct let mockFailure = false; const store = createStore({ isNamespaceManaged: () => true, @@ -316,8 +316,6 @@ describe('GlobalWritesStore Store', function () { failsOnShardingRequest: Sinon.fake(() => mockFailure), }); - // initial state === shard key correct - await store.dispatch(fetchClusterShardingData()); await waitFor(() => { expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); expect(store.getState().managedNamespace).to.equal(managedNamespace); @@ -336,7 +334,6 @@ describe('GlobalWritesStore Store', function () { isNamespaceManaged: () => true, hasShardingError: () => true, }); - await store.dispatch(fetchClusterShardingData()); await waitFor(() => { expect(store.getState().status).to.equal('SHARDING_ERROR'); expect(store.getState().managedNamespace).to.equal(managedNamespace); From 963e98e77d7f7ecd9cb3c0a9ad612935fd1ab861 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Thu, 10 Oct 2024 20:32:13 +0200 Subject: [PATCH 11/17] poll only if the plugin title is visible --- .../src/plugin-title.tsx | 27 ++++++++-- .../src/store/reducer.ts | 52 ++++++++++++++++++- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/packages/compass-global-writes/src/plugin-title.tsx b/packages/compass-global-writes/src/plugin-title.tsx index 0c484aecaac..14d0001e70a 100644 --- a/packages/compass-global-writes/src/plugin-title.tsx +++ b/packages/compass-global-writes/src/plugin-title.tsx @@ -1,6 +1,10 @@ import { connect } from 'react-redux'; -import React from 'react'; -import { type RootState, ShardingStatuses } from './store/reducer'; +import React, { useEffect } from 'react'; +import { + type RootState, + setPluginTitleVisibility, + ShardingStatuses, +} from './store/reducer'; import { Body, css, @@ -30,8 +34,20 @@ const iconStylesDark = css({ color: palette.yellow.base, }); -const PluginTitle = ({ showWarning }: { showWarning: boolean }) => { +const PluginTitle = ({ + showWarning, + onVisibilityChanged, +}: { + showWarning: boolean; + onVisibilityChanged: (isVisible: boolean) => void; +}) => { const darkMode = useDarkMode(); + useEffect(() => { + onVisibilityChanged(true); + return () => { + onVisibilityChanged(false); + }; + }); return (
Global Writes{' '} @@ -74,5 +90,8 @@ const PluginTitle = ({ showWarning }: { showWarning: boolean }) => { export const GlobalWritesTabTitle = connect( ({ managedNamespace, status }: RootState) => ({ showWarning: !managedNamespace && status !== ShardingStatuses.NOT_READY, - }) + }), + { + onVisibilityChanged: setPluginTitleVisibility, + } )(PluginTitle); diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index f754683dba0..a0e8b397460 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', + + PluginTitleVisibilityChanged = 'global-writes/PluginTitleVisibilityChanged', } type ManagedNamespaceFetchedAction = { @@ -115,6 +117,11 @@ type UnmanagingNamespaceErroredAction = { type: GlobalWritesActionTypes.UnmanagingNamespaceErrored; }; +type PluginTitleVisibilityChangedAction = { + type: GlobalWritesActionTypes.PluginTitleVisibilityChanged; + isVisible: boolean; +}; + export enum ShardingStatuses { /** * Initial status, no information available yet. @@ -193,6 +200,7 @@ export type RootState = { namespace: string; managedNamespace?: ManagedNamespace; shardZones: ShardZoneData[]; + isPluginTitleVisible: boolean; } & ( | { status: ShardingStatuses.NOT_READY; @@ -245,6 +253,7 @@ const initialState: RootState = { namespace: '', status: ShardingStatuses.NOT_READY, shardZones: [], + isPluginTitleVisible: true, }; const reducer: Reducer = (state = initialState, action) => { @@ -472,6 +481,19 @@ const reducer: Reducer = (state = initialState, action) => { }; } + if ( + isAction( + action, + GlobalWritesActionTypes.PluginTitleVisibilityChanged + ) + ) { + if (state.isPluginTitleVisible === action.isVisible) return state; + return { + ...state, + isPluginTitleVisible: action.isVisible, + }; + } + return state; }; @@ -646,7 +668,11 @@ const pollForShardKey = (): GlobalWritesThunkAction< NextPollingTimeoutSetAction > => { return (dispatch, getState) => { - if (getState().pollingTimeout) { + const { pollingTimeout, isPluginTitleVisible } = getState(); + if ( + !isPluginTitleVisible || // user is not in the Collection Workspace + pollingTimeout // prevent double polling + ) { return; } const timeout = setTimeout( @@ -753,6 +779,30 @@ export const fetchShardingZones = (): GlobalWritesThunkAction< }; }; +export const setPluginTitleVisibility = ( + isVisible: boolean +): GlobalWritesThunkAction => { + return (dispatch, getState) => { + const { + status, + pollingTimeout, + isPluginTitleVisible: previousIsVisible, + } = getState(); + dispatch({ + type: GlobalWritesActionTypes.PluginTitleVisibilityChanged, + isVisible, + }); + if ( + isVisible && + !previousIsVisible && + status === ShardingStatuses.SHARDING && + !pollingTimeout + ) { + dispatch(pollForShardKey()); + } + }; +}; + export const unmanageNamespace = (): GlobalWritesThunkAction< Promise, | UnmanagingNamespaceStartedAction From a35a63200eed5923c25fa0e74f18c5258b608144 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Thu, 10 Oct 2024 20:44:47 +0200 Subject: [PATCH 12/17] add test --- .../src/store/index.spec.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index adcdc2ecc43..6f68dd85c6e 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -7,6 +7,7 @@ import { TEST_POLLING_INTERVAL, unmanageNamespace, cancelSharding, + setPluginTitleVisibility, } from './reducer'; import sinon from 'sinon'; import type { @@ -278,6 +279,33 @@ describe('GlobalWritesStore Store', function () { expect(confirmationStub).to.have.been.called; }); + it('sharding -> pause sharding -> resume sharding -> valid shard key', async function () { + let mockShardKey = false; + confirmationStub.resolves(true); + // initial state === sharding + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: Sinon.fake(() => mockShardKey), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING'); + }); + + // user leaves the workspace + store.dispatch(setPluginTitleVisibility(false)); + mockShardKey = true; + await wait(TEST_POLLING_INTERVAL * 2); + expect(store.getState().pollingTimeout).to.be.undefined; + expect(store.getState().status).to.equal('SHARDING'); // no update + + // user comes back + store.dispatch(setPluginTitleVisibility(true)); + await wait(TEST_POLLING_INTERVAL); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); // now there is an update + }); + }); + it('valid shard key', async function () { const store = createStore({ isNamespaceManaged: () => true, From a3c7bda989cc45e3c39706391dfa982a9b85339d Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 11 Oct 2024 15:12:24 +0200 Subject: [PATCH 13/17] . --- packages/compass-global-writes/src/store/index.ts | 1 + packages/compass-global-writes/src/store/reducer.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/compass-global-writes/src/store/index.ts b/packages/compass-global-writes/src/store/index.ts index b00fad27e27..4f06184a775 100644 --- a/packages/compass-global-writes/src/store/index.ts +++ b/packages/compass-global-writes/src/store/index.ts @@ -66,6 +66,7 @@ export function activateGlobalWritesPlugin( namespace: options.namespace, status: ShardingStatuses.NOT_READY, shardZones: [], + isPluginTitleVisible: false, }, applyMiddleware( thunk.withExtraArgument({ diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index a0e8b397460..ee8970a1b21 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -253,7 +253,7 @@ const initialState: RootState = { namespace: '', status: ShardingStatuses.NOT_READY, shardZones: [], - isPluginTitleVisible: true, + isPluginTitleVisible: false, }; const reducer: Reducer = (state = initialState, action) => { @@ -798,7 +798,7 @@ export const setPluginTitleVisibility = ( status === ShardingStatuses.SHARDING && !pollingTimeout ) { - dispatch(pollForShardKey()); + void dispatch(fetchNamespaceShardKey()); } }; }; From 7cc3dcfc6229940f5b6dcb84d3c452aff70e84e2 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 14 Oct 2024 11:23:04 +0200 Subject: [PATCH 14/17] fix --- packages/compass-global-writes/src/store/index.ts | 2 +- packages/compass-global-writes/src/store/reducer.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/compass-global-writes/src/store/index.ts b/packages/compass-global-writes/src/store/index.ts index 4f06184a775..a0398db9542 100644 --- a/packages/compass-global-writes/src/store/index.ts +++ b/packages/compass-global-writes/src/store/index.ts @@ -66,7 +66,7 @@ export function activateGlobalWritesPlugin( namespace: options.namespace, status: ShardingStatuses.NOT_READY, shardZones: [], - isPluginTitleVisible: false, + isPluginTitleVisible: true, }, applyMiddleware( thunk.withExtraArgument({ diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index ee8970a1b21..39f7c933c6e 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -253,7 +253,7 @@ const initialState: RootState = { namespace: '', status: ShardingStatuses.NOT_READY, shardZones: [], - isPluginTitleVisible: false, + isPluginTitleVisible: true, }; const reducer: Reducer = (state = initialState, action) => { @@ -710,7 +710,12 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< getState, { atlasGlobalWritesService, logger, connectionInfoRef } ) => { - const { namespace, status } = getState(); + const { namespace, status, isPluginTitleVisible } = getState(); + + if (!isPluginTitleVisible) { + dispatch(stopPollingForShardKey()); + return; + } try { const [shardingError, shardKey] = await Promise.all([ From 84a6d707b8455625701f6096e9481e5fcadea5f0 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 14 Oct 2024 11:56:42 +0200 Subject: [PATCH 15/17] PluginTitle tests --- .../src/plugin-title.spec.tsx | 53 +++++++++++++++++++ .../src/plugin-title.tsx | 3 +- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 packages/compass-global-writes/src/plugin-title.spec.tsx diff --git a/packages/compass-global-writes/src/plugin-title.spec.tsx b/packages/compass-global-writes/src/plugin-title.spec.tsx new file mode 100644 index 00000000000..92fff2a0e7d --- /dev/null +++ b/packages/compass-global-writes/src/plugin-title.spec.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { expect } from 'chai'; +import { PluginTitle } from './plugin-title'; +import { render, screen } from '@mongodb-js/testing-library-compass'; +import Sinon from 'sinon'; + +describe('PluginTitle', function () { + let onVisibilityChanged: Sinon.SinonSpy; + + beforeEach(function () { + onVisibilityChanged = Sinon.spy(); + }); + + afterEach(function () { + onVisibilityChanged.resetHistory(); + }); + + it('Renders a warning', function () { + render( + + ); + expect(screen.getByLabelText('warning')).to.be.visible; + }); + + it('Does not render a warning', function () { + render( + + ); + expect(screen.queryByLabelText('warning')).not.to.exist; + }); + + it('Calls the onVisibilityChanged callback when dismounted', function () { + const mount = render( + + ); + + expect(onVisibilityChanged).to.have.been.calledOnceWith(true); + + mount.unmount(); + + expect(onVisibilityChanged).to.have.been.calledTwice; + expect(onVisibilityChanged.getCalls()[1].args[0]).to.equal(false); + }); +}); diff --git a/packages/compass-global-writes/src/plugin-title.tsx b/packages/compass-global-writes/src/plugin-title.tsx index 14d0001e70a..dd9a99bce23 100644 --- a/packages/compass-global-writes/src/plugin-title.tsx +++ b/packages/compass-global-writes/src/plugin-title.tsx @@ -34,7 +34,7 @@ const iconStylesDark = css({ color: palette.yellow.base, }); -const PluginTitle = ({ +export const PluginTitle = ({ showWarning, onVisibilityChanged, }: { @@ -66,6 +66,7 @@ const PluginTitle = ({ > Date: Mon, 14 Oct 2024 17:46:47 +0200 Subject: [PATCH 16/17] remove TEST_POLLING_INTERVAL --- .../src/store/index.spec.ts | 27 ++++++++++++++----- .../src/store/reducer.ts | 12 +++------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index 6f68dd85c6e..5feb66d6661 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -4,10 +4,10 @@ import { setupStore } from '../../tests/create-store'; import { createShardKey, type CreateShardKeyData, - TEST_POLLING_INTERVAL, unmanageNamespace, cancelSharding, setPluginTitleVisibility, + POLLING_INTERVAL, } from './reducer'; import sinon from 'sinon'; import type { @@ -152,6 +152,7 @@ function createStore({ describe('GlobalWritesStore Store', function () { let confirmationStub: Sinon.SinonStub; + let clock: Sinon.SinonFakeTimers; beforeEach(() => { confirmationStub = sinon @@ -161,6 +162,7 @@ describe('GlobalWritesStore Store', function () { afterEach(() => { sinon.restore(); + clock && clock.restore(); }); it('sets the initial state', function () { @@ -182,6 +184,9 @@ describe('GlobalWritesStore Store', function () { }); // user requests sharding + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, + }); const promise = store.dispatch(createShardKey(shardKeyData)); expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); await promise; @@ -189,7 +194,7 @@ describe('GlobalWritesStore Store', function () { // sharding ends with a shardKey mockShardKey = true; - await wait(TEST_POLLING_INTERVAL); + clock.tick(POLLING_INTERVAL); await waitFor(() => { expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); }); @@ -207,6 +212,9 @@ describe('GlobalWritesStore Store', function () { }); // user requests sharding + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, + }); const promise = store.dispatch(createShardKey(shardKeyData)); expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); await promise; @@ -214,7 +222,7 @@ describe('GlobalWritesStore Store', function () { // sharding ends with an error mockFailure = true; - await wait(TEST_POLLING_INTERVAL); + clock.tick(POLLING_INTERVAL); await waitFor(() => { expect(store.getState().status).to.equal('SHARDING_ERROR'); }); @@ -240,6 +248,9 @@ describe('GlobalWritesStore Store', function () { it('sharding -> valid shard key', async function () { let mockShardKey = false; // initial state === sharding + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, + }); const store = createStore({ isNamespaceManaged: () => true, hasShardKey: Sinon.fake(() => mockShardKey), @@ -251,7 +262,7 @@ describe('GlobalWritesStore Store', function () { // sharding ends with a shardKey mockShardKey = true; - await wait(TEST_POLLING_INTERVAL); + clock.tick(POLLING_INTERVAL); await waitFor(() => { expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); }); @@ -283,6 +294,9 @@ describe('GlobalWritesStore Store', function () { let mockShardKey = false; confirmationStub.resolves(true); // initial state === sharding + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, + }); const store = createStore({ isNamespaceManaged: () => true, hasShardKey: Sinon.fake(() => mockShardKey), @@ -294,13 +308,14 @@ describe('GlobalWritesStore Store', function () { // user leaves the workspace store.dispatch(setPluginTitleVisibility(false)); mockShardKey = true; - await wait(TEST_POLLING_INTERVAL * 2); + clock.tick(POLLING_INTERVAL); + clock.tick(POLLING_INTERVAL); expect(store.getState().pollingTimeout).to.be.undefined; expect(store.getState().status).to.equal('SHARDING'); // no update // user comes back store.dispatch(setPluginTitleVisibility(true)); - await wait(TEST_POLLING_INTERVAL); + await wait(POLLING_INTERVAL); await waitFor(() => { expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); // now there is an update }); diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index 39f7c933c6e..d4399ee7ff1 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -6,8 +6,7 @@ import { } from '@mongodb-js/compass-components'; import type { ManagedNamespace } from '../services/atlas-global-writes-service'; -const POLLING_INTERVAL = 5000; -export const TEST_POLLING_INTERVAL = 100; +export const POLLING_INTERVAL = 5000; export function isAction( action: Action, @@ -675,12 +674,9 @@ const pollForShardKey = (): GlobalWritesThunkAction< ) { return; } - const timeout = setTimeout( - () => { - void dispatch(fetchNamespaceShardKey()); - }, - process.env.NODE_ENV !== 'test' ? POLLING_INTERVAL : TEST_POLLING_INTERVAL - ); + const timeout = setTimeout(() => { + void dispatch(fetchNamespaceShardKey()); + }, POLLING_INTERVAL); dispatch({ type: GlobalWritesActionTypes.NextPollingTimeoutSet, From f67f6d5317ffa1b8b5d9b8f3108bb14c9d6d7f65 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 14 Oct 2024 18:46:49 +0200 Subject: [PATCH 17/17] remove visibility --- .../src/plugin-title.spec.tsx | 41 +------------- .../src/plugin-title.tsx | 27 ++------- .../src/store/index.spec.ts | 34 +---------- .../compass-global-writes/src/store/index.ts | 1 - .../src/store/reducer.ts | 56 +------------------ 5 files changed, 9 insertions(+), 150 deletions(-) diff --git a/packages/compass-global-writes/src/plugin-title.spec.tsx b/packages/compass-global-writes/src/plugin-title.spec.tsx index 92fff2a0e7d..ce4e843a822 100644 --- a/packages/compass-global-writes/src/plugin-title.spec.tsx +++ b/packages/compass-global-writes/src/plugin-title.spec.tsx @@ -2,52 +2,15 @@ import React from 'react'; import { expect } from 'chai'; import { PluginTitle } from './plugin-title'; import { render, screen } from '@mongodb-js/testing-library-compass'; -import Sinon from 'sinon'; describe('PluginTitle', function () { - let onVisibilityChanged: Sinon.SinonSpy; - - beforeEach(function () { - onVisibilityChanged = Sinon.spy(); - }); - - afterEach(function () { - onVisibilityChanged.resetHistory(); - }); - it('Renders a warning', function () { - render( - - ); + render(); expect(screen.getByLabelText('warning')).to.be.visible; }); it('Does not render a warning', function () { - render( - - ); + render(); expect(screen.queryByLabelText('warning')).not.to.exist; }); - - it('Calls the onVisibilityChanged callback when dismounted', function () { - const mount = render( - - ); - - expect(onVisibilityChanged).to.have.been.calledOnceWith(true); - - mount.unmount(); - - expect(onVisibilityChanged).to.have.been.calledTwice; - expect(onVisibilityChanged.getCalls()[1].args[0]).to.equal(false); - }); }); diff --git a/packages/compass-global-writes/src/plugin-title.tsx b/packages/compass-global-writes/src/plugin-title.tsx index dd9a99bce23..9106ae55c29 100644 --- a/packages/compass-global-writes/src/plugin-title.tsx +++ b/packages/compass-global-writes/src/plugin-title.tsx @@ -1,10 +1,6 @@ import { connect } from 'react-redux'; -import React, { useEffect } from 'react'; -import { - type RootState, - setPluginTitleVisibility, - ShardingStatuses, -} from './store/reducer'; +import React from 'react'; +import { type RootState, ShardingStatuses } from './store/reducer'; import { Body, css, @@ -34,20 +30,8 @@ const iconStylesDark = css({ color: palette.yellow.base, }); -export const PluginTitle = ({ - showWarning, - onVisibilityChanged, -}: { - showWarning: boolean; - onVisibilityChanged: (isVisible: boolean) => void; -}) => { +export const PluginTitle = ({ showWarning }: { showWarning: boolean }) => { const darkMode = useDarkMode(); - useEffect(() => { - onVisibilityChanged(true); - return () => { - onVisibilityChanged(false); - }; - }); return (
Global Writes{' '} @@ -91,8 +75,5 @@ export const PluginTitle = ({ export const GlobalWritesTabTitle = connect( ({ managedNamespace, status }: RootState) => ({ showWarning: !managedNamespace && status !== ShardingStatuses.NOT_READY, - }), - { - onVisibilityChanged: setPluginTitleVisibility, - } + }) )(PluginTitle); diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index 5feb66d6661..bc6194ba5d8 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -6,7 +6,6 @@ import { type CreateShardKeyData, unmanageNamespace, cancelSharding, - setPluginTitleVisibility, POLLING_INTERVAL, } from './reducer'; import sinon from 'sinon'; @@ -17,7 +16,7 @@ import type { ManagedNamespace, ShardZoneMapping, } from '../services/atlas-global-writes-service'; -import { wait, waitFor } from '@mongodb-js/testing-library-compass'; +import { waitFor } from '@mongodb-js/testing-library-compass'; import Sinon from 'sinon'; import * as globalWritesReducer from './reducer'; @@ -290,37 +289,6 @@ describe('GlobalWritesStore Store', function () { expect(confirmationStub).to.have.been.called; }); - it('sharding -> pause sharding -> resume sharding -> valid shard key', async function () { - let mockShardKey = false; - confirmationStub.resolves(true); - // initial state === sharding - clock = sinon.useFakeTimers({ - shouldAdvanceTime: true, - }); - const store = createStore({ - isNamespaceManaged: () => true, - hasShardKey: Sinon.fake(() => mockShardKey), - }); - await waitFor(() => { - expect(store.getState().status).to.equal('SHARDING'); - }); - - // user leaves the workspace - store.dispatch(setPluginTitleVisibility(false)); - mockShardKey = true; - clock.tick(POLLING_INTERVAL); - clock.tick(POLLING_INTERVAL); - expect(store.getState().pollingTimeout).to.be.undefined; - expect(store.getState().status).to.equal('SHARDING'); // no update - - // user comes back - store.dispatch(setPluginTitleVisibility(true)); - await wait(POLLING_INTERVAL); - await waitFor(() => { - expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); // now there is an update - }); - }); - it('valid shard key', async function () { const store = createStore({ isNamespaceManaged: () => true, diff --git a/packages/compass-global-writes/src/store/index.ts b/packages/compass-global-writes/src/store/index.ts index a0398db9542..b00fad27e27 100644 --- a/packages/compass-global-writes/src/store/index.ts +++ b/packages/compass-global-writes/src/store/index.ts @@ -66,7 +66,6 @@ export function activateGlobalWritesPlugin( namespace: options.namespace, status: ShardingStatuses.NOT_READY, shardZones: [], - isPluginTitleVisible: true, }, applyMiddleware( thunk.withExtraArgument({ diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index d4399ee7ff1..d596d52d51c 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -45,8 +45,6 @@ enum GlobalWritesActionTypes { UnmanagingNamespaceStarted = 'global-writes/UnmanagingNamespaceStarted', UnmanagingNamespaceFinished = 'global-writes/UnmanagingNamespaceFinished', UnmanagingNamespaceErrored = 'global-writes/UnmanagingNamespaceErrored', - - PluginTitleVisibilityChanged = 'global-writes/PluginTitleVisibilityChanged', } type ManagedNamespaceFetchedAction = { @@ -116,11 +114,6 @@ type UnmanagingNamespaceErroredAction = { type: GlobalWritesActionTypes.UnmanagingNamespaceErrored; }; -type PluginTitleVisibilityChangedAction = { - type: GlobalWritesActionTypes.PluginTitleVisibilityChanged; - isVisible: boolean; -}; - export enum ShardingStatuses { /** * Initial status, no information available yet. @@ -199,7 +192,6 @@ export type RootState = { namespace: string; managedNamespace?: ManagedNamespace; shardZones: ShardZoneData[]; - isPluginTitleVisible: boolean; } & ( | { status: ShardingStatuses.NOT_READY; @@ -252,7 +244,6 @@ const initialState: RootState = { namespace: '', status: ShardingStatuses.NOT_READY, shardZones: [], - isPluginTitleVisible: true, }; const reducer: Reducer = (state = initialState, action) => { @@ -480,19 +471,6 @@ const reducer: Reducer = (state = initialState, action) => { }; } - if ( - isAction( - action, - GlobalWritesActionTypes.PluginTitleVisibilityChanged - ) - ) { - if (state.isPluginTitleVisible === action.isVisible) return state; - return { - ...state, - isPluginTitleVisible: action.isVisible, - }; - } - return state; }; @@ -667,9 +645,8 @@ const pollForShardKey = (): GlobalWritesThunkAction< NextPollingTimeoutSetAction > => { return (dispatch, getState) => { - const { pollingTimeout, isPluginTitleVisible } = getState(); + const { pollingTimeout } = getState(); if ( - !isPluginTitleVisible || // user is not in the Collection Workspace pollingTimeout // prevent double polling ) { return; @@ -706,12 +683,7 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< getState, { atlasGlobalWritesService, logger, connectionInfoRef } ) => { - const { namespace, status, isPluginTitleVisible } = getState(); - - if (!isPluginTitleVisible) { - dispatch(stopPollingForShardKey()); - return; - } + const { namespace, status } = getState(); try { const [shardingError, shardKey] = await Promise.all([ @@ -780,30 +752,6 @@ export const fetchShardingZones = (): GlobalWritesThunkAction< }; }; -export const setPluginTitleVisibility = ( - isVisible: boolean -): GlobalWritesThunkAction => { - return (dispatch, getState) => { - const { - status, - pollingTimeout, - isPluginTitleVisible: previousIsVisible, - } = getState(); - dispatch({ - type: GlobalWritesActionTypes.PluginTitleVisibilityChanged, - isVisible, - }); - if ( - isVisible && - !previousIsVisible && - status === ShardingStatuses.SHARDING && - !pollingTimeout - ) { - void dispatch(fetchNamespaceShardKey()); - } - }; -}; - export const unmanageNamespace = (): GlobalWritesThunkAction< Promise, | UnmanagingNamespaceStartedAction