From 5de468c96e7a642772be3a5143ffdf4492e02e96 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Thu, 6 Mar 2025 13:20:14 +0100 Subject: [PATCH 01/10] feat: show error details on insert COMPASS-8864 (#6755) --- package-lock.json | 2 + .../components/modals/error-details-modal.tsx | 69 +++ packages/compass-components/src/index.ts | 1 + packages/compass-crud/package.json | 1 + packages/compass-crud/src/actions/index.ts | 2 + .../src/components/document-json-view.tsx | 1 + .../components/document-list-view.spec.tsx | 1 + .../src/components/document-list-view.tsx | 1 + .../src/components/document-list.tsx | 31 +- .../src/components/insert-document-dialog.tsx | 500 +++++++++--------- .../src/components/json-editor.tsx | 1 + .../src/stores/crud-store.spec.ts | 94 +++- .../compass-crud/src/stores/crud-store.ts | 69 ++- .../commands/try-to-insert-document.ts | 34 ++ .../compass-e2e-tests/helpers/selectors.ts | 7 + .../tests/collection-documents-tab.test.ts | 49 +- .../tests/connection.test.ts | 21 +- .../src/components/export-code-view.tsx | 3 + .../src/components/export-modal.tsx | 9 +- 19 files changed, 607 insertions(+), 289 deletions(-) create mode 100644 packages/compass-components/src/components/modals/error-details-modal.tsx create mode 100644 packages/compass-e2e-tests/helpers/commands/try-to-insert-document.ts diff --git a/package-lock.json b/package-lock.json index e2e009f3e9e..ac7f3058113 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44437,6 +44437,7 @@ "hadron-type-checker": "^7.4.6", "jsondiffpatch": "^0.5.0", "lodash": "^4.17.21", + "mongodb": "^6.12.0", "mongodb-data-service": "^22.25.6", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.3.0", @@ -56750,6 +56751,7 @@ "jsondiffpatch": "^0.5.0", "lodash": "^4.17.21", "mocha": "^10.2.0", + "mongodb": "^6.12.0", "mongodb-data-service": "^22.25.6", "mongodb-instance-model": "^12.26.6", "mongodb-ns": "^2.4.2", diff --git a/packages/compass-components/src/components/modals/error-details-modal.tsx b/packages/compass-components/src/components/modals/error-details-modal.tsx new file mode 100644 index 00000000000..c21e5fe05e5 --- /dev/null +++ b/packages/compass-components/src/components/modals/error-details-modal.tsx @@ -0,0 +1,69 @@ +import React, { useMemo } from 'react'; + +import { css, cx } from '@leafygreen-ui/emotion'; + +import { Modal } from './modal'; +import { Button, Code, ModalFooter } from '../leafygreen'; +import { ModalBody } from './modal-body'; +import { ModalHeader } from './modal-header'; + +const leftDirectionFooter = css({ + justifyContent: 'left', +}); + +type ModalProps = React.ComponentProps; +type ErrorDetailsModalProps = Omit & { + title?: string; + subtitle?: string; + details?: Record; + closeAction: 'close' | 'back'; + onClose: () => void; +}; + +function ErrorDetailsModal({ + title = 'Error details', + subtitle, + details, + closeAction, + onClose, + open, + ...modalProps +}: ErrorDetailsModalProps) { + const prettyDetails = useMemo( + () => JSON.stringify(details, undefined, 2), + [details] + ); + + return ( + + + + + {prettyDetails} + + + + + + + ); +} + +export { ErrorDetailsModal }; diff --git a/packages/compass-components/src/index.ts b/packages/compass-components/src/index.ts index a79645f7f9d..d8c05eb97be 100644 --- a/packages/compass-components/src/index.ts +++ b/packages/compass-components/src/index.ts @@ -92,6 +92,7 @@ export { ModalBody } from './components/modals/modal-body'; export { ModalHeader } from './components/modals/modal-header'; export { FormModal } from './components/modals/form-modal'; export { InfoModal } from './components/modals/info-modal'; +export { ErrorDetailsModal } from './components/modals/error-details-modal'; export type { FileInputBackend, diff --git a/packages/compass-crud/package.json b/packages/compass-crud/package.json index 3d6f13a7ec1..130d81c325a 100644 --- a/packages/compass-crud/package.json +++ b/packages/compass-crud/package.json @@ -95,6 +95,7 @@ "jsondiffpatch": "^0.5.0", "lodash": "^4.17.21", "mongodb-data-service": "^22.25.6", + "mongodb": "^6.12.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.3.0", "numeral": "^2.0.6", diff --git a/packages/compass-crud/src/actions/index.ts b/packages/compass-crud/src/actions/index.ts index 5600c4c103c..4eb718b3053 100644 --- a/packages/compass-crud/src/actions/index.ts +++ b/packages/compass-crud/src/actions/index.ts @@ -19,6 +19,8 @@ const configureActions = () => { 'toggleInsertDocumentView', 'toggleInsertDocument', 'openInsertDocumentDialog', + 'openErrorDetailsDialog', + 'closeErrorDetailsDialog', 'openBulkUpdateModal', 'updateBulkUpdatePreview', 'runBulkUpdate', diff --git a/packages/compass-crud/src/components/document-json-view.tsx b/packages/compass-crud/src/components/document-json-view.tsx index 700857879de..0cabaaf236d 100644 --- a/packages/compass-crud/src/components/document-json-view.tsx +++ b/packages/compass-crud/src/components/document-json-view.tsx @@ -33,6 +33,7 @@ export type DocumentJsonViewProps = { | 'removeDocument' | 'replaceDocument' | 'updateDocument' + | 'openErrorDetailsDialog' | 'openInsertDocumentDialog' >; diff --git a/packages/compass-crud/src/components/document-list-view.spec.tsx b/packages/compass-crud/src/components/document-list-view.spec.tsx index 536f48e66c2..bfb28904fb0 100644 --- a/packages/compass-crud/src/components/document-list-view.spec.tsx +++ b/packages/compass-crud/src/components/document-list-view.spec.tsx @@ -21,6 +21,7 @@ describe('', function () { replaceDocument={sinon.spy()} updateDocument={sinon.spy()} openInsertDocumentDialog={sinon.spy()} + openErrorDetailsDialog={sinon.spy()} /> ); diff --git a/packages/compass-crud/src/components/document-list-view.tsx b/packages/compass-crud/src/components/document-list-view.tsx index 938ecc6fd1b..825fe7538aa 100644 --- a/packages/compass-crud/src/components/document-list-view.tsx +++ b/packages/compass-crud/src/components/document-list-view.tsx @@ -97,6 +97,7 @@ class DocumentListView extends React.Component { replaceDocument: PropTypes.func, updateDocument: PropTypes.func, openInsertDocumentDialog: PropTypes.func, + openErrorDetailsDialog: PropTypes.func, copyToClipboard: PropTypes.func, className: PropTypes.string, }; diff --git a/packages/compass-crud/src/components/document-list.tsx b/packages/compass-crud/src/components/document-list.tsx index bc5fe807684..6ab97bcb1d6 100644 --- a/packages/compass-crud/src/components/document-list.tsx +++ b/packages/compass-crud/src/components/document-list.tsx @@ -9,6 +9,7 @@ import { WorkspaceContainer, spacing, withDarkMode, + ErrorDetailsModal, } from '@mongodb-js/compass-components'; import type { InsertDocumentDialogProps } from './insert-document-dialog'; import InsertDocumentDialog from './insert-document-dialog'; @@ -29,10 +30,12 @@ import { DOCUMENTS_STATUS_FETCHING, DOCUMENTS_STATUS_FETCHED_INITIAL, } from '../constants/documents-statuses'; -import { - type CrudStore, - type BSONObject, - type DocumentView, +import type { + CrudStore, + BSONObject, + DocumentView, + ErrorDetailsDialogOptions, + ErrorDetailsDialogState, } from '../stores/crud-store'; import { getToolbarSignal } from '../utils/toolbar-signal'; import BulkDeleteModal from './bulk-delete-modal'; @@ -70,6 +73,8 @@ const loaderContainerStyles = css({ export type DocumentListProps = { store: CrudStore; openInsertDocumentDialog?: (doc: BSONObject, cloned: boolean) => void; + openErrorDetailsDialog: (options: ErrorDetailsDialogOptions) => void; + closeErrorDetailsDialog: () => void; openBulkUpdateModal: () => void; updateBulkUpdatePreview: (updateText: string) => void; runBulkUpdate: () => void; @@ -77,6 +82,7 @@ export type DocumentListProps = { openImportFileDialog?: (origin: 'empty-state' | 'crud-toolbar') => void; docs: Document[]; view: DocumentView; + errorDetails: ErrorDetailsDialogState; insert: Partial & Required< Pick< @@ -84,7 +90,7 @@ export type DocumentListProps = { | 'doc' | 'csfleState' | 'isOpen' - | 'message' + | 'error' | 'mode' | 'jsonDoc' | 'isCommentNeeded' @@ -295,7 +301,10 @@ const DocumentList: React.FunctionComponent = (props) => { resultId, isCollectionScan, isSearchIndexesSupported, + errorDetails, openInsertDocumentDialog, + openErrorDetailsDialog, + closeErrorDetailsDialog, openImportFileDialog, openBulkUpdateModal, docs, @@ -581,8 +590,20 @@ const DocumentList: React.FunctionComponent = (props) => { version={version} ns={ns} updateComment={updateComment} + showErrorDetails={() => + openErrorDetailsDialog({ + details: insert.error.info || {}, + closeAction: 'back', + }) + } {...insert} /> + void; insertMany: () => void; isOpen: boolean; - message: string; + error: WriteError; mode: 'modifying' | 'error'; version: string; updateJsonDoc: (value: string | null) => void; @@ -61,119 +67,184 @@ export type InsertDocumentDialogProps = InsertCSFLEWarningBannerProps & { updateComment: (isCommentNeeded: boolean) => void; logger?: Logger; track?: TrackFunction; + showErrorDetails: () => void; }; -type InsertDocumentDialogState = { - insertInProgress: boolean; +const DocumentOrJsonView: React.FC<{ + jsonView: InsertDocumentDialogProps['jsonView']; + doc: InsertDocumentDialogProps['doc']; + hasManyDocuments: () => boolean; + updateJsonDoc: InsertDocumentDialogProps['updateJsonDoc']; + jsonDoc: InsertDocumentDialogProps['jsonDoc']; + isCommentNeeded: InsertDocumentDialogProps['isCommentNeeded']; + updateComment: InsertDocumentDialogProps['updateComment']; +}> = ({ + jsonView, + doc, + hasManyDocuments, + updateJsonDoc, + jsonDoc, + isCommentNeeded, + updateComment, +}) => { + if (!jsonView) { + if (hasManyDocuments()) { + return ( + + This view is not supported for multiple documents. To specify data + types and use other functionality of this view, please insert + documents one at a time. + + ); + } + + if (!doc) { + return null; + } + + return ; + } + + return ( + + ); }; /** * Component for the insert document dialog. */ -class InsertDocumentDialog extends React.PureComponent< - InsertDocumentDialogProps, - InsertDocumentDialogState -> { - invalidElements: Document['uuid'][]; +const InsertDocumentDialog: React.FC = ({ + isOpen, + jsonView, + jsonDoc, + doc, + isCommentNeeded, + error, + ns, + csfleState, + track, + insertMany, + insertDocument, + toggleInsertDocument, + toggleInsertDocumentView, + updateJsonDoc, + updateComment, + closeInsertDocumentDialog, + showErrorDetails, +}) => { + const [invalidElements, setInvalidElements] = useState( + [] + ); + const [insertInProgress, setInsertInProgress] = useState(false); - /** - * The component constructor. - * - * @param {Object} props - The properties. - */ - constructor(props: InsertDocumentDialogProps) { - super(props); - this.state = { insertInProgress: false }; - this.invalidElements = []; - } + const hasManyDocuments = useCallback(() => { + let parsed: unknown; + try { + parsed = JSON.parse(jsonDoc); + } catch { + return false; + } + return Array.isArray(parsed); + }, [jsonDoc]); /** - * Handle subscriptions to the document. + * Does the document have errors with the bson types? Checks for + * invalidElements in hadron doc if in HadronDocument view, or parsing error + * in JsonView of the modal * - * @param {Object} prevProps - The previous properties. + * Checks for invalidElements in hadron doc if in HadronDocument view, or + * parsing error in JsonView of the modal + * + * @returns {Boolean} If the document has errors. */ - componentDidUpdate( - prevProps: InsertDocumentDialogProps, - state: InsertDocumentDialogState - ) { - if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen) { - this.props.track && - this.props.track( - 'Screen', - { name: 'insert_document_modal' }, - undefined + const hasErrors = useCallback(() => { + if (jsonView) { + try { + JSON.parse(jsonDoc); + return false; + } catch { + return true; + } + } + return invalidElements.length > 0; + }, [invalidElements, jsonDoc, jsonView]); + + const handleInvalid = useCallback( + (el: Element) => { + if (!invalidElements.includes(el.uuid)) { + setInvalidElements((elements) => [...elements, el.uuid]); + } + }, + [invalidElements] + ); + + const handleValid = useCallback( + (el: Element) => { + if (hasErrors()) { + setInvalidElements((invalidElements) => + without(invalidElements, el.uuid) ); + } + }, + [hasErrors, setInvalidElements] + ); + + useEffect(() => { + if (isOpen && track) { + track('Screen', { name: 'insert_document_modal' }, undefined); } + }, [isOpen, track]); - if (this.props.isOpen && !this.hasManyDocuments()) { - if (prevProps.jsonView && !this.props.jsonView) { + const prevJsonView = useRef(jsonView); + useEffect(() => { + const viewHasChanged = prevJsonView.current !== jsonView; + prevJsonView.current = jsonView; + if (isOpen && !hasManyDocuments() && viewHasChanged) { + if (!jsonView) { // When switching to Hadron Document View. // Reset the invalid elements list, which contains the // uuids of each element that has BSON type cast errors. - this.invalidElements = []; + setInvalidElements([]); // Subscribe to the validation errors for BSON types on the document. - this.props.doc.on(Element.Events.Invalid, this.handleInvalid); - this.props.doc.on(Element.Events.Valid, this.handleValid); - } else if (!prevProps.jsonView && this.props.jsonView) { + doc.on(Element.Events.Invalid, handleInvalid); + doc.on(Element.Events.Valid, handleValid); + } else { // When switching to JSON View. // Remove the listeners to the BSON type validation errors in order to clean up properly. - this.props.doc.removeListener( - Element.Events.Invalid, - this.handleInvalid - ); - this.props.doc.removeListener(Element.Events.Valid, this.handleValid); + doc.removeListener(Element.Events.Invalid, handleInvalid); + doc.removeListener(Element.Events.Valid, handleValid); } } + }, [isOpen, jsonView, doc, handleValid, handleInvalid, hasManyDocuments]); - if (state.insertInProgress) { - this.setState({ insertInProgress: false }); + useEffect(() => { + if (insertInProgress) { + setInsertInProgress(false); } - } + }, [insertInProgress]); - componentWillUnount() { - if (!this.hasManyDocuments()) { + useEffect(() => { + if (!hasManyDocuments() && doc) { // When closing the modal. // Remove the listeners to the BSON type validation errors in order to clean up properly. - this.props.doc.removeListener(Element.Events.Invalid, this.handleInvalid); - this.props.doc.removeListener(Element.Events.Valid, this.handleValid); + doc.removeListener(Element.Events.Invalid, handleInvalid); + doc.removeListener(Element.Events.Valid, handleValid); } - } + }); - /** - * Handles an element in the document becoming valid from invalid. - * - * @param {Element} el - Element - */ - handleValid = (el: Element) => { - if (this.hasErrors()) { - pull(this.invalidElements, el.uuid); - this.forceUpdate(); - } - }; - - /** - * Handles a valid element in the document becoming invalid. - * - * @param {Element} el - Element - */ - handleInvalid = (el: Element) => { - if (!this.invalidElements.includes(el.uuid)) { - this.invalidElements.push(el.uuid); - this.forceUpdate(); - } - }; - - /** - * Handle the insert. - */ - handleInsert() { - this.setState({ insertInProgress: true }); - if (this.hasManyDocuments()) { - this.props.insertMany(); + const handleInsert = useCallback(() => { + setInsertInProgress(true); + if (hasManyDocuments()) { + insertMany(); } else { - this.props.insertDocument(); + insertDocument(); } - } + }, [setInsertInProgress, insertMany, insertDocument, hasManyDocuments]); /** * Switches between JSON and Hadron Document views. @@ -183,168 +254,109 @@ class InsertDocumentDialog extends React.PureComponent< * * @param {String} view - which view we are looking at: JSON or LIST. */ - switchInsertDocumentView(view: string) { - if (!this.hasManyDocuments()) { - this.props.toggleInsertDocument(view as 'JSON' | 'List'); - } else { - this.props.toggleInsertDocumentView(view as 'JSON' | 'List'); - } - } - - /** - * Does the document have errors with the bson types? Checks for - * invalidElements in hadron doc if in HadronDocument view, or parsing error - * in JsonView of the modal - * - * Checks for invalidElements in hadron doc if in HadronDocument view, or - * parsing error in JsonView of the modal - * - * @returns {Boolean} If the document has errors. - */ - hasErrors() { - if (this.props.jsonView) { - try { - JSON.parse(this.props.jsonDoc); - return false; - } catch { - return true; + const switchInsertDocumentView = useCallback( + (view: string) => { + if (!hasManyDocuments()) { + toggleInsertDocument(view as 'JSON' | 'List'); + } else { + toggleInsertDocumentView(view as 'JSON' | 'List'); } - } - return this.invalidElements.length > 0; - } + }, + [hasManyDocuments, toggleInsertDocument, toggleInsertDocumentView] + ); - /** - * Check if the json pasted is multiple documents (array). - * - * @returns {bool} If many documents are currently being inserted. - */ - hasManyDocuments() { - let jsonDoc: unknown; - try { - jsonDoc = JSON.parse(this.props.jsonDoc); - } catch { - return false; - } - return Array.isArray(jsonDoc); - } - - /** - * Render the document or json editor. - * - * @returns {React.Component} The component. - */ - renderDocumentOrJsonView() { - if (!this.props.jsonView) { - if (this.hasManyDocuments()) { - return ( - - This view is not supported for multiple documents. To specify data - types and use other functionality of this view, please insert - documents one at a time. - - ); - } - - if (!this.props.doc) { - return; - } - - return ; - } + const currentView = jsonView ? 'JSON' : 'List'; + const variant = insertInProgress ? 'info' : 'danger'; - return ( - - ); + if (hasErrors()) { + error = { message: INSERT_INVALID_MESSAGE }; } - /** - * Render the modal dialog. - * - * @returns {React.Component} The react component. - */ - render() { - const currentView = this.props.jsonView ? 'JSON' : 'List'; - const variant = this.state.insertInProgress ? 'info' : 'danger'; - - let message = this.props.message; - - if (this.hasErrors()) { - message = INSERT_INVALID_MESSAGE; - } - - if (this.state.insertInProgress) { - message = 'Inserting Document'; - } - - return ( - -
- - } - onClick={(evt) => { - // We override the `onClick` functionality to prevent form submission. - // The value changing occurs in the `onChange` in the `SegmentedControl`. - evt.preventDefault(); - }} - > - { - // We override the `onClick` functionality to prevent form submission. - // The value changing occurs in the `onChange` in the `SegmentedControl`. - evt.preventDefault(); - }} - glyph={} - > - -
-
- {this.renderDocumentOrJsonView()} -
- {message && ( - - {message} - - )} - -
- ); + if (insertInProgress) { + error = { message: 'Inserting Document' }; } -} + + return ( + +
+ + } + onClick={(evt) => { + // We override the `onClick` functionality to prevent form submission. + // The value changing occurs in the `onChange` in the `SegmentedControl`. + evt.preventDefault(); + }} + > + { + // We override the `onClick` functionality to prevent form submission. + // The value changing occurs in the `onChange` in the `SegmentedControl`. + evt.preventDefault(); + }} + glyph={} + > + +
+
+ +
+ {error && ( + + {error?.message} + {error?.info && ( + + )} + + )} + +
+ ); +}; export default withLogger(InsertDocumentDialog, 'COMPASS-CRUD-UI'); diff --git a/packages/compass-crud/src/components/json-editor.tsx b/packages/compass-crud/src/components/json-editor.tsx index 3c86cff5c68..b47c4fd6863 100644 --- a/packages/compass-crud/src/components/json-editor.tsx +++ b/packages/compass-crud/src/components/json-editor.tsx @@ -58,6 +58,7 @@ export type JSONEditorProps = { updateDocument?: CrudActions['updateDocument']; copyToClipboard?: CrudActions['copyToClipboard']; openInsertDocumentDialog?: CrudActions['openInsertDocumentDialog']; + openErrorDetailsDialog?: CrudActions['openErrorDetailsDialog']; }; const JSONEditor: React.FunctionComponent = ({ diff --git a/packages/compass-crud/src/stores/crud-store.spec.ts b/packages/compass-crud/src/stores/crud-store.spec.ts index 50eba4ff896..045b3cf9141 100644 --- a/packages/compass-crud/src/stores/crud-store.spec.ts +++ b/packages/compass-crud/src/stores/crud-store.spec.ts @@ -12,6 +12,7 @@ import type { CrudStore, CrudStoreOptions, DocumentsPluginServices, + ErrorDetailsDialogOptions, } from './crud-store'; import { findAndModifyWithFLEFallback, @@ -290,13 +291,13 @@ describe('store', function () { docsPerPage: 25, end: 0, error: null, + errorDetails: { isOpen: false }, insert: { doc: null, isCommentNeeded: true, isOpen: false, jsonDoc: null, jsonView: false, - message: '', csfleState: { state: 'none' }, mode: 'modifying', }, @@ -1125,7 +1126,7 @@ describe('store', function () { expect(state.insert.jsonDoc).to.equal(null); expect(state.insert.isOpen).to.equal(false); expect(state.insert.jsonView).to.equal(false); - expect(state.insert.message).to.equal(''); + expect(state.insert.error).to.equal(undefined); }); store.state.insert.doc = doc; @@ -1153,7 +1154,7 @@ describe('store', function () { expect(state.insert.jsonDoc).to.equal(null); expect(state.insert.isOpen).to.equal(false); expect(state.insert.jsonView).to.equal(false); - expect(state.insert.message).to.equal(''); + expect(state.insert.error).to.equal(undefined); }); void store.insertDocument(); @@ -1181,7 +1182,8 @@ describe('store', function () { expect(state.insert.jsonDoc).to.equal(jsonDoc); expect(state.insert.isOpen).to.equal(true); expect(state.insert.jsonView).to.equal(true); - expect(state.insert.message).to.not.equal(''); + expect(state.insert.error).to.exist; + expect(state.insert.error.message).to.not.be.empty; expect(state.insert.mode).to.equal('error'); }); @@ -1216,7 +1218,8 @@ describe('store', function () { expect(state.insert.jsonDoc).to.equal(jsonDoc); expect(state.insert.isOpen).to.equal(true); expect(state.insert.jsonView).to.equal(true); - expect(state.insert.message).to.not.equal(''); + expect(state.insert.error).to.exist; + expect(state.insert.error.message).to.not.be.empty; }); void store.insertDocument(); @@ -1246,7 +1249,8 @@ describe('store', function () { expect(state.insert.jsonDoc).to.equal(jsonDoc); expect(state.insert.isOpen).to.equal(true); expect(state.insert.jsonView).to.equal(false); - expect(state.insert.message).to.not.equal(''); + expect(state.insert.error).to.exist; + expect(state.insert.error.message).to.not.be.empty; }); store.state.insert.doc = doc; @@ -1255,6 +1259,40 @@ describe('store', function () { await listener; }); }); + + context('when it is a validation error', function () { + const hadronDoc = new HadronDocument({}); + // this should be invalid according to the validation rules + const jsonDoc = '{ "status": "testing" }'; + + beforeEach(function () { + store.state.insert.jsonView = true; + store.state.insert.doc = hadronDoc; + store.state.insert.jsonDoc = jsonDoc; + }); + + afterEach(async function () { + await dataService.deleteMany('compass-crud.test', {}); + }); + + it('does not insert the document', async function () { + const listener = waitForState(store, (state) => { + expect(state.docs.length).to.equal(0); + expect(state.count).to.equal(0); + expect(state.insert.doc).to.deep.equal(hadronDoc); + expect(state.insert.jsonDoc).to.equal(jsonDoc); + expect(state.insert.isOpen).to.equal(true); + expect(state.insert.jsonView).to.equal(true); + expect(state.insert.error).to.exist; + expect(state.insert.error.message).to.not.be.empty; + expect(state.insert.error.info).not.to.be.empty; + }); + + void store.insertDocument(); + + await listener; + }); + }); }); }); @@ -1287,7 +1325,7 @@ describe('store', function () { expect(state.insert.jsonDoc).to.equal(null); expect(state.insert.isOpen).to.equal(false); expect(state.insert.jsonView).to.equal(false); - expect(state.insert.message).to.equal(''); + expect(state.insert.error).to.equal(undefined); expect(state.status).to.equal('fetching'); expect(state.abortController).to.not.be.null; @@ -1347,7 +1385,7 @@ describe('store', function () { expect(state.insert.jsonDoc).to.equal(null); expect(state.insert.isOpen).to.equal(false); expect(state.insert.jsonView).to.equal(false); - expect(state.insert.message).to.equal(''); + expect(state.insert.error).to.equal(undefined); }); store.state.insert.jsonDoc = docs; @@ -1403,7 +1441,10 @@ describe('store', function () { expect(state.insert.jsonDoc).to.deep.equal(docs); expect(state.insert.isOpen).to.equal(true); expect(state.insert.jsonView).to.equal(true); - expect(state.insert.message).to.equal('Document failed validation'); + expect(state.insert.error).to.not.be.null; + expect(state.insert.error?.message).to.equal( + 'Document failed validation' + ); }); store.state.insert.jsonDoc = docs; @@ -1414,6 +1455,41 @@ describe('store', function () { }); }); + describe('#openErrorDetailsDialog #closeErrorDetailsDialog', function () { + const options: ErrorDetailsDialogOptions = { + details: { abc: 'abc' }, + closeAction: 'close', + }; + let store: CrudStore; + + beforeEach(function () { + const plugin = activatePlugin(); + store = plugin.store; + deactivate = () => plugin.deactivate(); + }); + + it('manages the errorDetails state', async function () { + const openListener = waitForState(store, (state) => { + expect(state.errorDetails).to.deep.equal({ + isOpen: true, + ...options, + }); + }); + + void store.openErrorDetailsDialog(options); + + await openListener; + + const closeListener = waitForState(store, (state) => { + expect(state.errorDetails.isOpen).to.be.false; + }); + + void store.closeErrorDetailsDialog(); + + await closeListener; + }); + }); + describe('#openInsertDocumentDialog', function () { const doc = { _id: 1, name: 'test' }; let store: CrudStore; diff --git a/packages/compass-crud/src/stores/crud-store.ts b/packages/compass-crud/src/stores/crud-store.ts index 13afb48418a..56db6a43287 100644 --- a/packages/compass-crud/src/stores/crud-store.ts +++ b/packages/compass-crud/src/stores/crud-store.ts @@ -2,6 +2,7 @@ import type { Listenable, Store } from 'reflux'; import Reflux from 'reflux'; import toNS from 'mongodb-ns'; import { findIndex, isEmpty, isEqual } from 'lodash'; +import type { MongoServerError } from 'mongodb'; import semver from 'semver'; import StateMixin from '@mongodb-js/reflux-state-mixin'; import type { Element } from 'hadron-document'; @@ -80,6 +81,23 @@ export type EmittedAppRegistryEvents = | 'document-deleted' | 'document-inserted'; +export type ErrorDetailsDialogState = + | { + isOpen: false; + details?: Record; + closeAction?: 'back' | 'close'; + } + | { + isOpen: true; + details: Record; + closeAction?: 'back' | 'close'; + }; + +export type ErrorDetailsDialogOptions = Omit< + Extract, + 'isOpen' +>; + export type CrudActions = { drillDown( doc: Document, @@ -93,6 +111,8 @@ export type CrudActions = { removeDocument(doc: Document): void; replaceDocument(doc: Document): void; openInsertDocumentDialog(doc: BSONObject, cloned: boolean): void; + openErrorDetailsDialog(options: ErrorDetailsDialogOptions): void; + closeErrorDetailsDialog(): void; copyToClipboard(doc: Document): void; //XXX openBulkDeleteDialog(): void; runBulkUpdate(): void; @@ -267,10 +287,15 @@ export type InsertCSFLEState = { encryptedFields?: string[]; }; +export type WriteError = { + message: string; + info?: Record; +}; + type InsertState = { doc: null | Document; jsonDoc: null | string; - message: string; + error?: WriteError; csfleState: InsertCSFLEState; mode: 'modifying' | 'error'; jsonView: boolean; @@ -335,6 +360,7 @@ type CrudState = { bulkDelete: BulkDeleteState; docsPerPage: number; collectionStats: CollectionStats | null; + errorDetails: ErrorDetailsDialogState; }; type CrudStoreActionsOptions = { @@ -441,6 +467,7 @@ class CrudStoreImpl this.instance.topologyDescription.type !== 'Single', docsPerPage: this.getInitialDocsPerPage(), collectionStats: extractCollectionStats(this.collection), + errorDetails: { isOpen: false }, }; } @@ -463,7 +490,6 @@ class CrudStoreImpl return { doc: null, jsonDoc: null, - message: '', csfleState: { state: 'none' }, mode: MODIFYING, jsonView: false, @@ -533,6 +559,13 @@ class CrudStoreImpl void navigator.clipboard.writeText(documentEJSON); } + getWriteError(error: Error): WriteError { + return { + message: error.message, + info: (error as MongoServerError).errInfo, + }; + } + updateMaxDocumentsPerPage(docsPerPage: number) { const previousDocsPerPage = this.state.docsPerPage; localStorage.setItem(MAX_DOCS_PER_PAGE_STORAGE_KEY, String(docsPerPage)); @@ -942,6 +975,24 @@ class CrudStoreImpl }); } + openErrorDetailsDialog(options: ErrorDetailsDialogOptions) { + this.setState({ + errorDetails: { + isOpen: true, + ...options, + }, + }); + } + + closeErrorDetailsDialog() { + this.setState({ + errorDetails: { + ...this.state.errorDetails, + isOpen: false, + }, + }); + } + /** * Open the insert document dialog. * @@ -1005,7 +1056,7 @@ class CrudStoreImpl doc: hadronDoc, jsonDoc: jsonDoc, jsonView: true, - message: '', + error: undefined, csfleState, mode: MODIFYING, isOpen: true, @@ -1268,7 +1319,7 @@ class CrudStoreImpl doc: this.state.insert.doc, jsonView: true, jsonDoc: jsonDoc ?? null, - message: '', + error: undefined, csfleState: this.state.insert.csfleState, mode: MODIFYING, isOpen: true, @@ -1289,7 +1340,7 @@ class CrudStoreImpl doc: hadronDoc, jsonView: false, jsonDoc: this.state.insert.jsonDoc, - message: '', + error: undefined, csfleState: this.state.insert.csfleState, mode: MODIFYING, isOpen: true, @@ -1311,7 +1362,7 @@ class CrudStoreImpl doc: new Document({}), jsonDoc: this.state.insert.jsonDoc, jsonView: jsonView, - message: '', + error: undefined, csfleState: this.state.insert.csfleState, mode: MODIFYING, isOpen: true, @@ -1332,7 +1383,7 @@ class CrudStoreImpl doc: new Document({}), jsonDoc: value, jsonView: true, - message: '', + error: undefined, csfleState: this.state.insert.csfleState, mode: MODIFYING, isOpen: true, @@ -1381,7 +1432,7 @@ class CrudStoreImpl doc: new Document({}), jsonDoc: this.state.insert.jsonDoc, jsonView: true, - message: (error as Error).message, + error: this.getWriteError(error as Error), csfleState: this.state.insert.csfleState, mode: ERROR, isOpen: true, @@ -1443,7 +1494,7 @@ class CrudStoreImpl doc: this.state.insert.doc, jsonDoc: this.state.insert.jsonDoc, jsonView: this.state.insert.jsonView, - message: (error as Error).message, + error: this.getWriteError(error as Error), csfleState: this.state.insert.csfleState, mode: ERROR, isOpen: true, diff --git a/packages/compass-e2e-tests/helpers/commands/try-to-insert-document.ts b/packages/compass-e2e-tests/helpers/commands/try-to-insert-document.ts new file mode 100644 index 00000000000..87334159004 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/commands/try-to-insert-document.ts @@ -0,0 +1,34 @@ +import type { CompassBrowser } from '../compass-browser'; +import * as Selectors from '../selectors'; +import { expect } from 'chai'; + +export async function tryToInsertDocument( + browser: CompassBrowser, + document?: string +) { + // browse to the "Insert to Collection" modal + await browser.clickVisible(Selectors.AddDataButton); + const insertDocumentOption = browser.$(Selectors.InsertDocumentOption); + await insertDocumentOption.waitForDisplayed(); + await browser.clickVisible(Selectors.InsertDocumentOption); + + // wait for the modal to appear + const insertDialog = browser.$(Selectors.InsertDialog); + await insertDialog.waitForDisplayed(); + + if (document) { + // set the text in the editor + await browser.setCodemirrorEditorValue( + Selectors.InsertJSONEditor, + document + ); + } + + // confirm + const insertConfirm = browser.$(Selectors.InsertConfirm); + // this selector is very brittle, so just make sure it works + expect(await insertConfirm.isDisplayed()).to.be.true; + expect(await insertConfirm.getText()).to.equal('Insert'); + await insertConfirm.waitForEnabled(); + await browser.clickVisible(Selectors.InsertConfirm); +} diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index 152a09d70fe..d4149e797ad 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -615,12 +615,19 @@ export const JSONEditDocumentButton = `${JSONDocumentCard} [data-testid="editor- export const ShowMoreFieldsButton = '[data-testid="show-more-fields-button"]'; export const OpenBulkUpdateButton = '[data-testid="crud-update"]'; export const OpenBulkDeleteButton = '[data-testid="crud-bulk-delete"]'; +export const ErrorDetailsJson = '[data-testid="error-details-json"]'; +export const ErrorDetailsBackButton = + '[data-testid="error-details-back-button"]'; +export const ErrorDetailsCloseButton = + '[data-testid="error-details-close-button"]'; // Insert Document modal export const InsertDialog = '[data-testid="insert-document-modal"]'; export const InsertDialogErrorMessage = '[data-testid="insert-document-banner"][data-variant="danger"]'; +export const InsertDialogErrorDetailsBtn = + 'button[data-testid="insert-document-error-details-button"]'; export const InsertJSONEditor = '[data-testid="insert-document-json-editor"]'; export const InsertConfirm = '[data-testid="insert-document-modal"] [data-testid="submit-button"]'; diff --git a/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts b/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts index 0757e4f6633..adc926a1d51 100644 --- a/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts +++ b/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts @@ -17,8 +17,9 @@ import { createNestedDocumentsCollection, createNumbersCollection, } from '../helpers/insert-data'; -import { context } from '../helpers/test-runner-context'; +import { context as testRunnerContext } from '../helpers/test-runner-context'; import type { ChainablePromiseElement } from 'webdriverio'; +import { tryToInsertDocument } from '../helpers/commands/try-to-insert-document'; const { expect } = chai; @@ -553,7 +554,7 @@ FindIterable result = collection.find(filter);`); }); it('can copy a document from the contextual toolbar', async function () { - if (context.disableClipboardUsage) { + if (testRunnerContext.disableClipboardUsage) { this.skip(); } @@ -685,4 +686,48 @@ FindIterable result = collection.find(filter);`); expect(numExpandedHadronElementsPostSwitch).to.equal(14); }); }); + + context('with existing validation rule', function () { + const REQUIRE_PHONE_VALIDATOR = + '{ $jsonSchema: { bsonType: "object", required: [ "phone" ] } }'; + beforeEach(async function () { + await browser.navigateToCollectionTab( + DEFAULT_CONNECTION_NAME_1, + 'test', + 'numbers', + 'Validation' + ); + await browser.clickVisible(Selectors.AddRuleButton); + const element = browser.$(Selectors.ValidationEditor); + await element.waitForDisplayed(); + await browser.setValidation(REQUIRE_PHONE_VALIDATOR); + }); + + it('Shows error info when inserting', async function () { + await browser.navigateToCollectionTab( + DEFAULT_CONNECTION_NAME_1, + 'test', + 'numbers', + 'Documents' + ); + await tryToInsertDocument(browser, '{}'); + + const errorElement = browser.$(Selectors.InsertDialogErrorMessage); + await errorElement.waitForDisplayed(); + expect(await errorElement.getText()).to.include( + 'Document failed validation' + ); + // enter details + const errorDetailsBtn = browser.$(Selectors.InsertDialogErrorDetailsBtn); + await errorElement.waitForDisplayed(); + await errorDetailsBtn.click(); + + const errorDetailsJson = browser.$(Selectors.ErrorDetailsJson); + await errorDetailsJson.waitForDisplayed(); + + // exit details + await browser.clickVisible(Selectors.ErrorDetailsBackButton); + await errorElement.waitForDisplayed(); + }); + }); }); diff --git a/packages/compass-e2e-tests/tests/connection.test.ts b/packages/compass-e2e-tests/tests/connection.test.ts index bf8627129cb..542ab573466 100644 --- a/packages/compass-e2e-tests/tests/connection.test.ts +++ b/packages/compass-e2e-tests/tests/connection.test.ts @@ -24,6 +24,7 @@ import { DEFAULT_CONNECTION_NAMES, isTestingWeb, } from '../helpers/test-runner-context'; +import { tryToInsertDocument } from '../helpers/commands/try-to-insert-document'; async function disconnect(browser: CompassBrowser) { try { @@ -162,25 +163,8 @@ async function assertCannotInsertData( 'Documents' ); - // browse to the "Insert to Collection" modal - await browser.clickVisible(Selectors.AddDataButton); - const insertDocumentOption = browser.$(Selectors.InsertDocumentOption); - await insertDocumentOption.waitForDisplayed(); - await browser.clickVisible(Selectors.InsertDocumentOption); - - // wait for the modal to appear - const insertDialog = browser.$(Selectors.InsertDialog); - await insertDialog.waitForDisplayed(); - // go with the default text which should just be a random new id and therefore valid - - // confirm - const insertConfirm = browser.$(Selectors.InsertConfirm); - // this selector is very brittle, so just make sure it works - expect(await insertConfirm.isDisplayed()).to.be.true; - expect(await insertConfirm.getText()).to.equal('Insert'); - await insertConfirm.waitForEnabled(); - await browser.clickVisible(Selectors.InsertConfirm); + await tryToInsertDocument(browser); // make sure that there's an error and that the insert button is disabled const errorElement = browser.$(Selectors.InsertDialogErrorMessage); @@ -190,6 +174,7 @@ async function assertCannotInsertData( ); // cancel and wait for the modal to go away + const insertDialog = browser.$(Selectors.InsertDialog); await browser.clickVisible(Selectors.InsertCancel); await insertDialog.waitForDisplayed({ reverse: true }); } diff --git a/packages/compass-import-export/src/components/export-code-view.tsx b/packages/compass-import-export/src/components/export-code-view.tsx index 4260ed16c34..0d91d5d9ea2 100644 --- a/packages/compass-import-export/src/components/export-code-view.tsx +++ b/packages/compass-import-export/src/components/export-code-view.tsx @@ -11,6 +11,8 @@ import { queryAsShellJSString, } from '../utils/get-shell-js-string'; +export const codeElementId = 'export-collection-code-preview-wrapper'; + const containerStyles = css({ marginBottom: spacing[3], }); @@ -74,6 +76,7 @@ function ExportCodeView({ diff --git a/packages/compass-import-export/src/components/export-modal.tsx b/packages/compass-import-export/src/components/export-modal.tsx index 82e101ed450..ca45128eed5 100644 --- a/packages/compass-import-export/src/components/export-modal.tsx +++ b/packages/compass-import-export/src/components/export-modal.tsx @@ -30,7 +30,7 @@ import type { ExportStatus, FieldsToExportOption } from '../modules/export'; import type { RootExportState } from '../stores/export-store'; import { SelectFileType } from './select-file-type'; import { ExportSelectFields } from './export-select-fields'; -import { ExportCodeView } from './export-code-view'; +import { codeElementId, ExportCodeView } from './export-code-view'; import type { ExportAggregation, ExportQuery } from '../export/export-types'; import { queryHasProjection } from '../utils/query-has-projection'; import { FieldsToExportOptions } from './export-field-options'; @@ -246,7 +246,12 @@ function ExportModal({ }, [isOpen, resetExportFormState]); return ( - + Date: Fri, 14 Mar 2025 10:57:50 +0100 Subject: [PATCH 02/10] feat: validation errors on import COMPAS-8867 (#6795) --- .../helpers/commands/set-validation.ts | 28 +++- .../compass-e2e-tests/helpers/selectors.ts | 4 + .../tests/collection-documents-tab.test.ts | 16 +-- .../tests/collection-import.test.ts | 133 ++++++++++++++++++ .../tests/collection-validation-tab.test.ts | 4 +- .../import-error-details-modal.spec.tsx | 72 ++++++++++ .../components/import-error-details-modal.tsx | 33 +++++ .../src/components/import-toast.tsx | 24 +++- .../src/import-plugin.tsx | 2 + .../src/import/import-types.ts | 2 +- .../src/import/import-utils.ts | 2 +- .../src/modules/import.ts | 47 ++++++- 12 files changed, 347 insertions(+), 20 deletions(-) create mode 100644 packages/compass-import-export/src/components/import-error-details-modal.spec.tsx create mode 100644 packages/compass-import-export/src/components/import-error-details-modal.tsx diff --git a/packages/compass-e2e-tests/helpers/commands/set-validation.ts b/packages/compass-e2e-tests/helpers/commands/set-validation.ts index da7e0d60265..586312ea9be 100644 --- a/packages/compass-e2e-tests/helpers/commands/set-validation.ts +++ b/packages/compass-e2e-tests/helpers/commands/set-validation.ts @@ -1,7 +1,7 @@ import type { CompassBrowser } from '../compass-browser'; import * as Selectors from '../selectors'; -export async function setValidation( +export async function setValidationWithinValidationTab( browser: CompassBrowser, value: string ): Promise { @@ -40,3 +40,29 @@ export async function setValidation( // replaced await browser.pause(2000); } + +export async function setValidation( + browser: CompassBrowser, + { + connectionName, + database, + collection, + validator, + }: { + connectionName: string; + database: string; + collection: string; + validator: string; + } +): Promise { + await browser.navigateToCollectionTab( + connectionName, + database, + collection, + 'Validation' + ); + await browser.clickVisible(Selectors.AddRuleButton); + 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 d4149e797ad..4db1898db71 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -654,6 +654,10 @@ export const ImportAnalyzeError = export const ImportConfirm = '[data-testid="import-modal"] [data-testid="import-button"]'; export const ImportToast = '[data-testid="toast-import-toast"]'; +export const ImportToastErrorDetailsBtn = + '[data-testid="toast-import-toast"] [data-testid="import-error-details-button"]'; +export const ImportErrorDetailsModal = + '[data-testid="import-error-details-modal"]'; export const ImportToastAbort = '[data-testid="toast-action-stop"]'; export const ImportFieldLabel = '[data-testid="import-modal"] .import-field-label'; diff --git a/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts b/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts index adc926a1d51..1819c645d6d 100644 --- a/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts +++ b/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts @@ -691,16 +691,12 @@ FindIterable result = collection.find(filter);`); const REQUIRE_PHONE_VALIDATOR = '{ $jsonSchema: { bsonType: "object", required: [ "phone" ] } }'; beforeEach(async function () { - await browser.navigateToCollectionTab( - DEFAULT_CONNECTION_NAME_1, - 'test', - 'numbers', - 'Validation' - ); - await browser.clickVisible(Selectors.AddRuleButton); - const element = browser.$(Selectors.ValidationEditor); - await element.waitForDisplayed(); - await browser.setValidation(REQUIRE_PHONE_VALIDATOR); + await browser.setValidation({ + connectionName: DEFAULT_CONNECTION_NAME_1, + database: 'test', + collection: 'numbers', + validator: REQUIRE_PHONE_VALIDATOR, + }); }); it('Shows error info when inserting', async function () { diff --git a/packages/compass-e2e-tests/tests/collection-import.test.ts b/packages/compass-e2e-tests/tests/collection-import.test.ts index e1712edb2cd..28a3c57e623 100644 --- a/packages/compass-e2e-tests/tests/collection-import.test.ts +++ b/packages/compass-e2e-tests/tests/collection-import.test.ts @@ -509,6 +509,139 @@ describe('Collection import', function () { await toastElement.waitForDisplayed({ reverse: true }); }); + context('with validation', function () { + beforeEach(async function () { + const FAILING_VALIDATOR = + '{ $jsonSchema: { bsonType: "object", required: [ "abcdefgh" ] } }'; + await browser.setValidation({ + connectionName: DEFAULT_CONNECTION_NAME_1, + database: 'test', + collection: 'extended-json-file', + validator: FAILING_VALIDATOR, + }); + }); + + afterEach(async function () { + await browser.navigateWithinCurrentCollectionTabs('Validation'); + await browser.setValidationWithinValidationTab('{}'); + }); + + it('with JSON + abort on error checked, it displays a validation error with details', async function () { + const jsonPath = path.resolve( + __dirname, + '..', + 'fixtures', + 'three-documents.json' + ); + + await browser.navigateWithinCurrentCollectionTabs('Documents'); + + // open the import modal + await browser.clickVisible(Selectors.AddDataButton); + const insertDocumentOption = browser.$(Selectors.ImportFileOption); + await insertDocumentOption.waitForDisplayed(); + await browser.clickVisible(Selectors.ImportFileOption); + + // Select the file. + await browser.selectFile(Selectors.ImportFileInput, jsonPath); + // Wait for the modal to appear. + const importModal = browser.$(Selectors.ImportModal); + await importModal.waitForDisplayed(); + + // Click the stop on errors checkbox. + const stopOnErrorsCheckbox = browser.$( + Selectors.ImportStopOnErrorsCheckbox + ); + const stopOnErrorsLabel = stopOnErrorsCheckbox.parentElement(); + await stopOnErrorsLabel.click(); + + // Confirm import. + await browser.clickVisible(Selectors.ImportConfirm); + + // Wait for the modal to go away. + await importModal.waitForDisplayed({ reverse: true }); + + // Wait for the error toast to appear + const toastElement = browser.$(Selectors.ImportToast); + await toastElement.waitForDisplayed(); + const errorText = await toastElement.getText(); + expect(errorText).to.include('Document failed validation'); + + // Visit error details + await browser.clickVisible(Selectors.ImportToastErrorDetailsBtn); + const errorDetailsModal = browser.$(Selectors.ImportErrorDetailsModal); + await errorDetailsModal.waitForDisplayed(); + expect(await errorDetailsModal.getText()).to.include( + 'schemaRulesNotSatisfied' + ); + await browser.clickVisible(Selectors.ErrorDetailsCloseButton); + + // Close the toast + await browser + .$(Selectors.closeToastButton(Selectors.ImportToast)) + .waitForDisplayed(); + await browser.clickVisible( + Selectors.closeToastButton(Selectors.ImportToast) + ); + await toastElement.waitForDisplayed({ reverse: true }); + }); + + it('with CSV + abort on error unchecked, it includes the details in a file', async function () { + const filename = 'array-documents.csv'; + const csvPath = path.resolve(__dirname, '..', 'fixtures', filename); + + await browser.navigateWithinCurrentCollectionTabs('Documents'); + + // open the import modal + await browser.clickVisible(Selectors.AddDataButton); + const insertDocumentOption = browser.$(Selectors.ImportFileOption); + await insertDocumentOption.waitForDisplayed(); + await browser.clickVisible(Selectors.ImportFileOption); + + // Select the file. + await browser.selectFile(Selectors.ImportFileInput, csvPath); + // Wait for the modal to appear. + const importModal = browser.$(Selectors.ImportModal); + await importModal.waitForDisplayed(); + + // Confirm import. + await browser.clickVisible(Selectors.ImportConfirm); + + // Wait for the modal to go away. + await importModal.waitForDisplayed({ reverse: true }); + + // Wait for the error toast to appear + const toastElement = browser.$(Selectors.ImportToast); + await toastElement.waitForDisplayed(); + const errorText = await toastElement.getText(); + console.log({ errorText }); + expect(errorText).to.include('Document failed validation'); + expect(errorText).to.include('VIEW LOG'); + + // Find the log file + const logFilePath = path.resolve( + compass.userDataPath || '', + compass.appName || '', + 'ImportErrorLogs', + `import-${filename}.log` + ); + await expect(fs.stat(logFilePath)).to.not.be.rejected; + + // Check the log file contents for 3 errors. + const logFileContent = await fs.readFile(logFilePath, 'utf-8'); + expect(logFileContent.includes('schemaRulesNotSatisfied:')); + + // Close the toast + await browser + .$(Selectors.closeToastButton(Selectors.ImportToast)) + .waitForDisplayed(); + await browser.clickVisible( + Selectors.closeToastButton(Selectors.ImportToast) + ); + await toastElement.waitForDisplayed({ reverse: true }); + }); + }); + it('supports CSV files', async function () { const csvPath = path.resolve(__dirname, '..', 'fixtures', 'listings.csv'); diff --git a/packages/compass-e2e-tests/tests/collection-validation-tab.test.ts b/packages/compass-e2e-tests/tests/collection-validation-tab.test.ts index 79c5358cc38..ae8fd286eeb 100644 --- a/packages/compass-e2e-tests/tests/collection-validation-tab.test.ts +++ b/packages/compass-e2e-tests/tests/collection-validation-tab.test.ts @@ -51,7 +51,7 @@ describe('Collection validation tab', function () { const element = browser.$(Selectors.ValidationEditor); await element.waitForDisplayed(); - await browser.setValidation(validation); + await browser.setValidationWithinValidationTab(validation); } context('when the schema validation is empty', function () { @@ -145,7 +145,7 @@ describe('Collection validation tab', function () { }); // the automatic indentation and brackets makes multi-line values very fiddly here - await browser.setValidation(PASSING_VALIDATOR); + await browser.setValidationWithinValidationTab(PASSING_VALIDATOR); await browser.clickVisible(Selectors.ValidationLoadSampleDocumentsBtn); // nothing failed, everything passed diff --git a/packages/compass-import-export/src/components/import-error-details-modal.spec.tsx b/packages/compass-import-export/src/components/import-error-details-modal.spec.tsx new file mode 100644 index 00000000000..c8c890bf1be --- /dev/null +++ b/packages/compass-import-export/src/components/import-error-details-modal.spec.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { + render, + screen, + userEvent, + waitFor, +} from '@mongodb-js/testing-library-compass'; +import { expect } from 'chai'; +import { configureStore } from '../stores/import-store'; +import { Provider } from 'react-redux'; + +import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; +import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider'; +import ImportErrorDetailsModal from './import-error-details-modal'; + +function renderModal(importState: any = {}) { + // TODO: mutating state directly doesn't guarantee that we are testing the + // component in a state that can actually be achieved when actions are emitted + // on the store. Refactor this to either test unconnected component, or to + // not mutate state directly for tests + const store = configureStore({ + dataService: {}, + globalAppRegistry: {}, + logger: createNoopLogger(), + track: createNoopTrack(), + connections: { + getConnectionById: () => ({ info: { id: 'TEST' } }), + }, + } as any); + const state = store.getState(); + state.import = { + ...state.import, + ...importState, + }; + const renderResult = render( + + + + ); + return { renderResult, store }; +} + +describe('ImportErrorDetailsModal Component', function () { + context('When import error details are open', function () { + const errorDetails = { details: 'abc' }; + + beforeEach(function () { + renderModal({ + errorDetails: { + isOpen: true, + details: errorDetails, + }, + }); + }); + + it('Should render error details and be closable', async function () { + const codeDetails = await screen.findByTestId('error-details-json'); + expect(codeDetails).to.be.visible; + expect(JSON.parse(codeDetails.textContent || '')).to.deep.equal( + errorDetails + ); + + const closeBtn = await screen.findByRole('button', { name: 'Close' }); + expect(closeBtn).to.be.visible; + + userEvent.click(closeBtn); + await waitFor(() => { + expect(screen.queryByTestId('import-error-details-modal')).not.to.exist; + }); + }); + }); +}); diff --git a/packages/compass-import-export/src/components/import-error-details-modal.tsx b/packages/compass-import-export/src/components/import-error-details-modal.tsx new file mode 100644 index 00000000000..c425ac44867 --- /dev/null +++ b/packages/compass-import-export/src/components/import-error-details-modal.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { ErrorDetailsModal } from '@mongodb-js/compass-components'; +import { connect } from 'react-redux'; +import type { RootImportState } from '../stores/import-store'; +import { onErrorDetailsClose } from '../modules/import'; + +const ImportErrorDetailsModal: React.FunctionComponent<{ + isOpen: boolean; + errorDetails?: Record; + onClose: () => void; +}> = ({ isOpen, errorDetails, onClose }) => { + return ( + + ); +}; + +const ConnectedImportErrorDetailsModal = connect( + (state: RootImportState) => ({ + isOpen: state.import.errorDetails.isOpen, + errorDetails: state.import.errorDetails.details, + }), + { + onClose: onErrorDetailsClose, + } +)(ImportErrorDetailsModal); + +export default ConnectedImportErrorDetailsModal; diff --git a/packages/compass-import-export/src/components/import-toast.tsx b/packages/compass-import-export/src/components/import-toast.tsx index 91bf7dc97f2..f95f2ea5c8c 100644 --- a/packages/compass-import-export/src/components/import-toast.tsx +++ b/packages/compass-import-export/src/components/import-toast.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { Body, + closeToast, css, + Link, openToast, ToastBody, } from '@mongodb-js/compass-components'; @@ -221,10 +223,28 @@ export function showCancelledToast({ }); } -export function showFailedToast(err: Error | undefined) { +export function showFailedToast( + err: Error | undefined, + showErrorDetails?: () => void +) { openToast(importToastId, { title: 'Failed to import with the following error:', - description: err?.message, + description: ( + <> + {err?.message}  + {showErrorDetails && ( + { + showErrorDetails(); + closeToast(importToastId); + }} + data-testid="import-error-details-button" + > + View error details + + )} + + ), variant: 'warning', }); } diff --git a/packages/compass-import-export/src/import-plugin.tsx b/packages/compass-import-export/src/import-plugin.tsx index e260279aa4b..3fd4fa31c66 100644 --- a/packages/compass-import-export/src/import-plugin.tsx +++ b/packages/compass-import-export/src/import-plugin.tsx @@ -1,12 +1,14 @@ import React from 'react'; import ImportModal from './components/import-modal'; import ImportInProgressModal from './components/import-in-progress-modal'; +import ImportErrorDetailsModal from './components/import-error-details-modal'; function ImportPlugin() { return ( <> + ); } diff --git a/packages/compass-import-export/src/import/import-types.ts b/packages/compass-import-export/src/import/import-types.ts index 252263298cb..564b9cd53a1 100644 --- a/packages/compass-import-export/src/import/import-types.ts +++ b/packages/compass-import-export/src/import/import-types.ts @@ -28,7 +28,7 @@ export type ErrorJSON = { index?: number; code?: string | number; op?: any; - errorInfo?: Document; + errInfo?: Document; numErrors?: number; }; diff --git a/packages/compass-import-export/src/import/import-utils.ts b/packages/compass-import-export/src/import/import-utils.ts index 4100b94bfaf..5d31b078830 100644 --- a/packages/compass-import-export/src/import/import-utils.ts +++ b/packages/compass-import-export/src/import/import-utils.ts @@ -40,7 +40,7 @@ export function errorToJSON(error: any): ErrorJSON { message: error.message, }; - for (const key of ['index', 'code', 'op', 'errorInfo'] as const) { + for (const key of ['index', 'code', 'op', 'errInfo'] as const) { if (error[key] !== undefined) { obj[key] = error[key]; } diff --git a/packages/compass-import-export/src/modules/import.ts b/packages/compass-import-export/src/modules/import.ts index e1df39a9993..3ebb0f3ab97 100644 --- a/packages/compass-import-export/src/modules/import.ts +++ b/packages/compass-import-export/src/modules/import.ts @@ -47,6 +47,8 @@ export const STARTED = `${PREFIX}/STARTED`; export const CANCELED = `${PREFIX}/CANCELED`; export const FINISHED = `${PREFIX}/FINISHED`; export const FAILED = `${PREFIX}/FAILED`; +export const ERROR_DETAILS_OPENED = `${PREFIX}/ERROR_DETAILS_OPENED`; +export const ERROR_DETAILS_CLOSED = `${PREFIX}/ERROR_DETAILS_CLOSED`; export const FILE_TYPE_SELECTED = `${PREFIX}/FILE_TYPE_SELECTED`; export const FILE_SELECTED = `${PREFIX}/FILE_SELECTED`; export const FILE_SELECT_ERROR = `${PREFIX}/FILE_SELECT_ERROR`; @@ -80,6 +82,16 @@ type FieldFromJSON = { }; type FieldType = FieldFromJSON | FieldFromCSV; +export type ErrorDetailsDialogState = + | { + isOpen: false; + details?: Record; + } + | { + isOpen: true; + details: Record; + }; + type ImportState = { isOpen: boolean; isInProgressMessageOpen: boolean; @@ -87,6 +99,7 @@ type ImportState = { fileType: AcceptedFileType | ''; fileName: string; errorLogFilePath: string; + errorDetails: ErrorDetailsDialogState; fileIsMultilineJSON: boolean; useHeaderLines: boolean; status: ProcessStatus; @@ -122,6 +135,7 @@ export const INITIAL_STATE: ImportState = { firstErrors: [], fileName: '', errorLogFilePath: '', + errorDetails: { isOpen: false }, fileIsMultilineJSON: false, useHeaderLines: true, status: PROCESS_STATUS.UNSPECIFIED, @@ -169,6 +183,8 @@ const onFinished = ({ const onFailed = (error: Error) => ({ type: FAILED, error }); +export const onErrorDetailsClose = () => ({ type: ERROR_DETAILS_CLOSED }); + const onFileSelectError = (error: Error) => ({ type: FILE_SELECT_ERROR, error, @@ -373,9 +389,14 @@ export const startImport = (): ImportThunkAction> => { debug('Error while importing:', err.stack); progressCallback.flush(); - showFailedToast(err); - - dispatch(onFailed(err)); + const errInfo = + err?.writeErrors?.length && err?.writeErrors[0]?.err?.errInfo; + const showErrorDetails: () => void | undefined = + errInfo && + (() => dispatch({ type: ERROR_DETAILS_OPENED, errorDetails: errInfo })); + showFailedToast(err as Error, showErrorDetails); + + dispatch(onFailed(err as Error)); return; } finally { errorLogWriteStream?.close(); @@ -1069,6 +1090,26 @@ export const importReducer: Reducer = ( }; } + if (action.type === ERROR_DETAILS_OPENED) { + return { + ...state, + errorDetails: { + isOpen: true, + details: action.errorDetails, + }, + }; + } + + if (action.type === ERROR_DETAILS_CLOSED) { + return { + ...state, + errorDetails: { + ...state.errorDetails, + isOpen: false, + }, + }; + } + if (action.type === STARTED) { return { ...state, From 22301854645e5731ee74a207fc1b6e7a9787225c Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 19 Mar 2025 14:39:47 +0100 Subject: [PATCH 03/10] feat: error details on update COMPASS-8865 (#6778) --- .../document-edit-actions-footer.tsx | 79 +++++++--- .../components/modals/error-details-modal.tsx | 69 --------- .../src/hooks/use-confirmation.tsx | 26 +++- .../src/hooks/use-error-details.tsx | 38 +++++ packages/compass-components/src/index.ts | 2 +- packages/compass-crud/src/actions/index.ts | 2 - .../src/components/document-json-view.tsx | 1 - .../components/document-list-view.spec.tsx | 1 - .../src/components/document-list-view.tsx | 1 - .../src/components/document-list.tsx | 27 +--- .../src/components/insert-document-dialog.tsx | 10 +- .../src/components/json-editor.tsx | 1 - .../src/stores/crud-store.spec.ts | 36 ----- .../compass-crud/src/stores/crud-store.ts | 39 ----- .../compass-e2e-tests/helpers/selectors.ts | 11 +- .../tests/collection-documents-tab.test.ts | 141 +++++++++++++++++- .../tests/collection-import.test.ts | 11 +- .../tests/collection-validation-tab.test.ts | 2 +- .../import-error-details-modal.spec.tsx | 72 --------- .../components/import-error-details-modal.tsx | 33 ---- .../src/import-plugin.tsx | 2 - .../src/modules/import.ts | 46 ++---- packages/hadron-document/src/document.ts | 5 +- 23 files changed, 290 insertions(+), 365 deletions(-) delete mode 100644 packages/compass-components/src/components/modals/error-details-modal.tsx create mode 100644 packages/compass-components/src/hooks/use-error-details.tsx delete mode 100644 packages/compass-import-export/src/components/import-error-details-modal.spec.tsx delete mode 100644 packages/compass-import-export/src/components/import-error-details-modal.tsx diff --git a/packages/compass-components/src/components/document-list/document-edit-actions-footer.tsx b/packages/compass-components/src/components/document-list/document-edit-actions-footer.tsx index 6468e605f5b..4dffbae7253 100644 --- a/packages/compass-components/src/components/document-list/document-edit-actions-footer.tsx +++ b/packages/compass-components/src/components/document-list/document-edit-actions-footer.tsx @@ -6,6 +6,7 @@ import { css } from '@leafygreen-ui/emotion'; import { palette } from '@leafygreen-ui/palette'; import { spacing } from '@leafygreen-ui/tokens'; import { useDarkMode } from '../../hooks/use-theme'; +import { showErrorDetails } from '../../hooks/use-error-details'; type Status = | 'Initial' @@ -81,13 +82,30 @@ function useHadronDocumentStatus( ? 'Deleting' : 'Initial'; }); - const [errorMessage, setErrorMessage] = useState(null); + const [error, setError] = useState<{ + message: string; + details?: Record; + } | null>(null); const invalidElementsRef = useRef(new Set()); - const updateStatus = useCallback((newStatus: Status, errorMessage = null) => { - setStatus(newStatus); - setErrorMessage(errorMessage); - }, []); + const updateStatus = useCallback( + ( + newStatus: Status, + error: Error | null = null, + errorDetails?: Record + ) => { + setStatus(newStatus); + setError( + error + ? { + message: error?.message, + details: errorDetails, + } + : null + ); + }, + [] + ); useEffect(() => { if (status !== 'Initial') { @@ -128,8 +146,11 @@ function useHadronDocumentStatus( const onUpdateSuccess = () => { updateStatus('UpdateSuccess'); }; - const onUpdateError = (err: string) => { - updateStatus('UpdateError', err); + const onUpdateError = ( + err: Error, + errorDetails?: Record + ) => { + updateStatus('UpdateError', err, errorDetails); }; const onRemoveStart = () => { updateStatus('DeleteStart'); @@ -137,8 +158,11 @@ function useHadronDocumentStatus( const onRemoveSuccess = () => { updateStatus('DeleteSuccess'); }; - const onRemoveError = (err: string) => { - updateStatus('DeleteError', err); + const onRemoveError = ( + err: Error, + errorDetails?: Record + ) => { + updateStatus('DeleteError', err, errorDetails); }; doc.on(Element.Events.Added, onUpdate); @@ -183,30 +207,30 @@ function useHadronDocumentStatus( } }, [status, updateStatus]); - return { status, updateStatus, errorMessage }; + return { status, updateStatus, error }; } const container = css({ display: 'flex', - paddingTop: spacing[2], - paddingRight: spacing[2], - paddingBottom: spacing[2], - paddingLeft: spacing[3], + paddingTop: spacing[200], + paddingRight: spacing[200], + paddingBottom: spacing[200], + paddingLeft: spacing[400], alignItems: 'center', - gap: spacing[2], + gap: spacing[200], borderBottomLeftRadius: 'inherit', borderBottomRightRadius: 'inherit', }); const message = css({ - overflow: 'hidden', - textOverflow: 'ellipsis', + overflow: 'scroll', }); const buttonGroup = css({ display: 'flex', marginLeft: 'auto', - gap: spacing[2], + gap: spacing[200], + flexShrink: 0, }); const button = css({ @@ -275,7 +299,7 @@ const EditActionsFooter: React.FunctionComponent<{ const { status: _status, updateStatus, - errorMessage, + error, } = useHadronDocumentStatus(doc, editing, deleting); const darkMode = useDarkMode(); @@ -303,10 +327,25 @@ const EditActionsFooter: React.FunctionComponent<{ data-status={status} >
- {errorMessage ?? statusMessage} + {error?.message ?? statusMessage}
{!isSuccess(status) && (
+ {error?.details && ( + + )} - - - ); -} - -export { ErrorDetailsModal }; diff --git a/packages/compass-components/src/hooks/use-confirmation.tsx b/packages/compass-components/src/hooks/use-confirmation.tsx index cc94cff7027..1fd30fd1bfa 100644 --- a/packages/compass-components/src/hooks/use-confirmation.tsx +++ b/packages/compass-components/src/hooks/use-confirmation.tsx @@ -2,6 +2,7 @@ import React, { useContext, useEffect, useRef, useState } from 'react'; import { Variant as ConfirmationModalVariant } from '@leafygreen-ui/confirmation-modal'; import ConfirmationModal from '../components/modals/confirmation-modal'; import { css } from '@leafygreen-ui/emotion'; +import type { ButtonProps } from '@leafygreen-ui/button'; export { ConfirmationModalVariant }; @@ -11,6 +12,7 @@ type ConfirmationProperties = Partial< Pick > & { buttonText?: React.ReactNode; + confirmButtonProps?: Omit; hideConfirmButton?: boolean; hideCancelButton?: boolean; description?: React.ReactNode; @@ -100,8 +102,28 @@ export const ConfirmationModalArea: React.FC = ({ children }) => { }); const callbackRef = useRef(); + const listenerRef = + useRef<(event: CustomEvent) => void>(); + const contextValue = React.useMemo( - () => ({ showConfirmation, isMounted: true }), + () => ({ + showConfirmation: (props: ConfirmationProperties) => { + return new Promise((resolve, reject) => { + const event = new CustomEvent( + 'show-confirmation', + { + detail: { + props: { ...props, confirmationId: ++confirmationId }, + resolve, + reject, + }, + } + ); + listenerRef.current?.(event); + }); + }, + isMounted: true, + }), [] ); @@ -127,6 +149,7 @@ export const ConfirmationModalArea: React.FC = ({ children }) => { }; props.signal?.addEventListener('abort', onAbort); }; + listenerRef.current = listener; globalConfirmation.addEventListener('show-confirmation', listener); return () => { globalConfirmation.removeEventListener('show-confirmation', listener); @@ -170,6 +193,7 @@ export const ConfirmationModalArea: React.FC = ({ children }) => { : undefined, children: confirmationProps.buttonText ?? 'Confirm', onClick: handleConfirm, + ...confirmationProps.confirmButtonProps, }} cancelButtonProps={{ className: confirmationProps.hideCancelButton diff --git a/packages/compass-components/src/hooks/use-error-details.tsx b/packages/compass-components/src/hooks/use-error-details.tsx new file mode 100644 index 00000000000..99302afa1c2 --- /dev/null +++ b/packages/compass-components/src/hooks/use-error-details.tsx @@ -0,0 +1,38 @@ +import { + type showConfirmation as originalShowConfirmation, + showConfirmation, +} from './use-confirmation'; +import { Code } from '../components/leafygreen'; +import React from 'react'; +import { ButtonVariant } from '..'; + +const getShowErrorDetails = ( + showConfirmation: typeof originalShowConfirmation +) => { + return ({ + details, + closeAction, + }: { + details: Record; + closeAction: 'back' | 'close'; + }) => + void showConfirmation({ + title: 'Error details', + description: ( + + {JSON.stringify(details, undefined, 2)} + + ), + hideCancelButton: true, + buttonText: closeAction.replace(/\b\w/g, (c) => c.toUpperCase()), + confirmButtonProps: { + variant: ButtonVariant.Default, + }, + }); +}; + +export const showErrorDetails = getShowErrorDetails(showConfirmation); diff --git a/packages/compass-components/src/index.ts b/packages/compass-components/src/index.ts index d8c05eb97be..2b47da41542 100644 --- a/packages/compass-components/src/index.ts +++ b/packages/compass-components/src/index.ts @@ -92,7 +92,6 @@ export { ModalBody } from './components/modals/modal-body'; export { ModalHeader } from './components/modals/modal-header'; export { FormModal } from './components/modals/form-modal'; export { InfoModal } from './components/modals/info-modal'; -export { ErrorDetailsModal } from './components/modals/error-details-modal'; export type { FileInputBackend, @@ -183,6 +182,7 @@ export { ConfirmationModalArea, showConfirmation, } from './hooks/use-confirmation'; +export { showErrorDetails } from './hooks/use-error-details'; export { useHotkeys, formatHotkey, diff --git a/packages/compass-crud/src/actions/index.ts b/packages/compass-crud/src/actions/index.ts index 4eb718b3053..5600c4c103c 100644 --- a/packages/compass-crud/src/actions/index.ts +++ b/packages/compass-crud/src/actions/index.ts @@ -19,8 +19,6 @@ const configureActions = () => { 'toggleInsertDocumentView', 'toggleInsertDocument', 'openInsertDocumentDialog', - 'openErrorDetailsDialog', - 'closeErrorDetailsDialog', 'openBulkUpdateModal', 'updateBulkUpdatePreview', 'runBulkUpdate', diff --git a/packages/compass-crud/src/components/document-json-view.tsx b/packages/compass-crud/src/components/document-json-view.tsx index 0cabaaf236d..700857879de 100644 --- a/packages/compass-crud/src/components/document-json-view.tsx +++ b/packages/compass-crud/src/components/document-json-view.tsx @@ -33,7 +33,6 @@ export type DocumentJsonViewProps = { | 'removeDocument' | 'replaceDocument' | 'updateDocument' - | 'openErrorDetailsDialog' | 'openInsertDocumentDialog' >; diff --git a/packages/compass-crud/src/components/document-list-view.spec.tsx b/packages/compass-crud/src/components/document-list-view.spec.tsx index bfb28904fb0..536f48e66c2 100644 --- a/packages/compass-crud/src/components/document-list-view.spec.tsx +++ b/packages/compass-crud/src/components/document-list-view.spec.tsx @@ -21,7 +21,6 @@ describe('', function () { replaceDocument={sinon.spy()} updateDocument={sinon.spy()} openInsertDocumentDialog={sinon.spy()} - openErrorDetailsDialog={sinon.spy()} /> ); diff --git a/packages/compass-crud/src/components/document-list-view.tsx b/packages/compass-crud/src/components/document-list-view.tsx index 825fe7538aa..938ecc6fd1b 100644 --- a/packages/compass-crud/src/components/document-list-view.tsx +++ b/packages/compass-crud/src/components/document-list-view.tsx @@ -97,7 +97,6 @@ class DocumentListView extends React.Component { replaceDocument: PropTypes.func, updateDocument: PropTypes.func, openInsertDocumentDialog: PropTypes.func, - openErrorDetailsDialog: PropTypes.func, copyToClipboard: PropTypes.func, className: PropTypes.string, }; diff --git a/packages/compass-crud/src/components/document-list.tsx b/packages/compass-crud/src/components/document-list.tsx index 6ab97bcb1d6..f9021c0cd1d 100644 --- a/packages/compass-crud/src/components/document-list.tsx +++ b/packages/compass-crud/src/components/document-list.tsx @@ -9,7 +9,6 @@ import { WorkspaceContainer, spacing, withDarkMode, - ErrorDetailsModal, } from '@mongodb-js/compass-components'; import type { InsertDocumentDialogProps } from './insert-document-dialog'; import InsertDocumentDialog from './insert-document-dialog'; @@ -30,13 +29,7 @@ import { DOCUMENTS_STATUS_FETCHING, DOCUMENTS_STATUS_FETCHED_INITIAL, } from '../constants/documents-statuses'; -import type { - CrudStore, - BSONObject, - DocumentView, - ErrorDetailsDialogOptions, - ErrorDetailsDialogState, -} from '../stores/crud-store'; +import type { CrudStore, BSONObject, DocumentView } from '../stores/crud-store'; import { getToolbarSignal } from '../utils/toolbar-signal'; import BulkDeleteModal from './bulk-delete-modal'; import { useTabState } from '@mongodb-js/compass-workspaces/provider'; @@ -73,8 +66,6 @@ const loaderContainerStyles = css({ export type DocumentListProps = { store: CrudStore; openInsertDocumentDialog?: (doc: BSONObject, cloned: boolean) => void; - openErrorDetailsDialog: (options: ErrorDetailsDialogOptions) => void; - closeErrorDetailsDialog: () => void; openBulkUpdateModal: () => void; updateBulkUpdatePreview: (updateText: string) => void; runBulkUpdate: () => void; @@ -82,7 +73,6 @@ export type DocumentListProps = { openImportFileDialog?: (origin: 'empty-state' | 'crud-toolbar') => void; docs: Document[]; view: DocumentView; - errorDetails: ErrorDetailsDialogState; insert: Partial & Required< Pick< @@ -301,10 +291,7 @@ const DocumentList: React.FunctionComponent = (props) => { resultId, isCollectionScan, isSearchIndexesSupported, - errorDetails, openInsertDocumentDialog, - openErrorDetailsDialog, - closeErrorDetailsDialog, openImportFileDialog, openBulkUpdateModal, docs, @@ -590,20 +577,8 @@ const DocumentList: React.FunctionComponent = (props) => { version={version} ns={ns} updateComment={updateComment} - showErrorDetails={() => - openErrorDetailsDialog({ - details: insert.error.info || {}, - closeAction: 'back', - }) - } {...insert} /> - void; logger?: Logger; track?: TrackFunction; - showErrorDetails: () => void; }; const DocumentOrJsonView: React.FC<{ @@ -135,7 +135,6 @@ const InsertDocumentDialog: React.FC = ({ updateJsonDoc, updateComment, closeInsertDocumentDialog, - showErrorDetails, }) => { const [invalidElements, setInvalidElements] = useState( [] @@ -346,7 +345,12 @@ const InsertDocumentDialog: React.FC = ({