Skip to content

Commit b75d6e9

Browse files
authored
feat: validation error details aggregation COMPASS-8868 (#6825)
1 parent f1a3722 commit b75d6e9

File tree

6 files changed

+158
-14
lines changed

6 files changed

+158
-14
lines changed

packages/compass-aggregations/src/components/pipeline-results-workspace/index.spec.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ describe('PipelineResultsWorkspace', function () {
7272
const onRetry = spy();
7373
await renderPipelineResultsWorkspace({
7474
isError: true,
75-
error: 'Something bad happened',
75+
error: { message: 'Something bad happened' },
7676
onRetry,
7777
});
7878
expect(screen.getByText('Something bad happened')).to.exist;
79-
userEvent.click(screen.getByText('Retry'), undefined, {
79+
userEvent.click(screen.getByText('RETRY'), undefined, {
8080
skipPointerEventsCheck: true,
8181
});
8282
expect(onRetry).to.be.calledOnce;

packages/compass-aggregations/src/components/pipeline-results-workspace/index.tsx

+57-8
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@ import {
66
cx,
77
spacing,
88
CancelLoader,
9-
ErrorSummary,
109
Subtitle,
1110
Button,
1211
palette,
12+
Banner,
13+
BannerVariant,
14+
showErrorDetails,
1315
} from '@mongodb-js/compass-components';
1416
import type { RootState } from '../../modules';
15-
import { cancelAggregation, retryAggregation } from '../../modules/aggregation';
17+
import {
18+
type AggregationError,
19+
cancelAggregation,
20+
retryAggregation,
21+
} from '../../modules/aggregation';
1622
import PipelineResultsList from './pipeline-results-list';
1723
import PipelineEmptyResults from './pipeline-empty-results';
1824
import {
@@ -52,6 +58,23 @@ const centered = css({
5258
justifyContent: 'center',
5359
});
5460

61+
const errorBannerStyles = css({
62+
width: '100%',
63+
});
64+
65+
const errorBannerContentStyles = css({
66+
display: 'flex',
67+
justifyContent: 'space-between',
68+
});
69+
70+
const errorBannerTextStyles = css({
71+
flex: 1,
72+
});
73+
74+
const errorDetailsBtnStyles = css({
75+
marginLeft: spacing[100],
76+
});
77+
5578
const ResultsContainer: React.FunctionComponent<{ center?: boolean }> = ({
5679
children,
5780
center,
@@ -102,7 +125,7 @@ type PipelineResultsWorkspaceProps = {
102125
documents: HadronDocument[];
103126
isLoading?: boolean;
104127
isError?: boolean;
105-
error?: string | null;
128+
error?: AggregationError;
106129
isEmpty?: boolean;
107130
isMergeOrOutPipeline?: boolean;
108131
mergeOrOutDestination?: string | null;
@@ -133,12 +156,38 @@ export const PipelineResultsWorkspace: React.FunctionComponent<
133156
if (isError && error) {
134157
results = (
135158
<ResultsContainer>
136-
<ErrorSummary
159+
<Banner
137160
data-testid="pipeline-results-error"
138-
errors={error}
139-
onAction={onRetry}
140-
actionText="Retry"
141-
/>
161+
variant={BannerVariant.Danger}
162+
className={errorBannerStyles}
163+
>
164+
<div className={errorBannerContentStyles}>
165+
<div className={errorBannerTextStyles}>{error?.message}</div>
166+
<Button
167+
size="xsmall"
168+
onClick={onRetry}
169+
data-testid="pipeline-results-error-retry-button"
170+
className={errorDetailsBtnStyles}
171+
>
172+
RETRY
173+
</Button>
174+
{error?.info && (
175+
<Button
176+
size="xsmall"
177+
onClick={() =>
178+
showErrorDetails({
179+
details: error.info!,
180+
closeAction: 'close',
181+
})
182+
}
183+
data-testid="pipeline-results-error-details-button"
184+
className={errorDetailsBtnStyles}
185+
>
186+
VIEW ERROR DETAILS
187+
</Button>
188+
)}
189+
</div>
190+
</Banner>
142191
</ResultsContainer>
143192
);
144193
} else if (isLoading) {

packages/compass-aggregations/src/modules/aggregation.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,14 @@ export type AggregationFinishedAction = {
7474
isLast: boolean;
7575
};
7676

77+
export type AggregationError = {
78+
message: string;
79+
info?: Record<string, unknown>;
80+
};
81+
7782
export type AggregationFailedAction = {
7883
type: ActionTypes.AggregationFailed;
79-
error: string;
84+
error: AggregationError;
8085
page: number;
8186
};
8287

@@ -111,7 +116,7 @@ export type State = {
111116
isLast: boolean;
112117
loading: boolean;
113118
abortController?: AbortController;
114-
error?: string;
119+
error?: AggregationError;
115120
previousPageData?: PreviousPageData;
116121
resultsViewType: 'document' | 'json';
117122
};
@@ -126,6 +131,13 @@ export const INITIAL_STATE: State = {
126131
resultsViewType: 'document',
127132
};
128133

134+
function getAggregationError(error: Error): AggregationError {
135+
return {
136+
message: error.message,
137+
info: (error as MongoServerError).errInfo,
138+
};
139+
}
140+
129141
const reducer: Reducer<State, Action> = (state = INITIAL_STATE, action) => {
130142
if (
131143
isAction<WorkspaceChangedAction>(
@@ -492,7 +504,7 @@ const fetchAggregationData = (
492504
if ((e as MongoServerError).code) {
493505
dispatch({
494506
type: ActionTypes.AggregationFailed,
495-
error: (e as Error).message,
507+
error: getAggregationError(e as Error),
496508
page,
497509
});
498510
if ((e as MongoServerError).codeName === 'MaxTimeMSExpired') {

packages/compass-e2e-tests/helpers/commands/set-validation.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ export async function setValidation(
6868
collection,
6969
'Validation'
7070
);
71-
await browser.clickVisible(Selectors.AddRuleButton);
71+
const startButton = browser.$(Selectors.AddRuleButton);
72+
if (await startButton.isExisting()) {
73+
await browser.clickVisible(startButton);
74+
}
7275
const element = browser.$(Selectors.ValidationEditor);
7376
await element.waitForDisplayed();
7477
await browser.setValidationWithinValidationTab(validator);

packages/compass-e2e-tests/helpers/selectors.ts

+2
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,8 @@ export const SavePipelineSaveAsAction =
852852
export const AggregationAutoPreviewToggle =
853853
'[data-testid="pipeline-toolbar-preview-toggle"]';
854854
export const AggregationErrorBanner = '[data-testid="pipeline-results-error"]';
855+
export const AggregationErrorDetailsBtn =
856+
'[data-testid="pipeline-results-error"] [data-testid="pipeline-results-error-details-button"]';
855857

856858
export const RunPipelineButton = `[data-testid="pipeline-toolbar-run-button"]`;
857859
export const EditPipelineButton = `[data-testid="pipeline-toolbar-edit-button"]`;

packages/compass-e2e-tests/tests/collection-aggregations-tab.test.ts

+78
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,84 @@ describe('Collection aggregations tab', function () {
615615
);
616616
});
617617

618+
context('with existing validation rule', function () {
619+
const REQUIRE_PHONE_VALIDATOR =
620+
'{ $jsonSchema: { bsonType: "object", required: [ "phone" ] } }';
621+
const VALIDATED_OUT_COLLECTION = 'nestedDocs';
622+
beforeEach(async function () {
623+
await browser.setValidation({
624+
connectionName: DEFAULT_CONNECTION_NAME_1,
625+
database: 'test',
626+
collection: VALIDATED_OUT_COLLECTION,
627+
validator: REQUIRE_PHONE_VALIDATOR,
628+
});
629+
await browser.navigateToCollectionTab(
630+
DEFAULT_CONNECTION_NAME_1,
631+
'test',
632+
'numbers',
633+
'Aggregations'
634+
);
635+
await addStage(browser, 1);
636+
});
637+
638+
afterEach(async function () {
639+
await browser.setValidation({
640+
connectionName: DEFAULT_CONNECTION_NAME_1,
641+
database: 'test',
642+
collection: VALIDATED_OUT_COLLECTION,
643+
validator: '{}',
644+
});
645+
});
646+
647+
it('Shows error info when inserting', async function () {
648+
await browser.selectStageOperator(0, '$out');
649+
await browser.setCodemirrorEditorValue(
650+
Selectors.stageEditor(0),
651+
`'${VALIDATED_OUT_COLLECTION}'`
652+
);
653+
654+
await waitForAnyText(browser, browser.$(Selectors.stageContent(0)));
655+
656+
// run the $out stage
657+
await browser.clickVisible(Selectors.RunPipelineButton);
658+
659+
// confirm the write operation
660+
const writeOperationConfirmationModal = browser.$(
661+
Selectors.AggregationWriteOperationConfirmationModal
662+
);
663+
await writeOperationConfirmationModal.waitForDisplayed();
664+
665+
const description = await browser
666+
.$(Selectors.AggregationWriteOperationConfirmationModalDescription)
667+
.getText();
668+
669+
expect(description).to.contain(`test.${VALIDATED_OUT_COLLECTION}`);
670+
671+
await browser.clickVisible(
672+
Selectors.AggregationWriteOperationConfirmButton
673+
);
674+
675+
await writeOperationConfirmationModal.waitForDisplayed({ reverse: true });
676+
677+
const errorElement = browser.$(Selectors.AggregationErrorBanner);
678+
await errorElement.waitForDisplayed();
679+
expect(await errorElement.getText()).to.include(
680+
'Document failed validation'
681+
);
682+
// enter details
683+
const errorDetailsBtn = browser.$(Selectors.AggregationErrorDetailsBtn);
684+
await errorElement.waitForDisplayed();
685+
await errorDetailsBtn.click();
686+
687+
const errorDetailsJson = browser.$(Selectors.ErrorDetailsJson);
688+
await errorDetailsJson.waitForDisplayed();
689+
690+
// exit details
691+
await browser.clickVisible(Selectors.confirmationModalConfirmButton());
692+
await errorElement.waitForDisplayed();
693+
});
694+
});
695+
618696
it('cancels pipeline with $out as the last stage', async function () {
619697
await browser.selectStageOperator(0, '$out');
620698
await browser.setCodemirrorEditorValue(

0 commit comments

Comments
 (0)