Skip to content

Commit 18e86cc

Browse files
authored
feat(workspaces, databases, collections, collection): allow to open shell with initial state; add shell entrypoints to tab headers (#5999)
1 parent 5f9a88f commit 18e86cc

File tree

11 files changed

+247
-40
lines changed

11 files changed

+247
-40
lines changed

package-lock.json

Lines changed: 8 additions & 6 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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,11 @@
5151
"@mongodb-js/compass-app-stores": "^7.21.0",
5252
"@mongodb-js/compass-components": "^1.27.0",
5353
"@mongodb-js/compass-connections": "^1.35.0",
54-
"@mongodb-js/connection-info": "^0.5.0",
5554
"@mongodb-js/compass-logging": "^1.4.0",
5655
"@mongodb-js/compass-telemetry": "^1.1.0",
5756
"@mongodb-js/compass-workspaces": "^0.16.0",
57+
"@mongodb-js/connection-info": "^0.5.0",
58+
"@mongodb-js/mongodb-constants": "^0.10.2",
5859
"compass-preferences-model": "^2.24.0",
5960
"hadron-app-registry": "^9.2.0",
6061
"mongodb-collection-model": "^5.22.0",

packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import {
88
import { useConnectionInfo } from '@mongodb-js/compass-connections/provider';
99
import { useOpenWorkspace } from '@mongodb-js/compass-workspaces/provider';
1010
import React from 'react';
11-
import { usePreference } from 'compass-preferences-model/provider';
11+
import { usePreferences } from 'compass-preferences-model/provider';
1212
import toNS from 'mongodb-ns';
13+
import { wrapField } from '@mongodb-js/mongodb-constants';
1314

1415
const collectionHeaderActionsStyles = css({
1516
display: 'flex',
@@ -50,14 +51,41 @@ const CollectionHeaderActions: React.FunctionComponent<
5051
sourcePipeline,
5152
}: CollectionHeaderActionsProps) => {
5253
const { id: connectionId, atlasMetadata } = useConnectionInfo();
53-
const { openCollectionWorkspace, openEditViewWorkspace } = useOpenWorkspace();
54-
const preferencesReadOnly = usePreference('readOnly');
54+
const { openCollectionWorkspace, openEditViewWorkspace, openShellWorkspace } =
55+
useOpenWorkspace();
56+
const {
57+
readOnly: preferencesReadOnly,
58+
enableShell,
59+
enableNewMultipleConnectionSystem,
60+
} = usePreferences([
61+
'readOnly',
62+
'enableShell',
63+
'enableNewMultipleConnectionSystem',
64+
]);
65+
66+
const { database, collection } = toNS(namespace);
67+
68+
const showOpenShellButton = enableShell && enableNewMultipleConnectionSystem;
5569

5670
return (
5771
<div
5872
className={collectionHeaderActionsStyles}
5973
data-testid="collection-header-actions"
6074
>
75+
{showOpenShellButton && (
76+
<Button
77+
size="small"
78+
onClick={() => {
79+
openShellWorkspace(connectionId, {
80+
initialEvaluate: `use ${database}`,
81+
initialInput: `db[${wrapField(collection, true)}].find()`,
82+
});
83+
}}
84+
leftGlyph={<Icon glyph="Shell"></Icon>}
85+
>
86+
Open MongoDB shell
87+
</Button>
88+
)}
6189
{atlasMetadata && (
6290
<Button
6391
data-testid="collection-header-visualize-your-data"

packages/compass-preferences-model/src/react.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import {
55
createElement,
66
createContext,
77
useContext,
8+
useRef,
9+
useCallback,
810
} from 'react';
911
import { type AllPreferences } from './';
1012
import type { PreferencesAccess } from './preferences';
1113
import { ReadOnlyPreferenceAccess } from './read-only-preferences-access';
1214
import { createServiceLocator } from 'hadron-app-registry';
15+
import { pick } from 'lodash';
1316

1417
const PreferencesContext = createContext<PreferencesAccess>(
1518
// Our context starts with our read-only preference access but we expect
@@ -44,11 +47,64 @@ export function usePreference<K extends keyof AllPreferences>(
4447
return value;
4548
}
4649

50+
/**
51+
* A version of usePreference hook that allows to get multiple preferences in a
52+
* single object for brevity
53+
*
54+
* @example
55+
* const {
56+
* enableShell,
57+
* readOnly,
58+
* } = usePreferences(['enableShell', 'readOnly']);
59+
*
60+
* @param keys list of preferences keys to return as an object
61+
*/
62+
export function usePreferences<K extends (keyof AllPreferences)[]>(
63+
keys: K
64+
): Pick<AllPreferences, K[number]> {
65+
const preferences = usePreferencesContext();
66+
const keysRef = useRef(keys);
67+
keysRef.current = keys;
68+
const [values, setValues] = useState(() => {
69+
return pick(preferences.getPreferences(), keys);
70+
});
71+
const updateValue = useCallback((key: keyof AllPreferences, newValue) => {
72+
setValues((values) => {
73+
if (newValue === values[key]) {
74+
return values;
75+
}
76+
return {
77+
...values,
78+
[key]: newValue,
79+
};
80+
});
81+
}, []);
82+
useEffect(() => {
83+
const unsubscribe = keysRef.current.map((key) => {
84+
return preferences.onPreferenceValueChanged(key, (newValue) => {
85+
updateValue(key, newValue);
86+
});
87+
});
88+
return () => {
89+
unsubscribe.forEach((fn) => {
90+
fn();
91+
});
92+
};
93+
}, [
94+
// An easy way to depend on actual array values instead of the array itself
95+
// and avoid extra effect calls if array defined inline
96+
keysRef.current.join(''),
97+
preferences,
98+
]);
99+
return values;
100+
}
101+
47102
type FirstArgument<F> = F extends (...args: [infer A, ...any]) => any
48103
? A
49104
: F extends { new (...args: [infer A, ...any]): any }
50105
? A
51106
: never;
107+
52108
type OptionalOmit<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
53109

54110
/** Use as: const WrappedComponent = withPreferences(Component, ['enableMaps', 'trackUsageStatistics'], React); */

packages/compass-shell/src/components/compass-shell/tab-compass-shell.tsx

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import { connect } from 'react-redux';
2-
import React, { useCallback, useRef, useState } from 'react';
3-
import { useTabState } from '@mongodb-js/compass-workspaces/provider';
2+
import React, { useCallback, useEffect, useRef, useState } from 'react';
3+
import {
4+
useOnTabReplace,
5+
useTabState,
6+
} from '@mongodb-js/compass-workspaces/provider';
47
import {
58
Banner,
69
Link,
710
css,
811
getScrollbarStyles,
912
palette,
13+
rafraf,
1014
spacing,
1115
} from '@mongodb-js/compass-components';
1216
import type { WorkerRuntime } from '@mongosh/node-runtime-worker-thread';
1317
import ShellInfoModal from '../shell-info-modal';
1418
import ShellHeader from '../shell-header/shell-header';
1519
import { usePreference } from 'compass-preferences-model/provider';
16-
import { Shell } from '@mongosh/browser-repl';
20+
import { Shell as _Shell } from '@mongosh/browser-repl';
1721
import type { RootState } from '../../stores/store';
1822
import { selectRuntimeById, saveHistory } from '../../stores/store';
1923

@@ -40,22 +44,70 @@ const compassShellContainerStyles = css({
4044
borderTop: `1px solid ${palette.gray.dark2}`,
4145
});
4246

43-
type ShellProps = React.ComponentProps<typeof Shell>;
47+
type ShellProps = React.ComponentProps<typeof _Shell>;
4448

4549
type ShellRef = Extract<Required<ShellProps>['ref'], { current: any }>;
4650

51+
type ShellType = ShellRef['current'];
52+
4753
type ShellOutputEntry = Required<ShellProps>['initialOutput'][number];
4854

4955
type CompassShellProps = {
5056
runtime: WorkerRuntime | null;
5157
initialHistory: string[] | null;
5258
onHistoryChange: (history: string[]) => void;
59+
initialEvaluate?: string | string[];
60+
initialInput?: string;
5361
};
5462

63+
function useInitialEval(initialEvaluate?: string | string[]) {
64+
const [initialEvalApplied, setInitialEvalApplied] = useTabState(
65+
'initialEvalApplied',
66+
false
67+
);
68+
useEffect(() => {
69+
setInitialEvalApplied(true);
70+
}, [setInitialEvalApplied]);
71+
return initialEvalApplied ? undefined : initialEvaluate;
72+
}
73+
74+
const Shell = React.forwardRef<ShellType, ShellProps>(function Shell(
75+
{ initialEvaluate: _initialEvaluate, ...props },
76+
ref
77+
) {
78+
const shellRef = useRef<ShellType | null>(null);
79+
const initialEvaluate = useInitialEval(_initialEvaluate);
80+
const mergeRef = useCallback(
81+
(shell: ShellType | null) => {
82+
shellRef.current = shell;
83+
if (typeof ref === 'function') {
84+
ref(shell);
85+
} else if (ref) {
86+
ref.current = shell;
87+
}
88+
},
89+
[ref]
90+
);
91+
useEffect(() => {
92+
return rafraf(() => {
93+
shellRef.current?.focusEditor();
94+
});
95+
}, []);
96+
return (
97+
<_Shell
98+
ref={mergeRef}
99+
initialEvaluate={initialEvaluate}
100+
{...props}
101+
></_Shell>
102+
);
103+
});
104+
55105
const CompassShell: React.FC<CompassShellProps> = ({
56106
runtime,
57107
initialHistory,
58108
onHistoryChange,
109+
initialEvaluate,
110+
initialInput,
59111
}) => {
60112
const enableShell = usePreference('enableShell');
61113
const shellRef: ShellRef = useRef(null);
@@ -64,7 +116,16 @@ const CompassShell: React.FC<CompassShellProps> = ({
64116
const [shellOutput, setShellOutput] = useTabState<
65117
readonly ShellOutputEntry[]
66118
>('shellOutput', []);
67-
const [shellInput, setShellInput] = useTabState('shellInput', '');
119+
const [shellInput, setShellInput] = useTabState(
120+
'shellInput',
121+
initialInput ?? ''
122+
);
123+
124+
useOnTabReplace(() => {
125+
// Never allow to replace the shell tab to avoid destroying the runtime
126+
// unnecessarily
127+
return false;
128+
});
68129

69130
const showInfoModal = useCallback(() => {
70131
setInfoModalVisible(true);
@@ -78,7 +139,7 @@ const CompassShell: React.FC<CompassShellProps> = ({
78139
if (shellRef.current && window.getSelection()?.type !== 'Range') {
79140
shellRef.current.focusEditor();
80141
}
81-
}, [shellRef]);
142+
}, []);
82143

83144
const updateShellOutput = useCallback(
84145
(output: readonly ShellOutputEntry[]) => {
@@ -95,6 +156,8 @@ const CompassShell: React.FC<CompassShellProps> = ({
95156
setIsOperationInProgress(false);
96157
}, []);
97158

159+
const canRenderShell = enableShell && initialHistory && runtime;
160+
98161
if (!enableShell) {
99162
return (
100163
<div className={infoBannerContainerStyles}>
@@ -110,7 +173,7 @@ const CompassShell: React.FC<CompassShellProps> = ({
110173
);
111174
}
112175

113-
if (!runtime || !initialHistory) {
176+
if (!canRenderShell) {
114177
return <div className={compassShellStyles} />;
115178
}
116179

@@ -140,6 +203,7 @@ const CompassShell: React.FC<CompassShellProps> = ({
140203
<Shell
141204
ref={shellRef}
142205
runtime={runtime}
206+
initialEvaluate={initialEvaluate}
143207
initialInput={shellInput}
144208
onInputChanged={setShellInput}
145209
initialOutput={shellOutput}
@@ -150,6 +214,8 @@ const CompassShell: React.FC<CompassShellProps> = ({
150214
}}
151215
onOperationStarted={notifyOperationStarted}
152216
onOperationEnd={notifyOperationEnd}
217+
maxOutputLength={1000}
218+
maxHistoryLength={1000}
153219
/>
154220
</div>
155221
</div>

packages/compass-shell/src/plugin.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,19 @@ import { Theme, ThemeProvider } from '@mongodb-js/compass-components';
2323

2424
const SHELL_THEME = { theme: Theme.Dark, enabled: true };
2525

26-
export function ShellPlugin() {
26+
type ShellPluginProps = {
27+
initialEvaluate?: string | string[];
28+
initialInput?: string;
29+
};
30+
31+
export function ShellPlugin(props: ShellPluginProps) {
2732
const multiConnectionsEnabled = usePreference(
2833
'enableNewMultipleConnectionSystem'
2934
);
3035
const ShellComponent = multiConnectionsEnabled ? TabShell : Shell;
3136
return (
3237
<ThemeProvider theme={SHELL_THEME}>
33-
<ShellComponent />
38+
<ShellComponent {...props} />
3439
</ThemeProvider>
3540
);
3641
}
@@ -48,7 +53,7 @@ export type ShellPluginExtraArgs = ShellPluginServices & {
4853
};
4954

5055
export function onActivated(
51-
_initialProps: unknown,
56+
_initialProps: ShellPluginProps,
5257
services: ShellPluginServices,
5358
{ addCleanup, cleanup }: ActivateHelpers
5459
) {

0 commit comments

Comments
 (0)