diff --git a/packages/compass-global-writes/src/components/index.tsx b/packages/compass-global-writes/src/components/index.tsx index 12031c5656f..dd6ea046be4 100644 --- a/packages/compass-global-writes/src/components/index.tsx +++ b/packages/compass-global-writes/src/components/index.tsx @@ -12,6 +12,8 @@ import { ShardingStatuses } from '../store/reducer'; import UnshardedState from './states/unsharded'; import ShardingState from './states/sharding'; import ShardKeyCorrect from './states/shard-key-correct'; +import ShardKeyInvalid from './states/shard-key-invalid'; +import ShardKeyMismatch from './states/shard-key-mismatch'; const containerStyles = css({ paddingLeft: spacing[400], @@ -19,6 +21,7 @@ const containerStyles = css({ display: 'flex', width: '100%', height: '100%', + maxWidth: '700px', }); const workspaceContentStyles = css({ @@ -70,6 +73,17 @@ function ShardingStateView({ return ; } + if (shardingStatus === ShardingStatuses.SHARD_KEY_INVALID) { + return ; + } + + if ( + shardingStatus === ShardingStatuses.SHARD_KEY_MISMATCH || + shardingStatus === ShardingStatuses.UNMANAGING_NAMESPACE_MISMATCH + ) { + return ; + } + return null; } diff --git a/packages/compass-global-writes/src/components/shard-key-markup.tsx b/packages/compass-global-writes/src/components/shard-key-markup.tsx new file mode 100644 index 00000000000..15d6ca8fb6d --- /dev/null +++ b/packages/compass-global-writes/src/components/shard-key-markup.tsx @@ -0,0 +1,53 @@ +import { Body, Code, css, spacing } from '@mongodb-js/compass-components'; +import React from 'react'; +import type { ShardKey } from '../store/reducer'; + +const codeBlockContainerStyles = css({ + display: 'flex', + flexDirection: 'column', + gap: spacing[100], +}); + +interface ShardKeyMarkupProps { + shardKey: ShardKey; + namespace: string; + showMetaData?: boolean; + type?: 'requested' | 'existing'; +} + +export function ShardKeyMarkup({ + namespace, + shardKey, + showMetaData, + type = 'existing', +}: ShardKeyMarkupProps) { + let markup = shardKey.fields + .map( + (field) => + `"${field.name}"` + + (showMetaData ? ` (${field.type.toLowerCase()})` : '') + ) + .join(', '); + if (showMetaData) { + markup += ` - unique: ${String(shardKey.isUnique)}`; + } + return ( +
+ + {type === 'existing' ? ( + <> + {namespace} is configured with the following shard + key: + + ) : ( + <>You requested to use the shard key: + )} + + + {markup} + +
+ ); +} + +export default ShardKeyMarkup; 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 86f938cb10a..504db736e4e 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 @@ -28,8 +28,8 @@ describe('Compass GlobalWrites Plugin', function () { namespace: 'db1.coll1', shardKey: { fields: [ - { type: 'HASHED', name: 'location' }, - { type: 'RANGE', name: 'secondary' }, + { type: 'RANGE', name: 'location' }, + { type: 'HASHED', name: 'secondary' }, ], isUnique: false, }, @@ -66,7 +66,7 @@ describe('Compass GlobalWrites Plugin', function () { await renderWithProps({ onUnmanageNamespace, isUnmanagingNamespace: true }); const btn = await screen.findByTestId( - 'shard-collection-button' + 'unmanage-collection-button' ); expect(btn).to.be.visible; expect(btn.getAttribute('aria-disabled')).to.equal('true'); @@ -103,12 +103,16 @@ describe('Compass GlobalWrites Plugin', function () { it('Describes the shardKey', async function () { await renderWithProps(); - const title = await screen.findByTestId('shardkey-description-title'); + const title = await screen.findByTestId( + 'existing-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'); + const list = await screen.findByTestId( + 'existing-shardkey-description-content' + ); expect(list).to.be.visible; expect(list.textContent).to.contain(`"location", "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 05a29add5a8..5a89380c17e 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 @@ -10,6 +10,7 @@ import { Subtitle, Label, Button, + ButtonVariant, } from '@mongodb-js/compass-components'; import { connect } from 'react-redux'; import { @@ -22,6 +23,7 @@ import { import toNS from 'mongodb-ns'; import { ShardZonesTable } from '../shard-zones-table'; import { useConnectionInfo } from '@mongodb-js/compass-connections/provider'; +import ShardKeyMarkup from '../shard-key-markup'; const nbsp = '\u00a0'; @@ -47,7 +49,7 @@ const paragraphStyles = css({ export type ShardKeyCorrectProps = { namespace: string; - shardKey?: ShardKey; + shardKey: ShardKey; shardZones: ShardZoneData[]; isUnmanagingNamespace: boolean; onUnmanageNamespace: () => void; @@ -60,10 +62,6 @@ 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]); @@ -92,17 +90,7 @@ export function ShardKeyCorrect({ {nbsp}We have included a table for reference below. - -
- - {namespace} is configured with the following shard - key: - - - {shardKey.fields.map((field) => `"${field.name}"`).join(', ')} - -
- + Example commands
@@ -184,9 +172,9 @@ export function ShardKeyCorrect({
+
+ + + {requestedShardKey && ( + + )} +
+ ); +} + +export default connect( + (state: RootState) => { + if (!state.shardKey) { + throw new Error('Shard key not found in ShardKeyMismatch'); + } + return { + namespace: state.namespace, + shardKey: state.shardKey, + requestedShardKey: + state.managedNamespace && getRequestedShardKey(state.managedNamespace), + isUnmanagingNamespace: + state.status === ShardingStatuses.UNMANAGING_NAMESPACE_MISMATCH, + }; + }, + { + onUnmanageNamespace: unmanageNamespace, + } +)(ShardKeyMismatch); 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 1e0355544b9..80390f1def2 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 @@ -54,7 +54,7 @@ export type AutomationAgentDeploymentStatusApiResponse = { }; }; -type AtlasShardKey = { +export type AtlasShardKey = { _id: string; unique: boolean; key: Record; diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index bc6194ba5d8..ee001aeb6f2 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -7,9 +7,11 @@ import { unmanageNamespace, cancelSharding, POLLING_INTERVAL, + type ShardKey, } from './reducer'; import sinon from 'sinon'; import type { + AtlasShardKey, AutomationAgentDeploymentStatusApiResponse, AutomationAgentProcess, ClusterDetailsApiResponse, @@ -74,14 +76,14 @@ function createStore({ | { isNamespaceManaged?: () => boolean; hasShardingError?: () => boolean; - hasShardKey?: () => boolean; + hasShardKey?: () => boolean | AtlasShardKey; failsOnShardingRequest?: () => boolean; authenticatedFetchStub?: never; } | { isNamespaceManaged?: never; hasShardingError?: never; - hasShardKey?: () => boolean; + hasShardKey?: () => boolean | ShardKey; failsOnShardingRequest?: never; authenticatedFetchStub?: () => void; } = {}): GlobalWritesStore { @@ -117,20 +119,24 @@ function createStore({ }), automationAgentAwait: (_meta: unknown, type: string) => { if (type === 'getShardKey') { + const shardKey = hasShardKey(); return { - response: hasShardKey() - ? [ - { - key: { - location: 'range', - secondary: shardKeyData.isCustomShardKeyHashed - ? 'hashed' - : 'range', + response: + shardKey === true + ? [ + { + key: { + location: 'range', + secondary: shardKeyData.isCustomShardKeyHashed + ? 'hashed' + : 'range', + }, + unique: true, }, - unique: true, - }, - ] - : [], + ] + : typeof shardKey === 'object' + ? [shardKey] + : [], }; } }, @@ -340,6 +346,102 @@ describe('GlobalWritesStore Store', function () { expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); }); + context('invalid and mismatching shard keys', function () { + it('there is no location : invalid', async function () { + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: () => ({ + _id: '123', + key: { + notLocation: 'range', // invalid + secondary: 'range', + }, + unique: true, + }), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_INVALID'); + }); + }); + + it('location is not a range : invalid', async function () { + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: () => ({ + _id: '123', + key: { + location: 'hashed', // invalid + secondary: 'range', + }, + unique: true, + }), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_INVALID'); + }); + }); + + it('secondary key does not match : mismatch', async function () { + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: () => ({ + _id: '123', + key: { + location: 'range', + tertiary: 'range', // this is a different secondary key + }, + unique: true, + }), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_MISMATCH'); + }); + }); + + it('uniqueness does not match : mismatch', async function () { + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: () => ({ + _id: '123', + key: { + location: 'range', + secondary: 'range', + }, + unique: false, // this does not match + }), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_MISMATCH'); + }); + }); + + it('mismatch -> unmanaged', async function () { + // initial state - mismatch + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: () => ({ + _id: '123', + key: { + location: 'range', + tertiary: 'range', + }, + unique: true, + }), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_MISMATCH'); + }); + + // user asks to unmanage + const promise = store.dispatch(unmanageNamespace()); + expect(store.getState().status).to.equal( + 'UNMANAGING_NAMESPACE_MISMATCH' + ); + await promise; + expect(store.getState().status).to.equal('UNSHARDED'); + }); + }); + it('sharding error', async function () { const store = createStore({ isNamespaceManaged: () => true, diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index d596d52d51c..4255bb49072 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -169,6 +169,7 @@ export enum ShardingStatuses { * Namespace is being unmanaged. */ UNMANAGING_NAMESPACE = 'UNMANAGING_NAMESPACE', + UNMANAGING_NAMESPACE_MISMATCH = 'UNMANAGING_NAMESPACE_MISMATCH', } export type ShardingStatus = keyof typeof ShardingStatuses; @@ -233,7 +234,8 @@ export type RootState = { | ShardingStatuses.SHARD_KEY_CORRECT | ShardingStatuses.SHARD_KEY_INVALID | ShardingStatuses.SHARD_KEY_MISMATCH - | ShardingStatuses.UNMANAGING_NAMESPACE; + | ShardingStatuses.UNMANAGING_NAMESPACE + | ShardingStatuses.UNMANAGING_NAMESPACE_MISMATCH; shardKey: ShardKey; shardingError?: never; pollingTimeout?: never; @@ -435,12 +437,14 @@ const reducer: Reducer = (state = initialState, action) => { GlobalWritesActionTypes.UnmanagingNamespaceStarted ) && (state.status === ShardingStatuses.SHARD_KEY_CORRECT || - state.status === ShardingStatuses.SHARD_KEY_INVALID || state.status === ShardingStatuses.SHARD_KEY_MISMATCH) ) { return { ...state, - status: ShardingStatuses.UNMANAGING_NAMESPACE, + status: + state.status === ShardingStatuses.SHARD_KEY_CORRECT + ? ShardingStatuses.UNMANAGING_NAMESPACE + : ShardingStatuses.UNMANAGING_NAMESPACE_MISMATCH, }; } @@ -449,7 +453,8 @@ const reducer: Reducer = (state = initialState, action) => { action, GlobalWritesActionTypes.UnmanagingNamespaceFinished ) && - state.status === ShardingStatuses.UNMANAGING_NAMESPACE + (state.status === ShardingStatuses.UNMANAGING_NAMESPACE || + state.status === ShardingStatuses.UNMANAGING_NAMESPACE_MISMATCH) ) { return { ...state,