Skip to content

Commit 8b169d5

Browse files
authored
feat(compass-import-export): add export modal COMPASS-6577 (#4262)
1 parent 00526bf commit 8b169d5

26 files changed

+2117
-101
lines changed

package-lock.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-collection/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"@mongodb-js/compass-components": "^1.7.0",
6262
"@mongodb-js/compass-logging": "^1.1.4",
6363
"bson": "^5.0.1",
64+
"compass-preferences-model": "^2.7.0",
6465
"hadron-app-registry": "^9.0.5",
6566
"hadron-ipc": "^3.1.1",
6667
"react": "^17.0.2"
@@ -69,6 +70,7 @@
6970
"@mongodb-js/compass-components": "^1.7.0",
7071
"@mongodb-js/compass-logging": "^1.1.4",
7172
"bson": "^5.0.1",
73+
"compass-preferences-model": "^2.7.0",
7274
"hadron-app-registry": "^9.0.5",
7375
"hadron-ipc": "^3.1.1"
7476
},

packages/compass-collection/src/stores/index.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { AnyAction } from 'redux';
66
import thunk from 'redux-thunk';
77
import toNS from 'mongodb-ns';
88
import type { DataService } from 'mongodb-data-service';
9+
import preferences from 'compass-preferences-model';
910

