diff --git a/package-lock.json b/package-lock.json index 07cae6cc3b3..58de3f6582f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47344,7 +47344,8 @@ "@mongodb-js/compass-components": "^1.22.1", "@mongodb-js/compass-intercom": "^0.2.1", "@mongodb-js/compass-logging": "^1.2.14", - "compass-preferences-model": "^2.18.1" + "compass-preferences-model": "^2.18.1", + "hadron-app-registry": "^9.1.8" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.0.17", @@ -47387,7 +47388,8 @@ "@mongodb-js/compass-intercom": "^0.2.1", "@mongodb-js/compass-logging": "^1.2.14", "compass-preferences-model": "^2.18.1", - "react": "*" + "hadron-app-registry": "^9.1.8", + "react": "^17.0.2" } }, "packages/compass-generative-ai/node_modules/diff": { @@ -47503,7 +47505,6 @@ "@mongodb-js/compass-workspaces": "^0.5.1", "@mongodb-js/connection-info": "^0.1.5", "@mongodb-js/connection-storage": "^0.8.1", - "compass-preferences-model": "^2.18.1", "hadron-app-registry": "^9.1.8", "hadron-ipc": "^3.2.12", "mongodb-data-service": "^22.18.1" @@ -47520,6 +47521,7 @@ "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "chai": "^4.1.2", + "compass-preferences-model": "^2.18.1", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", @@ -47562,7 +47564,6 @@ "@mongodb-js/compass-workspaces": "^0.5.1", "@mongodb-js/connection-info": "^0.1.5", "@mongodb-js/connection-storage": "^0.8.1", - "compass-preferences-model": "^2.18.1", "hadron-app-registry": "^9.1.8", "hadron-ipc": "^3.2.12", "mongodb-data-service": "^22.18.1", @@ -48023,6 +48024,7 @@ "license": "SSPL", "dependencies": { "debug": "^4.3.4", + "hadron-app-registry": "^9.1.8", "is-electron-renderer": "^2.0.1", "mongodb-log-writer": "^1.3.0", "react": "^17.0.2" @@ -48154,6 +48156,7 @@ "@mongodb-js/compass-logging": "^1.2.14", "@mongodb-js/compass-user-data": "^0.1.17", "bson": "^6.3.0", + "hadron-app-registry": "^9.1.8", "hadron-ipc": "^3.2.12", "js-yaml": "^4.1.0", "lodash": "^4.17.21", @@ -49841,6 +49844,7 @@ "@mongodb-js/connection-info": "^0.1.5", "bson": "^6.3.0", "electron": "^28.2.5", + "hadron-app-registry": "^9.1.8", "hadron-ipc": "^3.2.12", "keytar": "^7.9.0", "lodash": "^4.17.21", @@ -49905,6 +49909,7 @@ "@mongodb-js/devtools-connect": "^2.4.2", "@mongodb-js/oidc-plugin": "^0.3.1", "@mongodb-js/ssh-tunnel": "^2.1.13", + "hadron-app-registry": "^9.1.8", "lodash": "^4.17.21", "mongodb-build-info": "^1.7.0", "mongodb-connection-string-url": "^2.6.0", @@ -51604,7 +51609,8 @@ "dependencies": { "@mongodb-js/compass-editor": "^0.21.1", "@mongodb-js/compass-user-data": "^0.1.17", - "bson": "^6.3.0" + "bson": "^6.3.0", + "hadron-app-registry": "^9.1.8" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.0.17", @@ -60198,6 +60204,7 @@ "ejson-shell-parser": "^2.0.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", + "hadron-app-registry": "^9.1.8", "mocha": "^10.2.0", "mongodb": "^6.3.0", "mongodb-runner": "^5.4.4", @@ -60668,6 +60675,7 @@ "debug": "^4.3.4", "depcheck": "^1.4.1", "eslint": "^7.25.0", + "hadron-app-registry": "^9.1.8", "hadron-ipc": "^3.2.12", "is-electron-renderer": "^2.0.1", "mocha": "^10.2.0", @@ -62291,6 +62299,7 @@ "depcheck": "^1.4.1", "electron": "^28.2.5", "eslint": "^7.25.0", + "hadron-app-registry": "^9.1.8", "hadron-ipc": "^3.2.12", "keytar": "^7.9.0", "lodash": "^4.17.21", @@ -63606,6 +63615,7 @@ "depcheck": "^1.4.1", "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", + "hadron-app-registry": "^9.1.8", "mocha": "^10.2.0", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -73287,6 +73297,7 @@ "chai": "^4.3.6", "depcheck": "^1.4.1", "eslint": "^7.25.0", + "hadron-app-registry": "^9.1.8", "hadron-ipc": "^3.2.12", "js-yaml": "^4.1.0", "lodash": "^4.17.21", @@ -85702,6 +85713,7 @@ "chai-as-promised": "^7.1.1", "depcheck": "^1.4.1", "eslint": "^7.25.0", + "hadron-app-registry": "^9.1.8", "kerberos": "^2.1.0", "lodash": "^4.17.21", "mocha": "^10.2.0", diff --git a/packages/atlas-service/src/provider.tsx b/packages/atlas-service/src/provider.tsx index 943572eea99..50642978d12 100644 --- a/packages/atlas-service/src/provider.tsx +++ b/packages/atlas-service/src/provider.tsx @@ -3,22 +3,36 @@ import type { AtlasAuthService } from './atlas-auth-service'; import { AtlasService, type AtlasServiceOptions } from './atlas-service'; import { preferencesLocator } from 'compass-preferences-model/provider'; import { useLoggerAndTelemetry } from '@mongodb-js/compass-logging/provider'; +import { + createServiceLocator, + createServiceProvider, +} from 'hadron-app-registry'; const AtlasAuthServiceContext = createContext(null); + export const AtlasAuthServiceProvider = AtlasAuthServiceContext.Provider; -function useAtlasAuthServiceLocator(): AtlasAuthService { + +function useAtlasAuthServiceContext(): AtlasAuthService { const service = useContext(AtlasAuthServiceContext); if (!service) { throw new Error('No AtlasAuthService available in this context'); } return service; } -export const atlasAuthServiceLocator = useAtlasAuthServiceLocator; + +export const atlasAuthServiceLocator = createServiceLocator( + useAtlasAuthServiceContext, + 'atlasAuthServiceLocator' +); const AtlasServiceContext = createContext(null); + export const AtlasServiceProvider: React.FC<{ options?: AtlasServiceOptions; -}> = ({ options, children }) => { +}> = createServiceProvider(function AtlasServiceProvider({ + options, + children, +}) { const logger = useLoggerAndTelemetry('ATLAS-SERVICE'); const preferences = preferencesLocator(); const authService = atlasAuthServiceLocator(); @@ -32,15 +46,20 @@ export const AtlasServiceProvider: React.FC<{ {children} ); -}; -function useAtlasServiceLocator(): AtlasService { +}); + +function useAtlasServiceContext(): AtlasService { const service = useContext(AtlasServiceContext); if (!service) { throw new Error('No AtlasService available in this context'); } return service; } -export const atlasServiceLocator = useAtlasServiceLocator; + +export const atlasServiceLocator = createServiceLocator( + useAtlasServiceContext, + 'atlasServiceLocator' +); export { AtlasAuthService } from './atlas-auth-service'; export type { AtlasService } from './atlas-service'; diff --git a/packages/compass-app-stores/src/provider.ts b/packages/compass-app-stores/src/provider.ts index 4c3dcb53cde..e85cb252e6c 100644 --- a/packages/compass-app-stores/src/provider.ts +++ b/packages/compass-app-stores/src/provider.ts @@ -1,3 +1,4 @@ +import { createServiceLocator } from 'hadron-app-registry'; import type { MongoDBInstance } from 'mongodb-instance-model'; import { createContext, useContext } from 'react'; @@ -5,12 +6,14 @@ export const InstanceContext = createContext(null); export const MongoDBInstanceProvider = InstanceContext.Provider; -export const mongoDBInstanceLocator = (): MongoDBInstance => { - const instance = useContext(InstanceContext); - if (!instance) { - throw new Error('No MongoDBInstance available in this context'); +export const mongoDBInstanceLocator = createServiceLocator( + function mongoDBInstanceLocator(): MongoDBInstance { + const instance = useContext(InstanceContext); + if (!instance) { + throw new Error('No MongoDBInstance available in this context'); + } + return instance; } - return instance; -}; +); export type { MongoDBInstance }; diff --git a/packages/compass-connections/src/components/connections.tsx b/packages/compass-connections/src/components/connections.tsx index 1782fad7ae9..0fcca7f83b9 100644 --- a/packages/compass-connections/src/components/connections.tsx +++ b/packages/compass-connections/src/components/connections.tsx @@ -15,7 +15,7 @@ import { import { useLoggerAndTelemetry } from '@mongodb-js/compass-logging/provider'; import ConnectionForm from '@mongodb-js/connection-form'; import { type ConnectionInfo } from '@mongodb-js/connection-storage/renderer'; -import { connectionStorageLocator } from '@mongodb-js/connection-storage/provider'; +import { useConnectionStorageContext } from '@mongodb-js/connection-storage/provider'; import type AppRegistry from 'hadron-app-registry'; import type { DataService } from 'mongodb-data-service'; import { connect } from 'mongodb-data-service'; @@ -102,8 +102,10 @@ function Connections({ connectFn?: ConnectFn; }): React.ReactElement { const { log, mongoLogId } = useLoggerAndTelemetry('COMPASS-CONNECTIONS'); - // @TODO: Extract to a prop COMPASS-7397 - const connectionStorage = connectionStorageLocator(); + // TODO(COMPASS-7397): services should not be used directly in render method, + // when this code is refactored to use the hadron plugin interface, storage + // should be handled through the plugin activation lifecycle + const connectionStorage = useConnectionStorageContext(); const { state, diff --git a/packages/compass-connections/src/stores/connections-store.ts b/packages/compass-connections/src/stores/connections-store.ts index 7523b12c3e0..5416656930f 100644 --- a/packages/compass-connections/src/stores/connections-store.ts +++ b/packages/compass-connections/src/stores/connections-store.ts @@ -12,7 +12,7 @@ import { type ConnectionRepository, type PartialConnectionInfo, } from '@mongodb-js/connection-storage/main'; -import { connectionRepositoryLocator } from '@mongodb-js/connection-storage/provider'; +import { useConnectionRepositoryContext } from '@mongodb-js/connection-storage/provider'; import { cloneDeep, merge } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import type { ConnectionAttempt } from 'mongodb-data-service'; @@ -285,7 +285,10 @@ export function useConnections({ removeConnection: (connectionInfo: ConnectionInfo) => void; reloadConnections: () => void; } { - const connectionRepository = connectionRepositoryLocator(); + // TODO(COMPASS-7397): services should not be used directly in render method, + // when this code is refactored to use the hadron plugin interface, storage + // should be handled through the plugin activation lifecycle + const connectionRepository = useConnectionRepositoryContext(); const { openToast } = useToast('compass-connections'); const persistOIDCTokens = usePreference('persistOIDCTokens'); diff --git a/packages/compass-generative-ai/package.json b/packages/compass-generative-ai/package.json index 3587ca30c90..820bf67cc63 100644 --- a/packages/compass-generative-ai/package.json +++ b/packages/compass-generative-ai/package.json @@ -61,19 +61,21 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "peerDependencies": { - "@mongodb-js/compass-components": "^1.22.1", - "compass-preferences-model": "^2.18.1", - "@mongodb-js/compass-logging": "^1.2.14", "@mongodb-js/atlas-service": "^0.15.1", + "@mongodb-js/compass-components": "^1.22.1", "@mongodb-js/compass-intercom": "^0.2.1", - "react": "*" + "@mongodb-js/compass-logging": "^1.2.14", + "compass-preferences-model": "^2.18.1", + "hadron-app-registry": "^9.1.8", + "react": "^17.0.2" }, "dependencies": { + "@mongodb-js/atlas-service": "^0.15.1", "@mongodb-js/compass-components": "^1.22.1", + "@mongodb-js/compass-intercom": "^0.2.1", "@mongodb-js/compass-logging": "^1.2.14", - "@mongodb-js/atlas-service": "^0.15.1", "compass-preferences-model": "^2.18.1", - "@mongodb-js/compass-intercom": "^0.2.1" + "hadron-app-registry": "^9.1.8" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.0.17", diff --git a/packages/compass-generative-ai/src/provider.tsx b/packages/compass-generative-ai/src/provider.tsx index 0754b8d1bc8..1d598cab191 100644 --- a/packages/compass-generative-ai/src/provider.tsx +++ b/packages/compass-generative-ai/src/provider.tsx @@ -6,32 +6,38 @@ import { atlasAuthServiceLocator, atlasServiceLocator, } from '@mongodb-js/atlas-service/provider'; +import { + createServiceLocator, + createServiceProvider, +} from 'hadron-app-registry'; const AtlasAiServiceContext = createContext(null); -export const AtlasAiServiceProvider: React.FC = ({ children }) => { - const logger = useLoggerAndTelemetry('ATLAS-AI-SERVICE'); - const preferences = preferencesLocator(); - const atlasAuthService = atlasAuthServiceLocator(); - const atlasService = atlasServiceLocator(); +export const AtlasAiServiceProvider: React.FC = createServiceProvider( + function AtlasAiServiceProvider({ children }) { + const logger = useLoggerAndTelemetry('ATLAS-AI-SERVICE'); + const preferences = preferencesLocator(); + const atlasAuthService = atlasAuthServiceLocator(); + const atlasService = atlasServiceLocator(); - const aiService = useMemo(() => { - return new AtlasAiService( - atlasService, - atlasAuthService, - preferences, - logger - ); - }, [atlasAuthService, preferences, logger, atlasService]); + const aiService = useMemo(() => { + return new AtlasAiService( + atlasService, + atlasAuthService, + preferences, + logger + ); + }, [atlasAuthService, preferences, logger, atlasService]); - return ( - - {children} - - ); -}; + return ( + + {children} + + ); + } +); -function useAtlasServiceLocator(): AtlasAiService { +function useAtlasAiServiceContext(): AtlasAiService { const service = useContext(AtlasAiServiceContext); if (!service) { throw new Error('No AtlasAiService available in this context'); @@ -39,5 +45,8 @@ function useAtlasServiceLocator(): AtlasAiService { return service; } -export const atlasAiServiceLocator = useAtlasServiceLocator; +export const atlasAiServiceLocator = createServiceLocator( + useAtlasAiServiceContext, + 'atlasAiServiceLocator' +); export { AtlasAiService } from './atlas-ai-service'; diff --git a/packages/compass-home/package.json b/packages/compass-home/package.json index cc028b681ca..8222abd676c 100644 --- a/packages/compass-home/package.json +++ b/packages/compass-home/package.json @@ -55,13 +55,12 @@ "@mongodb-js/compass-schema-validation": "^6.24.1", "@mongodb-js/compass-serverstats": "^16.23.1", "@mongodb-js/compass-settings": "^0.26.1", - "@mongodb-js/connection-storage": "^0.8.1", "@mongodb-js/compass-shell": "^3.23.1", "@mongodb-js/compass-sidebar": "^5.24.1", "@mongodb-js/compass-welcome": "^0.22.1", "@mongodb-js/compass-workspaces": "^0.5.1", "@mongodb-js/connection-info": "^0.1.5", - "compass-preferences-model": "^2.18.1", + "@mongodb-js/connection-storage": "^0.8.1", "hadron-app-registry": "^9.1.8", "hadron-ipc": "^3.2.12", "mongodb-data-service": "^22.18.1" @@ -89,12 +88,11 @@ "@mongodb-js/compass-serverstats": "^16.23.1", "@mongodb-js/compass-settings": "^0.26.1", "@mongodb-js/compass-shell": "^3.23.1", - "@mongodb-js/connection-storage": "^0.8.1", "@mongodb-js/compass-sidebar": "^5.24.1", "@mongodb-js/compass-welcome": "^0.22.1", "@mongodb-js/compass-workspaces": "^0.5.1", "@mongodb-js/connection-info": "^0.1.5", - "compass-preferences-model": "^2.18.1", + "@mongodb-js/connection-storage": "^0.8.1", "hadron-app-registry": "^9.1.8", "hadron-ipc": "^3.2.12", "mongodb-data-service": "^22.18.1", @@ -112,6 +110,7 @@ "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "chai": "^4.1.2", + "compass-preferences-model": "^2.18.1", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", diff --git a/packages/compass-home/src/components/home.tsx b/packages/compass-home/src/components/home.tsx index ecea7d21c0b..15fb5e737bd 100644 --- a/packages/compass-home/src/components/home.tsx +++ b/packages/compass-home/src/components/home.tsx @@ -48,7 +48,6 @@ import { CompassInstanceStorePlugin } from '@mongodb-js/compass-app-stores'; import FieldStorePlugin from '@mongodb-js/compass-field-store'; import { AtlasAuthPlugin } from '@mongodb-js/atlas-service/renderer'; import type { WorkspaceTab } from '@mongodb-js/compass-workspaces'; -import { preferencesLocator } from 'compass-preferences-model/provider'; import { ConnectionStorageContext, ConnectionRepositoryContextProvider, @@ -170,11 +169,13 @@ function notifyMainProcessOfDisconnect() { function Home({ appName, getAutoConnectInfo, + isWelcomeModalOpenByDefault = false, __TEST_MONGODB_DATA_SERVICE_CONNECT_FN, __TEST_CONNECTION_STORAGE, }: { appName: string; getAutoConnectInfo?: () => Promise; + isWelcomeModalOpenByDefault?: boolean; __TEST_MONGODB_DATA_SERVICE_CONNECT_FN?: () => Promise; __TEST_CONNECTION_STORAGE?: typeof ConnectionStorage; }): React.ReactElement | null { @@ -277,18 +278,9 @@ function Home({ remote ? createElectronFileInputBackend(remote) : null ); - const [isWelcomeOpen, setIsWelcomeOpen] = useState(false); - const preferences = preferencesLocator(); - - useLayoutEffect(() => { - // If we haven't showed welcome modal that points users to network opt in - // yet, show the modal and update preferences with default values to reflect - // that - if (preferences.getPreferences().showedNetworkOptIn === false) { - setIsWelcomeOpen(true); - void preferences.ensureDefaultConfigurableUserPreferences(); - } - }, [preferences]); + const [isWelcomeOpen, setIsWelcomeOpen] = useState( + isWelcomeModalOpenByDefault + ); const closeWelcomeModal = useCallback( (showSettings?: boolean) => { diff --git a/packages/compass-logging/package.json b/packages/compass-logging/package.json index 754fca19e82..8298eae9c7c 100644 --- a/packages/compass-logging/package.json +++ b/packages/compass-logging/package.json @@ -55,6 +55,7 @@ }, "dependencies": { "debug": "^4.3.4", + "hadron-app-registry": "^9.1.8", "is-electron-renderer": "^2.0.1", "mongodb-log-writer": "^1.3.0", "react": "^17.0.2" @@ -64,8 +65,8 @@ "@mongodb-js/mocha-config-compass": "^1.3.7", "@mongodb-js/prettier-config-compass": "^1.0.1", "@mongodb-js/tsconfig-compass": "^1.0.3", - "@types/debug": "^4.1.9", "@types/chai": "^4.2.21", + "@types/debug": "^4.1.9", "@types/mocha": "^9.0.0", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.4", diff --git a/packages/compass-logging/src/provider.ts b/packages/compass-logging/src/provider.ts index 3bfd77511a3..06bfb29c432 100644 --- a/packages/compass-logging/src/provider.ts +++ b/packages/compass-logging/src/provider.ts @@ -4,6 +4,7 @@ import type { LoggingAndTelemetryPreferences, } from './logger'; import type { MongoLogId, MongoLogWriter } from 'mongodb-log-writer'; +import { createServiceLocator } from 'hadron-app-registry'; export type { LoggerAndTelemetry } from './logger'; @@ -44,7 +45,10 @@ const LoggerAndTelemetryContext = React.createContext<{ export const LoggerAndTelemetryProvider = LoggerAndTelemetryContext.Provider; export function createLoggerAndTelemetryLocator(component: string) { - return useLoggerAndTelemetry.bind(null, component); + return createServiceLocator( + useLoggerAndTelemetry.bind(null, component), + 'createLoggerAndTelemetryLocator' + ); } export function useLoggerAndTelemetry(component: string): LoggerAndTelemetry { diff --git a/packages/compass-preferences-model/package.json b/packages/compass-preferences-model/package.json index 5c3db35ce55..7934dcf0e91 100644 --- a/packages/compass-preferences-model/package.json +++ b/packages/compass-preferences-model/package.json @@ -54,6 +54,7 @@ "@mongodb-js/compass-logging": "^1.2.14", "@mongodb-js/compass-user-data": "^0.1.17", "bson": "^6.3.0", + "hadron-app-registry": "^9.1.8", "hadron-ipc": "^3.2.12", "js-yaml": "^4.1.0", "lodash": "^4.17.21", diff --git a/packages/compass-preferences-model/src/react.ts b/packages/compass-preferences-model/src/react.ts index 9ac317f213b..c83b8752daa 100644 --- a/packages/compass-preferences-model/src/react.ts +++ b/packages/compass-preferences-model/src/react.ts @@ -9,6 +9,7 @@ import { import { type AllPreferences } from './'; import type { PreferencesAccess } from './preferences'; import { ReadOnlyPreferenceAccess } from './read-only-preferences-access'; +import { createServiceLocator } from 'hadron-app-registry'; const PreferencesContext = createContext( // Our context starts with our read-only preference access but we expect @@ -18,16 +19,22 @@ const PreferencesContext = createContext( export const PreferencesProvider = PreferencesContext.Provider; -export function preferencesLocator(): PreferencesAccess { +export function usePreferencesContext() { return useContext(PreferencesContext); } + +export const preferencesLocator = createServiceLocator( + usePreferencesContext, + 'preferencesLocator' +); + export type { PreferencesAccess }; /** Use as: const enableMaps = usePreference('enableMaps', React); */ export function usePreference( key: K ): AllPreferences[K] { - const preferences = preferencesLocator(); + const preferences = usePreferencesContext(); const [value, setValue] = useState(preferences.getPreferences()[key]); useEffect(() => { return preferences.onPreferenceValueChanged(key, (value) => { diff --git a/packages/compass-workspaces/src/provider.tsx b/packages/compass-workspaces/src/provider.tsx index eab371c06e8..90a8a5220e7 100644 --- a/packages/compass-workspaces/src/provider.tsx +++ b/packages/compass-workspaces/src/provider.tsx @@ -9,6 +9,7 @@ import { getActiveTab, openWorkspace as openWorkspaceAction, } from './stores/workspaces'; +import { createServiceLocator } from 'hadron-app-registry'; function useWorkspacesStore() { try { @@ -225,7 +226,10 @@ export function useActiveWorkspace() { return service.getActiveWorkspace(); } -export const workspacesServiceLocator = useWorkspacesService; +export const workspacesServiceLocator = createServiceLocator( + useWorkspacesService, + 'workspacesServiceLocator' +); export { useWorkspacePlugins } from './components/workspaces-provider'; export { useTabState } from './components/workspace-tab-state-provider'; diff --git a/packages/compass/src/app/index.tsx b/packages/compass/src/app/index.tsx index 2b7881fd61f..d2488b0deae 100644 --- a/packages/compass/src/app/index.tsx +++ b/packages/compass/src/app/index.tsx @@ -95,13 +95,13 @@ import { getAppName, getAppVersion } from '@mongodb-js/compass-utils'; const { log, mongoLogId, track } = createLoggerAndTelemetry('COMPASS-APP'); const WithPreferencesAndLoggerProviders: React.FC = ({ children }) => { - const loggerProviderValue = { + const loggerProviderValue = useRef({ createLogger: createLoggerAndTelemetry, preferences: defaultPreferencesInstance, - }; + }); return ( - - + + {children} @@ -260,6 +260,15 @@ const Application = View.extend({ this.el = document.querySelector('#application'); this.renderWithTemplate(this); + const wasNetworkOptInShown = + defaultPreferencesInstance.getPreferences().showedNetworkOptIn === true; + + // If we haven't showed welcome modal that points users to network opt in + // yet, update preferences with default values to reflect that ... + if (!wasNetworkOptInShown) { + await defaultPreferencesInstance.ensureDefaultConfigurableUserPreferences(); + } + ReactDOM.render( @@ -269,6 +278,8 @@ const Application = View.extend({ diff --git a/packages/connection-storage/package.json b/packages/connection-storage/package.json index 5f60697ddc6..ad5aaaab8c5 100644 --- a/packages/connection-storage/package.json +++ b/packages/connection-storage/package.json @@ -56,12 +56,13 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/connection-info": "^0.1.5", "@mongodb-js/compass-logging": "^1.2.14", "@mongodb-js/compass-user-data": "^0.1.17", "@mongodb-js/compass-utils": "^0.6.1", + "@mongodb-js/connection-info": "^0.1.5", "bson": "^6.3.0", "electron": "^28.2.5", + "hadron-app-registry": "^9.1.8", "hadron-ipc": "^3.2.12", "keytar": "^7.9.0", "lodash": "^4.17.21", @@ -79,13 +80,13 @@ "@types/mocha": "^9.0.0", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", + "chai-as-promised": "^7.1.1", "depcheck": "^1.4.1", "eslint": "^7.25.0", "mocha": "^10.2.0", "nyc": "^15.1.0", "prettier": "^2.7.1", "sinon": "^9.2.3", - "typescript": "^5.0.4", - "chai-as-promised": "^7.1.1" + "typescript": "^5.0.4" } } diff --git a/packages/connection-storage/src/provider.ts b/packages/connection-storage/src/provider.ts index dc39b2e57f2..906531c7297 100644 --- a/packages/connection-storage/src/provider.ts +++ b/packages/connection-storage/src/provider.ts @@ -1,44 +1,61 @@ import { createContext, createElement, useMemo, useContext } from 'react'; import { ConnectionRepository } from './connection-repository'; import type { ConnectionStorage } from './renderer'; +import { + createServiceLocator, + createServiceProvider, +} from 'hadron-app-registry'; export const ConnectionStorageContext = createContext< typeof ConnectionStorage | null >(null); -export function connectionStorageLocator(): typeof ConnectionStorage { +// TODO(COMPASS-7397): storage context should not be leaking out of the service +// provider export, but the way the connection plugin is currently implemented +// prevents us from avoiding this +export function useConnectionStorageContext() { const connectionStorage = useContext(ConnectionStorageContext); if (!connectionStorage) { throw new Error( 'Could not find the current ConnectionStorage. Did you forget to setup the ConnectionStorageContext?' ); } - return connectionStorage; } +export const connectionStorageLocator = createServiceLocator( + useConnectionStorageContext, + 'connectionStorageLocator' +); + export const ConnectionRepositoryContext = createContext(null); -export const ConnectionRepositoryContextProvider: React.FunctionComponent< - object -> = ({ children }) => { - const storage = connectionStorageLocator(); - const value = useMemo(() => new ConnectionRepository(storage), [storage]); - - return createElement(ConnectionRepositoryContext.Provider, { - value, +export const ConnectionRepositoryContextProvider: React.FunctionComponent = + createServiceProvider(function ConnectionRepositoryContextProvider({ children, + }) { + const storage = connectionStorageLocator(); + const value = useMemo(() => new ConnectionRepository(storage), [storage]); + + return createElement(ConnectionRepositoryContext.Provider, { + value, + children, + }); }); -}; -export function connectionRepositoryLocator(): ConnectionRepository { +// TODO(COMPASS-7397): see above +export function useConnectionRepositoryContext() { const connectionRepository = useContext(ConnectionRepositoryContext); if (!connectionRepository) { throw new Error( 'Could not find the current ConnectionRepository. Did you forget to setup the ConnectionRepositoryContext?' ); } - return connectionRepository; } + +export const connectionRepositoryLocator = createServiceLocator( + useConnectionRepositoryContext, + 'connectionRepositoryLocator' +); diff --git a/packages/data-service/package.json b/packages/data-service/package.json index 72702976ad7..3e9e24d27e6 100644 --- a/packages/data-service/package.json +++ b/packages/data-service/package.json @@ -64,6 +64,7 @@ "@mongodb-js/devtools-connect": "^2.4.2", "@mongodb-js/oidc-plugin": "^0.3.1", "@mongodb-js/ssh-tunnel": "^2.1.13", + "hadron-app-registry": "^9.1.8", "lodash": "^4.17.21", "mongodb-build-info": "^1.7.0", "mongodb-connection-string-url": "^2.6.0", diff --git a/packages/data-service/src/provider.ts b/packages/data-service/src/provider.ts index d7e657612a5..44ca99afa4e 100644 --- a/packages/data-service/src/provider.ts +++ b/packages/data-service/src/provider.ts @@ -1,5 +1,6 @@ import { createContext, useContext } from 'react'; import type DataService from './data-service'; +import { createServiceLocator } from 'hadron-app-registry'; const DataServiceContext = createContext(null); @@ -15,15 +16,17 @@ export type DataServiceLocator< * available methods on the injected service (only on compilation time, doesn't * have the effect otherwise) */ -export function dataServiceLocator< - K extends keyof DataService = keyof DataService, - L extends keyof DataService = K ->(): Pick & Partial> { - const ds = useContext(DataServiceContext); - if (!ds) { - throw new Error('DataService is not available in the component context'); +export const dataServiceLocator = createServiceLocator( + function dataServiceLocator< + K extends keyof DataService = keyof DataService, + L extends keyof DataService = K + >(): Pick & Partial> { + const ds = useContext(DataServiceContext); + if (!ds) { + throw new Error('DataService is not available in the component context'); + } + return ds; } - return ds; -} +); export type { DataService }; diff --git a/packages/hadron-app-registry/src/index.ts b/packages/hadron-app-registry/src/index.ts index 76885876895..8b5f1b6ead2 100644 --- a/packages/hadron-app-registry/src/index.ts +++ b/packages/hadron-app-registry/src/index.ts @@ -10,5 +10,10 @@ export type { HadronPluginConfig, ActivateHelpers, } from './register-plugin'; -export { registerHadronPlugin, createActivateHelpers } from './register-plugin'; +export { + registerHadronPlugin, + createActivateHelpers, + createServiceLocator, + createServiceProvider, +} from './register-plugin'; export default AppRegistry; diff --git a/packages/hadron-app-registry/src/register-plugin.spec.tsx b/packages/hadron-app-registry/src/register-plugin.spec.tsx index 23c943e2969..dc33b144a40 100644 --- a/packages/hadron-app-registry/src/register-plugin.spec.tsx +++ b/packages/hadron-app-registry/src/register-plugin.spec.tsx @@ -6,6 +6,7 @@ import { AppRegistryProvider, registerHadronPlugin, createActivateHelpers, + createServiceLocator, } from './'; import { createStore } from 'redux'; import { connect } from 'react-redux'; @@ -105,7 +106,7 @@ describe('registerHadronPlugin', function () { it('allows registering a plugin with external services dependencies', function () { const dummy = { value: 'blah' }; const blahContext = createContext(dummy); - const useBlah = () => useContext(blahContext); + const useBlah = createServiceLocator(() => useContext(blahContext)); const connector = connect(); const component = sinon.stub().callsFake(() => <>); diff --git a/packages/hadron-app-registry/src/register-plugin.tsx b/packages/hadron-app-registry/src/register-plugin.tsx index 6d0e446daff..f29644c1369 100644 --- a/packages/hadron-app-registry/src/register-plugin.tsx +++ b/packages/hadron-app-registry/src/register-plugin.tsx @@ -160,6 +160,62 @@ const useMockOption = (key: T): MockOptions[T] => { return useContext(MockOptionsContext)[key]; }; +let serviceLocationInProgress = false; + +const kLocator = Symbol('CompassServiceLocatorFunction'); + +/** + * Helper method to "create" a service locator function for a particular + * service. This method guarantees that service locators are used only in + * appropriate context. Every service locator function in the application should + * be created with this method. + */ +export function createServiceLocator< + T extends (this: any, ...args: any[]) => any +>(fn: T, name = fn.name): T { + return Object.assign( + function (this: any, ...args: any[]) { + if (!serviceLocationInProgress) { + throw new Error( + `Using service locator function "${name}" outside of the service location lifecycle. ` + + `Make sure that service locator function is passed as a second argument to the registerHadronPlugin method and is not used directly in a React render method.` + ); + } + return fn.call(this, ...args); + }, + { [kLocator]: true } + ) as unknown as T; +} + +/** + * Optional service provider creation method. In some cases service providers + * need access to other service locators to facilitate service injections. In + * these cases service provider can be wrapped with the createServiceProvider + * function to allow usage of serviceLocator functions in providers outside of + * the usual hadron plugin "activate" lifecycle. + */ +export function createServiceProvider( + fn: T +): T { + const displayName = `ServiceProvider(${fn.displayName ?? fn.name})`; + const Provider = function (props: React.ComponentProps) { + let content: React.ReactElement | null; + try { + serviceLocationInProgress = true; + content = fn(props); + } finally { + serviceLocationInProgress = false; + } + return <>{content}; + }; + Provider.displayName = displayName; + return Provider as T; +} + +function isServiceLocator(val: any): boolean { + return Object.prototype.hasOwnProperty.call(val, kLocator); +} + function useHadronPluginActivate unknown>>( config: HadronPluginConfig, services: S | undefined, @@ -172,30 +228,47 @@ function useHadronPluginActivate unknown>>( const globalAppRegistry = useGlobalAppRegistry(); const localAppRegistry = useLocalAppRegistry(); - const serviceImpls = Object.fromEntries( - Object.keys({ - ...(isMockedEnvironment ? mockServices : {}), - ...services, - }).map((key) => { - try { - return [ - key, - isMockedEnvironment && mockServices?.[key] - ? mockServices[key] - : services?.[key](), - ]; - } catch (err) { + let serviceImpls: Services; + + try { + serviceLocationInProgress = true; + serviceImpls = Object.fromEntries( + Object.keys({ + ...(isMockedEnvironment ? mockServices : {}), + ...services, + }).map((key) => { + const usingMockService = isMockedEnvironment && !!mockServices?.[key]; + if ( - err && - typeof err === 'object' && - 'message' in err && - typeof err.message === 'string' - ) - err.message += ` [locating service '${key}' for '${registryName}']`; - throw err; - } - }) - ) as Services; + !usingMockService && + services?.[key] && + !isServiceLocator(services[key]) + ) { + throw new Error( + `Function "${services[key].name}" that is being passed as a service locator can not be identified as one. ` + + `Make sure that you created a service locator function using createServiceLocator helper function.` + ); + } + + const svc = usingMockService ? mockServices[key] : services?.[key](); + + try { + return [key, svc]; + } catch (err) { + if ( + err && + typeof err === 'object' && + 'message' in err && + typeof err.message === 'string' + ) + err.message += ` [locating service '${key}' for '${registryName}']`; + throw err; + } + }) + ) as Services; + } finally { + serviceLocationInProgress = false; + } const [{ store, actions, context }] = useState( () => diff --git a/packages/my-queries-storage/package.json b/packages/my-queries-storage/package.json index 595ed25500d..9044f81b1ae 100644 --- a/packages/my-queries-storage/package.json +++ b/packages/my-queries-storage/package.json @@ -75,7 +75,8 @@ "dependencies": { "@mongodb-js/compass-editor": "^0.21.1", "@mongodb-js/compass-user-data": "^0.1.17", - "bson": "^6.3.0" + "bson": "^6.3.0", + "hadron-app-registry": "^9.1.8" }, "peerDependencies": { "react": "^17.0.2" diff --git a/packages/my-queries-storage/src/provider.ts b/packages/my-queries-storage/src/provider.ts index 6389849dd1e..1b83eb7ccbb 100644 --- a/packages/my-queries-storage/src/provider.ts +++ b/packages/my-queries-storage/src/provider.ts @@ -2,6 +2,7 @@ import { createContext, useContext } from 'react'; import type { QueryStorageOptions } from './compass-query-storage'; import type { PipelineStorage } from './pipeline-storage'; import type { FavoriteQueryStorage, RecentQueryStorage } from './query-storage'; +import { createServiceLocator } from 'hadron-app-registry'; export type { PipelineStorage, FavoriteQueryStorage, RecentQueryStorage }; @@ -29,12 +30,21 @@ export const FavoriteQueryStorageProvider = export const RecentQueryStorageProvider = RecentQueryStorageContext.Provider; export const usePipelineStorage = () => useContext(PipelineStorageContext); -export const pipelineStorageLocator = usePipelineStorage; +export const pipelineStorageLocator = createServiceLocator( + usePipelineStorage, + 'pipelineStorageLocator' +); export const useFavoriteQueryStorageAccess = () => useContext(FavoriteQueryStorageContext); -export const favoriteQueryStorageAccessLocator = useFavoriteQueryStorageAccess; +export const favoriteQueryStorageAccessLocator = createServiceLocator( + useFavoriteQueryStorageAccess, + 'favoriteQueryStorageAccessLocator' +); export const useRecentQueryStorageAccess = () => useContext(RecentQueryStorageContext); -export const recentQueryStorageAccessLocator = useRecentQueryStorageAccess; +export const recentQueryStorageAccessLocator = createServiceLocator( + useRecentQueryStorageAccess, + 'recentQueryStorageAccessLocator' +);