diff --git a/packages/compass-aggregations/src/components/pipeline-results-workspace/index.spec.tsx b/packages/compass-aggregations/src/components/pipeline-results-workspace/index.spec.tsx index 938ced09c4c..caa342d3d29 100644 --- a/packages/compass-aggregations/src/components/pipeline-results-workspace/index.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-results-workspace/index.spec.tsx @@ -72,11 +72,11 @@ describe('PipelineResultsWorkspace', function () { const onRetry = spy(); await renderPipelineResultsWorkspace({ isError: true, - error: 'Something bad happened', + error: { message: 'Something bad happened' }, onRetry, }); expect(screen.getByText('Something bad happened')).to.exist; - userEvent.click(screen.getByText('Retry'), undefined, { + userEvent.click(screen.getByText('RETRY'), undefined, { skipPointerEventsCheck: true, }); expect(onRetry).to.be.calledOnce; diff --git a/packages/compass-aggregations/src/components/pipeline-results-workspace/index.tsx b/packages/compass-aggregations/src/components/pipeline-results-workspace/index.tsx index fa2759de2cc..83a2a5dc57d 100644 --- a/packages/compass-aggregations/src/components/pipeline-results-workspace/index.tsx +++ b/packages/compass-aggregations/src/components/pipeline-results-workspace/index.tsx @@ -6,13 +6,19 @@ import { cx, spacing, CancelLoader, - ErrorSummary, Subtitle, Button, palette, + Banner, + BannerVariant, + showErrorDetails, } from '@mongodb-js/compass-components'; import type { RootState } from '../../modules'; -import { cancelAggregation, retryAggregation } from '../../modules/aggregation'; +import { + type AggregationError, + cancelAggregation, + retryAggregation, +} from '../../modules/aggregation'; import PipelineResultsList from './pipeline-results-list'; import PipelineEmptyResults from './pipeline-empty-results'; import { @@ -52,6 +58,23 @@ const centered = css({ justifyContent: 'center', }); +const errorBannerStyles = css({ + width: '100%', +}); + +const errorBannerContentStyles = css({ + display: 'flex', + justifyContent: 'space-between', +}); + +const errorBannerTextStyles = css({ + flex: 1, +}); + +const errorDetailsBtnStyles = css({ + marginLeft: spacing[100], +}); + const ResultsContainer: React.FunctionComponent<{ center?: boolean }> = ({ children, center, @@ -102,7 +125,7 @@ type PipelineResultsWorkspaceProps = { documents: HadronDocument[]; isLoading?: boolean; isError?: boolean; - error?: string | null; + error?: AggregationError; isEmpty?: boolean; isMergeOrOutPipeline?: boolean; mergeOrOutDestination?: string | null; @@ -133,12 +156,38 @@ export const PipelineResultsWorkspace: React.FunctionComponent< if (isError && error) { results = ( - + variant={BannerVariant.Danger} + className={errorBannerStyles} + > +
+
{error?.message}
+ + {error?.info && ( + + )} +
+
); } else if (isLoading) { diff --git a/packages/compass-aggregations/src/modules/aggregation.ts b/packages/compass-aggregations/src/modules/aggregation.ts index ed9b9e39930..df3737e8189 100644 --- a/packages/compass-aggregations/src/modules/aggregation.ts +++ b/packages/compass-aggregations/src/modules/aggregation.ts @@ -73,9 +73,14 @@ export type AggregationFinishedAction = { isLast: boolean; }; +export type AggregationError = { + message: string; + info?: Record; +}; + export type AggregationFailedAction = { type: ActionTypes.AggregationFailed; - error: string; + error: AggregationError; page: number; }; @@ -110,7 +115,7 @@ export type State = { isLast: boolean; loading: boolean; abortController?: AbortController; - error?: string; + error?: AggregationError; previousPageData?: PreviousPageData; resultsViewType: 'document' | 'json'; }; @@ -125,6 +130,13 @@ export const INITIAL_STATE: State = { resultsViewType: 'document', }; +function getAggregationError(error: Error): AggregationError { + return { + message: error.message, + info: (error as MongoServerError).errInfo, + }; +} + const reducer: Reducer = (state = INITIAL_STATE, action) => { if ( isAction( @@ -477,7 +489,7 @@ const fetchAggregationData = ( if ((e as MongoServerError).code) { dispatch({ type: ActionTypes.AggregationFailed, - error: (e as Error).message, + error: getAggregationError(e as Error), page, }); if ((e as MongoServerError).codeName === 'MaxTimeMSExpired') { diff --git a/packages/compass-e2e-tests/helpers/commands/set-validation.ts b/packages/compass-e2e-tests/helpers/commands/set-validation.ts index c4a0052fca0..d7ddc662971 100644 --- a/packages/compass-e2e-tests/helpers/commands/set-validation.ts +++ b/packages/compass-e2e-tests/helpers/commands/set-validation.ts @@ -68,7 +68,10 @@ export async function setValidation( collection, 'Validation' ); - await browser.clickVisible(Selectors.AddRuleButton); + const startButton = browser.$(Selectors.AddRuleButton); + if (await startButton.isExisting()) { + await browser.clickVisible(startButton); + } const element = browser.$(Selectors.ValidationEditor); await element.waitForDisplayed(); await browser.setValidationWithinValidationTab(validator); diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index 92d2af1a24a..a117ef0b46a 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -852,6 +852,8 @@ export const SavePipelineSaveAsAction = export const AggregationAutoPreviewToggle = '[data-testid="pipeline-toolbar-preview-toggle"]'; export const AggregationErrorBanner = '[data-testid="pipeline-results-error"]'; +export const AggregationErrorDetailsBtn = + '[data-testid="pipeline-results-error"] [data-testid="pipeline-results-error-details-button"]'; export const RunPipelineButton = `[data-testid="pipeline-toolbar-run-button"]`; export const EditPipelineButton = `[data-testid="pipeline-toolbar-edit-button"]`; 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 1bd2be6dea7..2ed295d81ce 100644 --- a/packages/compass-e2e-tests/tests/collection-aggregations-tab.test.ts +++ b/packages/compass-e2e-tests/tests/collection-aggregations-tab.test.ts @@ -615,6 +615,84 @@ describe('Collection aggregations tab', function () { ); }); + context('with existing validation rule', function () { + const REQUIRE_PHONE_VALIDATOR = + '{ $jsonSchema: { bsonType: "object", required: [ "phone" ] } }'; + const VALIDATED_OUT_COLLECTION = 'nestedDocs'; + beforeEach(async function () { + await browser.setValidation({ + connectionName: DEFAULT_CONNECTION_NAME_1, + database: 'test', + collection: VALIDATED_OUT_COLLECTION, + validator: REQUIRE_PHONE_VALIDATOR, + }); + await browser.navigateToCollectionTab( + DEFAULT_CONNECTION_NAME_1, + 'test', + 'numbers', + 'Aggregations' + ); + await addStage(browser, 1); + }); + + afterEach(async function () { + await browser.setValidation({ + connectionName: DEFAULT_CONNECTION_NAME_1, + database: 'test', + collection: VALIDATED_OUT_COLLECTION, + validator: '{}', + }); + }); + + it('Shows error info when inserting', async function () { + await browser.selectStageOperator(0, '$out'); + await browser.setCodemirrorEditorValue( + Selectors.stageEditor(0), + `'${VALIDATED_OUT_COLLECTION}'` + ); + + await waitForAnyText(browser, browser.$(Selectors.stageContent(0))); + + // run the $out stage + await browser.clickVisible(Selectors.RunPipelineButton); + + // confirm the write operation + const writeOperationConfirmationModal = browser.$( + Selectors.AggregationWriteOperationConfirmationModal + ); + await writeOperationConfirmationModal.waitForDisplayed(); + + const description = await browser + .$(Selectors.AggregationWriteOperationConfirmationModalDescription) + .getText(); + + expect(description).to.contain(`test.${VALIDATED_OUT_COLLECTION}`); + + await browser.clickVisible( + Selectors.AggregationWriteOperationConfirmButton + ); + + await writeOperationConfirmationModal.waitForDisplayed({ reverse: true }); + + const errorElement = browser.$(Selectors.AggregationErrorBanner); + await errorElement.waitForDisplayed(); + expect(await errorElement.getText()).to.include( + 'Document failed validation' + ); + // enter details + const errorDetailsBtn = browser.$(Selectors.AggregationErrorDetailsBtn); + await errorElement.waitForDisplayed(); + await errorDetailsBtn.click(); + + const errorDetailsJson = browser.$(Selectors.ErrorDetailsJson); + await errorDetailsJson.waitForDisplayed(); + + // exit details + await browser.clickVisible(Selectors.confirmationModalConfirmButton()); + await errorElement.waitForDisplayed(); + }); + }); + it('cancels pipeline with $out as the last stage', async function () { await browser.selectStageOperator(0, '$out'); await browser.setCodemirrorEditorValue(