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(