Skip to content

Commit 4bd3129

Browse files
chore(compass-sidebar, compass-connections-import-export): adds an entrypoint in the multiple connections sidebar for import and export of saved connections COMPASS-7981 (#5950)
1 parent 6afa340 commit 4bd3129

File tree

20 files changed

+599
-164
lines changed

20 files changed

+599
-164
lines changed

package-lock.json

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

packages/compass-connection-import-export/src/components/export-modal.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function ExportConnectionsModal({
3030
trackingProps,
3131
}: {
3232
open: boolean;
33-
setOpen: (newOpen: boolean) => void;
33+
setOpen: (newOpen: boolean, trackingProps?: Record<string, unknown>) => void;
3434
afterExport?: () => void;
3535
trackingProps?: Record<string, unknown>;
3636
}): React.ReactElement {
@@ -51,7 +51,15 @@ export function ExportConnectionsModal({
5151
[afterExport, openToast, setOpen]
5252
);
5353

54-
useOpenModalThroughIpc(open, setOpen, 'compass:open-export-connections');
54+
const openModalThroughIpc = useCallback(() => {
55+
setOpen(true, { context: 'menuBar' });
56+
}, [setOpen]);
57+
58+
useOpenModalThroughIpc(
59+
open,
60+
openModalThroughIpc,
61+
'compass:open-export-connections'
62+
);
5563

5664
const protectConnectionStrings = !!usePreference('protectConnectionStrings');
5765
const {

packages/compass-connection-import-export/src/components/import-modal.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { SelectTable } from './select-table';
1414
import type { ImportExportResult } from '../hooks/common';
1515
import { useOpenModalThroughIpc } from '../hooks/common';
1616
import { useImportConnections } from '../hooks/use-import';
17+
import { usePreference } from 'compass-preferences-model/provider';
1718

1819
const TOAST_TIMEOUT_MS = 5000;
1920

@@ -34,9 +35,12 @@ export function ImportConnectionsModal({
3435
trackingProps,
3536
}: {
3637
open: boolean;
37-
setOpen: (newOpen: boolean) => void;
38+
setOpen: (newOpen: boolean, trackingProps?: Record<string, unknown>) => void;
3839
trackingProps?: Record<string, unknown>;
3940
}): React.ReactElement {
41+
const multipleConnectionsEnabled = usePreference(
42+
'enableNewMultipleConnectionSystem'
43+
);
4044
const { openToast } = useToast('compass-connection-import-export');
4145
const finish = useCallback(
4246
(result: ImportExportResult) => {
@@ -53,7 +57,15 @@ export function ImportConnectionsModal({
5357
[openToast, setOpen]
5458
);
5559

56-
useOpenModalThroughIpc(open, setOpen, 'compass:open-import-connections');
60+
const openModalThroughIpc = useCallback(() => {
61+
setOpen(true, { context: 'menuBar' });
62+
}, [setOpen]);
63+
64+
useOpenModalThroughIpc(
65+
open,
66+
openModalThroughIpc,
67+
'compass:open-import-connections'
68+
);
5769

5870
const {
5971
onSubmit,
@@ -82,21 +94,22 @@ export function ImportConnectionsModal({
8294
displayName: (
8395
<>
8496
{conn.name}
85-
{conn.isExistingFavorite && (
97+
{conn.isExistingConnection && (
8698
<Badge
8799
className={existingFavoriteBadgeStyles}
88100
variant={conn.selected ? 'yellow' : 'lightgray'}
89101
data-testid={`existing-favorite-badge-${conn.id}`}
90102
>
91-
Existing Favorite
103+
Existing{' '}
104+
{multipleConnectionsEnabled ? 'Connection' : 'Favorite'}
92105
</Badge>
93106
)}
94107
</>
95108
),
96109
})),
97-
connectionList.some((conn) => conn.isExistingFavorite && conn.selected),
110+
connectionList.some((conn) => conn.isExistingConnection && conn.selected),
98111
];
99-
}, [connectionList]);
112+
}, [connectionList, multipleConnectionsEnabled]);
100113

101114
return (
102115
<FormModal
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import React, { useCallback, useContext, useRef, useState } from 'react';
2+
import { ImportConnectionsModal } from './components/import-modal';
3+
import { ExportConnectionsModal } from './components/export-modal';
4+
5+
type ConnectionImportExportService = {
6+
getHandlers(): {
7+
openConnectionImportModal(trackingProps?: Record<string, unknown>): void;
8+
openConnectionExportModal(trackingProps?: Record<string, unknown>): void;
9+
};
10+
};
11+
12+
const ConnectionImportExportServiceContext =
13+
React.createContext<ConnectionImportExportService | null>(null);
14+
15+
export const ConnectionImportExportProvider: React.FC = ({ children }) => {
16+
const [importModalState, setImportModalState] = useState<{
17+
opened: boolean;
18+
// trackingProps are passed to deserializeConnections implementation in
19+
// compass-main-storage. Known props passed are:
20+
// { context: 'CLI' | 'connectionsList' | 'menuBar' }
21+
trackingProps?: Record<string, unknown>;
22+
}>({
23+
opened: false,
24+
});
25+
const [exportModalState, setExportModalState] = useState<{
26+
opened: boolean;
27+
// trackingProps are passed to serializeConnections implementation in
28+
// compass-main-storage. Known props passed are:
29+
// { context: 'CLI' | 'connectionsList' | 'menuBar' }
30+
trackingProps?: Record<string, unknown>;
31+
}>({
32+
opened: false,
33+
});
34+
35+
const setImportModalOpen = useCallback(
36+
(isOpened: boolean, trackingProps?: Record<string, unknown>) => {
37+
setImportModalState({
38+
opened: isOpened,
39+
trackingProps,
40+
});
41+
},
42+
[]
43+
);
44+
45+
const openConnectionImportModal = useCallback(
46+
(trackingProps?: Record<string, unknown>) => {
47+
setImportModalState({
48+
opened: true,
49+
trackingProps,
50+
});
51+
},
52+
[]
53+
);
54+
55+
const setExportModalOpen = useCallback(
56+
(isOpened: boolean, trackingProps?: Record<string, unknown>) => {
57+
setExportModalState({
58+
opened: isOpened,
59+
trackingProps,
60+
});
61+
},
62+
[]
63+
);
64+
65+
const openConnectionExportModal = useCallback(
66+
(trackingProps?: Record<string, unknown>) => {
67+
setExportModalState({
68+
opened: true,
69+
trackingProps,
70+
});
71+
},
72+
[]
73+
);
74+
75+
const connectionImportExportServiceRef =
76+
useRef<ConnectionImportExportService>({
77+
getHandlers() {
78+
return {
79+
openConnectionImportModal,
80+
openConnectionExportModal,
81+
};
82+
},
83+
});
84+
85+
return (
86+
<ConnectionImportExportServiceContext.Provider
87+
value={connectionImportExportServiceRef.current}
88+
>
89+
{children}
90+
<ImportConnectionsModal
91+
open={importModalState.opened}
92+
setOpen={setImportModalOpen}
93+
trackingProps={importModalState.trackingProps}
94+
/>
95+
<ExportConnectionsModal
96+
open={exportModalState.opened}
97+
setOpen={setExportModalOpen}
98+
trackingProps={exportModalState.trackingProps}
99+
/>
100+
</ConnectionImportExportServiceContext.Provider>
101+
);
102+
};
103+
104+
export type ConnectionImportExportAction =
105+
| 'import-saved-connections'
106+
| 'export-saved-connections';
107+
108+
export const useOpenConnectionImportExportModal = (
109+
trackingProps?: Record<string, unknown>
110+
): {
111+
supportsConnectionImportExport: boolean;
112+
openConnectionImportExportModal: (
113+
action: ConnectionImportExportAction
114+
) => void;
115+
} => {
116+
const service = useContext(ConnectionImportExportServiceContext);
117+
if (!service) {
118+
return {
119+
supportsConnectionImportExport: false,
120+
openConnectionImportExportModal() {
121+
// noop
122+
},
123+
};
124+
}
125+
126+
return {
127+
supportsConnectionImportExport: true,
128+
openConnectionImportExportModal(action: ConnectionImportExportAction) {
129+
if (action === 'import-saved-connections') {
130+
service.getHandlers().openConnectionImportModal(trackingProps);
131+
} else if (action === 'export-saved-connections') {
132+
service.getHandlers().openConnectionExportModal(trackingProps);
133+
} else {
134+
throw new Error(`Unidentified action ${action} passed to handler`);
135+
}
136+
},
137+
};
138+
};

packages/compass-connection-import-export/src/hooks/common.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,12 @@ describe('common utilities', function () {
6262

6363
const { result } = renderHook(() => {
6464
const [open, setOpen] = useState(false);
65-
useOpenModalThroughIpc(open, setOpen, event, fakeIpc as any);
65+
useOpenModalThroughIpc(
66+
open,
67+
() => setOpen(true),
68+
event,
69+
fakeIpc as any
70+
);
6671
return { open, setOpen };
6772
});
6873

packages/compass-connection-import-export/src/hooks/common.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function useImportExportConnectionsCommon<S>(
5858

5959
export function useOpenModalThroughIpc(
6060
open: boolean,
61-
setOpen: (newValue: boolean) => void,
61+
openModal: () => void,
6262
ipcEvent: string,
6363
ipcForTesting: HadronIpcRenderer | undefined = undefined
6464
): void {
@@ -67,12 +67,12 @@ export function useOpenModalThroughIpc(
6767
useEffect(() => {
6868
if (ipc?.on && !open) {
6969
const listener = () => {
70-
setOpen(true);
70+
openModal();
7171
};
7272
ipc.on(ipcEvent, listener);
7373
return () => {
7474
ipc.off(ipcEvent, listener);
7575
};
7676
}
77-
}, [open, setOpen]);
77+
}, [open, openModal]);
7878
}

packages/compass-connection-import-export/src/hooks/use-export.spec.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,4 +271,84 @@ describe('useExportConnections', function () {
271271

272272
expect(result.current.exportConnections.state.error).to.equal('');
273273
});
274+
275+
context('when multiple connections is enabled', function () {
276+
beforeEach(async function () {
277+
const preferences = await createSandboxFromDefaultPreferences();
278+
await preferences.savePreferences({
279+
enableNewMultipleConnectionSystem: true,
280+
});
281+
const wrapper: React.FC = ({ children }) =>
282+
createElement(PreferencesProvider, {
283+
value: preferences,
284+
children: createElement(ConnectionStorageProvider, {
285+
value: connectionStorage,
286+
children,
287+
}),
288+
});
289+
renderHookResult = renderHook(
290+
(props: Partial<UseExportConnectionsProps> = {}) => {
291+
return {
292+
connectionRepository: useConnectionRepository(),
293+
exportConnections: useExportConnections({
294+
...defaultProps,
295+
...props,
296+
}),
297+
};
298+
},
299+
{ wrapper }
300+
);
301+
({ result, rerender } = renderHookResult);
302+
});
303+
304+
it('includes also the non-favorites connections in the export list', async function () {
305+
expect(
306+
result.current.exportConnections.state.connectionList
307+
).to.deep.equal([]);
308+
309+
// expecting to include the non-favorite connections as well
310+
await act(async () => {
311+
await result.current.connectionRepository.saveConnection({
312+
id: 'id1',
313+
connectionOptions: { connectionString: 'mongodb://localhost:2020' },
314+
favorite: {
315+
name: 'name1',
316+
},
317+
savedConnectionType: 'recent',
318+
});
319+
});
320+
321+
rerender({});
322+
expect(
323+
result.current.exportConnections.state.connectionList
324+
).to.deep.equal([{ id: 'id1', name: 'name1', selected: true }]);
325+
326+
act(() => {
327+
result.current.exportConnections.onChangeConnectionList([
328+
{ id: 'id1', name: 'name1', selected: false },
329+
]);
330+
});
331+
expect(
332+
result.current.exportConnections.state.connectionList
333+
).to.deep.equal([{ id: 'id1', name: 'name1', selected: false }]);
334+
335+
await act(async () => {
336+
await result.current.connectionRepository.saveConnection({
337+
id: 'id2',
338+
connectionOptions: { connectionString: 'mongodb://localhost:2020' },
339+
favorite: {
340+
name: 'name2',
341+
},
342+
savedConnectionType: 'recent',
343+
});
344+
});
345+
346+
expect(
347+
result.current.exportConnections.state.connectionList
348+
).to.deep.equal([
349+
{ id: 'id1', name: 'name1', selected: false },
350+
{ id: 'id2', name: 'name2', selected: true },
351+
]);
352+
});
353+
});
274354
});

0 commit comments

Comments
 (0)