diff --git a/.evergreen/print-compass-env.sh b/.evergreen/print-compass-env.sh index 8e725ff7cae..ad1c1b41d4f 100755 --- a/.evergreen/print-compass-env.sh +++ b/.evergreen/print-compass-env.sh @@ -2,7 +2,7 @@ set -e -export MONGODB_DEFAULT_VERSION=6.0.x +export MONGODB_DEFAULT_VERSION=7.0.x if [[ $OSTYPE == "cygwin" ]]; then export PLATFORM='win32' diff --git a/AUTHORS b/AUTHORS index d33515d2c0e..e9a04ad3d3d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -91,3 +91,4 @@ Vivian Xiao <57568527+VivianTNT@users.noreply.github.com> admin-token-bot <36773031+admin-token-bot@users.noreply.github.com> Kræn Hansen Kræn Hansen +Ruchitha Rajaghatta <77162985+ruchitharajaghatta@users.noreply.github.com> diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 88497bba185..5a5315ee178 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -1,5 +1,5 @@ The following third-party software is used by and included in **Mongodb Compass**. -This document was automatically generated on Tue Oct 15 2024. +This document was automatically generated on Fri Oct 18 2024. ## List of dependencies diff --git a/docs/tracking-plan.md b/docs/tracking-plan.md index 746b79ea0e5..f32b0abd1ad 100644 --- a/docs/tracking-plan.md +++ b/docs/tracking-plan.md @@ -1,7 +1,7 @@ # Compass Tracking Plan -Generated on Tue, Oct 15, 2024 at 05:11 AM +Generated on Fri, Oct 18, 2024 at 04:41 PM ## Table of Contents diff --git a/packages/atlas-service/src/main.ts b/packages/atlas-service/src/main.ts index 028a04ece9b..c5c7dd56ac4 100644 --- a/packages/atlas-service/src/main.ts +++ b/packages/atlas-service/src/main.ts @@ -376,10 +376,7 @@ export class CompassAuthService { await throwIfNotOk(res); - const userInfo = (await res.json()) as AtlasUserInfo; - - // TODO: Remove hadcoded `enabledAIFeature: true` when Atlas returns the actual value. - return { ...userInfo, enabledAIFeature: true }; + return (await res.json()) as AtlasUserInfo; })(); return this.currentUser; } diff --git a/packages/atlas-service/src/util.ts b/packages/atlas-service/src/util.ts index fb27ab37e99..6a7070c1743 100644 --- a/packages/atlas-service/src/util.ts +++ b/packages/atlas-service/src/util.ts @@ -9,7 +9,7 @@ export type AtlasUserInfo = { lastName: string; primaryEmail: string; login: string; -} & { enabledAIFeature: boolean }; +}; export type IntrospectInfo = { active: boolean }; diff --git a/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-preview.spec.tsx b/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-preview.spec.tsx index 4b8de9984b0..3e34249c211 100644 --- a/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-preview.spec.tsx +++ b/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-preview.spec.tsx @@ -86,6 +86,36 @@ describe('FocusModeStagePreview', function () { const preview = screen.getByTestId('focus-mode-stage-preview'); expect(within(preview).getByText(/no preview documents/i)).to.exist; }); + for (const stageOperator of ['$search', '$searchMeta', '$vectorSearch']) { + it(`renders missing search index text for ${stageOperator}`, async function () { + await renderFocusModePreview({ + stageOperator, + documents: [], + }); + expect(screen.getByText('No preview documents')).to.exist; + expect( + screen.getByText( + 'This may be because your search has no results or your search index does not exist.' + ) + ).to.exist; + }); + + it(`does not render missing search index text for ${stageOperator} and documents.length > 0`, async function () { + await renderFocusModePreview({ + stageOperator, + documents: [ + new HadronDocument({ _id: 12345 }), + new HadronDocument({ _id: 54321 }), + ], + }); + expect(screen.queryByText('No preview documents')).to.not.exist; + expect( + screen.queryByText( + 'This may be because your search has no results or your search index does not exist.' + ) + ).to.not.exist; + }); + } it('renders $out stage preview', async function () { await renderFocusModePreview( { diff --git a/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-preview.tsx b/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-preview.tsx index 5e8839295db..1a0f1487e93 100644 --- a/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-preview.tsx +++ b/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-preview.tsx @@ -16,6 +16,7 @@ import OutputStagePreview from '../stage-preview/output-stage-preview'; import { AtlasStagePreview } from '../stage-preview/atlas-stage-preview'; import { isAtlasOnlyStage, + isSearchStage, isMissingAtlasStageSupport, isOutputStage, } from '../../utils/stage'; @@ -24,6 +25,7 @@ import { expandPreviewDocsForStage, } from '../../modules/pipeline-builder/stage-editor'; import type { StoreStage } from '../../modules/pipeline-builder/stage-editor'; +import SearchNoResults from '../search-no-results'; const containerStyles = css({ display: 'flex', @@ -149,6 +151,8 @@ export const FocusModePreview = ({ className={documentListStyles} /> ); + } else if (isSearchStage(stageOperator)) { + content = ; } else { content = (
diff --git a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-preview.spec.tsx b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-preview.spec.tsx index 23cfa7706d6..11294b6f45d 100644 --- a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-preview.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-preview.spec.tsx @@ -55,6 +55,19 @@ describe('PipelinePreview', function () { expect(screen.getByText(/No preview documents/)).to.exist; }); + it('renders missing search index text for $search', async function () { + await renderPipelineEditor({ + atlasOperator: '$search', + previewDocs: [], + }); + expect(screen.getByText('No preview documents')).to.exist; + expect( + screen.getByText( + 'This may be because your search has no results or your search index does not exist.' + ) + ).to.exist; + }); + it('renders document list', async function () { await renderPipelineEditor({ previewDocs: [{ _id: 1 }, { _id: 2 }].map( diff --git a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-preview.tsx b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-preview.tsx index 8a0e5601ff0..671b1e1357b 100644 --- a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-preview.tsx +++ b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-preview.tsx @@ -25,6 +25,7 @@ import { expandPreviewDocs, collapsePreviewDocs, } from '../../../modules/pipeline-builder/text-editor-pipeline'; +import SearchNoResults from '../../search-no-results'; const containerStyles = css({ display: 'flex', @@ -126,6 +127,9 @@ const PreviewResults = ({ } if (previewDocs.length === 0) { + if (atlasOperator) { + return ; + } return (
diff --git a/packages/compass-aggregations/src/components/search-no-results.tsx b/packages/compass-aggregations/src/components/search-no-results.tsx new file mode 100644 index 00000000000..58c327ee7e5 --- /dev/null +++ b/packages/compass-aggregations/src/components/search-no-results.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import { + css, + palette, + spacing, + Body, + useDarkMode, + Subtitle, +} from '@mongodb-js/compass-components'; + +const centeredContent = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + padding: spacing[3], + flexDirection: 'column', + textAlign: 'center', +}); + +const missingAtlasIndexLightStyles = css({ + color: palette.green.dark2, +}); + +const missingAtlasIndexDarkStyles = css({ + color: palette.green.base, +}); + +export default function SearchNoResults() { + const darkMode = useDarkMode(); + + return ( +
+ + No preview documents + + + This may be because your search has no results or your search index does + not exist. + +
+ ); +} diff --git a/packages/compass-aggregations/src/components/stage-preview/index.spec.tsx b/packages/compass-aggregations/src/components/stage-preview/index.spec.tsx index 80a6e3f9c66..b1beedab6d0 100644 --- a/packages/compass-aggregations/src/components/stage-preview/index.spec.tsx +++ b/packages/compass-aggregations/src/components/stage-preview/index.spec.tsx @@ -106,7 +106,7 @@ describe('StagePreview', function () { stageOperator: '$search', documents: [], }); - expect(screen.getByText('No results found')).to.exist; + expect(screen.getByText('No preview documents')).to.exist; expect( screen.getByText( 'This may be because your search has no results or your search index does not exist.' diff --git a/packages/compass-aggregations/src/components/stage-preview/index.tsx b/packages/compass-aggregations/src/components/stage-preview/index.tsx index b3d1868c684..b1424fae37f 100644 --- a/packages/compass-aggregations/src/components/stage-preview/index.tsx +++ b/packages/compass-aggregations/src/components/stage-preview/index.tsx @@ -9,13 +9,12 @@ import { Body, KeylineCard, useDarkMode, - Subtitle, } from '@mongodb-js/compass-components'; import { Document } from '@mongodb-js/compass-crud'; import type { RootState } from '../../modules'; import { - isAtlasOnlyStage, + isSearchStage, isMissingAtlasStageSupport, isOutputStage, } from '../../utils/stage'; @@ -26,6 +25,8 @@ import OutputStagePreivew from './output-stage-preview'; import StagePreviewHeader from './stage-preview-header'; import type { StoreStage } from '../../modules/pipeline-builder/stage-editor'; +import SearchNoResults from '../search-no-results'; + const centeredContent = css({ display: 'flex', alignItems: 'center', @@ -52,7 +53,7 @@ const emptyStylesLight = css({ stroke: palette.gray.base, }); -function EmptyIcon() { +function NoPreviewDocuments() { const darkMode = useDarkMode(); return ( @@ -97,14 +98,6 @@ const documentStyles = css({ padding: 0, }); -const missingAtlasIndexLightStyles = css({ - color: palette.green.dark2, -}); - -const missingAtlasIndexDarkStyles = css({ - color: palette.green.base, -}); - type StagePreviewProps = { index: number; isLoading: boolean; @@ -123,9 +116,8 @@ function StagePreviewBody({ shouldRenderStage, isLoading, }: StagePreviewProps) { - const darkMode = useDarkMode(); if (!shouldRenderStage) { - return ; + return ; } if (isMissingAtlasOnlyStageSupport) { @@ -153,24 +145,8 @@ function StagePreviewBody({ ); } - if (isAtlasOnlyStage(stageOperator ?? '') && documents?.length === 0) { - return ( -
- - No results found - - - This may be because your search has no results or your search index - does not exist. - -
- ); + if (isSearchStage(stageOperator) && documents?.length === 0) { + return ; } if (documents && documents.length > 0) { @@ -186,7 +162,7 @@ function StagePreviewBody({ return
{docs}
; } - return ; + return ; } const containerStyles = css({ @@ -208,7 +184,7 @@ export function StagePreview(props: StagePreviewProps) { if (props.isDisabled) { return (
- +
); } diff --git a/packages/compass-aggregations/src/utils/stage.ts b/packages/compass-aggregations/src/utils/stage.ts index 89b416ebcd6..b850b25ab1f 100644 --- a/packages/compass-aggregations/src/utils/stage.ts +++ b/packages/compass-aggregations/src/utils/stage.ts @@ -203,6 +203,21 @@ export function isAtlasOnlyStage( return !!stageOperator && ATLAS_ONLY_OPERATOR_NAMES.has(stageOperator); } +/* +Atlas Search does not return an error if there is no search index - it just +returns no results. So if the connection has access to Atlas Search and the +aggregation used a search-related stage and got no results we want to display a +different error. +*/ +export function isSearchStage( + stageOperator: string | null | undefined +): stageOperator is '$search' | '$searchMeta' | '$vectorSearch' { + if (!stageOperator) { + return false; + } + return ['$search', '$searchMeta', '$vectorSearch'].includes(stageOperator); +} + const STAGE_OPERATORS_MAP = new Map( STAGE_OPERATORS.map((stage) => [stage.value, stage]) ); diff --git a/packages/compass-e2e-tests/helpers/commands/index.ts b/packages/compass-e2e-tests/helpers/commands/index.ts index 50e7b830042..2ad04e81f01 100644 --- a/packages/compass-e2e-tests/helpers/commands/index.ts +++ b/packages/compass-e2e-tests/helpers/commands/index.ts @@ -62,3 +62,5 @@ export * from './unhide-index'; export * from './hide-visible-modal'; export * from './hide-visible-toasts'; export * from './sidebar-collection'; +export * from './read-first-document-content'; +export * from './read-stage-operators'; diff --git a/packages/compass-e2e-tests/helpers/read-first-document-content.ts b/packages/compass-e2e-tests/helpers/commands/read-first-document-content.ts similarity index 85% rename from packages/compass-e2e-tests/helpers/read-first-document-content.ts rename to packages/compass-e2e-tests/helpers/commands/read-first-document-content.ts index bfd7ea771cf..7f82f10e8fb 100644 --- a/packages/compass-e2e-tests/helpers/read-first-document-content.ts +++ b/packages/compass-e2e-tests/helpers/commands/read-first-document-content.ts @@ -1,8 +1,8 @@ import chai from 'chai'; const { expect } = chai; -import type { CompassBrowser } from '../helpers/compass-browser'; -import * as Selectors from '../helpers/selectors'; +import type { CompassBrowser } from '../compass-browser'; +import * as Selectors from '../selectors'; export async function getFirstListDocument(browser: CompassBrowser) { // We check the total from the header area so it is probably good enough to diff --git a/packages/compass-e2e-tests/helpers/read-stage-operators.ts b/packages/compass-e2e-tests/helpers/commands/read-stage-operators.ts similarity index 76% rename from packages/compass-e2e-tests/helpers/read-stage-operators.ts rename to packages/compass-e2e-tests/helpers/commands/read-stage-operators.ts index 495698254f5..0dbdaaf7396 100644 --- a/packages/compass-e2e-tests/helpers/read-stage-operators.ts +++ b/packages/compass-e2e-tests/helpers/commands/read-stage-operators.ts @@ -1,5 +1,5 @@ -import type { CompassBrowser } from '../helpers/compass-browser'; -import * as Selectors from '../helpers/selectors'; +import type { CompassBrowser } from '../compass-browser'; +import * as Selectors from '../selectors'; export async function getStageOperators( browser: CompassBrowser, diff --git a/packages/compass-e2e-tests/tests/collection-aggregations-tab.test.ts b/packages/compass-e2e-tests/tests/collection-aggregations-tab.test.ts index 253aad23f80..2dd52fbe505 100644 --- a/packages/compass-e2e-tests/tests/collection-aggregations-tab.test.ts +++ b/packages/compass-e2e-tests/tests/collection-aggregations-tab.test.ts @@ -16,7 +16,6 @@ import { createNestedDocumentsCollection, createNumbersCollection, } from '../helpers/insert-data'; -import { getStageOperators } from '../helpers/read-stage-operators'; import { saveAggregationPipeline } from '../helpers/commands/save-aggregation-pipeline'; import { Key } from 'webdriverio'; @@ -166,7 +165,7 @@ describe('Collection aggregations tab', function () { }); it('supports the right stages for the environment', async function () { - const options = await getStageOperators(browser, 0); + const options = await browser.getStageOperators(0); const expectedAggregations = [ '$addFields', diff --git a/packages/compass-e2e-tests/tests/collection-ai-query.test.ts b/packages/compass-e2e-tests/tests/collection-ai-query.test.ts index e7cb589de9c..7275118299d 100644 --- a/packages/compass-e2e-tests/tests/collection-ai-query.test.ts +++ b/packages/compass-e2e-tests/tests/collection-ai-query.test.ts @@ -16,7 +16,6 @@ import * as Selectors from '../helpers/selectors'; import { createNumbersCollection } from '../helpers/insert-data'; import { startMockAtlasServiceServer } from '../helpers/atlas-service'; import type { MockAtlasServerResponse } from '../helpers/atlas-service'; -import { getFirstListDocument } from '../helpers/read-first-document-content'; describe('Collection ai query', function () { let compass: Compass; @@ -147,7 +146,7 @@ describe('Collection ai query', function () { // Run it and check that the correct documents are shown. await browser.runFind('Documents', true); - const modifiedResult = await getFirstListDocument(browser); + const modifiedResult = await browser.getFirstListDocument(); expect(modifiedResult.i).to.be.equal('51'); }); }); diff --git a/packages/compass-e2e-tests/tests/collection-import.test.ts b/packages/compass-e2e-tests/tests/collection-import.test.ts index d52c3df1e25..2ebd8b206ad 100644 --- a/packages/compass-e2e-tests/tests/collection-import.test.ts +++ b/packages/compass-e2e-tests/tests/collection-import.test.ts @@ -11,7 +11,6 @@ import { TEST_COMPASS_WEB, DEFAULT_CONNECTION_NAME_1, } from '../helpers/compass'; -import { getFirstListDocument } from '../helpers/read-first-document-content'; import type { Compass } from '../helpers/compass'; import * as Selectors from '../helpers/selectors'; import { startTelemetryServer } from '../helpers/telemetry'; @@ -189,7 +188,7 @@ describe('Collection import', function () { return text === '1 – 1 of 1'; }); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); expect(result._id).to.exist; delete result._id; @@ -274,7 +273,7 @@ describe('Collection import', function () { return text === '1 – 1 of 1'; }); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); expect(result._id).to.exist; delete result._id; @@ -334,7 +333,7 @@ describe('Collection import', function () { return text === '1 – 25 of 1000'; }); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); expect(result._id).to.exist; delete result._id; @@ -402,7 +401,7 @@ describe('Collection import', function () { const text = await messageElement.getText(); expect(text).to.equal('1 – 25 of 16116'); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); expect(result._id).to.exist; delete result._id; @@ -452,7 +451,7 @@ describe('Collection import', function () { const text = await messageElement.getText(); expect(text).to.equal('1 – 1 of 1'); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); expect(result._id).to.exist; delete result._id; @@ -608,7 +607,7 @@ describe('Collection import', function () { const text = await messageElement.getText(); expect(text).to.equal('1 – 25 of 16116'); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); // _id is different every time expect(result._id).to.exist; @@ -811,7 +810,7 @@ describe('Collection import', function () { // show the array and object fields await browser.clickVisible(Selectors.ShowMoreFieldsButton); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); expect(result._id).to.equal('1001265'); }); @@ -894,7 +893,7 @@ describe('Collection import', function () { const text = await messageElement.getText(); expect(text).to.equal('1 – 1 of 1'); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); // _id is different every time expect(result._id).to.exist; @@ -1044,7 +1043,7 @@ describe('Collection import', function () { const text = await messageElement.getText(); expect(text).to.equal('1 – 1 of 1'); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); // _id is different every time expect(result._id).to.exist; diff --git a/packages/compass-e2e-tests/tests/in-use-encryption.test.ts b/packages/compass-e2e-tests/tests/in-use-encryption.test.ts index bf5a07bc33a..1203a249b3c 100644 --- a/packages/compass-e2e-tests/tests/in-use-encryption.test.ts +++ b/packages/compass-e2e-tests/tests/in-use-encryption.test.ts @@ -10,7 +10,6 @@ import { } from '../helpers/compass'; import type { Compass } from '../helpers/compass'; import * as Selectors from '../helpers/selectors'; -import { getFirstListDocument } from '../helpers/read-first-document-content'; import { MongoClient } from 'mongodb'; import delay from '../helpers/delay'; @@ -512,7 +511,7 @@ describe('CSFLE / QE', function () { // wait for the modal to go away await insertDialog.waitForDisplayed({ reverse: true }); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); expect(result._id).to.exist; expect(result.__safeContent__).to.exist; @@ -591,7 +590,7 @@ describe('CSFLE / QE', function () { 'Documents' ); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); expect(result[field]).to.be.equal(toString(oldValueJS)); const document = await browser.$(Selectors.DocumentListEntry); @@ -638,7 +637,7 @@ describe('CSFLE / QE', function () { : `{ ${field}: ${newValue} }` ); - const modifiedResult = await getFirstListDocument(browser); + const modifiedResult = await browser.getFirstListDocument(); expect(modifiedResult[field]).to.be.equal(toString(newValueJS)); expect(modifiedResult._id).to.be.equal(result._id); }); @@ -696,7 +695,7 @@ describe('CSFLE / QE', function () { "{ phoneNumber: '10101010' }" ); - const modifiedResult = await getFirstListDocument(browser); + const modifiedResult = await browser.getFirstListDocument(); expect(modifiedResult.phoneNumber).to.be.equal('"10101010"'); }); @@ -847,7 +846,7 @@ describe('CSFLE / QE', function () { await browser.runFindOperation('Documents', "{ name: 'Third' }"); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); delete result._id; delete result.__safeContent__; @@ -896,7 +895,7 @@ describe('CSFLE / QE', function () { 'Documents' ); - let decryptedResult = await getFirstListDocument(browser); + let decryptedResult = await browser.getFirstListDocument(); delete decryptedResult._id; delete decryptedResult.__safeContent__; @@ -926,7 +925,7 @@ describe('CSFLE / QE', function () { .$(Selectors.CSFLEConnectionModal) .waitForDisplayed({ reverse: true }); - const encryptedResult = await getFirstListDocument(browser); + const encryptedResult = await browser.getFirstListDocument(); delete encryptedResult._id; delete encryptedResult.__safeContent__; @@ -956,7 +955,7 @@ describe('CSFLE / QE', function () { .$(Selectors.CSFLEConnectionModal) .waitForDisplayed({ reverse: true }); - decryptedResult = await getFirstListDocument(browser); + decryptedResult = await browser.getFirstListDocument(); delete decryptedResult._id; delete decryptedResult.__safeContent__; @@ -1075,7 +1074,7 @@ describe('CSFLE / QE', function () { collection, 'Documents' ); - const result = await getFirstListDocument(browser); + const result = await browser.getFirstListDocument(); expect(result.phoneNumber).to.be.equal(JSON.stringify(value)); } @@ -1261,13 +1260,13 @@ describe('CSFLE / QE', function () { // { v: "123", _id: 'asdf' } // { v: "456", _id: 'ghjk' } - let decryptedResult = await getFirstListDocument(browser); + let decryptedResult = await browser.getFirstListDocument(); delete decryptedResult.__safeContent__; expect(decryptedResult).to.deep.equal({ v: '"123"', _id: '"asdf"' }); // We can't search for the encrypted value, but it does get decrypted await browser.runFindOperation('Documents', '{ _id: "ghjk" }'); - decryptedResult = await getFirstListDocument(browser); + decryptedResult = await browser.getFirstListDocument(); delete decryptedResult.__safeContent__; expect(decryptedResult).to.deep.equal({ v: '"456"', _id: '"ghjk"' }); }); diff --git a/packages/compass-e2e-tests/tests/read-only.test.ts b/packages/compass-e2e-tests/tests/read-only.test.ts index c99fd6989bc..1866470ce58 100644 --- a/packages/compass-e2e-tests/tests/read-only.test.ts +++ b/packages/compass-e2e-tests/tests/read-only.test.ts @@ -9,7 +9,6 @@ import { import { expect } from 'chai'; import * as Selectors from '../helpers/selectors'; import { createNumbersCollection } from '../helpers/insert-data'; -import { getStageOperators } from '../helpers/read-stage-operators'; import type { Compass } from '../helpers/compass'; import type { CompassBrowser } from '../helpers/compass-browser'; @@ -262,7 +261,7 @@ describe('readOnly: true / Read-Only Edition', function () { const stageContainers = await browser.$$(Selectors.StageCard); expect(stageContainers).to.have.lengthOf(1); - let options = await getStageOperators(browser, 0); + let options = await browser.getStageOperators(0); expect(options).to.include('$match'); expect(options).to.include('$out'); @@ -291,7 +290,7 @@ describe('readOnly: true / Read-Only Edition', function () { await browser.focusStageOperator(0); - options = await getStageOperators(browser, 0); + options = await browser.getStageOperators(0); expect(options).to.include('$match'); expect(options).to.not.include('$out'); diff --git a/packages/compass-generative-ai/src/atlas-ai-service.spec.ts b/packages/compass-generative-ai/src/atlas-ai-service.spec.ts index d5c5b2a34cb..b274e6d0ad7 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.spec.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.spec.ts @@ -4,11 +4,9 @@ import { AtlasAiService } from './atlas-ai-service'; import type { PreferencesAccess } from 'compass-preferences-model'; import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; -import { AtlasAuthService } from '@mongodb-js/atlas-service/provider'; import { ObjectId } from 'mongodb'; const ATLAS_USER = { - enabledAIFeature: true, firstName: 'John', lastName: 'Doe', login: 'johndoe', @@ -23,24 +21,6 @@ const PREFERENCES_USER = { const BASE_URL = 'http://example.com'; -class MockAtlasAuthService extends AtlasAuthService { - isAuthenticated() { - return Promise.resolve(true); - } - async getUserInfo() { - return Promise.resolve({} as any); - } - async signIn() { - return Promise.resolve({} as any); - } - async signOut() { - return Promise.resolve(); - } - async getAuthHeaders() { - return Promise.resolve({}); - } -} - class MockAtlasService { getCurrentUser = () => Promise.resolve(ATLAS_USER); adminApiEndpoint = (url: string, requestId?: string) => @@ -76,7 +56,6 @@ describe('AtlasAiService', function () { atlasAiService = new AtlasAiService( new MockAtlasService() as any, - new MockAtlasAuthService(), preferences, createNoopLogger() ); @@ -204,7 +183,7 @@ describe('AtlasAiService', function () { userInput: 'test', collectionName: 'test', databaseName: 'peanut', - sampleDocuments: [{ test: '4'.repeat(600000) }], + sampleDocuments: [{ test: '4'.repeat(5120001) }], requestId: 'abc', signal: new AbortController().signal, }); @@ -231,7 +210,7 @@ describe('AtlasAiService', function () { { a: '1' }, { a: '2' }, { a: '3' }, - { a: '4'.repeat(500000) }, + { a: '4'.repeat(5120001) }, ], requestId: 'abc', signal: new AbortController().signal, diff --git a/packages/compass-generative-ai/src/atlas-ai-service.ts b/packages/compass-generative-ai/src/atlas-ai-service.ts index dbfa1fb23e8..5cc49001be7 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.ts @@ -3,10 +3,7 @@ import { type PreferencesAccess, isAIFeatureEnabled, } from 'compass-preferences-model/provider'; -import type { - AtlasAuthService, - AtlasService, -} from '@mongodb-js/atlas-service/provider'; +import type { AtlasService } from '@mongodb-js/atlas-service/provider'; import { AtlasServiceError } from '@mongodb-js/atlas-service/renderer'; import type { Document } from 'mongodb'; import type { Logger } from '@mongodb-js/compass-logging'; @@ -24,7 +21,7 @@ type GenerativeAiInput = { // The size/token validation happens on the server, however, we do // want to ensure we're not uploading massive documents (some folks have documents > 1mb). -const AI_MAX_REQUEST_SIZE = 100000; +const AI_MAX_REQUEST_SIZE = 5120000; const AI_MIN_SAMPLE_DOCUMENTS = 1; const USER_AI_URI = (userId: string) => `unauth/ai/api/v1/hello/${userId}`; const AGGREGATION_URI = 'ai/api/v1/mql-aggregation'; @@ -200,14 +197,13 @@ export class AtlasAiService { constructor( private atlasService: AtlasService, - private atlasAuthService: AtlasAuthService, private preferences: PreferencesAccess, private logger: Logger ) { this.initPromise = this.setupAIAccess(); } - private async throwIfAINotEnabled() { + private throwIfAINotEnabled() { if (process.env.COMPASS_E2E_SKIP_ATLAS_SIGNIN === 'true') { return; } @@ -216,13 +212,6 @@ export class AtlasAiService { "Compass' AI functionality is not currently enabled. Please try again later." ); } - // Only throw if we actually have userInfo / logged in. Otherwise allow - // request to fall through so that we can get a proper network error - if ( - (await this.atlasAuthService.getUserInfo()).enabledAIFeature === false - ) { - throw new Error("Can't use AI before accepting terms and conditions"); - } } private async getAIFeatureEnablement(): Promise { @@ -277,7 +266,7 @@ export class AtlasAiService { validationFn: (res: any) => asserts res is T ): Promise => { await this.initPromise; - await this.throwIfAINotEnabled(); + this.throwIfAINotEnabled(); const { signal, requestId, ...rest } = input; const msgBody = buildQueryOrAggregationMessageBody(rest); diff --git a/packages/compass-generative-ai/src/components/generative-ai-input.spec.tsx b/packages/compass-generative-ai/src/components/generative-ai-input.spec.tsx index 26c24067eb9..7e5ff722a01 100644 --- a/packages/compass-generative-ai/src/components/generative-ai-input.spec.tsx +++ b/packages/compass-generative-ai/src/components/generative-ai-input.spec.tsx @@ -112,7 +112,7 @@ describe('GenerativeAIInput Component', function () { ], [ 'PROMPT_TOO_LONG', - 'Sorry, your collections have too many fields to process. Please try using this feature on a collection with smaller documents.', + 'Sorry, your request is too large. Please use a smaller prompt or try using this feature on a collection with smaller documents.', ], [ 'TOO_MANY_REQUESTS', diff --git a/packages/compass-generative-ai/src/components/generative-ai-input.tsx b/packages/compass-generative-ai/src/components/generative-ai-input.tsx index 36274cd90d4..81670398118 100644 --- a/packages/compass-generative-ai/src/components/generative-ai-input.tsx +++ b/packages/compass-generative-ai/src/components/generative-ai-input.tsx @@ -569,7 +569,7 @@ const AIError = ({ // able to fix the issue on their own it cases where the schema is too big. return ( <> - Sorry, your collections have too many fields to process. Please try + Sorry, your request is too large. Please use a smaller prompt or try using this feature on a collection with smaller documents. ); diff --git a/packages/compass-generative-ai/src/provider.tsx b/packages/compass-generative-ai/src/provider.tsx index 850848d8313..6df896c8439 100644 --- a/packages/compass-generative-ai/src/provider.tsx +++ b/packages/compass-generative-ai/src/provider.tsx @@ -2,10 +2,7 @@ import React, { createContext, useContext, useMemo } from 'react'; import { AtlasAiService } from './atlas-ai-service'; import { preferencesLocator } from 'compass-preferences-model/provider'; import { useLogger } from '@mongodb-js/compass-logging/provider'; -import { - atlasAuthServiceLocator, - atlasServiceLocator, -} from '@mongodb-js/atlas-service/provider'; +import { atlasServiceLocator } from '@mongodb-js/atlas-service/provider'; import { createServiceLocator, createServiceProvider, @@ -17,17 +14,11 @@ export const AtlasAiServiceProvider: React.FC = createServiceProvider( function AtlasAiServiceProvider({ children }) { const logger = useLogger('ATLAS-AI-SERVICE'); const preferences = preferencesLocator(); - const atlasAuthService = atlasAuthServiceLocator(); const atlasService = atlasServiceLocator(); const aiService = useMemo(() => { - return new AtlasAiService( - atlasService, - atlasAuthService, - preferences, - logger - ); - }, [atlasAuthService, preferences, logger, atlasService]); + return new AtlasAiService(atlasService, preferences, logger); + }, [preferences, logger, atlasService]); return ( diff --git a/packages/compass-global-writes/src/components/index.tsx b/packages/compass-global-writes/src/components/index.tsx index 77aa286de46..dd6ea046be4 100644 --- a/packages/compass-global-writes/src/components/index.tsx +++ b/packages/compass-global-writes/src/components/index.tsx @@ -5,12 +5,15 @@ import { spacing, WorkspaceContainer, SpinLoaderWithLabel, + ConfirmationModalArea, } from '@mongodb-js/compass-components'; 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'; +import ShardKeyInvalid from './states/shard-key-invalid'; +import ShardKeyMismatch from './states/shard-key-mismatch'; const containerStyles = css({ paddingLeft: spacing[400], @@ -18,6 +21,7 @@ const containerStyles = css({ display: 'flex', width: '100%', height: '100%', + maxWidth: '700px', }); const workspaceContentStyles = css({ @@ -55,7 +59,10 @@ function ShardingStateView({ return ; } - if (shardingStatus === ShardingStatuses.SHARDING) { + if ( + shardingStatus === ShardingStatuses.SHARDING || + shardingStatus === ShardingStatuses.CANCELLING_SHARDING + ) { return ; } @@ -66,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; } @@ -73,7 +91,9 @@ export function GlobalWrites({ shardingStatus }: GlobalWritesProps) { return (
- + + +
); 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/components/states/sharding.spec.tsx b/packages/compass-global-writes/src/components/states/sharding.spec.tsx index c3d8fb542be..69b767fe87e 100644 --- a/packages/compass-global-writes/src/components/states/sharding.spec.tsx +++ b/packages/compass-global-writes/src/components/states/sharding.spec.tsx @@ -1,13 +1,20 @@ import React from 'react'; import { expect } from 'chai'; -import { screen } from '@mongodb-js/testing-library-compass'; +import { screen, userEvent } from '@mongodb-js/testing-library-compass'; import { ShardingState } from './sharding'; import { renderWithStore } from '../../../tests/create-store'; +import Sinon from 'sinon'; function renderWithProps( props?: Partial> ) { - return renderWithStore(); + return renderWithStore( + {}} + isCancellingSharding={false} + {...props} + /> + ); } describe('Sharding', function () { @@ -15,4 +22,31 @@ describe('Sharding', function () { await renderWithProps(); expect(screen.getByRole('alert')).to.exist; }); + + it('sharding request can be cancelled', async function () { + const onCancelSharding = Sinon.spy(); + await renderWithProps({ + onCancelSharding, + }); + const btn = screen.getByRole('button', { name: 'Cancel Request' }); + expect(btn).to.be.visible; + + userEvent.click(btn); + + expect(onCancelSharding).to.have.been.calledOnce; + }); + + it('when cancelling is in progress, it cannot be triggered again', async function () { + const onCancelSharding = Sinon.spy(); + await renderWithProps({ + isCancellingSharding: true, + onCancelSharding, + }); + const btn = screen.getByTestId('cancel-sharding-btn'); + expect(btn.getAttribute('aria-disabled')).to.equal('true'); + + userEvent.click(btn); + + expect(onCancelSharding).not.to.have.been.calledOnce; + }); }); diff --git a/packages/compass-global-writes/src/components/states/sharding.tsx b/packages/compass-global-writes/src/components/states/sharding.tsx index f64e788a4ab..3bef1cf75d8 100644 --- a/packages/compass-global-writes/src/components/states/sharding.tsx +++ b/packages/compass-global-writes/src/components/states/sharding.tsx @@ -3,11 +3,17 @@ import { Banner, BannerVariant, Body, + Button, css, Link, spacing, } from '@mongodb-js/compass-components'; import { connect } from 'react-redux'; +import { + cancelSharding, + type RootState, + ShardingStatuses, +} from '../../store/reducer'; const nbsp = '\u00a0'; @@ -17,12 +23,33 @@ const containerStyles = css({ gap: spacing[400], }); -export function ShardingState() { +const btnStyles = css({ + float: 'right', + height: spacing[600], +}); + +interface ShardingStateProps { + isCancellingSharding: boolean; + onCancelSharding: () => void; +} + +export function ShardingState({ + isCancellingSharding, + onCancelSharding, +}: ShardingStateProps) { return (
Sharding your collection … {nbsp}this should not take too long. + Once your collection is sharded, this tab will show instructions on @@ -39,4 +66,11 @@ export function ShardingState() { ); } -export default connect()(ShardingState); +export default connect( + (state: RootState) => ({ + isCancellingSharding: state.status === ShardingStatuses.CANCELLING_SHARDING, + }), + { + onCancelSharding: cancelSharding, + } +)(ShardingState); diff --git a/packages/compass-global-writes/src/plugin-title.spec.tsx b/packages/compass-global-writes/src/plugin-title.spec.tsx new file mode 100644 index 00000000000..ce4e843a822 --- /dev/null +++ b/packages/compass-global-writes/src/plugin-title.spec.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { expect } from 'chai'; +import { PluginTitle } from './plugin-title'; +import { render, screen } from '@mongodb-js/testing-library-compass'; + +describe('PluginTitle', function () { + it('Renders a warning', function () { + render(); + expect(screen.getByLabelText('warning')).to.be.visible; + }); + + it('Does not render a warning', function () { + render(); + expect(screen.queryByLabelText('warning')).not.to.exist; + }); +}); diff --git a/packages/compass-global-writes/src/plugin-title.tsx b/packages/compass-global-writes/src/plugin-title.tsx index 0c484aecaac..9106ae55c29 100644 --- a/packages/compass-global-writes/src/plugin-title.tsx +++ b/packages/compass-global-writes/src/plugin-title.tsx @@ -30,7 +30,7 @@ const iconStylesDark = css({ color: palette.yellow.base, }); -const PluginTitle = ({ showWarning }: { showWarning: boolean }) => { +export const PluginTitle = ({ showWarning }: { showWarning: boolean }) => { const darkMode = useDarkMode(); return (
@@ -50,6 +50,7 @@ const PluginTitle = ({ showWarning }: { showWarning: boolean }) => { > ; diff --git a/packages/compass-global-writes/src/store/index.spec.ts b/packages/compass-global-writes/src/store/index.spec.ts index 7aa4c56e6d6..ee001aeb6f2 100644 --- a/packages/compass-global-writes/src/store/index.spec.ts +++ b/packages/compass-global-writes/src/store/index.spec.ts @@ -2,18 +2,25 @@ import { expect } from 'chai'; import { type GlobalWritesStore } from '.'; import { setupStore } from '../../tests/create-store'; import { - fetchClusterShardingData, createShardKey, type CreateShardKeyData, + unmanageNamespace, + cancelSharding, + POLLING_INTERVAL, + type ShardKey, } from './reducer'; import sinon from 'sinon'; import type { + AtlasShardKey, AutomationAgentDeploymentStatusApiResponse, + AutomationAgentProcess, ClusterDetailsApiResponse, ManagedNamespace, ShardZoneMapping, } from '../services/atlas-global-writes-service'; import { waitFor } from '@mongodb-js/testing-library-compass'; +import Sinon from 'sinon'; +import * as globalWritesReducer from './reducer'; const DB = 'test'; const COLL = 'coll'; @@ -28,22 +35,24 @@ const clusterDetails: ClusterDetailsApiResponse = { replicationSpecList: [], }; +const shardKeyData: CreateShardKeyData = { + customShardKey: 'secondary', + isCustomShardKeyHashed: true, + isShardKeyUnique: true, + numInitialChunks: 1, + presplitHashedZones: true, +}; + const managedNamespace: ManagedNamespace = { db: DB, collection: COLL, - customShardKey: 'secondary', - isCustomShardKeyHashed: false, - isShardKeyUnique: false, - numInitialChunks: null, - presplitHashedZones: false, + ...shardKeyData, }; -const shardKeyData: CreateShardKeyData = { - customShardKey: 'test', - isCustomShardKeyHashed: true, - isShardKeyUnique: false, - numInitialChunks: 1, - presplitHashedZones: true, +const failedShardingProcess: AutomationAgentProcess = { + statusType: 'ERROR', + workingOnShort: 'ShardingCollections', + errorText: `Failed to shard ${NS}`, }; function createAuthFetchResponse< @@ -57,7 +66,85 @@ function createAuthFetchResponse< }; } -function createStore(atlasService: any = {}): GlobalWritesStore { +function createStore({ + isNamespaceManaged = () => false, + hasShardingError = () => false, + hasShardKey = () => false, + failsOnShardingRequest = () => false, + authenticatedFetchStub, +}: + | { + isNamespaceManaged?: () => boolean; + hasShardingError?: () => boolean; + hasShardKey?: () => boolean | AtlasShardKey; + failsOnShardingRequest?: () => boolean; + authenticatedFetchStub?: never; + } + | { + isNamespaceManaged?: never; + hasShardingError?: never; + hasShardKey?: () => boolean | ShardKey; + failsOnShardingRequest?: never; + authenticatedFetchStub?: () => void; + } = {}): GlobalWritesStore { + const atlasService = { + authenticatedFetch: (uri: string) => { + if (uri.includes(`/geoSharding`) && failsOnShardingRequest()) { + return Promise.reject(new Error('Failed to shard')); + } + + if (uri.includes('/clusters/')) { + return createAuthFetchResponse({ + ...clusterDetails, + geoSharding: { + ...clusterDetails.geoSharding, + managedNamespaces: isNamespaceManaged() ? [managedNamespace] : [], + }, + }); + } + + if (uri.includes('/deploymentStatus/')) { + return createAuthFetchResponse({ + automationStatus: { + processes: hasShardingError() ? [failedShardingProcess] : [], + }, + }); + } + + return createAuthFetchResponse({}); + }, + automationAgentRequest: (_meta: unknown, type: string) => ({ + _id: '123', + requestType: type, + }), + automationAgentAwait: (_meta: unknown, type: string) => { + if (type === 'getShardKey') { + const shardKey = hasShardKey(); + return { + response: + shardKey === true + ? [ + { + key: { + location: 'range', + secondary: shardKeyData.isCustomShardKeyHashed + ? 'hashed' + : 'range', + }, + unique: true, + }, + ] + : typeof shardKey === 'object' + ? [shardKey] + : [], + }; + } + }, + } as any; + + if (authenticatedFetchStub) + atlasService.authenticatedFetch = authenticatedFetchStub; + return setupStore( { namespace: NS, @@ -69,6 +156,20 @@ function createStore(atlasService: any = {}): GlobalWritesStore { } describe('GlobalWritesStore Store', function () { + let confirmationStub: Sinon.SinonStub; + let clock: Sinon.SinonFakeTimers; + + beforeEach(() => { + confirmationStub = sinon + .stub(globalWritesReducer, 'showConfirmation') + .resolves(true); + }); + + afterEach(() => { + sinon.restore(); + clock && clock.restore(); + }); + it('sets the initial state', function () { const store = createStore(); expect(store.getState().namespace).to.equal(NS); @@ -76,96 +177,288 @@ describe('GlobalWritesStore Store', function () { }); context('scenarios', function () { - it('not managed -> sharding', async function () { + it('not managed -> sharding -> valid shard key', async function () { + let mockShardKey = false; + // initial state === unsharded const store = createStore({ - authenticatedFetch: () => createAuthFetchResponse(clusterDetails), + hasShardKey: Sinon.fake(() => mockShardKey), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().managedNamespace).to.equal(undefined); }); - await store.dispatch(fetchClusterShardingData()); - expect(store.getState().status).to.equal('UNSHARDED'); - expect(store.getState().managedNamespace).to.equal(undefined); + // user requests sharding + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, + }); const promise = store.dispatch(createShardKey(shardKeyData)); expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); await promise; expect(store.getState().status).to.equal('SHARDING'); + + // sharding ends with a shardKey + mockShardKey = true; + clock.tick(POLLING_INTERVAL); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + }); }); - it('not managed -> failed sharding attempt', async function () { + it('not managed -> sharding -> sharding error', async function () { + let mockFailure = false; + // initial state === unsharded const store = createStore({ - authenticatedFetch: (uri: string) => { - if (uri.includes('/geoSharding')) { - return Promise.reject(new Error('Failed to shard')); - } + hasShardingError: Sinon.fake(() => mockFailure), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().managedNamespace).to.equal(undefined); + }); - return createAuthFetchResponse(clusterDetails); - }, + // user requests sharding + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, }); - 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'); + // sharding ends with an error + mockFailure = true; + clock.tick(POLLING_INTERVAL); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING_ERROR'); + }); + }); + + it('not managed -> not managed (failed sharding request)', async function () { + // initial state === not managed + const store = createStore({ + failsOnShardingRequest: () => true, + }); + await waitFor(() => { + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().managedNamespace).to.equal(undefined); + }); + + // user tries to submit for sharding, but the request fails const promise = store.dispatch(createShardKey(shardKeyData)); expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); await promise; expect(store.getState().status).to.equal('UNSHARDED'); }); - it('when the namespace is managed', async function () { + it('sharding -> valid shard key', async function () { + let mockShardKey = false; + // initial state === sharding + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, + }); const store = createStore({ - authenticatedFetch: (uri: string) => { - if (uri.includes('/clusters/')) { - return createAuthFetchResponse({ - ...clusterDetails, - geoSharding: { - ...clusterDetails.geoSharding, - managedNamespaces: [managedNamespace], - }, - }); - } - - if (uri.includes('/deploymentStatus/')) { - return createAuthFetchResponse({ - automationStatus: { - processes: [], - }, - }); - } - - return createAuthFetchResponse({}); - }, - automationAgentRequest: (_meta: unknown, type: string) => ({ - _id: '123', - requestType: type, - }), - automationAgentAwait: (_meta: unknown, type: string) => { - if (type === 'getShardKey') { - return { - response: [ - { - key: { - location: 'HASHED', - secondary: 'HASHED', - }, - unique: false, - }, - ], - }; - } - }, + isNamespaceManaged: () => true, + hasShardKey: Sinon.fake(() => mockShardKey), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + + // sharding ends with a shardKey + mockShardKey = true; + clock.tick(POLLING_INTERVAL); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + }); + }); + + it('sharding -> cancelling request -> not managed', async function () { + let mockManagedNamespace = true; + confirmationStub.resolves(true); + // initial state === sharding + const store = createStore({ + isNamespaceManaged: Sinon.fake(() => mockManagedNamespace), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING'); + expect(store.getState().pollingTimeout).not.to.be.undefined; + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + + // user cancels the sharding request + const promise = store.dispatch(cancelSharding()); + mockManagedNamespace = false; + await promise; + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().pollingTimeout).to.be.undefined; + expect(confirmationStub).to.have.been.called; + }); + + it('valid shard key', async function () { + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: () => true, }); - await store.dispatch(fetchClusterShardingData()); await waitFor(() => { expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); expect(store.getState().managedNamespace).to.equal(managedNamespace); }); }); + it('valid shard key -> not managed', async function () { + // initial state === shard key correct + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: () => true, + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + + // user asks to unmanage + const promise = store.dispatch(unmanageNamespace()); + expect(store.getState().status).to.equal('UNMANAGING_NAMESPACE'); + await promise; + expect(store.getState().status).to.equal('UNSHARDED'); + }); + + it('valid shard key -> valid shard key (failed unmanage attempt)', async function () { + // initial state === shard key correct + let mockFailure = false; + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: () => true, + failsOnShardingRequest: Sinon.fake(() => mockFailure), + }); + + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + + // user asks to unmanage + mockFailure = true; + const promise = store.dispatch(unmanageNamespace()); + expect(store.getState().status).to.equal('UNMANAGING_NAMESPACE'); + await promise; + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + }); + + 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, + hasShardingError: () => true, + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING_ERROR'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + }); + it('sends correct data to the server when creating a shard key', async function () { const alreadyManagedNamespaces = [ { db: 'test', collection: 'one', - customShardKey: 'test', + customShardKey: 'secondary', isCustomShardKeyHashed: true, isShardKeyUnique: false, numInitialChunks: 1, @@ -196,7 +489,7 @@ describe('GlobalWritesStore Store', function () { .resolves(); const store = createStore({ - authenticatedFetch: fetchStub, + authenticatedFetchStub: fetchStub, }); await store.dispatch(createShardKey(shardKeyData)); diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index 85981a039df..4255bb49072 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -1,8 +1,13 @@ import type { Action, Reducer } from 'redux'; import type { GlobalWritesThunkAction } from '.'; -import { openToast } from '@mongodb-js/compass-components'; +import { + openToast, + showConfirmation as showConfirmationModal, +} from '@mongodb-js/compass-components'; import type { ManagedNamespace } from '../services/atlas-global-writes-service'; +export const POLLING_INTERVAL = 5000; + export function isAction( action: Action, type: A['type'] @@ -30,6 +35,13 @@ enum GlobalWritesActionTypes { SubmittingForShardingFinished = 'global-writes/SubmittingForShardingFinished', SubmittingForShardingErrored = 'global-writes/SubmittingForShardingErrored', + CancellingShardingStarted = 'global-writes/CancellingShardingStarted', + CancellingShardingFinished = 'global-writes/CancellingShardingFinished', + CancellingShardingErrored = 'global-writes/CancellingShardingErrored', + + NextPollingTimeoutSet = 'global-writes/NextPollingTimeoutSet', + NextPollingTimeoutCleared = 'global-writes/NextPollingTimeoutCleared', + UnmanagingNamespaceStarted = 'global-writes/UnmanagingNamespaceStarted', UnmanagingNamespaceFinished = 'global-writes/UnmanagingNamespaceFinished', UnmanagingNamespaceErrored = 'global-writes/UnmanagingNamespaceErrored', @@ -68,6 +80,28 @@ type SubmittingForShardingErroredAction = { type: GlobalWritesActionTypes.SubmittingForShardingErrored; }; +type CancellingShardingStartedAction = { + type: GlobalWritesActionTypes.CancellingShardingStarted; +}; + +type CancellingShardingFinishedAction = { + type: GlobalWritesActionTypes.CancellingShardingFinished; + managedNamespace?: ManagedNamespace; +}; + +type CancellingShardingErroredAction = { + type: GlobalWritesActionTypes.CancellingShardingErrored; +}; + +type NextPollingTimeoutSetAction = { + type: GlobalWritesActionTypes.NextPollingTimeoutSet; + timeout: NodeJS.Timeout; +}; + +type NextPollingTimeoutClearedAction = { + type: GlobalWritesActionTypes.NextPollingTimeoutCleared; +}; + type UnmanagingNamespaceStartedAction = { type: GlobalWritesActionTypes.UnmanagingNamespaceStarted; }; @@ -102,6 +136,12 @@ export enum ShardingStatuses { */ SHARDING = 'SHARDING', + /** + * State when user cancels the sharding and + * we are waiting for server to accept the request. + */ + CANCELLING_SHARDING = 'CANCELLING_SHARDING', + /** * Sharding failed. */ @@ -129,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; @@ -157,32 +198,47 @@ export type RootState = { status: ShardingStatuses.NOT_READY; shardKey?: never; shardingError?: never; + pollingTimeout?: never; } | { status: | ShardingStatuses.UNSHARDED | ShardingStatuses.SUBMITTING_FOR_SHARDING - | ShardingStatuses.SHARDING; + | ShardingStatuses.CANCELLING_SHARDING; /** * note: shardKey might exist even for unsharded. * if the collection was sharded previously and then unmanaged */ shardKey?: ShardKey; shardingError?: never; + pollingTimeout?: never; + } + | { + status: ShardingStatuses.SHARDING; + /** + * note: shardKey might exist + * if the collection was sharded previously and then unmanaged + */ + shardKey?: ShardKey; + shardingError?: never; + pollingTimeout?: NodeJS.Timeout; } | { status: ShardingStatuses.SHARDING_ERROR; shardKey?: never; shardingError: string; + pollingTimeout?: never; } | { status: | 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; } ); @@ -214,13 +270,18 @@ const reducer: Reducer = (state = initialState, action) => { action, GlobalWritesActionTypes.NamespaceShardingErrorFetched ) && - state.status === ShardingStatuses.NOT_READY + (state.status === ShardingStatuses.NOT_READY || + state.status === ShardingStatuses.SHARDING) ) { + if (state.pollingTimeout) { + throw new Error('Polling was not stopped'); + } return { ...state, status: ShardingStatuses.SHARDING_ERROR, shardKey: undefined, shardingError: action.error, + pollingTimeout: state.pollingTimeout, }; } @@ -229,13 +290,18 @@ const reducer: Reducer = (state = initialState, action) => { action, GlobalWritesActionTypes.NamespaceShardKeyFetched ) && - state.status === ShardingStatuses.NOT_READY + (state.status === ShardingStatuses.NOT_READY || + state.status === ShardingStatuses.SHARDING) ) { + if (state.pollingTimeout) { + throw new Error('Polling was not stopped'); + } return { ...state, status: getStatusFromShardKey(action.shardKey, state.managedNamespace), shardKey: action.shardKey, shardingError: undefined, + pollingTimeout: state.pollingTimeout, }; } @@ -269,7 +335,8 @@ const reducer: Reducer = (state = initialState, action) => { action, GlobalWritesActionTypes.SubmittingForShardingFinished ) && - state.status === ShardingStatuses.SUBMITTING_FOR_SHARDING + (state.status === ShardingStatuses.SUBMITTING_FOR_SHARDING || + state.status === ShardingStatuses.NOT_READY) ) { return { ...state, @@ -278,6 +345,78 @@ const reducer: Reducer = (state = initialState, action) => { }; } + if ( + isAction( + action, + GlobalWritesActionTypes.NextPollingTimeoutSet + ) && + state.status === ShardingStatuses.SHARDING + ) { + return { + ...state, + pollingTimeout: action.timeout, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.NextPollingTimeoutCleared + ) && + state.status === ShardingStatuses.SHARDING + ) { + return { + ...state, + pollingTimeout: undefined, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.CancellingShardingStarted + ) && + state.status === ShardingStatuses.SHARDING + ) { + if (state.pollingTimeout) { + throw new Error('Polling was not stopped'); + } + return { + ...state, + status: ShardingStatuses.CANCELLING_SHARDING, + pollingTimeout: state.pollingTimeout, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.CancellingShardingErrored + ) && + state.status === ShardingStatuses.CANCELLING_SHARDING + ) { + return { + ...state, + status: ShardingStatuses.SHARDING, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.CancellingShardingFinished + ) && + (state.status === ShardingStatuses.CANCELLING_SHARDING || + state.status === ShardingStatuses.SHARDING_ERROR) + // the error might come before the cancel request was processed + ) { + return { + ...state, + status: ShardingStatuses.UNSHARDED, + shardingError: undefined, + }; + } + if ( isAction( action, @@ -298,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, }; } @@ -312,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, @@ -433,6 +575,64 @@ export const createShardKey = ( }; }; +// Exporting this for test only to stub it and set +// its value. This enables to test cancelSharding action. +export const showConfirmation = showConfirmationModal; + +export const cancelSharding = (): GlobalWritesThunkAction< + Promise, + | CancellingShardingStartedAction + | CancellingShardingFinishedAction + | CancellingShardingErroredAction +> => { + return async (dispatch, getState, { atlasGlobalWritesService, logger }) => { + const confirmed = await showConfirmation({ + title: 'Confirmation', + description: 'Are you sure you want to cancel the sharding request?', + }); + + if (!confirmed) { + return; + } + + const { namespace, status } = getState(); + + if (status === ShardingStatuses.SHARDING) { + dispatch(stopPollingForShardKey()); + } + dispatch({ + type: GlobalWritesActionTypes.CancellingShardingStarted, + }); + + try { + await atlasGlobalWritesService.unmanageNamespace(namespace); + dispatch({ + type: GlobalWritesActionTypes.CancellingShardingFinished, + }); + } catch (error) { + logger.log.error( + logger.mongoLogId(1_001_000_334), + 'AtlasFetchError', + 'Error cancelling the sharding process', + { + error: (error as Error).message, + } + ); + openToast('global-writes-cancel-sharding-error', { + title: `Failed to cancel the sharding process: ${ + (error as Error).message + }`, + dismissible: true, + timeout: 5000, + variant: 'important', + }); + dispatch({ + type: GlobalWritesActionTypes.CancellingShardingErrored, + }); + } + }; +}; + const setNamespaceBeingSharded = ( managedNamespace?: ManagedNamespace ): GlobalWritesThunkAction => { @@ -441,6 +641,41 @@ const setNamespaceBeingSharded = ( type: GlobalWritesActionTypes.SubmittingForShardingFinished, managedNamespace, }); + dispatch(pollForShardKey()); + }; +}; + +const pollForShardKey = (): GlobalWritesThunkAction< + void, + NextPollingTimeoutSetAction +> => { + return (dispatch, getState) => { + const { pollingTimeout } = getState(); + if ( + pollingTimeout // prevent double polling + ) { + return; + } + const timeout = setTimeout(() => { + void dispatch(fetchNamespaceShardKey()); + }, POLLING_INTERVAL); + + dispatch({ + type: GlobalWritesActionTypes.NextPollingTimeoutSet, + timeout, + }); + }; +}; + +const stopPollingForShardKey = (): GlobalWritesThunkAction< + void, + NextPollingTimeoutClearedAction +> => { + return (dispatch, getState) => { + const { pollingTimeout } = getState(); + if (!pollingTimeout) return; + clearTimeout(pollingTimeout); + dispatch({ type: GlobalWritesActionTypes.NextPollingTimeoutCleared }); }; }; @@ -453,7 +688,7 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< getState, { atlasGlobalWritesService, logger, connectionInfoRef } ) => { - const { namespace } = getState(); + const { namespace, status } = getState(); try { const [shardingError, shardKey] = await Promise.all([ @@ -462,6 +697,9 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< ]); if (shardingError) { + if (status === ShardingStatuses.SHARDING) { + dispatch(stopPollingForShardKey()); + } dispatch({ type: GlobalWritesActionTypes.NamespaceShardingErrorFetched, error: shardingError, @@ -474,6 +712,9 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< return; } + if (status === ShardingStatuses.SHARDING) { + dispatch(stopPollingForShardKey()); + } dispatch({ type: GlobalWritesActionTypes.NamespaceShardKeyFetched, shardKey, diff --git a/packages/compass-settings/src/components/settings/atlas-login.spec.tsx b/packages/compass-settings/src/components/settings/atlas-login.spec.tsx index 153a431e063..16f7e90ea92 100644 --- a/packages/compass-settings/src/components/settings/atlas-login.spec.tsx +++ b/packages/compass-settings/src/components/settings/atlas-login.spec.tsx @@ -205,9 +205,7 @@ describe('AtlasLoginSettings', function () { it('should not reset sign in state if there is no sign in attempt in progress', async function () { const atlasAuthService = { - signIn: sandbox - .stub() - .resolves({ login: 'user@mongodb.com', enabledAIFeature: false }), + signIn: sandbox.stub().resolves({ login: 'user@mongodb.com' }), }; const { store } = renderAtlasLoginSettings(atlasAuthService); diff --git a/packages/compass-settings/src/components/settings/atlas-login.tsx b/packages/compass-settings/src/components/settings/atlas-login.tsx index 5aea12d947a..27173f776a0 100644 --- a/packages/compass-settings/src/components/settings/atlas-login.tsx +++ b/packages/compass-settings/src/components/settings/atlas-login.tsx @@ -70,7 +70,6 @@ const atlasLoginEmailStyles = css({ export const AtlasLoginSettings: React.FunctionComponent<{ isSignInInProgress: boolean; userLogin: string | null; - isAIFeatureEnabled: boolean; onSignInClick(): void; onSignOutClick(): void; }> = ({ isSignInInProgress, userLogin, onSignInClick, onSignOutClick }) => { @@ -174,7 +173,6 @@ export const ConnectedAtlasLoginSettings = connect( return { isSignInInProgress: state.atlasLogin.status === 'in-progress', userLogin: state.atlasLogin.userInfo?.login ?? null, - isAIFeatureEnabled: Boolean(state.atlasLogin.userInfo?.enabledAIFeature), }; }, { diff --git a/packages/data-service/src/connect-mongo-client.spec.ts b/packages/data-service/src/connect-mongo-client.spec.ts index dc11a07fe57..9a4ba568356 100644 --- a/packages/data-service/src/connect-mongo-client.spec.ts +++ b/packages/data-service/src/connect-mongo-client.spec.ts @@ -252,8 +252,10 @@ describe('connectMongoClient', function () { expect(error).to.be.instanceOf(Error); // propagates the tunnel error - expect(error.message).to.match( - /(All configured authentication methods failed|ENOTFOUND compass-tests\.fakehost\.localhost)/ + // NOTE: this heavily depends on which server version we're running on + const message = error.errors ? error.errors[0].message : error.message; + expect(message).to.match( + /(All configured authentication methods failed|ENOTFOUND compass-tests\.fakehost\.localhost)|ECONNREFUSED 127.0.0.1:22/ ); for (let i = 0; i < 10; i++) { diff --git a/packages/data-service/src/data-service.spec.ts b/packages/data-service/src/data-service.spec.ts index 1e63d597735..bad3d369259 100644 --- a/packages/data-service/src/data-service.spec.ts +++ b/packages/data-service/src/data-service.spec.ts @@ -1199,10 +1199,7 @@ describe('DataService', function () { it('throws an error', async function () { await expect( dataService.getSearchIndexes(testNamespace) - ).to.be.rejectedWith( - MongoServerError, - /Unrecognized pipeline stage name: '\$listSearchIndexes'"|\$listSearchIndexes stage is only allowed on MongoDB Atlas/ - ); + ).to.be.rejectedWith(MongoServerError); }); }); @@ -1213,10 +1210,7 @@ describe('DataService', function () { name: 'my-index', definition: {}, }) - ).to.be.rejectedWith( - MongoServerError, - "no such command: 'createSearchIndexes'" - ); + ).to.be.rejectedWith(MongoServerError); }); }); @@ -1224,10 +1218,7 @@ describe('DataService', function () { it('throws an error', async function () { await expect( dataService.updateSearchIndex(testNamespace, 'my-index', {}) - ).to.be.rejectedWith( - MongoServerError, - "no such command: 'updateSearchIndex'" - ); + ).to.be.rejectedWith(MongoServerError); }); }); @@ -1235,10 +1226,7 @@ describe('DataService', function () { it('throws an error', async function () { await expect( dataService.dropSearchIndex(testNamespace, 'my-index') - ).to.be.rejectedWith( - MongoServerError, - "no such command: 'dropSearchIndex'" - ); + ).to.be.rejectedWith(MongoServerError); }); });