From 9c255d6bb5e2e0690046b4c0cc8ba4b49224c291 Mon Sep 17 00:00:00 2001 From: Basit Chonka Date: Mon, 30 Sep 2024 16:24:41 +0200 Subject: [PATCH 01/17] setup sharded state --- .../src/make-automation-agent-op-request.ts | 17 + .../src/components/index.tsx | 8 + .../src/components/shard-zones-table.tsx | 48 +++ .../components/states/shard-key-correct.tsx | 192 +++++++++ .../src/plugin-title.tsx | 4 +- .../services/atlas-global-writes-service.ts | 222 +++++++++- .../compass-global-writes/src/store/index.ts | 7 +- .../src/store/reducer.ts | 403 +++++++++++++++--- 8 files changed, 814 insertions(+), 87 deletions(-) create mode 100644 packages/compass-global-writes/src/components/shard-zones-table.tsx create mode 100644 packages/compass-global-writes/src/components/states/shard-key-correct.tsx diff --git a/packages/atlas-service/src/make-automation-agent-op-request.ts b/packages/atlas-service/src/make-automation-agent-op-request.ts index 1d0c5c0def1..37758f2222d 100644 --- a/packages/atlas-service/src/make-automation-agent-op-request.ts +++ b/packages/atlas-service/src/make-automation-agent-op-request.ts @@ -19,6 +19,10 @@ export type AutomationAgentRequestTypes = { collection: string; name: string; }; + getShardKey: ClusterOrServerlessId & { + db: string; + collection: string; + }; }; type AutomationAgentRequestOpTypes = keyof AutomationAgentRequestTypes; @@ -60,6 +64,11 @@ export type AutomationAgentAwaitResponseTypes = { status: 'rolling build' | 'building' | 'exists'; }[]; dropIndex: never[]; + getShardKey: Array<{ + _id: string; + unique: boolean; + key: Record; + }>; }; type AutomationAgentAwaitOpTypes = keyof AutomationAgentAwaitResponseTypes; @@ -121,6 +130,10 @@ function unwrapAutomationAgentAwaitResponse( json: any, opType: 'dropIndex' ): UnwrappedAutomationAgentAwaitResponse<'dropIndex'>; +function unwrapAutomationAgentAwaitResponse( + json: any, + opType: 'getShardKey' +): UnwrappedAutomationAgentAwaitResponse<'getShardKey'>; function unwrapAutomationAgentAwaitResponse(json: any, opType: string): never; function unwrapAutomationAgentAwaitResponse( json: any, @@ -136,6 +149,10 @@ function unwrapAutomationAgentAwaitResponse( assertAutomationAgentAwaitResponse(json, opType); return json.response; } + if (opType === 'getShardKey') { + assertAutomationAgentAwaitResponse(json, opType); + return json.response; + } throw new Error(`Unsupported await response type: ${opType}`); } diff --git a/packages/compass-global-writes/src/components/index.tsx b/packages/compass-global-writes/src/components/index.tsx index ff0a5b02fa4..77aa286de46 100644 --- a/packages/compass-global-writes/src/components/index.tsx +++ b/packages/compass-global-writes/src/components/index.tsx @@ -10,6 +10,7 @@ import type { RootState, ShardingStatus } from '../store/reducer'; import { ShardingStatuses } from '../store/reducer'; import UnshardedState from './states/unsharded'; import ShardingState from './states/sharding'; +import ShardKeyCorrect from './states/shard-key-correct'; const containerStyles = css({ paddingLeft: spacing[400], @@ -58,6 +59,13 @@ function ShardingStateView({ return ; } + if ( + shardingStatus === ShardingStatuses.SHARD_KEY_CORRECT || + shardingStatus === ShardingStatuses.UNMANAGING_NAMESPACE + ) { + return ; + } + return null; } diff --git a/packages/compass-global-writes/src/components/shard-zones-table.tsx b/packages/compass-global-writes/src/components/shard-zones-table.tsx new file mode 100644 index 00000000000..9a567c1b93c --- /dev/null +++ b/packages/compass-global-writes/src/components/shard-zones-table.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { + Table, + TableBody, + TableHead, + HeaderRow, + HeaderCell, + Row, + Cell, + css, +} from '@mongodb-js/compass-components'; +import type { ShardZoneData } from '../store/reducer'; + +const containerStyles = css({ + maxWidth: '700px', + height: '400px', +}); + +export function ShardZonesTable({ + shardZones, +}: { + shardZones: ShardZoneData[]; +}) { + return ( + // TODO: Add option to search and group zones by ShardZoneData.typeOneIsoCode + // and display them in nested row + + + + Location Name + Zone + + + + {shardZones.map((shardZone, index) => { + return ( + + {shardZone.country} + + {shardZone.zoneName}({shardZone.zoneLocations.join(', ')}) + + + ); + })} + +
+ ); +} diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx new file mode 100644 index 00000000000..a4a351aed6a --- /dev/null +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx @@ -0,0 +1,192 @@ +import React, { useMemo } from 'react'; +import { + Banner, + BannerVariant, + Body, + css, + Link, + spacing, + Code, + Subtitle, + Label, + SpinLoader, + Button, +} from '@mongodb-js/compass-components'; +import { connect } from 'react-redux'; +import { + ShardingStatuses, + unmanageNamespace, + type RootState, + type ShardKey, + type ShardZoneData, +} from '../../store/reducer'; +import toNS from 'mongodb-ns'; +import { ShardZonesTable } from '../shard-zones-table'; + +const nbsp = '\u00a0'; + +const containerStyles = css({ + display: 'flex', + flexDirection: 'column', + gap: spacing[400], +}); + +const codeBlockContainerStyles = css({ + display: 'flex', + flexDirection: 'column', + gap: spacing[100], + maxWidth: '700px', +}); + +const paragraphStyles = css({ + display: 'flex', + flexDirection: 'column', + gap: spacing[100], +}); + +type ShardKeyCorrectProps = { + namespace: string; + shardKey: ShardKey; + shardZones: ShardZoneData[]; + isUnmanagingNamespace: boolean; + onUnmanageNamespace: () => void; +}; + +export function ShardKeyCorrect({ + namespace, + shardKey, + shardZones, + isUnmanagingNamespace, + onUnmanageNamespace, +}: ShardKeyCorrectProps) { + const customShardKeyField = useMemo(() => { + return shardKey.fields[1].name; + }, [shardKey]); + + const sampleCodes = useMemo(() => { + const { collection } = toNS(namespace); + return { + findingDocuments: `use ${collection}\ndb.${collection}.find({"location": "US-NY", "${customShardKeyField}": ""})`, + insertingDocuments: `use ${collection}\ndb.${collection}.insertOne({"location": "US-NY", "${customShardKeyField}": "",...})`, + }; + }, [namespace, customShardKeyField]); + + return ( +
+ + + All documents in your collection should contain both the ‘location’ + field (with a ISO country or subdivision code) and your{' '} + {customShardKeyField} field at insert time. + + {nbsp}We have included a table for reference below. + + +
+ + {namespace} is configured with the following shard + key: + + {namespace} +
+ + Example commands +
+ + Start querying your database with some of the most{' '} + + common commands + {' '} + for Global Writes. + + + Replace the text to perform operations on different documents. US-NY + is an ISO 3166 location code referring to New York, United States. You + can look up other ISO 3166 location codes below. + +
+ +
+ + {sampleCodes.findingDocuments} +
+ +
+ + {sampleCodes.insertingDocuments} +
+ + Location Codes +
+ + Each document’s first field should include an ISO 3166-1 Alpha-2 code + for the location it belongs to. + + + We also support ISO 3166-2 subdivision codes for countries containing + a cloud provider data center (both ISO 3166-1 and ISO 3166-2 codes may + be used for these countries). All valid country codes and the zones to + which they map are listed in the table below. Additionally, you can + view a list of all location codes{' '} + + here + + . + + + Locations’ zone mapping can be changed by navigating to this clusters{' '} + + Edit Configuration + {' '} + page and clicking the Configure Location Mappings’ link above the map. + +
+ + + + Unmanage this collection + + Documents belonging to this collection will no longer be distributed + across the shards of your global clusters. + +
+ +
+
+ ); +} + +export default connect( + (state: RootState) => ({ + namespace: state.namespace, + // For this view, sharKey is always defined + shardKey: state.shardKey as ShardKey, + shardZones: state.shardZones, + isUnmanagingNamespace: + state.status === ShardingStatuses.UNMANAGING_NAMESPACE, + }), + { + onUnmanageNamespace: unmanageNamespace, + } +)(ShardKeyCorrect); diff --git a/packages/compass-global-writes/src/plugin-title.tsx b/packages/compass-global-writes/src/plugin-title.tsx index e7fd917d6d2..0c484aecaac 100644 --- a/packages/compass-global-writes/src/plugin-title.tsx +++ b/packages/compass-global-writes/src/plugin-title.tsx @@ -72,7 +72,7 @@ const PluginTitle = ({ showWarning }: { showWarning: boolean }) => { }; export const GlobalWritesTabTitle = connect( - ({ isNamespaceSharded, status }: RootState) => ({ - showWarning: !isNamespaceSharded && status !== ShardingStatuses.NOT_READY, + ({ managedNamespace, status }: RootState) => ({ + showWarning: !managedNamespace && status !== ShardingStatuses.NOT_READY, }) )(PluginTitle); 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 d548eae05c3..9b24f784a00 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 @@ -1,8 +1,15 @@ import toNS from 'mongodb-ns'; +import _ from 'lodash'; import type { AtlasService } from '@mongodb-js/atlas-service/provider'; import type { CreateShardKeyData } from '../store/reducer'; +import type { ConnectionInfoRef } from '@mongodb-js/compass-connections/provider'; -type ZoneMapping = unknown; +export type ShardZoneMapping = { + isoCode: string; + typeOneIsoCode: string; + zoneId: string; + country: string; +}; export type ManagedNamespace = { db: string; collection: string; @@ -14,18 +21,36 @@ export type ManagedNamespace = { }; type GeoShardingData = { - customZoneMapping: Record; + customZoneMapping: Record; managedNamespaces: ManagedNamespace[]; selfManagedSharding: boolean; }; +type ReplicationItem = { + id: string; + regionConfigs: { + regionView: { + location: string; + }; + }[]; + zoneId: string; + zoneName: string; +}; type ClusterDetailsApiResponse = { geoSharding: GeoShardingData; + replicationSpecList: ReplicationItem[]; +}; + +type AutomationAgentProcess = { + statusType: string; + workingOnShort: string; + errorText: string; }; -type AtlasCluterInfo = { - projectId: string; - clusterName: string; +type AutomationAgentDeploymentStatusApiResponse = { + automationStatus: { + processes: AutomationAgentProcess[]; + }; }; function assertDataIsClusterDetailsApiResponse( @@ -41,15 +66,44 @@ function assertDataIsClusterDetailsApiResponse( 'Invalid cluster details API response geoSharding.customZoneMapping' ); } + if (!Array.isArray(data?.replicationSpecList)) { + throw new Error('Invalid cluster details API response replicationSpecList'); + } +} + +function assertDataIsAutomationAgentDeploymentStatusApiResponse( + data: any +): asserts data is AutomationAgentDeploymentStatusApiResponse { + if (!Array.isArray(data?.automationStatus?.processes)) { + throw new Error( + 'Invalid automation agent deployment status API response automationStatus.processes' + ); + } +} + +function assertDataIsShardZonesApiResponse( + data: any +): asserts data is Record { + if (typeof data !== 'object') { + throw new Error('Invalid shard zones API response'); + } } export class AtlasGlobalWritesService { - constructor(private atlasService: AtlasService) {} + constructor( + private atlasService: AtlasService, + private connectionInfo: ConnectionInfoRef + ) {} - private async fetchClusterDetails({ - clusterName, - projectId, - }: AtlasCluterInfo): Promise { + private getAtlasMetadata() { + if (!this.connectionInfo.current?.atlasMetadata) { + throw new Error('Atlas metadata is not available'); + } + return this.connectionInfo.current.atlasMetadata; + } + + private async getClusterDetails(): Promise { + const { projectId, clusterName } = this.getAtlasMetadata(); const uri = this.atlasService.cloudEndpoint( `nds/clusters/${projectId}/${clusterName}` ); @@ -59,13 +113,10 @@ export class AtlasGlobalWritesService { return clusterDetails; } - async isNamespaceManaged( - namespace: string, - atlasClusterInfo: AtlasCluterInfo - ) { - const clusterDetails = await this.fetchClusterDetails(atlasClusterInfo); + async getManagedNamespace(namespace: string) { + const clusterDetails = await this.getClusterDetails(); const { database, collection } = toNS(namespace); - return clusterDetails.geoSharding.managedNamespaces.some( + return clusterDetails.geoSharding.managedNamespaces.find( (managedNamespace) => { return ( managedNamespace.db === database && @@ -75,12 +126,8 @@ export class AtlasGlobalWritesService { ); } - async createShardKey( - namespace: string, - keyData: CreateShardKeyData, - atlasClusterInfo: AtlasCluterInfo - ) { - const clusterDetails = await this.fetchClusterDetails(atlasClusterInfo); + async createShardKey(namespace: string, keyData: CreateShardKeyData) { + const clusterDetails = await this.getClusterDetails(); const { database, collection } = toNS(namespace); const requestData: GeoShardingData = { ...clusterDetails.geoSharding, @@ -94,8 +141,120 @@ export class AtlasGlobalWritesService { ], }; + const { projectId, clusterName } = this.getAtlasMetadata(); + const uri = this.atlasService.cloudEndpoint( + `nds/clusters/${projectId}/${clusterName}/geoSharding` + ); + + const response = await this.atlasService.authenticatedFetch(uri, { + method: 'PATCH', + body: JSON.stringify(requestData), + headers: { + 'Content-Type': 'application/json', + }, + }); + assertDataIsClusterDetailsApiResponse(await response.json()); + + const managedNamespace = requestData.managedNamespaces.find( + (managedNamespace) => + managedNamespace.db === database && + managedNamespace.collection === collection + ); + if (!managedNamespace) { + throw new Error('Managed namespace not found'); + } + return managedNamespace; + } + + async getShardingError(namespace: string) { + const { projectId } = this.getAtlasMetadata(); + const uri = this.atlasService.cloudEndpoint( + `/automation/deploymentStatus/${projectId}` + ); + const response = await this.atlasService.authenticatedFetch(uri); + const data = await response.json(); + assertDataIsAutomationAgentDeploymentStatusApiResponse(data); + const namespaceShardingError = data.automationStatus.processes.find( + (process) => + process.statusType === 'ERROR' && + process.workingOnShort === 'ShardingCollections' && + process.errorText.indexOf(namespace) !== -1 + ); + return namespaceShardingError?.errorText; + } + + async getShardingKeys(namespace: string) { + const { database: db, collection } = toNS(namespace); + + const data = await this.atlasService.automationAgentFetch( + this.getAtlasMetadata(), + 'getShardKey', + { + db, + collection, + } + ); + + if (data.length === 0) { + return null; + } + const { key, unique } = data[0]; + + return { + fields: Object.keys(key).map( + (field) => + ({ + name: field, + type: key[field] === 'hashed' ? 'HASHED' : 'RANGE', + } as const) + ), + isUnique: !!unique, + }; + } + + async getShardingZones() { + const { projectId } = this.getAtlasMetadata(); + const { + replicationSpecList: replicationSpecs, + geoSharding: { customZoneMapping }, + } = await this.getClusterDetails(); + const uri = this.atlasService.cloudEndpoint( - `nds/clusters/${atlasClusterInfo.projectId}/${atlasClusterInfo.clusterName}/geoSharding` + `/nds/geoSharding/${projectId}/newFormLocationMapping` + ); + const response = await this.atlasService.authenticatedFetch(uri, { + method: 'POST', + body: JSON.stringify({ + replicationSpecs, + customZoneMapping, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await response.json(); + assertDataIsShardZonesApiResponse(data); + return transformZoneData(Object.values(data), replicationSpecs); + } + + async unmanageNamespace(namespace: string) { + const clusterDetails = await this.getClusterDetails(); + const { database, collection } = toNS(namespace); + + const newManagedNamespaces = + clusterDetails.geoSharding.managedNamespaces.filter( + (managedNamespace) => + managedNamespace.db !== database || + managedNamespace.collection !== collection + ); + const requestData: GeoShardingData = { + ...clusterDetails.geoSharding, + managedNamespaces: newManagedNamespaces, + }; + + const { projectId, clusterName } = this.getAtlasMetadata(); + const uri = this.atlasService.cloudEndpoint( + `nds/clusters/${projectId}/${clusterName}/geoSharding` ); await this.atlasService.authenticatedFetch(uri, { @@ -107,3 +266,20 @@ export class AtlasGlobalWritesService { }); } } + +function transformZoneData( + zoneData: ShardZoneMapping[], + replicationSpecs: ReplicationItem[] +) { + const replicationSpecsMap = _.keyBy(replicationSpecs, 'zoneId'); + return zoneData.map((zone) => ({ + zoneId: zone.zoneId, + country: zone.country, + isoCode: zone.isoCode, + typeOneIsoCode: zone.typeOneIsoCode, + zoneName: replicationSpecsMap[zone.zoneId].zoneName, + zoneLocations: replicationSpecsMap[zone.zoneId].regionConfigs.map( + (regionConfig) => regionConfig.regionView.location + ), + })); +} diff --git a/packages/compass-global-writes/src/store/index.ts b/packages/compass-global-writes/src/store/index.ts index 334034a52a6..b00fad27e27 100644 --- a/packages/compass-global-writes/src/store/index.ts +++ b/packages/compass-global-writes/src/store/index.ts @@ -56,13 +56,16 @@ export function activateGlobalWritesPlugin( }: GlobalWritesPluginServices, { cleanup }: ActivateHelpers ) { - const atlasGlobalWritesService = new AtlasGlobalWritesService(atlasService); + const atlasGlobalWritesService = new AtlasGlobalWritesService( + atlasService, + connectionInfoRef + ); const store: GlobalWritesStore = createStore( reducer, { namespace: options.namespace, - isNamespaceSharded: false, status: ShardingStatuses.NOT_READY, + shardZones: [], }, applyMiddleware( thunk.withExtraArgument({ diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index 17e1dba65db..a5a2201d196 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -20,15 +20,39 @@ export type CreateShardKeyData = Pick< >; enum GlobalWritesActionTypes { - IsManagedNamespaceFetched = 'global-writes/IsManagedNamespaceFetched', + ManagedNamespaceFetched = 'global-writes/ManagedNamespaceFetched', + NamespaceShardingErrorFetched = 'global-writes/NamespaceShardingErrorFetched', + NamespaceShardKeyFetched = 'global-writes/NamespaceShardKeyFetched', + + ShardZonesFetched = 'global-writes/ShardZonesFetched', + SubmittingForShardingStarted = 'global-writes/SubmittingForShardingStarted', SubmittingForShardingFinished = 'global-writes/SubmittingForShardingFinished', SubmittingForShardingErrored = 'global-writes/SubmittingForShardingErrored', + + UnmanagingNamespaceStarted = 'global-writes/UnmanagingNamespaceStarted', + UnmanagingNamespaceFinished = 'global-writes/UnmanagingNamespaceFinished', + UnmanagingNamespaceErrored = 'global-writes/UnmanagingNamespaceErrored', } -type IsManagedNamespaceFetchedAction = { - type: GlobalWritesActionTypes.IsManagedNamespaceFetched; - isNamespaceManaged: boolean; +type ManagedNamespaceFetchedAction = { + type: GlobalWritesActionTypes.ManagedNamespaceFetched; + managedNamespace?: ManagedNamespace; +}; + +type NamespaceShardingErrorFetchedAction = { + type: GlobalWritesActionTypes.NamespaceShardingErrorFetched; + error: string; +}; + +type NamespaceShardKeyFetchedAction = { + type: GlobalWritesActionTypes.NamespaceShardKeyFetched; + shardKey: ShardKey; +}; + +type ShardZonesFetchedAction = { + type: GlobalWritesActionTypes.ShardZonesFetched; + shardZones: ShardZoneData[]; }; type SubmittingForShardingStartedAction = { @@ -37,12 +61,25 @@ type SubmittingForShardingStartedAction = { type SubmittingForShardingFinishedAction = { type: GlobalWritesActionTypes.SubmittingForShardingFinished; + managedNamespace?: ManagedNamespace; }; type SubmittingForShardingErroredAction = { type: GlobalWritesActionTypes.SubmittingForShardingErrored; }; +type UnmanagingNamespaceStartedAction = { + type: GlobalWritesActionTypes.UnmanagingNamespaceStarted; +}; + +type UnmanagingNamespaceFinishedAction = { + type: GlobalWritesActionTypes.UnmanagingNamespaceFinished; +}; + +type UnmanagingNamespaceErroredAction = { + type: GlobalWritesActionTypes.UnmanagingNamespaceErrored; +}; + export enum ShardingStatuses { /** * Initial status, no information available yet. @@ -64,38 +101,121 @@ export enum ShardingStatuses { * Namespace is being sharded. */ SHARDING = 'SHARDING', + + /** + * Sharding failed. + */ + SHARDING_ERROR = 'SHARDING_ERROR', + + /** + * If the first key is not valid location key or the key is not compound. + */ + SHARD_KEY_INVALID = 'SHARD_KEY_INVALID', + + /** + * If the first key is valid (location key) and second key is not valid. + * The second key valid means that it matches with the managedNamespace's + * customShardKey and is of the correct type. + */ + SHARD_KEY_MISMATCH = 'SHARD_KEY_MISMATCH', + + /** + * Namespace is geo-sharded. Both, first key is valid + * location key and second key is valid custom key. + */ + SHARD_KEY_CORRECT = 'SHARD_KEY_CORRECT', + + /** + * Namespace is being unmanaged. + */ + UNMANAGING_NAMESPACE = 'UNMANAGING_NAMESPACE', } export type ShardingStatus = keyof typeof ShardingStatuses; - +export type ShardKey = { + fields: Array<{ + type: 'HASHED' | 'RANGE'; + name: string; + }>; + isUnique: boolean; +}; +export type ShardZoneData = { + zoneId: string; + country: string; + isoCode: string; + typeOneIsoCode: string; + zoneName: string; + zoneLocations: string[]; +}; export type RootState = { namespace: string; - isNamespaceSharded: boolean; + managedNamespace?: ManagedNamespace; status: ShardingStatus; + shardingError?: string; + shardKey?: ShardKey; + shardZones: ShardZoneData[]; }; const initialState: RootState = { namespace: '', - isNamespaceSharded: false, status: ShardingStatuses.NOT_READY, + shardZones: [], }; const reducer: Reducer = (state = initialState, action) => { if ( - isAction( + isAction( action, - GlobalWritesActionTypes.IsManagedNamespaceFetched + GlobalWritesActionTypes.ManagedNamespaceFetched ) ) { return { ...state, - isNamespaceSharded: action.isNamespaceManaged, - status: !action.isNamespaceManaged + managedNamespace: action.managedNamespace, + status: !action.managedNamespace ? ShardingStatuses.UNSHARDED : state.status, }; } + if ( + isAction( + action, + GlobalWritesActionTypes.NamespaceShardingErrorFetched + ) + ) { + return { + ...state, + status: ShardingStatuses.SHARDING_ERROR, + shardingError: action.error, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.NamespaceShardKeyFetched + ) + ) { + return { + ...state, + status: getStatusFromShardKey(action.shardKey, state.managedNamespace), + shardKey: action.shardKey, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.ShardZonesFetched + ) + ) { + return { + ...state, + shardZones: action.shardZones, + }; + } + if ( isAction( action, @@ -116,7 +236,7 @@ const reducer: Reducer = (state = initialState, action) => { ) { return { ...state, - isNamespaceSharded: true, + managedNamespace: action.managedNamespace || state.managedNamespace, status: ShardingStatuses.SHARDING, }; } @@ -129,45 +249,72 @@ const reducer: Reducer = (state = initialState, action) => { ) { return { ...state, + managedNamespace: undefined, status: ShardingStatuses.UNSHARDED, }; } + if ( + isAction( + action, + GlobalWritesActionTypes.UnmanagingNamespaceStarted + ) + ) { + return { + ...state, + status: ShardingStatuses.UNMANAGING_NAMESPACE, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.UnmanagingNamespaceFinished + ) + ) { + return { + ...state, + managedNamespace: undefined, + status: ShardingStatuses.UNSHARDED, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.UnmanagingNamespaceErrored + ) + ) { + return { + ...state, + status: ShardingStatuses.SHARD_KEY_CORRECT, + }; + } + return state; }; export const fetchClusterShardingData = - (): GlobalWritesThunkAction, IsManagedNamespaceFetchedAction> => - async ( - dispatch, - getState, - { atlasGlobalWritesService, connectionInfoRef, logger } - ) => { - if (!connectionInfoRef.current.atlasMetadata) { - return; - } - + (): GlobalWritesThunkAction, ManagedNamespaceFetchedAction> => + async (dispatch, getState, { atlasGlobalWritesService, logger }) => { const { namespace } = getState(); - const { clusterName, projectId } = connectionInfoRef.current.atlasMetadata; - try { // Call the API to check if the namespace is managed. If the namespace is managed, // we would want to fetch more data that is needed to figure out the state and // accordingly show the UI to the user. - const isNamespaceManaged = - await atlasGlobalWritesService.isNamespaceManaged(namespace, { - projectId, - clusterName, - }); + const managedNamespace = + await atlasGlobalWritesService.getManagedNamespace(namespace); dispatch({ - type: GlobalWritesActionTypes.IsManagedNamespaceFetched, - isNamespaceManaged, + type: GlobalWritesActionTypes.ManagedNamespaceFetched, + managedNamespace, }); - if (!isNamespaceManaged) { + if (!managedNamespace) { return; } - // TODO (COMPASS-8277): Now fetch the sharding key and possible process error. + + // At this point, the namespace is managed and we want to fetch the sharding key. + void dispatch(fetchNamespaceShardKey()); } catch (error) { logger.log.error( logger.mongoLogId(1_001_000_330), @@ -186,39 +333,26 @@ export const fetchClusterShardingData = } }; -export const createShardKey = - ( - data: CreateShardKeyData - ): GlobalWritesThunkAction< - Promise, - | SubmittingForShardingStartedAction - | SubmittingForShardingFinishedAction - | SubmittingForShardingErroredAction - > => - async ( - dispatch, - getState, - { connectionInfoRef, atlasGlobalWritesService, logger } - ) => { - if (!connectionInfoRef.current.atlasMetadata) { - return; - } - +export const createShardKey = ( + data: CreateShardKeyData +): GlobalWritesThunkAction< + Promise, + | SubmittingForShardingStartedAction + | SubmittingForShardingFinishedAction + | SubmittingForShardingErroredAction +> => { + return async (dispatch, getState, { atlasGlobalWritesService, logger }) => { const { namespace } = getState(); - const { clusterName, projectId } = connectionInfoRef.current.atlasMetadata; - dispatch({ type: GlobalWritesActionTypes.SubmittingForShardingStarted, }); try { - await atlasGlobalWritesService.createShardKey(namespace, data, { - projectId, - clusterName, - }); - dispatch({ - type: GlobalWritesActionTypes.SubmittingForShardingFinished, - }); + const managedNamespace = await atlasGlobalWritesService.createShardKey( + namespace, + data + ); + dispatch(setNamespaceBeingSharded(managedNamespace)); } catch (error) { logger.log.error( logger.mongoLogId(1_001_000_331), @@ -240,5 +374,154 @@ export const createShardKey = }); } }; +}; + +const setNamespaceBeingSharded = ( + managedNamespace?: ManagedNamespace +): GlobalWritesThunkAction => { + return (dispatch) => { + // TODO: Scope this to the workspace. + const toastId = 'global-writes-sharding-in-progress'; + dispatch({ + type: GlobalWritesActionTypes.SubmittingForShardingFinished, + managedNamespace, + }); + openToast(toastId, { + title: 'Sharding your collection …', + description: 'This should not take too long.', + dismissible: true, + variant: 'progress', + // TODO: Add actionElement to cancel sharding. + }); + }; +}; + +export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< + Promise, + NamespaceShardingErrorFetchedAction | NamespaceShardKeyFetchedAction +> => { + return async (dispatch, getState, { atlasGlobalWritesService, logger }) => { + const { namespace } = getState(); + + try { + const [shardingError, shardKey] = await Promise.all([ + atlasGlobalWritesService.getShardingError(namespace), + atlasGlobalWritesService.getShardingKeys(namespace), + ]); + + if (shardingError) { + dispatch({ + type: GlobalWritesActionTypes.NamespaceShardingErrorFetched, + error: shardingError, + }); + return; + } + + if (!shardKey) { + dispatch(setNamespaceBeingSharded()); + return; + } + + dispatch({ + type: GlobalWritesActionTypes.NamespaceShardKeyFetched, + shardKey, + }); + void dispatch(fetchShardingZones()); + } catch (error) { + logger.log.error( + logger.mongoLogId(1_001_000_330), + 'AtlasFetchError', + 'Error fetching cluster sharding data', + (error as Error).message + ); + openToast('global-writes-fetch-shard-key-error', { + title: `Failed to fetch shard key: ${(error as Error).message}`, + dismissible: true, + timeout: 5000, + variant: 'important', + }); + } + }; +}; + +export const fetchShardingZones = (): GlobalWritesThunkAction< + Promise, + ShardZonesFetchedAction +> => { + return async (dispatch, getState, { atlasGlobalWritesService }) => { + const { shardZones } = getState(); + if (shardZones.length > 0) { + return; + } + const shardingZones = await atlasGlobalWritesService.getShardingZones(); + dispatch({ + type: GlobalWritesActionTypes.ShardZonesFetched, + shardZones: shardingZones, + }); + }; +}; + +export const unmanageNamespace = (): GlobalWritesThunkAction< + Promise, + | UnmanagingNamespaceStartedAction + | UnmanagingNamespaceFinishedAction + | UnmanagingNamespaceErroredAction +> => { + return async (dispatch, getState, { atlasGlobalWritesService }) => { + const { namespace } = getState(); + + dispatch({ + type: GlobalWritesActionTypes.UnmanagingNamespaceStarted, + }); + + try { + await atlasGlobalWritesService.unmanageNamespace(namespace); + dispatch({ + type: GlobalWritesActionTypes.UnmanagingNamespaceFinished, + }); + } catch (error) { + dispatch({ + type: GlobalWritesActionTypes.UnmanagingNamespaceErrored, + }); + openToast('global-writes-unmanage-namespace-error', { + title: `Failed to unmanage namespace: ${(error as Error).message}`, + dismissible: true, + timeout: 5000, + variant: 'important', + }); + } + }; +}; + +export function getStatusFromShardKey( + shardKey: ShardKey, + managedNamespace?: ManagedNamespace +) { + const [firstShardKey, secondShardKey] = shardKey.fields; + + // For a shard key to be valid: + // 1. The first key must be location and of type RANGE. + // 2. The second key name must match managedNamespace.customShardKey and + // the type must match the managedNamespace.isCustomShardKeyHashed. + + const isLocatonKeyValid = + firstShardKey.name === 'location' && firstShardKey.type === 'RANGE'; + const isCustomKeyValid = + managedNamespace && + managedNamespace.isShardKeyUnique === shardKey.isUnique && + secondShardKey.name === managedNamespace.customShardKey && + secondShardKey.type === + (managedNamespace.isCustomShardKeyHashed ? 'HASHED' : 'RANGE'); + + if (!isLocatonKeyValid || !secondShardKey) { + return ShardingStatuses.SHARD_KEY_INVALID; + } + + if (!isCustomKeyValid) { + return ShardingStatuses.SHARD_KEY_MISMATCH; + } + + return ShardingStatuses.SHARD_KEY_CORRECT; +} export default reducer; From 177124297b154536b050219d2aca010546bb7d70 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Tue, 1 Oct 2024 14:35:39 +0200 Subject: [PATCH 02/17] small fixes --- .../src/components/shard-zones-table.tsx | 34 ++++++++++++------- .../components/states/shard-key-correct.tsx | 4 ++- .../services/atlas-global-writes-service.ts | 2 ++ .../src/store/reducer.ts | 1 + 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/compass-global-writes/src/components/shard-zones-table.tsx b/packages/compass-global-writes/src/components/shard-zones-table.tsx index 9a567c1b93c..aa1cb4acc84 100644 --- a/packages/compass-global-writes/src/components/shard-zones-table.tsx +++ b/packages/compass-global-writes/src/components/shard-zones-table.tsx @@ -21,9 +21,15 @@ export function ShardZonesTable({ }: { shardZones: ShardZoneData[]; }) { + console.log( + 'SHARD ZONES', + shardZones.filter((zone) => zone.typeOneIsoCode === 'DE') + ); return ( - // TODO: Add option to search and group zones by ShardZoneData.typeOneIsoCode - // and display them in nested row + // TODO(COMPASS-8336): + // Add search + // group zones by ShardZoneData.typeOneIsoCode + // and display them in a single row that can be expanded @@ -32,16 +38,20 @@ export function ShardZonesTable({ - {shardZones.map((shardZone, index) => { - return ( - - {shardZone.country} - - {shardZone.zoneName}({shardZone.zoneLocations.join(', ')}) - - - ); - })} + {shardZones.map( + ({ readableName, zoneName, zoneLocations, isoCode }, index) => { + return ( + + + {readableName} ({isoCode}) + + + {zoneName} ({zoneLocations.join(', ')}) + + + ); + } + )}
); diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx index a4a351aed6a..98d5be498da 100644 --- a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx @@ -87,7 +87,9 @@ export function ShardKeyCorrect({ {namespace} is configured with the following shard key: - {namespace} + + {shardKey.fields.map((field) => `"${field.name}"`).join(', ')} + Example commands 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 48729a889fd..d208d50516d 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 @@ -9,6 +9,7 @@ export type ShardZoneMapping = { typeOneIsoCode: string; zoneId: string; country: string; + readableName: string; }; export type ManagedNamespace = { db: string; @@ -295,6 +296,7 @@ function transformZoneData( return zoneData.map((zone) => ({ zoneId: zone.zoneId, country: zone.country, + readableName: zone.readableName, isoCode: zone.isoCode, typeOneIsoCode: zone.typeOneIsoCode, zoneName: replicationSpecsMap[zone.zoneId].zoneName, diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index a5a2201d196..b1cd139355f 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -142,6 +142,7 @@ export type ShardKey = { export type ShardZoneData = { zoneId: string; country: string; + readableName: string; isoCode: string; typeOneIsoCode: string; zoneName: string; From b62fb1c57d36b30a5e66e257eff02cd9c5003677 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Tue, 1 Oct 2024 18:07:05 +0200 Subject: [PATCH 03/17] some more fixes + tests --- .../src/components/index.spec.tsx | 16 +-- .../src/components/shard-zones-table.spec.tsx | 39 +++++++ .../src/components/shard-zones-table.tsx | 4 - .../states/shard-key-correct.spec.tsx | 102 ++++++++++++++++++ .../components/states/shard-key-correct.tsx | 40 +++---- .../src/components/states/sharding.spec.tsx | 4 +- .../src/components/states/usharded.spec.tsx | 12 +-- .../tests/create-store.tsx | 29 +++-- 8 files changed, 201 insertions(+), 45 deletions(-) create mode 100644 packages/compass-global-writes/src/components/shard-zones-table.spec.tsx create mode 100644 packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx diff --git a/packages/compass-global-writes/src/components/index.spec.tsx b/packages/compass-global-writes/src/components/index.spec.tsx index e264d94197d..4aff0336345 100644 --- a/packages/compass-global-writes/src/components/index.spec.tsx +++ b/packages/compass-global-writes/src/components/index.spec.tsx @@ -5,25 +5,25 @@ import { GlobalWrites } from './index'; import { renderWithStore } from './../../tests/create-store'; describe('Compass GlobalWrites Plugin', function () { - it('renders plugin in NOT_READY state', function () { - renderWithStore(); + it('renders plugin in NOT_READY state', async function () { + await renderWithStore(); expect(screen.getByText(/loading/i)).to.exist; }); - it('renders plugin in UNSHARDED state', function () { - renderWithStore(); + it('renders plugin in UNSHARDED state', async function () { + await renderWithStore(); expect(screen.getByTestId('shard-collection-button')).to.exist; }); - it('renders plugin in SUBMITTING_FOR_SHARDING state', function () { - renderWithStore( + it('renders plugin in SUBMITTING_FOR_SHARDING state', async function () { + await renderWithStore( ); expect(screen.getByTestId('shard-collection-button')).to.exist; }); - it('renders plugin in SHARDING state', function () { - renderWithStore(); + it('renders plugin in SHARDING state', async function () { + await renderWithStore(); expect(screen.getByText(/sharding your collection/i)).to.exist; }); }); diff --git a/packages/compass-global-writes/src/components/shard-zones-table.spec.tsx b/packages/compass-global-writes/src/components/shard-zones-table.spec.tsx new file mode 100644 index 00000000000..5aae046e76e --- /dev/null +++ b/packages/compass-global-writes/src/components/shard-zones-table.spec.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { expect } from 'chai'; +import { render, screen, within } from '@mongodb-js/testing-library-compass'; +import { ShardZonesTable } from './shard-zones-table'; +import { type ShardZoneData } from '../store/reducer'; + +describe('Compass GlobalWrites Plugin', function () { + const shardZones: ShardZoneData[] = [ + { + zoneId: '45893084', + country: 'Germany', + readableName: 'Germany', + isoCode: 'DE', + typeOneIsoCode: 'DE', + zoneName: 'EMEA', + zoneLocations: ['Frankfurt'], + }, + { + zoneId: '43829408', + country: 'Germany', + readableName: 'Germany - Berlin', + isoCode: 'DE-BE', + typeOneIsoCode: 'DE', + zoneName: 'EMEA', + zoneLocations: ['Frankfurt'], + }, + ]; + + it('renders the Location name & Zone for all items', function () { + render(); + + const rows = screen.getAllByRole('row'); + expect(rows).to.have.lengthOf(3); // 1 header, 2 items + expect(within(rows[1]).getByText('Germany (DE)')).to.be.visible; + expect(within(rows[1]).getByText('EMEA (Frankfurt)')).to.be.visible; + expect(within(rows[2]).getByText('Germany - Berlin (DE-BE)')).to.be.visible; + expect(within(rows[2]).getByText('EMEA (Frankfurt)')).to.be.visible; + }); +}); diff --git a/packages/compass-global-writes/src/components/shard-zones-table.tsx b/packages/compass-global-writes/src/components/shard-zones-table.tsx index aa1cb4acc84..abc866c668d 100644 --- a/packages/compass-global-writes/src/components/shard-zones-table.tsx +++ b/packages/compass-global-writes/src/components/shard-zones-table.tsx @@ -21,10 +21,6 @@ export function ShardZonesTable({ }: { shardZones: ShardZoneData[]; }) { - console.log( - 'SHARD ZONES', - shardZones.filter((zone) => zone.typeOneIsoCode === 'DE') - ); return ( // TODO(COMPASS-8336): // Add search diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx new file mode 100644 index 00000000000..e75fb171988 --- /dev/null +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { expect } from 'chai'; +import { screen, userEvent } from '@mongodb-js/testing-library-compass'; +import { + ShardKeyCorrect, + type ShardKeyCorrectProps, +} from './shard-key-correct'; +import { type ShardZoneData } from '../../store/reducer'; +import Sinon from 'sinon'; +import { renderWithStore } from '../../../tests/create-store'; +import { ConnectionInfo } from '@mongodb-js/compass-connections/provider'; + +describe('Compass GlobalWrites Plugin', function () { + const shardZones: ShardZoneData[] = [ + { + zoneId: '45893084', + country: 'Germany', + readableName: 'Germany', + isoCode: 'DE', + typeOneIsoCode: 'DE', + zoneName: 'EMEA', + zoneLocations: ['Frankfurt'], + }, + ]; + + const baseProps: ShardKeyCorrectProps = { + shardZones, + namespace: 'db1.coll1', + shardKey: { + fields: [ + { type: 'HASHED', name: 'location' }, + { type: 'RANGE', name: 'second' }, + ], + isUnique: false, + }, + isUnmanagingNamespace: false, + onUnmanageNamespace: () => {}, + }; + + function renderWithProps( + props?: Partial, + options?: Parameters[1] + ) { + return renderWithStore( + , + options + ); + } + + it('Provides button to unmanage', async function () { + const onUnmanageNamespace = Sinon.spy(); + await renderWithProps({ onUnmanageNamespace }); + + const btn = await screen.findByRole('button', { + name: /Unmanage collection/, + }); + expect(btn).to.be.visible; + + userEvent.click(btn); + + expect(onUnmanageNamespace).to.have.been.calledOnce; + }); + + it('Unmanage btn is disabled when the action is in progress', async function () { + const onUnmanageNamespace = Sinon.spy(); + await renderWithProps({ onUnmanageNamespace, isUnmanagingNamespace: true }); + + const btn = await screen.findByRole('button', { + name: /Unmanage collection/, + }); + expect(btn).to.be.visible; + expect(btn).to.have.property('disabled'); + + userEvent.click(btn); + + expect(onUnmanageNamespace).not.to.have.been.called; + }); + + it.only('Provides link to Edit Configuration', async function () { + const connectionInfo = { + id: 'testConnection', + connectionOptions: { + connectionString: 'mongodb://test', + }, + atlasMetadata: { + projectId: 'project1', + clusterName: 'myCluster', + } as ConnectionInfo['atlasMetadata'], + }; + await renderWithProps(undefined, { + connectionInfo, + }); + + const link = await screen.findByRole('link', { + name: /Edit Configuration/, + }); + const expectedHref = `/v2/${connectionInfo.atlasMetadata?.projectId}/clusters/edit/${connectionInfo.atlasMetadata?.clusterName}`; + + expect(link).to.be.visible; + expect(link).to.have.attribute('href', expectedHref); + }); +}); diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx index 98d5be498da..6b79d028098 100644 --- a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx @@ -22,6 +22,7 @@ import { } from '../../store/reducer'; import toNS from 'mongodb-ns'; import { ShardZonesTable } from '../shard-zones-table'; +import { useConnectionInfo } from '@mongodb-js/compass-connections/provider'; const nbsp = '\u00a0'; @@ -44,7 +45,7 @@ const paragraphStyles = css({ gap: spacing[100], }); -type ShardKeyCorrectProps = { +export type ShardKeyCorrectProps = { namespace: string; shardKey: ShardKey; shardZones: ShardZoneData[]; @@ -63,11 +64,13 @@ export function ShardKeyCorrect({ return shardKey.fields[1].name; }, [shardKey]); + const { atlasMetadata } = useConnectionInfo(); + const sampleCodes = useMemo(() => { - const { collection } = toNS(namespace); + const { collection, database } = toNS(namespace); return { - findingDocuments: `use ${collection}\ndb.${collection}.find({"location": "US-NY", "${customShardKeyField}": ""})`, - insertingDocuments: `use ${collection}\ndb.${collection}.insertOne({"location": "US-NY", "${customShardKeyField}": "",...})`, + findingDocuments: `use ${database}\ndb.${collection}.find({"location": "US-NY", "${customShardKeyField}": ""})`, + insertingDocuments: `use ${database}\ndb.${collection}.insertOne({"location": "US-NY", "${customShardKeyField}": "",...})`, }; }, [namespace, customShardKeyField]); @@ -133,23 +136,22 @@ export function ShardKeyCorrect({ be used for these countries). All valid country codes and the zones to which they map are listed in the table below. Additionally, you can view a list of all location codes{' '} - - here - - . + here. - Locations’ zone mapping can be changed by navigating to this clusters{' '} - - Edit Configuration - {' '} - page and clicking the Configure Location Mappings’ link above the map. + {atlasMetadata?.projectId && atlasMetadata?.clusterName && ( + <> + Locations’ zone mapping can be changed by navigating to this + clusters{' '} + + Edit Configuration + {' '} + page and clicking the Configure Location Mappings’ link above the + map. + + )} 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 beb6e372928..c3d8fb542be 100644 --- a/packages/compass-global-writes/src/components/states/sharding.spec.tsx +++ b/packages/compass-global-writes/src/components/states/sharding.spec.tsx @@ -11,8 +11,8 @@ function renderWithProps( } describe('Sharding', function () { - it('renders the info banner', function () { - renderWithProps(); + it('renders the info banner', async function () { + await renderWithProps(); expect(screen.getByRole('alert')).to.exist; }); }); diff --git a/packages/compass-global-writes/src/components/states/usharded.spec.tsx b/packages/compass-global-writes/src/components/states/usharded.spec.tsx index a26d5b228de..74d57ccb925 100644 --- a/packages/compass-global-writes/src/components/states/usharded.spec.tsx +++ b/packages/compass-global-writes/src/components/states/usharded.spec.tsx @@ -34,21 +34,21 @@ function setShardingKeyFieldValue(value: string) { } describe('UnshardedState', function () { - it('renders the warning banner', function () { - renderWithProps(); + it('renders the warning banner', async function () { + await renderWithProps(); expect(screen.getByRole('alert')).to.exist; }); - it('renders the text to the user', function () { - renderWithProps(); + it('renders the text to the user', async function () { + await renderWithProps(); expect(screen.getByTestId('unsharded-text-description')).to.exist; }); context('shard collection form', function () { let onCreateShardKeySpy: sinon.SinonSpy; - beforeEach(function () { + beforeEach(async function () { onCreateShardKeySpy = sinon.spy(); - renderWithProps({ onCreateShardKey: onCreateShardKeySpy }); + await renderWithProps({ onCreateShardKey: onCreateShardKeySpy }); }); it('renders location form field as disabled', function () { diff --git a/packages/compass-global-writes/tests/create-store.tsx b/packages/compass-global-writes/tests/create-store.tsx index 0aad67f78dc..a4a509b46cd 100644 --- a/packages/compass-global-writes/tests/create-store.tsx +++ b/packages/compass-global-writes/tests/create-store.tsx @@ -7,10 +7,13 @@ import { activateGlobalWritesPlugin } from '../src/store'; import { createActivateHelpers } from 'hadron-app-registry'; import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider'; -import type { ConnectionInfoRef } from '@mongodb-js/compass-connections/provider'; +import type { + ConnectionInfo, + ConnectionInfoRef, +} from '@mongodb-js/compass-connections/provider'; import type { AtlasService } from '@mongodb-js/atlas-service/provider'; import { Provider } from 'react-redux'; -import { render } from '@mongodb-js/testing-library-compass'; +import { renderWithActiveConnection } from '@mongodb-js/testing-library-compass'; import clusterApiResponse from './cluster-api-response.json'; @@ -70,10 +73,24 @@ export const setupStore = ( export const renderWithStore = ( component: JSX.Element, - services: Partial = {}, - options: Partial = {} + { + services = {}, + options = {}, + connectionInfo = { + id: 'testConnection', + connectionOptions: { + connectionString: 'mongodb://localhost', + }, + }, + }: { + services?: Partial; + options?: Partial; + connectionInfo?: ConnectionInfo; + } = {} ) => { const store = setupStore(options, services); - render({component}); - return store; + return renderWithActiveConnection( + {component}, + connectionInfo + ); }; From 9da1f6bf1188dd86bdc53076278f0c66a7d5ba6c Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 4 Oct 2024 11:15:35 +0200 Subject: [PATCH 04/17] more tests --- .../states/shard-key-correct.spec.tsx | 39 +++++++++++++++++-- .../components/states/shard-key-correct.tsx | 12 ++++-- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx index e75fb171988..ebaf690cbbd 100644 --- a/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx @@ -8,7 +8,7 @@ import { import { type ShardZoneData } from '../../store/reducer'; import Sinon from 'sinon'; import { renderWithStore } from '../../../tests/create-store'; -import { ConnectionInfo } from '@mongodb-js/compass-connections/provider'; +import { type ConnectionInfo } from '@mongodb-js/compass-connections/provider'; describe('Compass GlobalWrites Plugin', function () { const shardZones: ShardZoneData[] = [ @@ -29,7 +29,7 @@ describe('Compass GlobalWrites Plugin', function () { shardKey: { fields: [ { type: 'HASHED', name: 'location' }, - { type: 'RANGE', name: 'second' }, + { type: 'RANGE', name: 'secondary' }, ], isUnique: false, }, @@ -76,7 +76,7 @@ describe('Compass GlobalWrites Plugin', function () { expect(onUnmanageNamespace).not.to.have.been.called; }); - it.only('Provides link to Edit Configuration', async function () { + it('Provides link to Edit Configuration', async function () { const connectionInfo = { id: 'testConnection', connectionOptions: { @@ -99,4 +99,37 @@ describe('Compass GlobalWrites Plugin', function () { expect(link).to.be.visible; expect(link).to.have.attribute('href', expectedHref); }); + + it('Describes the shardKey', async function () { + await renderWithProps(); + + const title = await screen.findByTestId('shardkey-description-title'); + expect(title).to.be.visible; + expect(title.textContent).to.equal( + `${baseProps.namespace} is configured with the following shard key:` + ); + const list = await screen.findByTestId('shardkey-description-content'); + expect(list).to.be.visible; + expect(list.textContent).to.contain(`"location", "secondary"`); + }); + + it('Contains sample codes', async function () { + await renderWithProps(); + + const findingDocumentsSample = await screen.findByTestId( + 'sample-finding-documents' + ); + expect(findingDocumentsSample).to.be.visible; + expect(findingDocumentsSample.textContent).to.contain( + `use db1db.coll1.find({"location": "US-NY", "secondary": ""})` + ); + + const insertingDocumentsSample = await screen.findByTestId( + 'sample-inserting-documents' + ); + expect(insertingDocumentsSample).to.be.visible; + expect(insertingDocumentsSample.textContent).to.contain( + `use db1db.coll1.insertOne({"location": "US-NY", "secondary": "",...})` + ); + }); }); diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx index 6b79d028098..0ad79ce79f4 100644 --- a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx @@ -86,11 +86,11 @@ export function ShardKeyCorrect({
- + {namespace} is configured with the following shard key: - + {shardKey.fields.map((field) => `"${field.name}"`).join(', ')}
@@ -116,12 +116,16 @@ export function ShardKeyCorrect({
- {sampleCodes.findingDocuments} + + {sampleCodes.findingDocuments} +
- {sampleCodes.insertingDocuments} + + {sampleCodes.insertingDocuments} +
Location Codes From 3709b00b35a1a9a1d7fca1fe02d469644473f0ef Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 4 Oct 2024 11:55:52 +0200 Subject: [PATCH 05/17] update tests --- .../services/atlas-global-writes-service.ts | 2 +- .../src/store/index.spec.ts | 28 +++++++---- .../tests/create-store.tsx | 47 +++++++++---------- 3 files changed, 42 insertions(+), 35 deletions(-) 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 d208d50516d..0173bebef77 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 @@ -37,7 +37,7 @@ type ReplicationItem = { zoneId: string; zoneName: string; }; -type ClusterDetailsApiResponse = { +export type ClusterDetailsApiResponse = { geoSharding: GeoShardingData; replicationSpecList: ReplicationItem[]; }; diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index 643869dc71b..92e11cff4fc 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -7,12 +7,22 @@ import { type CreateShardKeyData, } from './reducer'; import sinon from 'sinon'; +import type { ClusterDetailsApiResponse } from '../services/atlas-global-writes-service'; const DB = 'test'; const COLL = 'coll'; const NS = `${DB}.${COLL}`; -function createJsonResponse(data: any) { +const clusterDetails: ClusterDetailsApiResponse = { + geoSharding: { + customZoneMapping: {}, + managedNamespaces: [], + selfManagedSharding: false, + }, + replicationSpecList: [], +}; + +function createClusterDetailsResponse(data: ClusterDetailsApiResponse) { return { json: () => Promise.resolve(data), }; @@ -41,13 +51,11 @@ describe('GlobalWritesStore Store', function () { it('when the namespace is not managed', async function () { const store = createStore({ authenticatedFetch: () => - createJsonResponse({ - geoSharding: { customZoneMapping: {}, managedNamespaces: [] }, - }), + createClusterDetailsResponse(clusterDetails), }); await store.dispatch(fetchClusterShardingData()); expect(store.getState().status).to.equal('UNSHARDED'); - expect(store.getState().isNamespaceSharded).to.equal(false); + expect(store.getState().managedNamespace).to.equal(undefined); }); // TODO (COMPASS-8277): Add more test for fetching shard key and process errors @@ -65,9 +73,7 @@ describe('GlobalWritesStore Store', function () { it('sets SUBMITTING_FOR_SHARDING state when starting to create shard key and sets to SHARDING on success', async function () { const store = createStore({ authenticatedFetch: () => - createJsonResponse({ - geoSharding: { customZoneMapping: {}, managedNamespaces: [] }, - }), + createClusterDetailsResponse(clusterDetails), }); const promise = store.dispatch(createShardKey(shardKeyData)); @@ -102,9 +108,10 @@ describe('GlobalWritesStore Store', function () { }, ]; - const getClusterInfoApiResponse = createJsonResponse({ + const getClusterInfoApiResponse = createClusterDetailsResponse({ + ...clusterDetails, geoSharding: { - customZoneMapping: {}, + ...clusterDetails.geoSharding, managedNamespaces: alreadyManagedNamespaces, }, }); @@ -137,6 +144,7 @@ describe('GlobalWritesStore Store', function () { ...alreadyManagedNamespaces, { ...shardKeyData, db: DB, collection: COLL }, ], + selfManagedSharding: false, }); }); }); diff --git a/packages/compass-global-writes/tests/create-store.tsx b/packages/compass-global-writes/tests/create-store.tsx index a4a509b46cd..631a6d6d5d9 100644 --- a/packages/compass-global-writes/tests/create-store.tsx +++ b/packages/compass-global-writes/tests/create-store.tsx @@ -7,16 +7,25 @@ import { activateGlobalWritesPlugin } from '../src/store'; import { createActivateHelpers } from 'hadron-app-registry'; import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider'; -import type { - ConnectionInfo, - ConnectionInfoRef, -} from '@mongodb-js/compass-connections/provider'; +import type { ConnectionInfo } from '@mongodb-js/compass-connections/provider'; import type { AtlasService } from '@mongodb-js/atlas-service/provider'; import { Provider } from 'react-redux'; import { renderWithActiveConnection } from '@mongodb-js/testing-library-compass'; import clusterApiResponse from './cluster-api-response.json'; +const TEST_CONNECTION_INFO = { + id: 'TEST', + connectionOptions: { + connectionString: 'mongodb://localhost', + }, + atlasMetadata: { + clusterName: 'Cluster0', + clusterType: 'UNSHARDED', + projectId: 'Project0', + } as unknown as ConnectionInfo['atlasMetadata'], +}; + const atlasService = { cloudEndpoint: (p: string) => { return `https://example.com/${p}`; @@ -39,19 +48,9 @@ const atlasService = { export const setupStore = ( options: Partial = {}, - services: Partial = {} + services: Partial = {}, + connectionInfo: ConnectionInfo = TEST_CONNECTION_INFO ) => { - const connectionInfoRef = { - current: { - id: 'TEST', - atlasMetadata: { - clusterName: 'Cluster0', - clusterType: 'GEOSHARDED', - projectId: 'Project0', - }, - }, - } as ConnectionInfoRef; - return activateGlobalWritesPlugin( { namespace: 'airbnb.listings', @@ -60,7 +59,12 @@ export const setupStore = ( { logger: createNoopLogger('TEST'), track: createNoopTrack(), - connectionInfoRef, + connectionInfoRef: { + current: { + ...connectionInfo, + title: 'My connection', + }, + }, ...services, atlasService: { ...atlasService, @@ -76,19 +80,14 @@ export const renderWithStore = ( { services = {}, options = {}, - connectionInfo = { - id: 'testConnection', - connectionOptions: { - connectionString: 'mongodb://localhost', - }, - }, + connectionInfo = TEST_CONNECTION_INFO, }: { services?: Partial; options?: Partial; connectionInfo?: ConnectionInfo; } = {} ) => { - const store = setupStore(options, services); + const store = setupStore(options, services, connectionInfo); return renderWithActiveConnection( {component}, connectionInfo From 7cb7a519c663acfcc3ad5ba2c33e365c9d0efa70 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 4 Oct 2024 16:40:24 +0200 Subject: [PATCH 06/17] lodash --- package-lock.json | 2 ++ packages/compass-global-writes/package.json | 1 + .../src/services/atlas-global-writes-service.ts | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 397cebf3545..c76ffbe2b20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45279,6 +45279,7 @@ "@mongodb-js/compass-logging": "^1.4.8", "@mongodb-js/compass-telemetry": "^1.2.0", "hadron-app-registry": "^9.2.7", + "lodash": "^4.17.21", "mongodb-ns": "^2.4.2", "react": "^17.0.2", "react-redux": "^8.1.3", @@ -57025,6 +57026,7 @@ "depcheck": "^1.4.1", "eslint": "^7.25.0", "hadron-app-registry": "^9.2.7", + "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb-ns": "^2.4.2", "nyc": "^15.1.0", diff --git a/packages/compass-global-writes/package.json b/packages/compass-global-writes/package.json index ab2f3f13335..cd3b029ccaa 100644 --- a/packages/compass-global-writes/package.json +++ b/packages/compass-global-writes/package.json @@ -56,6 +56,7 @@ "@mongodb-js/compass-logging": "^1.4.8", "@mongodb-js/compass-telemetry": "^1.2.0", "hadron-app-registry": "^9.2.7", + "lodash": "^4.17.21", "@mongodb-js/compass-field-store": "^9.18.1", "mongodb-ns": "^2.4.2", "react": "^17.0.2", 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 0173bebef77..ed183e5a3a5 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 @@ -1,5 +1,5 @@ import toNS from 'mongodb-ns'; -import _ from 'lodash'; +import keyBy from 'lodash/keyBy'; import type { AtlasService } from '@mongodb-js/atlas-service/provider'; import type { CreateShardKeyData } from '../store/reducer'; import type { ConnectionInfoRef } from '@mongodb-js/compass-connections/provider'; @@ -292,7 +292,7 @@ function transformZoneData( zoneData: ShardZoneMapping[], replicationSpecs: ReplicationItem[] ) { - const replicationSpecsMap = _.keyBy(replicationSpecs, 'zoneId'); + const replicationSpecsMap = keyBy(replicationSpecs, 'zoneId'); return zoneData.map((zone) => ({ zoneId: zone.zoneId, country: zone.country, From e6fb8e903571a7160c4207d007aa2f8fb9e92bee Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 7 Oct 2024 11:20:19 +0200 Subject: [PATCH 07/17] logId --- packages/compass-global-writes/src/store/reducer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index b1cd139355f..e51800e5d97 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -430,9 +430,9 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< void dispatch(fetchShardingZones()); } catch (error) { logger.log.error( - logger.mongoLogId(1_001_000_330), + logger.mongoLogId(1_001_000_332), 'AtlasFetchError', - 'Error fetching cluster sharding data', + 'Error fetching shard key', (error as Error).message ); openToast('global-writes-fetch-shard-key-error', { From facd106c4e7eccd3831718c4143d3df1bd6f060d Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 7 Oct 2024 11:32:36 +0200 Subject: [PATCH 08/17] logId --- packages/compass/src/main/application.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compass/src/main/application.ts b/packages/compass/src/main/application.ts index 37207a21ced..04c47a61d93 100644 --- a/packages/compass/src/main/application.ts +++ b/packages/compass/src/main/application.ts @@ -316,7 +316,7 @@ class CompassApplication { ); log.warn( - mongoLogId(1_001_000_332), + mongoLogId(1_001_000_333), logContext, 'Unable to set proxy configuration', { From 9bce1fa17ee9ec31b32c61188f24c7be12989387 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 7 Oct 2024 16:12:04 +0200 Subject: [PATCH 09/17] address PR suggestions --- .../components/states/shard-key-correct.tsx | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx index 0ad79ce79f4..d550521c003 100644 --- a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx @@ -9,7 +9,6 @@ import { Code, Subtitle, Label, - SpinLoader, Button, } from '@mongodb-js/compass-components'; import { connect } from 'react-redux'; @@ -30,6 +29,7 @@ const containerStyles = css({ display: 'flex', flexDirection: 'column', gap: spacing[400], + marginBottom: spacing[400], }); const codeBlockContainerStyles = css({ @@ -47,7 +47,7 @@ const paragraphStyles = css({ export type ShardKeyCorrectProps = { namespace: string; - shardKey: ShardKey; + shardKey?: ShardKey; shardZones: ShardZoneData[]; isUnmanagingNamespace: boolean; onUnmanageNamespace: () => void; @@ -60,6 +60,10 @@ export function ShardKeyCorrect({ isUnmanagingNamespace, onUnmanageNamespace, }: ShardKeyCorrectProps) { + if (!shardKey) { + throw new Error('Shard key not found in ShardKeyCorrect'); + } + const customShardKeyField = useMemo(() => { return shardKey.fields[1].name; }, [shardKey]); @@ -69,8 +73,8 @@ export function ShardKeyCorrect({ const sampleCodes = useMemo(() => { const { collection, database } = toNS(namespace); return { - findingDocuments: `use ${database}\ndb.${collection}.find({"location": "US-NY", "${customShardKeyField}": ""})`, - insertingDocuments: `use ${database}\ndb.${collection}.insertOne({"location": "US-NY", "${customShardKeyField}": "",...})`, + findingDocuments: `use ${database}\ndb.["${collection}"].find({"location": "US-NY", "${customShardKeyField}": ""})`, + insertingDocuments: `use ${database}\ndb.["${collection}"].insertOne({"location": "US-NY", "${customShardKeyField}": "",...})`, }; }, [namespace, customShardKeyField]); @@ -116,14 +120,22 @@ export function ShardKeyCorrect({
- + {sampleCodes.findingDocuments}
- + {sampleCodes.insertingDocuments}
@@ -170,13 +182,8 @@ export function ShardKeyCorrect({ @@ -188,8 +195,7 @@ export function ShardKeyCorrect({ export default connect( (state: RootState) => ({ namespace: state.namespace, - // For this view, sharKey is always defined - shardKey: state.shardKey as ShardKey, + shardKey: state.shardKey, shardZones: state.shardZones, isUnmanagingNamespace: state.status === ShardingStatuses.UNMANAGING_NAMESPACE, From 5093e365c73ed35c7314dd5ce0b2a325698542d6 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 7 Oct 2024 16:32:33 +0200 Subject: [PATCH 10/17] update test --- .../src/components/states/shard-key-correct.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx index ebaf690cbbd..72a1bc0167a 100644 --- a/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx @@ -121,7 +121,7 @@ describe('Compass GlobalWrites Plugin', function () { ); expect(findingDocumentsSample).to.be.visible; expect(findingDocumentsSample.textContent).to.contain( - `use db1db.coll1.find({"location": "US-NY", "secondary": ""})` + `use db1db.["coll1"].find({"location": "US-NY", "secondary": ""})` ); const insertingDocumentsSample = await screen.findByTestId( @@ -129,7 +129,7 @@ describe('Compass GlobalWrites Plugin', function () { ); expect(insertingDocumentsSample).to.be.visible; expect(insertingDocumentsSample.textContent).to.contain( - `use db1db.coll1.insertOne({"location": "US-NY", "secondary": "",...})` + `use db1db.["coll1"].insertOne({"location": "US-NY", "secondary": "",...})` ); }); }); From 734225b0e818921846df42b5b335aa9ba02efc30 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 7 Oct 2024 17:12:26 +0200 Subject: [PATCH 11/17] update another test --- .../src/components/states/shard-key-correct.spec.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx index 72a1bc0167a..a8ba8ba7cab 100644 --- a/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx @@ -65,11 +65,11 @@ describe('Compass GlobalWrites Plugin', function () { const onUnmanageNamespace = Sinon.spy(); await renderWithProps({ onUnmanageNamespace, isUnmanagingNamespace: true }); - const btn = await screen.findByRole('button', { - name: /Unmanage collection/, - }); + const btn = await screen.findByTestId( + 'shard-collection-button' + ); expect(btn).to.be.visible; - expect(btn).to.have.property('disabled'); + expect(btn.getAttribute('aria-disabled')).to.equal('true'); userEvent.click(btn); From eabd0ff8a9ffe3c33ddb6476e7229c6e640b7c43 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Tue, 8 Oct 2024 10:32:44 +0200 Subject: [PATCH 12/17] Update packages/compass-global-writes/src/components/states/shard-key-correct.tsx Co-authored-by: Sergey Petushkov --- .../src/components/states/shard-key-correct.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx index d550521c003..19181c64f28 100644 --- a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx @@ -73,8 +73,8 @@ export function ShardKeyCorrect({ const sampleCodes = useMemo(() => { const { collection, database } = toNS(namespace); return { - findingDocuments: `use ${database}\ndb.["${collection}"].find({"location": "US-NY", "${customShardKeyField}": ""})`, - insertingDocuments: `use ${database}\ndb.["${collection}"].insertOne({"location": "US-NY", "${customShardKeyField}": "",...})`, + findingDocuments: `use ${database}\ndb[JSON.stringify(collection)].find({"location": "US-NY", "${customShardKeyField}": ""})`, + insertingDocuments: `use ${database}\ndb[JSON.stringify(collection)].insertOne({"location": "US-NY", "${customShardKeyField}": "",...})`, }; }, [namespace, customShardKeyField]); From 7d9dda9235867f0fa009c40ee53545ff97c999fb Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Tue, 8 Oct 2024 14:35:03 +0200 Subject: [PATCH 13/17] address PR suggestions & update tests --- .../states/shard-key-correct.spec.tsx | 4 +- .../components/states/shard-key-correct.tsx | 8 +- .../services/atlas-global-writes-service.ts | 2 +- .../src/store/index.spec.ts | 248 +++++++++++------- .../src/store/reducer.ts | 168 ++++++++---- packages/compass/src/main/application.ts | 2 +- 6 files changed, 280 insertions(+), 152 deletions(-) diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx index a8ba8ba7cab..e5460453351 100644 --- a/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx @@ -121,7 +121,7 @@ describe('Compass GlobalWrites Plugin', function () { ); expect(findingDocumentsSample).to.be.visible; expect(findingDocumentsSample.textContent).to.contain( - `use db1db.["coll1"].find({"location": "US-NY", "secondary": ""})` + `use db1db["coll1"].find({"location": "US-NY", "secondary": ""})` ); const insertingDocumentsSample = await screen.findByTestId( @@ -129,7 +129,7 @@ describe('Compass GlobalWrites Plugin', function () { ); expect(insertingDocumentsSample).to.be.visible; expect(insertingDocumentsSample.textContent).to.contain( - `use db1db.["coll1"].insertOne({"location": "US-NY", "secondary": "",...})` + `use db1db["coll1"].insertOne({"location": "US-NY", "secondary": "",...})` ); }); }); diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx index 19181c64f28..bd68f742d77 100644 --- a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx @@ -73,8 +73,12 @@ export function ShardKeyCorrect({ const sampleCodes = useMemo(() => { const { collection, database } = toNS(namespace); return { - findingDocuments: `use ${database}\ndb[JSON.stringify(collection)].find({"location": "US-NY", "${customShardKeyField}": ""})`, - insertingDocuments: `use ${database}\ndb[JSON.stringify(collection)].insertOne({"location": "US-NY", "${customShardKeyField}": "",...})`, + findingDocuments: `use ${database}\ndb[${JSON.stringify( + collection + )}].find({"location": "US-NY", "${customShardKeyField}": ""})`, + insertingDocuments: `use ${database}\ndb[${JSON.stringify( + collection + )}].insertOne({"location": "US-NY", "${customShardKeyField}": "",...})`, }; }, [namespace, customShardKeyField]); 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 ed183e5a3a5..db41cb5f24f 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 @@ -48,7 +48,7 @@ type AutomationAgentProcess = { errorText: string; }; -type AutomationAgentDeploymentStatusApiResponse = { +export type AutomationAgentDeploymentStatusApiResponse = { automationStatus: { processes: AutomationAgentProcess[]; }; diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index 92e11cff4fc..8091a6191af 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -5,9 +5,15 @@ import { fetchClusterShardingData, createShardKey, type CreateShardKeyData, + type ShardKey, } from './reducer'; import sinon from 'sinon'; -import type { ClusterDetailsApiResponse } from '../services/atlas-global-writes-service'; +import type { + AutomationAgentDeploymentStatusApiResponse, + ClusterDetailsApiResponse, + ManagedNamespace, + ShardZoneMapping, +} from '../services/atlas-global-writes-service'; const DB = 'test'; const COLL = 'coll'; @@ -22,7 +28,30 @@ const clusterDetails: ClusterDetailsApiResponse = { replicationSpecList: [], }; -function createClusterDetailsResponse(data: ClusterDetailsApiResponse) { +const managedNamespace: ManagedNamespace = { + db: DB, + collection: COLL, + customShardKey: 'secondary', + isCustomShardKeyHashed: false, + isShardKeyUnique: false, + numInitialChunks: null, + presplitHashedZones: false, +}; + +const shardKeyData: CreateShardKeyData = { + customShardKey: 'test', + isCustomShardKeyHashed: true, + isShardKeyUnique: false, + numInitialChunks: 1, + presplitHashedZones: true, +}; + +function createAuthFetchResponse< + TResponse extends + | ClusterDetailsApiResponse + | AutomationAgentDeploymentStatusApiResponse + | Record +>(data: TResponse) { return { json: () => Promise.resolve(data), }; @@ -46,106 +75,139 @@ describe('GlobalWritesStore Store', function () { expect(store.getState().status).to.equal('NOT_READY'); }); - context('actions', function () { - context('fetchClusterShardingData', function () { - it('when the namespace is not managed', async function () { - const store = createStore({ - authenticatedFetch: () => - createClusterDetailsResponse(clusterDetails), - }); - await store.dispatch(fetchClusterShardingData()); - expect(store.getState().status).to.equal('UNSHARDED'); - expect(store.getState().managedNamespace).to.equal(undefined); + context('scenarios', function () { + it('not managed -> sharding', async function () { + const store = createStore({ + authenticatedFetch: () => createAuthFetchResponse(clusterDetails), }); - - // TODO (COMPASS-8277): Add more test for fetching shard key and process errors + await store.dispatch(fetchClusterShardingData()); + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().managedNamespace).to.equal(undefined); + + const promise = store.dispatch(createShardKey(shardKeyData)); + expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); + await promise; + expect(store.getState().status).to.equal('SHARDING'); }); - context('createShardKey', function () { - const shardKeyData: CreateShardKeyData = { - customShardKey: 'test', - isCustomShardKeyHashed: true, - isShardKeyUnique: false, - numInitialChunks: 1, - presplitHashedZones: true, - }; - - it('sets SUBMITTING_FOR_SHARDING state when starting to create shard key and sets to SHARDING on success', async function () { - const store = createStore({ - authenticatedFetch: () => - createClusterDetailsResponse(clusterDetails), - }); - - const promise = store.dispatch(createShardKey(shardKeyData)); - expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); - - await promise; - expect(store.getState().status).to.equal('SHARDING'); + it('not managed -> failed sharding attempt', async function () { + const store = createStore({ + authenticatedFetch: (uri: string) => { + if (uri.includes('/geoSharding')) { + return Promise.reject(new Error('Failed to shard')); + } + + return createAuthFetchResponse(clusterDetails); + }, }); + await store.dispatch(fetchClusterShardingData()); + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().managedNamespace).to.equal(undefined); + + const promise = store.dispatch(createShardKey(shardKeyData)); + expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); + await promise; + expect(store.getState().status).to.equal('UNSHARDED'); + }); - it('sets SUBMITTING_FOR_SHARDING state when starting to create shard key and sets to UNSHARDED on failure', async function () { - const store = createStore({ - authenticatedFetch: () => Promise.reject(new Error('error')), - }); + it('when the namespace is managed', 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, + }, + ], + }; + } + }, + }); + await store.dispatch(fetchClusterShardingData()); + // expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); // no idea why this does not work + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); - const promise = store.dispatch(createShardKey(shardKeyData)); - expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); + it('sends correct data to the server when creating a shard key', async function () { + const alreadyManagedNamespaces = [ + { + db: 'test', + collection: 'one', + customShardKey: 'test', + isCustomShardKeyHashed: true, + isShardKeyUnique: false, + numInitialChunks: 1, + presplitHashedZones: true, + }, + ]; + + const getClusterInfoApiResponse = createAuthFetchResponse({ + ...clusterDetails, + geoSharding: { + ...clusterDetails.geoSharding, + managedNamespaces: alreadyManagedNamespaces, + }, + }); - await promise; - expect(store.getState().status).to.equal('UNSHARDED'); + // We call cluster API when store is activated to get the initial state. + // When creating a shard key, we call the same API to fetch the latest list of + // managed namespaces & then send it to the server along with the shard key data. + // So, we mock first and second call with same data. And then third call + // should be to create the shard key. + const fetchStub = sinon + .stub() + .onFirstCall() + .returns(getClusterInfoApiResponse) + .onSecondCall() + .returns(getClusterInfoApiResponse) + .onThirdCall() + .resolves(); + + const store = createStore({ + authenticatedFetch: fetchStub, }); - it('sends correct data to the server when creating a shard key', async function () { - const alreadyManagedNamespaces = [ - { - db: 'test', - collection: 'one', - customShardKey: 'test', - isCustomShardKeyHashed: true, - isShardKeyUnique: false, - numInitialChunks: 1, - presplitHashedZones: true, - }, - ]; - - const getClusterInfoApiResponse = createClusterDetailsResponse({ - ...clusterDetails, - geoSharding: { - ...clusterDetails.geoSharding, - managedNamespaces: alreadyManagedNamespaces, - }, - }); - - // We call cluster API when store is activated to get the initial state. - // When creating a shard key, we call the same API to fetch the latest list of - // managed namespaces & then send it to the server along with the shard key data. - // So, we mock first and second call with same data. And then third call - // should be to create the shard key. - const fetchStub = sinon - .stub() - .onFirstCall() - .returns(getClusterInfoApiResponse) - .onSecondCall() - .returns(getClusterInfoApiResponse) - .onThirdCall() - .resolves(); - - const store = createStore({ - authenticatedFetch: fetchStub, - }); - - await store.dispatch(createShardKey(shardKeyData)); - - const options = fetchStub.getCall(2).args[1]; - expect(options.method).to.equal('PATCH'); - expect(JSON.parse(options.body)).to.deep.equal({ - customZoneMapping: {}, - managedNamespaces: [ - ...alreadyManagedNamespaces, - { ...shardKeyData, db: DB, collection: COLL }, - ], - selfManagedSharding: false, - }); + await store.dispatch(createShardKey(shardKeyData)); + + const options = fetchStub.getCall(2).args[1]; + expect(options.method).to.equal('PATCH'); + expect(JSON.parse(options.body)).to.deep.equal({ + customZoneMapping: {}, + managedNamespaces: [ + ...alreadyManagedNamespaces, + { ...shardKeyData, db: DB, collection: COLL }, + ], + selfManagedSharding: false, }); }); }); diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index e51800e5d97..0b902e39411 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -151,11 +151,41 @@ export type ShardZoneData = { export type RootState = { namespace: string; managedNamespace?: ManagedNamespace; - status: ShardingStatus; - shardingError?: string; - shardKey?: ShardKey; + // status: ShardingStatus; shardZones: ShardZoneData[]; -}; +} & ( + | { + status: ShardingStatuses.NOT_READY; + shardKey?: never; + shardingError?: never; + } + | { + status: + | ShardingStatuses.UNSHARDED + | ShardingStatuses.SUBMITTING_FOR_SHARDING + | ShardingStatuses.SHARDING; + /** + * note: shardKey might exist even for unsharded. + * if the collection was sharded previously and then unmanaged + */ + shardKey?: ShardKey; + shardingError?: never; + } + | { + status: ShardingStatuses.SHARDING_ERROR; + shardKey?: never; + shardingError: string; + } + | { + status: + | ShardingStatuses.SHARD_KEY_CORRECT + | ShardingStatuses.SHARD_KEY_INVALID + | ShardingStatuses.SHARD_KEY_MISMATCH + | ShardingStatuses.UNMANAGING_NAMESPACE; + shardKey: ShardKey; + shardingError?: never; + } +); const initialState: RootState = { namespace: '', @@ -168,7 +198,8 @@ const reducer: Reducer = (state = initialState, action) => { isAction( action, GlobalWritesActionTypes.ManagedNamespaceFetched - ) + ) && + state.status === ShardingStatuses.NOT_READY ) { return { ...state, @@ -183,11 +214,13 @@ const reducer: Reducer = (state = initialState, action) => { isAction( action, GlobalWritesActionTypes.NamespaceShardingErrorFetched - ) + ) && + state.status === ShardingStatuses.NOT_READY ) { return { ...state, status: ShardingStatuses.SHARDING_ERROR, + shardKey: undefined, shardingError: action.error, }; } @@ -196,12 +229,14 @@ const reducer: Reducer = (state = initialState, action) => { isAction( action, GlobalWritesActionTypes.NamespaceShardKeyFetched - ) + ) && + state.status === ShardingStatuses.NOT_READY ) { return { ...state, status: getStatusFromShardKey(action.shardKey, state.managedNamespace), shardKey: action.shardKey, + shardingError: undefined, }; } @@ -221,7 +256,8 @@ const reducer: Reducer = (state = initialState, action) => { isAction( action, GlobalWritesActionTypes.SubmittingForShardingStarted - ) + ) && + state.status === ShardingStatuses.UNSHARDED ) { return { ...state, @@ -233,7 +269,8 @@ const reducer: Reducer = (state = initialState, action) => { isAction( action, GlobalWritesActionTypes.SubmittingForShardingFinished - ) + ) && + state.status === ShardingStatuses.SUBMITTING_FOR_SHARDING ) { return { ...state, @@ -246,7 +283,8 @@ const reducer: Reducer = (state = initialState, action) => { isAction( action, GlobalWritesActionTypes.SubmittingForShardingErrored - ) + ) && + state.status === ShardingStatuses.SUBMITTING_FOR_SHARDING ) { return { ...state, @@ -259,7 +297,10 @@ const reducer: Reducer = (state = initialState, action) => { isAction( action, GlobalWritesActionTypes.UnmanagingNamespaceStarted - ) + ) && + (state.status === ShardingStatuses.SHARD_KEY_CORRECT || + state.status === ShardingStatuses.SHARD_KEY_INVALID || + state.status === ShardingStatuses.SHARD_KEY_MISMATCH) ) { return { ...state, @@ -271,7 +312,8 @@ const reducer: Reducer = (state = initialState, action) => { isAction( action, GlobalWritesActionTypes.UnmanagingNamespaceFinished - ) + ) && + state.status === ShardingStatuses.UNMANAGING_NAMESPACE ) { return { ...state, @@ -284,7 +326,8 @@ const reducer: Reducer = (state = initialState, action) => { isAction( action, GlobalWritesActionTypes.UnmanagingNamespaceErrored - ) + ) && + state.status === ShardingStatuses.UNMANAGING_NAMESPACE ) { return { ...state, @@ -297,7 +340,11 @@ const reducer: Reducer = (state = initialState, action) => { export const fetchClusterShardingData = (): GlobalWritesThunkAction, ManagedNamespaceFetchedAction> => - async (dispatch, getState, { atlasGlobalWritesService, logger }) => { + async ( + dispatch, + getState, + { atlasGlobalWritesService, logger, connectionInfoRef } + ) => { const { namespace } = getState(); try { // Call the API to check if the namespace is managed. If the namespace is managed, @@ -323,14 +370,17 @@ export const fetchClusterShardingData = 'Error fetching cluster sharding data', (error as Error).message ); - openToast('global-writes-fetch-shard-info-error', { - title: `Failed to fetch sharding information: ${ - (error as Error).message - }`, - dismissible: true, - timeout: 5000, - variant: 'important', - }); + 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', + } + ); } }; @@ -342,7 +392,11 @@ export const createShardKey = ( | SubmittingForShardingFinishedAction | SubmittingForShardingErroredAction > => { - return async (dispatch, getState, { atlasGlobalWritesService, logger }) => { + return async ( + dispatch, + getState, + { atlasGlobalWritesService, logger, connectionInfoRef } + ) => { const { namespace } = getState(); dispatch({ type: GlobalWritesActionTypes.SubmittingForShardingStarted, @@ -364,12 +418,15 @@ export const createShardKey = ( data, } ); - openToast('global-writes-create-shard-key-error', { - title: `Failed to create shard key: ${(error as Error).message}`, - dismissible: true, - timeout: 5000, - variant: 'important', - }); + openToast( + `global-writes-create-shard-key-error-${connectionInfoRef.current.id}-${namespace}`, + { + title: `Failed to create shard key: ${(error as Error).message}`, + dismissible: true, + timeout: 5000, + variant: 'important', + } + ); dispatch({ type: GlobalWritesActionTypes.SubmittingForShardingErrored, }); @@ -381,19 +438,10 @@ const setNamespaceBeingSharded = ( managedNamespace?: ManagedNamespace ): GlobalWritesThunkAction => { return (dispatch) => { - // TODO: Scope this to the workspace. - const toastId = 'global-writes-sharding-in-progress'; dispatch({ type: GlobalWritesActionTypes.SubmittingForShardingFinished, managedNamespace, }); - openToast(toastId, { - title: 'Sharding your collection …', - description: 'This should not take too long.', - dismissible: true, - variant: 'progress', - // TODO: Add actionElement to cancel sharding. - }); }; }; @@ -401,7 +449,11 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< Promise, NamespaceShardingErrorFetchedAction | NamespaceShardKeyFetchedAction > => { - return async (dispatch, getState, { atlasGlobalWritesService, logger }) => { + return async ( + dispatch, + getState, + { atlasGlobalWritesService, logger, connectionInfoRef } + ) => { const { namespace } = getState(); try { @@ -430,17 +482,20 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< void dispatch(fetchShardingZones()); } catch (error) { logger.log.error( - logger.mongoLogId(1_001_000_332), + logger.mongoLogId(1_001_000_333), 'AtlasFetchError', 'Error fetching shard key', (error as Error).message ); - openToast('global-writes-fetch-shard-key-error', { - title: `Failed to fetch shard key: ${(error as Error).message}`, - dismissible: true, - timeout: 5000, - variant: 'important', - }); + 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', + } + ); } }; }; @@ -468,7 +523,11 @@ export const unmanageNamespace = (): GlobalWritesThunkAction< | UnmanagingNamespaceFinishedAction | UnmanagingNamespaceErroredAction > => { - return async (dispatch, getState, { atlasGlobalWritesService }) => { + return async ( + dispatch, + getState, + { atlasGlobalWritesService, connectionInfoRef } + ) => { const { namespace } = getState(); dispatch({ @@ -484,12 +543,15 @@ export const unmanageNamespace = (): GlobalWritesThunkAction< dispatch({ type: GlobalWritesActionTypes.UnmanagingNamespaceErrored, }); - openToast('global-writes-unmanage-namespace-error', { - title: `Failed to unmanage namespace: ${(error as Error).message}`, - dismissible: true, - timeout: 5000, - variant: 'important', - }); + openToast( + `global-writes-unmanage-namespace-error-${connectionInfoRef.current.id}-${namespace}`, + { + title: `Failed to unmanage namespace: ${(error as Error).message}`, + dismissible: true, + timeout: 5000, + variant: 'important', + } + ); } }; }; diff --git a/packages/compass/src/main/application.ts b/packages/compass/src/main/application.ts index 04c47a61d93..37207a21ced 100644 --- a/packages/compass/src/main/application.ts +++ b/packages/compass/src/main/application.ts @@ -316,7 +316,7 @@ class CompassApplication { ); log.warn( - mongoLogId(1_001_000_333), + mongoLogId(1_001_000_332), logContext, 'Unable to set proxy configuration', { From 6075f25af3c99aaa65da2fe1b99addb0a77946e7 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Tue, 8 Oct 2024 15:15:47 +0200 Subject: [PATCH 14/17] . --- packages/compass-global-writes/src/store/index.spec.ts | 1 - 1 file changed, 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 8091a6191af..845d0e3862c 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -5,7 +5,6 @@ import { fetchClusterShardingData, createShardKey, type CreateShardKeyData, - type ShardKey, } from './reducer'; import sinon from 'sinon'; import type { From 525f044dffe4b64bed21a4e477e4cdf4f8eae020 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 9 Oct 2024 10:19:12 +0200 Subject: [PATCH 15/17] Update packages/compass-global-writes/src/store/reducer.ts Co-authored-by: Sergey Petushkov --- packages/compass-global-writes/src/store/reducer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index 0b902e39411..85981a039df 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -151,7 +151,6 @@ export type ShardZoneData = { export type RootState = { namespace: string; managedNamespace?: ManagedNamespace; - // status: ShardingStatus; shardZones: ShardZoneData[]; } & ( | { From e61b1200557793edf11a81bc3d16cc8b47393c3f Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 9 Oct 2024 10:20:54 +0200 Subject: [PATCH 16/17] fix test --- packages/compass-global-writes/src/store/index.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index 845d0e3862c..7aa4c56e6d6 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -13,6 +13,7 @@ import type { ManagedNamespace, ShardZoneMapping, } from '../services/atlas-global-writes-service'; +import { waitFor } from '@mongodb-js/testing-library-compass'; const DB = 'test'; const COLL = 'coll'; @@ -153,8 +154,10 @@ describe('GlobalWritesStore Store', function () { }, }); await store.dispatch(fetchClusterShardingData()); - // expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); // no idea why this does not work - expect(store.getState().managedNamespace).to.equal(managedNamespace); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); }); it('sends correct data to the server when creating a shard key', async function () { From bc0793a9d8153af9a1207fa54c648cd5390fbdd2 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 9 Oct 2024 10:51:11 +0200 Subject: [PATCH 17/17] fix link --- .../src/components/states/shard-key-correct.spec.tsx | 2 +- .../src/components/states/shard-key-correct.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx index e5460453351..86f938cb10a 100644 --- a/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.spec.tsx @@ -94,7 +94,7 @@ describe('Compass GlobalWrites Plugin', function () { const link = await screen.findByRole('link', { name: /Edit Configuration/, }); - const expectedHref = `/v2/${connectionInfo.atlasMetadata?.projectId}/clusters/edit/${connectionInfo.atlasMetadata?.clusterName}`; + const expectedHref = `/v2/${connectionInfo.atlasMetadata?.projectId}#/clusters/edit/${connectionInfo.atlasMetadata?.clusterName}`; expect(link).to.be.visible; expect(link).to.have.attribute('href', expectedHref); diff --git a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx index bd68f742d77..05a29add5a8 100644 --- a/packages/compass-global-writes/src/components/states/shard-key-correct.tsx +++ b/packages/compass-global-writes/src/components/states/shard-key-correct.tsx @@ -164,7 +164,7 @@ export function ShardKeyCorrect({ Locations’ zone mapping can be changed by navigating to this clusters{' '} Edit Configuration {' '}