1011
import appRegistry, {
1112
appRegistryActivated,
@@ -344,10 +345,23 @@ store.onActivated = (appRegistry: AppRegistry) => {
344345
if (activeTab) {
345346
const crudStore = activeTab.localAppRegistry.getStore('CRUD.Store');
346347
const { query: crudQuery, count } = crudStore.state;
347-
const { filter, limit, skip } = crudQuery;
348+
// TODO(COMPASS-6580): Remove feature flag, use new export.
349+
if (preferences.getPreferences().useNewExport) {
350+
const { filter, project, collation, limit, skip, sort } = crudQuery;
351+
appRegistry.emit('open-export', {
352+
exportFullCollection: true,
353+
namespace: activeTab.namespace,
354+
query: { filter, project, collation, limit, skip, sort },
355+
count,
356+
});
357+
return;
358+
}
359+
360+
const { filter, project, collation, limit, skip, sort } = crudQuery;
348361
appRegistry.emit('open-export', {
362+
exportFullCollection: true,
349363
namespace: activeTab.namespace,
350-
query: { filter, limit, skip },
364+
query: { filter, project, collation, limit, skip, sort },
351365
count,
352366
});
353367
}

packages/compass-crud/src/components/crud-toolbar.tsx

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging';
44
import {
55
Body,
66
Button,
7+
DropdownMenuButton,
78
Icon,
89
IconButton,
910
SegmentedControl,
@@ -15,6 +16,8 @@ import {
1516
WarningSummary,
1617
ErrorSummary,
1718
} from '@mongodb-js/compass-components';
19+
import { usePreference } from 'compass-preferences-model';
20+
import type { MenuAction } from '@mongodb-js/compass-components';
1821

1922
import { AddDataMenu } from './add-data-menu';
2023

@@ -56,6 +59,12 @@ const exportCollectionButtonStyles = css({
5659
whiteSpace: 'nowrap',
5760
});
5861

62+
type ExportDataOption = 'export-query' | 'export-full-collection';
63+
const exportDataActions: MenuAction<ExportDataOption>[] = [
64+
{ action: 'export-query', label: 'Export query results' },
65+
{ action: 'export-full-collection', label: 'Export the full collection' },
66+
];
67+
5968
const OUTDATED_WARNING = `The content is outdated and no longer in sync
6069
with the current query. Press "Find" again to see the results for
6170
the current query.`;
@@ -93,7 +102,7 @@ export type CrudToolbarProps = {
93102
localAppRegistry: AppRegistry;
94103
onApplyClicked: () => void;
95104
onResetClicked: () => void;
96-
openExportFileDialog: () => void;
105+
openExportFileDialog: (exportFullCollection?: boolean) => void;
97106
outdated: boolean;
98107
page: number;
99108
readonly: boolean;
@@ -128,6 +137,8 @@ const CrudToolbar: React.FunctionComponent<CrudToolbarProps> = ({
128137
}) => {
129138
const queryBarRole = localAppRegistry.getRole('Query.QueryBar')![0];
130139

140+
const useNewExport = usePreference('useNewExport', React);
141+
131142
const queryBarRef = useRef(
132143
isExportable
133144
? {
@@ -183,15 +194,32 @@ const CrudToolbar: React.FunctionComponent<CrudToolbarProps> = ({
183194
instanceDescription={instanceDescription}
184195
/>
185196
)}
186-
<Button
187-
className={exportCollectionButtonStyles}
188-
leftGlyph={<Icon glyph="Export" />}
189-
data-testid="export-collection-button"
190-
size="xsmall"
191-
onClick={openExportFileDialog}
192-
>
193-
Export Collection
194-
</Button>
197+
{/* TODO(COMPASS-6580): Remove feature flag, use next export. */}
198+
{useNewExport ? (
199+
<DropdownMenuButton<ExportDataOption>
200+
data-testid="export-collection-button"
201+
actions={exportDataActions}
202+
onAction={(action: ExportDataOption) =>
203+
openExportFileDialog(action === 'export-full-collection')
204+
}
205+
buttonText="Export Data"
206+
buttonProps={{
207+
className: exportCollectionButtonStyles,
208+
size: 'xsmall',
209+
leftGlyph: <Icon glyph="Export" />,
210+
}}
211+
/>
212+
) : (
213+
<Button
214+
className={exportCollectionButtonStyles}
215+
leftGlyph={<Icon glyph="Export" />}
216+
data-testid="export-collection-button"
217+
size="xsmall"
218+
onClick={() => openExportFileDialog(undefined)}
219+
>
220+
Export Collection
221+
</Button>
222+
)}
195223
</div>
196224
<div className={toolbarRightActionStyles}>
197225
<Body data-testid="crud-document-count-display">

packages/compass-crud/src/components/insert-document-dialog.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,6 @@ class InsertDocumentDialog extends React.PureComponent<
329329
<Banner
330330
data-testid="insert-document-banner"
331331
data-variant={variant}
332-
dismissable={false}
333332
variant={variant}
334333
className={bannerStyles}
335334
>

packages/compass-crud/src/stores/crud-store.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import type { Element } from 'hadron-document';
88
import { Document } from 'hadron-document';
99
import HadronDocument from 'hadron-document';
1010
import createLoggerAndTelemetry from '@mongodb-js/compass-logging';
11+
import preferences, {
12+
capMaxTimeMSAtPreferenceLimit,
13+
} from 'compass-preferences-model';
14+
1115
import {
1216
findDocuments,
1317
countDocuments,
@@ -24,7 +28,6 @@ import {
2428
DOCUMENTS_STATUS_FETCHED_CUSTOM,
2529
DOCUMENTS_STATUS_FETCHED_PAGINATION,
2630
} from '../constants/documents-statuses';
27-
import { capMaxTimeMSAtPreferenceLimit } from 'compass-preferences-model';
2831

2932
import type { DataService } from 'mongodb-data-service';
3033
import type {
@@ -986,7 +989,20 @@ class CrudStoreImpl
986989
* Open an export file dialog from compass-import-export-plugin.
987990
* Emits a global app registry event the plugin listens to.
988991
*/
989-
openExportFileDialog() {
992+
openExportFileDialog(exportFullCollection?: boolean) {
993+
// TODO(COMPASS-6580): Remove feature flag, use new export.
994+
if (preferences.getPreferences().useNewExport) {
995+
const { filter, project, collation, limit, skip, sort } =
996+
this.state.query;
997+
998+
this.globalAppRegistry.emit('open-export', {
999+
namespace: this.state.ns,
1000+
query: { filter, project, collation, limit, skip, sort },
1001+
exportFullCollection,
1002+
});
1003+
return;
1004+
}
1005+
9901006
// Only three query fields that export modal will handle
9911007
const { filter, limit, skip } = this.state.query;
9921008
this.globalAppRegistry.emit('open-export', {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { expect } from 'chai';
4+
import { ObjectId } from 'bson';
5+
6+
import { UnconnectedExportCodeView as ExportCodeView } from './export-code-view';
7+
8+
function renderExportCodeView(
9+
props?: Partial<React.ComponentProps<typeof ExportCodeView>>
10+
) {
11+
return render(
12+
<ExportCodeView
13+
ns="test.zebra"
14+
query={{
15+
filter: {
16+
_id: new ObjectId(),
17+
},
18+
}}
19+
fields={{}}
20+
selectedFieldOption="all-fields"
21+
{...props}
22+
/>
23+
);
24+
}
25+
26+
describe('ExportCodeView [Component]', function () {
27+
describe('when rendered', function () {
28+
beforeEach(function () {
29+
renderExportCodeView();
30+
});
31+
32+
it('should render the query code', function () {
33+
expect(screen.getByTestId('export-code-view-code')).to.be.visible;
34+
const codeText = screen.getByTestId('export-code-view-code').textContent;
35+
expect(codeText).to.include('db.getCollection("zebra").find(');
36+
expect(screen.queryByText('Export results from the query below')).to.be
37+
.visible;
38+
});
39+
});
40+
41+
describe('when rendered with selected fields', function () {
42+
beforeEach(function () {
43+
renderExportCodeView({
44+
selectedFieldOption: 'select-fields',
45+
query: {
46+
filter: {},
47+
},
48+
fields: {
49+
name: {
50+
path: ['name'],
51+
selected: true,
52+
},
53+
test: {
54+
path: ['test'],
55+
selected: false,
56+
},
57+
},
58+
});
59+
});
60+
61+
it('should render the projection using the fields', function () {
62+
expect(screen.getByTestId('export-code-view-code')).to.be.visible;
63+
const codeText = screen.getByTestId('export-code-view-code').textContent;
64+
expect(codeText).to.include('name: true');
65+
expect(codeText).to.not.include('test: true');
66+
});
67+
});
68+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React, { useMemo } from 'react';
2+
import { connect } from 'react-redux';
3+
import { Body, Code, css, spacing } from '@mongodb-js/compass-components';
4+
5+
import type { FieldsToExportOption, FieldsToExport } from '../modules/export';
6+
import type { RootExportState } from '../stores/export-store';
7+
import { createProjectionFromSchemaFields } from '../export/gather-fields';
8+
import type { ExportQuery } from '../export/export-types';
9+
import { newGetQueryAsShellJSString } from '../utils/get-shell-js';
10+
11+
const containerStyles = css({
12+
marginBottom: spacing[3],
13+
});
14+
15+
type ExportCodeViewProps = {
16+
ns: string;
17+
query?: ExportQuery;
18+
fields: FieldsToExport;
19+
selectedFieldOption: undefined | FieldsToExportOption;
20+
};
21+
22+
function ExportCodeView({
23+
ns,
24+
query,
25+
fields,
26+
selectedFieldOption,
27+
}: ExportCodeViewProps) {
28+
const code = useMemo(() => {
29+
if (selectedFieldOption === 'select-fields') {
30+
return newGetQueryAsShellJSString({
31+
query: {
32+
...(query ?? {
33+
filter: {},
34+
}),
35+
projection: createProjectionFromSchemaFields(
36+
Object.values(fields)
37+
.filter((field) => field.selected)
38+
.map((field) => field.path)
39+
),
40+
},
41+
ns,
42+
});
43+
}
44+
45+
return newGetQueryAsShellJSString({
46+
query: query ?? {
47+
filter: {},
48+
},
49+
ns,
50+
});
51+
}, [fields, query, ns, selectedFieldOption]);
52+
53+
return (
54+
<div className={containerStyles}>
55+
<Body>Export results from the query below</Body>
56+
<Code data-testid="export-code-view-code" language="javascript" copyable>
57+
{code}
58+
</Code>
59+
</div>
60+
);
61+
}
62+
63+
const ConnectedExportCodeView = connect(
64+
(state: RootExportState) => ({
65+
ns: state.export.namespace,
66+
fields: state.export.fieldsToExport,
67+
query: state.export.query,
68+
selectedFieldOption: state.export.selectedFieldOption,
69+
}),
70+
null
71+
)(ExportCodeView);
72+
73+
export { ExportCodeView as UnconnectedExportCodeView };
74+
export { ConnectedExportCodeView as ExportCodeView };

0 commit comments

Comments
 (0)