From 052c51dc10e9a9131a1b8d84fcf6229c492174bf Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 7 Nov 2023 14:23:11 +0100 Subject: [PATCH] chore: add new registerHadronPlugin() mechanism COMPASS-7321 (#5060) * chore: refactor AppRegistryContext and move it to hadron-app-registry pkg * chore(hadron-app-registry): add new registerHadronPlugin() mechanism COMPASS-7321 Largely based on 8054bf8b8a0a2696aad4b3d70a3f3b56cf61bed2. * chore(compass-home): convert plugin to use new interface * chore(compass-settings): convert plugin to use new interface Co-authored-by: Sergey Petushkov --- package-lock.json | 24 ++- .../dnd-wrapper.tsx | 2 +- .../pipeline-toolbar/pipeline-ai.tsx | 2 +- .../stage-preview/atlas-stage-preview.tsx | 2 +- .../src/modules/focus-mode.ts | 2 +- .../modules/pipeline-builder/pipeline-ai.ts | 2 +- .../src/components/collection-tab.tsx | 2 +- .../src/stores/collection-tab.ts | 6 +- .../src/components/form-help/form-help.tsx | 2 +- .../src/modules/connection-attempt.ts | 2 +- .../src/components/insert-document-dialog.tsx | 8 +- .../compass-crud/src/stores/crud-store.ts | 2 +- .../src/utils/cancellable-queries.ts | 6 +- .../src/stores/explain-plan-modal-store.ts | 2 +- packages/compass-home/package.json | 2 + .../compass-home/src/components/home.spec.tsx | 27 ++- packages/compass-home/src/components/home.tsx | 14 +- .../src/components/workspace-content.spec.tsx | 15 +- .../src/components/workspace-content.tsx | 14 +- .../compass-home/src/components/workspace.tsx | 13 +- .../src/contexts/app-registry-context.ts | 70 ------ packages/compass-home/src/index.ts | 33 ++- packages/compass-home/src/plugin.tsx | 21 -- .../src/components/export-modal.tsx | 5 +- .../src/components/import-modal.tsx | 5 +- .../src/components/import-toast.tsx | 2 +- .../src/modules/export.ts | 2 +- .../src/modules/import.ts | 2 +- .../create-index-modal/create-index-modal.tsx | 5 +- .../src/components/drop-index-modal/index.tsx | 5 +- .../base-search-index-modal.tsx | 5 +- packages/compass-logging/package.json | 13 +- packages/compass-logging/provider.d.ts | 1 + packages/compass-logging/src/index.spec.ts | 2 +- packages/compass-logging/src/index.ts | 8 +- packages/compass-logging/src/ipc-logger.ts | 25 +++ packages/compass-logging/src/logger.ts | 34 +-- packages/compass-logging/src/provider.ts | 71 +++++++ packages/compass-logging/src/react.ts | 59 ------ .../src/components/query-ai.tsx | 2 +- .../query-history-button-popover.tsx | 5 +- .../src/components/query-history/index.tsx | 5 +- .../src/stores/ai-query-reducer.ts | 2 +- .../src/modules/schema-analysis.ts | 2 +- .../compass-settings/src/components/index.tsx | 6 +- .../src/components/modal.spec.tsx | 3 +- .../src/components/settings/general.spec.tsx | 3 +- .../settings/oidc-settings.spec.tsx | 3 +- .../src/components/settings/privacy.spec.tsx | 3 +- packages/compass-settings/src/index.ts | 29 +-- packages/compass-settings/src/stores/index.ts | 74 ++++--- .../compass-settings/src/stores/settings.ts | 21 +- packages/compass-shell/package.json | 1 - .../shell-info-modal/shell-info-modal.jsx | 2 +- packages/compass-shell/src/stores/store.js | 2 +- .../src/components/csfle-connection-modal.tsx | 5 +- .../components/non-genuine-warning-modal.tsx | 5 +- packages/compass/src/app/index.jsx | 25 +-- .../src/app/intercom/intercom-script.ts | 2 +- .../src/app/intercom/setup-intercom.ts | 2 +- packages/compass/src/main/application.ts | 2 +- .../compass/src/main/protocol-handling.ts | 2 +- .../src/connection-storage.ts | 2 +- .../src/import-export-connection.ts | 2 +- packages/databases-collections/package.json | 1 - .../src/components/drop-collection-modal.tsx | 5 +- .../src/components/drop-database-modal.tsx | 5 +- packages/hadron-app-registry/.mocharc.js | 2 +- packages/hadron-app-registry/package.json | 7 +- .../hadron-app-registry/src/app-registry.ts | 49 ++++- packages/hadron-app-registry/src/index.ts | 12 ++ .../hadron-app-registry/src/react-context.tsx | 142 +++++++++++++ .../src/register-plugin.spec.tsx | 134 ++++++++++++ .../src/register-plugin.tsx | 200 ++++++++++++++++++ packages/hadron-app-registry/tsconfig.json | 2 +- 75 files changed, 876 insertions(+), 410 deletions(-) delete mode 100644 packages/compass-home/src/contexts/app-registry-context.ts delete mode 100644 packages/compass-home/src/plugin.tsx create mode 100644 packages/compass-logging/provider.d.ts create mode 100644 packages/compass-logging/src/ipc-logger.ts create mode 100644 packages/compass-logging/src/provider.ts delete mode 100644 packages/compass-logging/src/react.ts create mode 100644 packages/hadron-app-registry/src/react-context.tsx create mode 100644 packages/hadron-app-registry/src/register-plugin.spec.tsx create mode 100644 packages/hadron-app-registry/src/register-plugin.tsx diff --git a/package-lock.json b/package-lock.json index 9644ade5334..39c877dd9a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,6 @@ "configs/*", "scripts" ], - "dependencies": { - "electron": "^25.9.3" - }, "devDependencies": { "@babel/core": "7.16.0", "@babel/parser": "7.16.0", @@ -46040,6 +46037,7 @@ "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-connections": "^1.20.0", "@mongodb-js/compass-logging": "^1.2.5", + "@mongodb-js/compass-settings": "^0.21.0", "@mongodb-js/compass-welcome": "^0.18.0", "@mongodb-js/connection-storage": "^0.6.5", "compass-preferences-model": "^2.15.5", @@ -46077,6 +46075,7 @@ "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-connections": "^1.20.0", "@mongodb-js/compass-logging": "^1.2.5", + "@mongodb-js/compass-settings": "^0.21.0", "@mongodb-js/compass-welcome": "^0.18.0", "@mongodb-js/connection-storage": "^0.6.5", "compass-preferences-model": "^2.15.5", @@ -46426,7 +46425,8 @@ "dependencies": { "debug": "^4.3.4", "is-electron-renderer": "^2.0.1", - "mongodb-log-writer": "^1.3.0" + "mongodb-log-writer": "^1.3.0", + "react": "^17.0.2" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.0.11", @@ -47120,7 +47120,6 @@ "enzyme": "^3.11.0", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.13", - "hadron-ipc": "^3.2.4", "mocha": "^10.2.0", "nyc": "^15.1.0", "prop-types": "^15.7.2", @@ -47833,7 +47832,6 @@ "eslint": "^7.25.0", "hadron-app": "^5.15.0", "hadron-app-registry": "^9.0.13", - "hadron-ipc": "^3.2.4", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.0.0", @@ -48074,6 +48072,9 @@ "dependencies": { "debug": "^4.2.0", "eventemitter3": "^4.0.0", + "react": "^17.0.2", + "react-redux": "^8.0.5", + "redux": "^4.2.1", "reflux": "^0.4.1" }, "devDependencies": { @@ -48081,6 +48082,7 @@ "@mongodb-js/mocha-config-compass": "^1.3.2", "@mongodb-js/prettier-config-compass": "^1.0.1", "@mongodb-js/tsconfig-compass": "^1.0.3", + "@testing-library/react": "^12.1.4", "@types/chai": "^4.2.21", "@types/mocha": "^9.0.0", "@types/reflux": "^6.4.3", @@ -48090,6 +48092,7 @@ "eslint-config-mongodb-js": "^5.0.3", "mocha": "^10.2.0", "prettier": "^2.7.1", + "reflux-state-mixin": "github:mongodb-js/reflux-state-mixin", "sinon": "^9.0.0", "typescript": "^5.0.4" } @@ -58683,7 +58686,6 @@ "eslint": "^7.25.0", "hadron-app": "^5.15.0", "hadron-app-registry": "^9.0.13", - "hadron-ipc": "^3.2.4", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.0.0", @@ -59188,6 +59190,7 @@ "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-connections": "^1.20.0", "@mongodb-js/compass-logging": "^1.2.5", + "@mongodb-js/compass-settings": "^0.21.0", "@mongodb-js/compass-welcome": "^0.18.0", "@mongodb-js/connection-storage": "^0.6.5", "@mongodb-js/eslint-config-compass": "^1.0.11", @@ -59500,6 +59503,7 @@ "mongodb-log-writer": "^1.3.0", "nyc": "^15.1.0", "prettier": "^2.7.1", + "react": "^17.0.2", "sinon": "^9.2.3", "typescript": "^5.0.4" }, @@ -60185,7 +60189,6 @@ "enzyme": "^3.11.0", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.13", - "hadron-ipc": "^3.2.4", "mocha": "^10.2.0", "nyc": "^15.1.0", "prop-types": "^15.7.2", @@ -78452,6 +78455,7 @@ "@mongodb-js/mocha-config-compass": "^1.3.2", "@mongodb-js/prettier-config-compass": "^1.0.1", "@mongodb-js/tsconfig-compass": "^1.0.3", + "@testing-library/react": "^12.1.4", "@types/chai": "^4.2.21", "@types/mocha": "^9.0.0", "@types/reflux": "^6.4.3", @@ -78463,7 +78467,11 @@ "eventemitter3": "^4.0.0", "mocha": "^10.2.0", "prettier": "^2.7.1", + "react": "^17.0.2", + "react-redux": "^8.0.5", + "redux": "^4.2.1", "reflux": "^0.4.1", + "reflux-state-mixin": "github:mongodb-js/reflux-state-mixin", "sinon": "^9.0.0", "typescript": "^5.0.4" }, diff --git a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-builder-ui-workspace/dnd-wrapper.tsx b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-builder-ui-workspace/dnd-wrapper.tsx index 88f87998bb6..00692363edb 100644 --- a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-builder-ui-workspace/dnd-wrapper.tsx +++ b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-builder-ui-workspace/dnd-wrapper.tsx @@ -8,7 +8,7 @@ import { DragOverlay, } from '@dnd-kit/core'; import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { indexFromDroppableId, diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-ai.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-ai.tsx index 62f17b47501..5da37d731c1 100644 --- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-ai.tsx +++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-ai.tsx @@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react'; import { openToast } from '@mongodb-js/compass-components'; import { GenerativeAIInput } from '@mongodb-js/compass-generative-ai'; import { connect } from 'react-redux'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { usePreference } from 'compass-preferences-model'; import { diff --git a/packages/compass-aggregations/src/components/stage-preview/atlas-stage-preview.tsx b/packages/compass-aggregations/src/components/stage-preview/atlas-stage-preview.tsx index 0f4bdca51d5..0c52d9b7812 100644 --- a/packages/compass-aggregations/src/components/stage-preview/atlas-stage-preview.tsx +++ b/packages/compass-aggregations/src/components/stage-preview/atlas-stage-preview.tsx @@ -6,7 +6,7 @@ import { AtlasNavGraphic, Body, } from '@mongodb-js/compass-components'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; const { track } = createLoggerAndTelemetry('COMPASS-AGGREGATIONS-UI'); const ATLAS_LINK = diff --git a/packages/compass-aggregations/src/modules/focus-mode.ts b/packages/compass-aggregations/src/modules/focus-mode.ts index 888861c3505..a5c051cc822 100644 --- a/packages/compass-aggregations/src/modules/focus-mode.ts +++ b/packages/compass-aggregations/src/modules/focus-mode.ts @@ -2,7 +2,7 @@ import type { AnyAction } from 'redux'; import type { PipelineBuilderThunkAction } from '.'; import { isAction } from '../utils/is-action'; import { addStage, pipelineFromStore } from './pipeline-builder/stage-editor'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; const { track } = createLoggerAndTelemetry('COMPASS-AGGREGATIONS-UI'); diff --git a/packages/compass-aggregations/src/modules/pipeline-builder/pipeline-ai.ts b/packages/compass-aggregations/src/modules/pipeline-builder/pipeline-ai.ts index a843fca0370..8128a4787ac 100644 --- a/packages/compass-aggregations/src/modules/pipeline-builder/pipeline-ai.ts +++ b/packages/compass-aggregations/src/modules/pipeline-builder/pipeline-ai.ts @@ -1,5 +1,5 @@ import type { Reducer } from 'redux'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { getSimplifiedSchema } from 'mongodb-schema'; import toNS from 'mongodb-ns'; import preferences from 'compass-preferences-model'; diff --git a/packages/compass-collection/src/components/collection-tab.tsx b/packages/compass-collection/src/components/collection-tab.tsx index e89885fff1d..c2663e6662f 100644 --- a/packages/compass-collection/src/components/collection-tab.tsx +++ b/packages/compass-collection/src/components/collection-tab.tsx @@ -12,7 +12,7 @@ import { } from '../modules/collection-tab'; import { css, ErrorBoundary, TabNavBar } from '@mongodb-js/compass-components'; import CollectionHeader from './collection-header'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; const { log, mongoLogId, track } = createLoggerAndTelemetry( 'COMPASS-COLLECTION-TAB-UI' diff --git a/packages/compass-collection/src/stores/collection-tab.ts b/packages/compass-collection/src/stores/collection-tab.ts index 12f7ea4e1c3..b2da69a6fc2 100644 --- a/packages/compass-collection/src/stores/collection-tab.ts +++ b/packages/compass-collection/src/stores/collection-tab.ts @@ -46,9 +46,9 @@ export function configureStore(options: CollectionTabOptions) { throw new Error('Expected to get instance from App.InstanceStore'); } - const configureFieldStore = globalAppRegistry.getStore('Field.Store') as ( - ...args: any - ) => void | undefined; // our handcrafted d.ts file doesn't match the actual code + const configureFieldStore = globalAppRegistry.getStore( + 'Field.Store' + ) as unknown as (...args: any) => void | undefined; // Field.Store is odd because it registers a configure method, not the actual store configureFieldStore?.({ localAppRegistry: localAppRegistry, diff --git a/packages/compass-connections/src/components/form-help/form-help.tsx b/packages/compass-connections/src/components/form-help/form-help.tsx index d953a9d3ca0..6b32a35925a 100644 --- a/packages/compass-connections/src/components/form-help/form-help.tsx +++ b/packages/compass-connections/src/components/form-help/form-help.tsx @@ -13,7 +13,7 @@ import { useDarkMode, } from '@mongodb-js/compass-components'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; const { track } = createLoggerAndTelemetry('COMPASS-CONNECT-UI'); const formHelpContainerStyles = css({ diff --git a/packages/compass-connections/src/modules/connection-attempt.ts b/packages/compass-connections/src/modules/connection-attempt.ts index 4a8adaee70c..a4362469189 100644 --- a/packages/compass-connections/src/modules/connection-attempt.ts +++ b/packages/compass-connections/src/modules/connection-attempt.ts @@ -1,4 +1,4 @@ -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { isCancelError, raceWithAbort } from '@mongodb-js/compass-utils'; import type { ConnectionOptions, DataService } from 'mongodb-data-service'; import { connect } from 'mongodb-data-service'; diff --git a/packages/compass-crud/src/components/insert-document-dialog.tsx b/packages/compass-crud/src/components/insert-document-dialog.tsx index 72e542ade24..17d66a72274 100644 --- a/packages/compass-crud/src/components/insert-document-dialog.tsx +++ b/packages/compass-crud/src/components/insert-document-dialog.tsx @@ -17,7 +17,7 @@ import InsertCSFLEWarningBanner from './insert-csfle-warning-banner'; import InsertJsonDocument from './insert-json-document'; import InsertDocument from './insert-document'; import type { LoggerAndTelemetry } from '@mongodb-js/compass-logging'; -import { withLoggerAndTelemetry } from '@mongodb-js/compass-logging'; +import { withLoggerAndTelemetry } from '@mongodb-js/compass-logging/provider'; /** * The insert invalid message. @@ -341,8 +341,4 @@ class InsertDocumentDialog extends React.PureComponent< } } -export default withLoggerAndTelemetry( - InsertDocumentDialog, - 'COMPASS-CRUD-UI', - React -); +export default withLoggerAndTelemetry(InsertDocumentDialog, 'COMPASS-CRUD-UI'); diff --git a/packages/compass-crud/src/stores/crud-store.ts b/packages/compass-crud/src/stores/crud-store.ts index 5aeac32d95b..e887186b244 100644 --- a/packages/compass-crud/src/stores/crud-store.ts +++ b/packages/compass-crud/src/stores/crud-store.ts @@ -9,7 +9,7 @@ import type { Element } from 'hadron-document'; import { Document } from 'hadron-document'; import HadronDocument from 'hadron-document'; import _parseShellBSON, { ParseMode } from 'ejson-shell-parser'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { capMaxTimeMSAtPreferenceLimit } from 'compass-preferences-model'; import type { Stage } from '@mongodb-js/explain-plan-helper'; import { ExplainPlan } from '@mongodb-js/explain-plan-helper'; diff --git a/packages/compass-crud/src/utils/cancellable-queries.ts b/packages/compass-crud/src/utils/cancellable-queries.ts index de0b8f0ad8c..56aecea8256 100644 --- a/packages/compass-crud/src/utils/cancellable-queries.ts +++ b/packages/compass-crud/src/utils/cancellable-queries.ts @@ -1,9 +1,11 @@ -import createLogger from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { capMaxTimeMSAtPreferenceLimit } from 'compass-preferences-model'; import type { DataService } from 'mongodb-data-service'; import type { BSONObject } from '../stores/crud-store'; -const { log, mongoLogId, debug } = createLogger('cancellable-queries'); +const { log, mongoLogId, debug } = createLoggerAndTelemetry( + 'COMPASS-CANCELLABLE-QUERIES' +); export async function countDocuments( dataService: DataService, diff --git a/packages/compass-explain-plan/src/stores/explain-plan-modal-store.ts b/packages/compass-explain-plan/src/stores/explain-plan-modal-store.ts index dc662477432..4f6debad684 100644 --- a/packages/compass-explain-plan/src/stores/explain-plan-modal-store.ts +++ b/packages/compass-explain-plan/src/stores/explain-plan-modal-store.ts @@ -8,7 +8,7 @@ import type { Action, AnyAction, Reducer } from 'redux'; import { applyMiddleware, createStore } from 'redux'; import type { ThunkAction } from 'redux-thunk'; import thunk from 'redux-thunk'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; const { log, mongoLogId, track } = createLoggerAndTelemetry('COMPASS-EXPLAIN-UI'); diff --git a/packages/compass-home/package.json b/packages/compass-home/package.json index 0a33ba775b5..35f11992b17 100644 --- a/packages/compass-home/package.json +++ b/packages/compass-home/package.json @@ -38,6 +38,7 @@ "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-connections": "^1.20.0", "@mongodb-js/compass-logging": "^1.2.5", + "@mongodb-js/compass-settings": "^0.21.0", "@mongodb-js/compass-welcome": "^0.18.0", "@mongodb-js/connection-storage": "^0.6.5", "compass-preferences-model": "^2.15.5", @@ -49,6 +50,7 @@ "@mongodb-js/compass-components": "^1.19.0", "@mongodb-js/compass-connections": "^1.20.0", "@mongodb-js/compass-logging": "^1.2.5", + "@mongodb-js/compass-settings": "^0.21.0", "@mongodb-js/compass-welcome": "^0.18.0", "@mongodb-js/connection-storage": "^0.6.5", "compass-preferences-model": "^2.15.5", diff --git a/packages/compass-home/src/components/home.spec.tsx b/packages/compass-home/src/components/home.spec.tsx index 7f55c011ec4..f06804044ab 100644 --- a/packages/compass-home/src/components/home.spec.tsx +++ b/packages/compass-home/src/components/home.spec.tsx @@ -2,10 +2,9 @@ import React from 'react'; import { once } from 'events'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; import { expect } from 'chai'; -import AppRegistry from 'hadron-app-registry'; -import ipc from 'hadron-ipc'; +import AppRegistry, { AppRegistryProvider } from 'hadron-app-registry'; +import { ipcRenderer } from 'hadron-ipc'; import sinon from 'sinon'; -import AppRegistryContext from '../contexts/app-registry-context'; import Home from '.'; const getComponent = (name: string) => { @@ -61,9 +60,9 @@ describe('Home [Component]', function () { describe('is not connected', function () { beforeEach(function () { render( - + - + ); }); @@ -82,9 +81,9 @@ describe('Home [Component]', function () { connectionOptions = { connectionString: 'mongodb+srv://mongodb.net/' } ) { render( - + - + ); testAppRegistry.emit('data-service-connected', null, dataService, { connectionOptions, @@ -128,16 +127,16 @@ describe('Home [Component]', function () { describe('on `app:disconnect`', function () { // Skip disconnect testing when we're not running in a renderer instance. // eslint-disable-next-line mocha/no-setup-in-describe - if (!ipc.ipcRenderer) { + if (!ipcRenderer) { // eslint-disable-next-line mocha/no-setup-in-describe, no-console console.warn( 'Skipping "app:disconnect" ipc event tests on non-renderer environment.' ); - return; + return this; } beforeEach(async function () { - ipc.ipcRenderer.emit('app:disconnect'); + ipcRenderer?.emit('app:disconnect'); await once(testAppRegistry, 'data-service-disconnected'); }); @@ -159,9 +158,9 @@ describe('Home [Component]', function () { describe('when rendered', function () { beforeEach(function () { render( - + - + ); }); @@ -184,9 +183,9 @@ describe('Home [Component]', function () { describe('on dismount', function () { beforeEach(function () { const { unmount } = render( - + - + ); unmount(); }); diff --git a/packages/compass-home/src/components/home.tsx b/packages/compass-home/src/components/home.tsx index 40afc536284..aa8010875e5 100644 --- a/packages/compass-home/src/components/home.tsx +++ b/packages/compass-home/src/components/home.tsx @@ -33,16 +33,13 @@ import React, { } from 'react'; import preferences from 'compass-preferences-model'; import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; -import { - AppRegistryRoles, - useAppRegistryContext, - useAppRegistryRole, -} from '../contexts/app-registry-context'; +import { useLocalAppRegistry, useAppRegistryRole } from 'hadron-app-registry'; import updateTitle from '../modules/update-title'; import Workspace from './workspace'; import { SignalHooksProvider } from '@mongodb-js/compass-components'; import { AtlasSignIn } from '@mongodb-js/atlas-service/renderer'; import type { CollectionMetadata } from 'mongodb-collection-model'; +import { CompassSettingsPlugin } from '@mongodb-js/compass-settings'; const { track } = createLoggerAndTelemetry('COMPASS-HOME-UI'); @@ -169,7 +166,7 @@ function Home({ appName: string; getAutoConnectInfo?: () => Promise; }): React.ReactElement | null { - const appRegistry = useAppRegistryContext(); + const appRegistry = useLocalAppRegistry(); const connectedDataService = useRef(); const [ @@ -320,7 +317,7 @@ function Home({ }; }, [appRegistry, onDataServiceDisconnected]); - const globalModals = useAppRegistryRole(AppRegistryRoles.GLOBAL_MODAL); + const globalModals = useAppRegistryRole('Global.Modal'); return ( { return ; })} + ); @@ -380,7 +378,7 @@ function ThemedHome( ): ReturnType { const [scrollbarsContainerRef, setScrollbarsContainerRef] = useState(null); - const appRegistry = useAppRegistryContext(); + const appRegistry = useLocalAppRegistry(); const [theme, setTheme] = useState({ theme: getCurrentTheme(), diff --git a/packages/compass-home/src/components/workspace-content.spec.tsx b/packages/compass-home/src/components/workspace-content.spec.tsx index ebdea683fa7..a0aec357ef5 100644 --- a/packages/compass-home/src/components/workspace-content.spec.tsx +++ b/packages/compass-home/src/components/workspace-content.spec.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { cleanup, render, screen } from '@testing-library/react'; import { expect } from 'chai'; -import AppRegistry from 'hadron-app-registry'; -import AppRegistryContext from '../contexts/app-registry-context'; +import AppRegistry, { AppRegistryProvider } from 'hadron-app-registry'; import WorkspaceContent from './workspace-content'; const getComponent = (name: string) => { @@ -39,9 +38,9 @@ describe('WorkspaceContent [Component]', function () { describe('namespace is unset', function () { beforeEach(function () { render( - + - + ); }); @@ -55,9 +54,9 @@ describe('WorkspaceContent [Component]', function () { describe('namespace has a db', function () { beforeEach(function () { render( - + - + ); }); @@ -71,9 +70,9 @@ describe('WorkspaceContent [Component]', function () { describe('namespace has db and collection', function () { beforeEach(function () { render( - + - + ); }); diff --git a/packages/compass-home/src/components/workspace-content.tsx b/packages/compass-home/src/components/workspace-content.tsx index 17d5d59f10b..6c9ec835b6b 100644 --- a/packages/compass-home/src/components/workspace-content.tsx +++ b/packages/compass-home/src/components/workspace-content.tsx @@ -1,9 +1,6 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import { - AppRegistryComponents, - useAppRegistryComponent, -} from '../contexts/app-registry-context'; +import { useAppRegistryComponent } from 'hadron-app-registry'; import type Namespace from '../types/namespace'; const EmptyComponent: React.FunctionComponent = () => null; @@ -12,14 +9,11 @@ const WorkspaceContent: React.FunctionComponent<{ namespace: Namespace }> = ({ namespace, }) => { const Collection = - useAppRegistryComponent(AppRegistryComponents.COLLECTION_WORKSPACE) ?? - EmptyComponent; + useAppRegistryComponent('Collection.Workspace') ?? EmptyComponent; const Database = - useAppRegistryComponent(AppRegistryComponents.DATABASE_WORKSPACE) ?? - EmptyComponent; + useAppRegistryComponent('Database.Workspace') ?? EmptyComponent; const Instance = - useAppRegistryComponent(AppRegistryComponents.INSTANCE_WORKSPACE) ?? - EmptyComponent; + useAppRegistryComponent('Instance.Workspace') ?? EmptyComponent; if (namespace.collection) { return ; diff --git a/packages/compass-home/src/components/workspace.tsx b/packages/compass-home/src/components/workspace.tsx index b51f75b143c..63a0b79970f 100644 --- a/packages/compass-home/src/components/workspace.tsx +++ b/packages/compass-home/src/components/workspace.tsx @@ -3,10 +3,7 @@ import { css } from '@mongodb-js/compass-components'; import WorkspaceContent from './workspace-content'; import type Namespace from '../types/namespace'; -import { - AppRegistryComponents, - useAppRegistryComponent, -} from '../contexts/app-registry-context'; +import { useAppRegistryComponent } from 'hadron-app-registry'; const verticalSplitStyles = css({ width: '100vw', @@ -42,12 +39,8 @@ export default function Workspace({ }: { namespace: Namespace; }): React.ReactElement { - const SidebarComponent = useAppRegistryComponent( - AppRegistryComponents.SIDEBAR_COMPONENT - ); - const GlobalShellComponent = useAppRegistryComponent( - AppRegistryComponents.SHELL_COMPONENT - ); + const SidebarComponent = useAppRegistryComponent('Sidebar.Component'); + const GlobalShellComponent = useAppRegistryComponent('Global.Shell'); return ( <> diff --git a/packages/compass-home/src/contexts/app-registry-context.ts b/packages/compass-home/src/contexts/app-registry-context.ts deleted file mode 100644 index edea88865a8..00000000000 --- a/packages/compass-home/src/contexts/app-registry-context.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { createContext, useContext, useState } from 'react'; -import type React from 'react'; -import AppRegistry from 'hadron-app-registry'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; - -const { debug } = createLoggerAndTelemetry('COMPASS-HOME-UI'); - -const AppRegistryContext = createContext(new AppRegistry()); -export default AppRegistryContext; - -export enum AppRegistryRoles { - APPLICATION_CONNECT = 'Application.Connect', - GLOBAL_MODAL = 'Global.Modal', -} - -export enum AppRegistryComponents { - SIDEBAR_COMPONENT = 'Sidebar.Component', - SHELL_COMPONENT = 'Global.Shell', - COLLECTION_WORKSPACE = 'Collection.Workspace', - DATABASE_WORKSPACE = 'Database.Workspace', - INSTANCE_WORKSPACE = 'Instance.Workspace', -} - -export const useAppRegistryContext = (): AppRegistry => - useContext(AppRegistryContext); -export const useAppRegistryComponent = ( - componentName: AppRegistryComponents -): React.JSXElementConstructor | null => { - const appRegistry = useContext(AppRegistryContext); - - const [component] = useState(() => { - const newComponent = appRegistry.getComponent(componentName); - if (!newComponent) { - debug( - `home plugin loading component, but ${String(componentName)} is NULL` - ); - } - return newComponent; - }); - - return component ? component : null; -}; - -export function useAppRegistryRole( - roleName: AppRegistryRoles.APPLICATION_CONNECT | AppRegistryRoles.GLOBAL_MODAL -): - | { - component: React.JSXElementConstructor; - name: string; - }[] - | null; -export function useAppRegistryRole(roleName: AppRegistryRoles): - | { - component: React.JSXElementConstructor<{ - isDataLake?: boolean; - }>; - }[] - | null { - const appRegistry = useContext(AppRegistryContext); - - const [role] = useState(() => { - const newRole = appRegistry.getRole(roleName); - if (!newRole) { - debug(`home plugin loading role, but ${String(roleName)} is NULL`); - } - return newRole; - }); - - return role ? role : null; -} diff --git a/packages/compass-home/src/index.ts b/packages/compass-home/src/index.ts index 094fe4ecf63..a0e553e4015 100644 --- a/packages/compass-home/src/index.ts +++ b/packages/compass-home/src/index.ts @@ -1,22 +1,37 @@ -import type AppRegistry from 'hadron-app-registry'; -import HomePlugin from './plugin'; +import { registerHadronPlugin } from 'hadron-app-registry'; +import Home from './components/home'; /** * Activate all the components in the Home package. - * @param {Object} appRegistry - The Hadron appRegisrty to activate this plugin with. **/ -function activate(appRegistry: AppRegistry): void { - appRegistry.registerComponent('Home.Home', HomePlugin); +function activate(): void { + // noop } /** * Deactivate all the components in the Home package. - * @param {Object} appRegistry - The Hadron appRegisrty to deactivate this plugin with. **/ -function deactivate(appRegistry: AppRegistry): void { - appRegistry.deregisterComponent('Home.Home'); +function deactivate(): void { + // noop } -export default HomePlugin; +export const CompassHomePlugin = registerHadronPlugin({ + name: 'CompassHome', + component: Home, + activate(/* ..., { globalAppRegistry, localAppRegistry } */) { + // TODO: This is where we should be subscribing to appRegistry events + // instead of passing it directly to the Home component. Keeping it as-is + // as cleaning up compass-home is a bigger refactor that is not in scope + return { + store: { + state: {}, + }, + deactivate() { + /* noop */ + }, + }; + }, +}); + export { activate, deactivate }; export { default as metadata } from '../package.json'; diff --git a/packages/compass-home/src/plugin.tsx b/packages/compass-home/src/plugin.tsx deleted file mode 100644 index a0b4b854085..00000000000 --- a/packages/compass-home/src/plugin.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import type AppRegistry from 'hadron-app-registry'; -import Home from './components/home'; -import AppRegistryContext from './contexts/app-registry-context'; - -function Plugin({ - appRegistry, - ...homeProps -}: { - appRegistry: AppRegistry; -} & React.ComponentProps): React.ReactElement { - return ( - - - - ); -} - -Plugin.displayName = 'HomePlugin'; - -export default Plugin; diff --git a/packages/compass-import-export/src/components/export-modal.tsx b/packages/compass-import-export/src/components/export-modal.tsx index 791842997bb..548ef708bb5 100644 --- a/packages/compass-import-export/src/components/export-modal.tsx +++ b/packages/compass-import-export/src/components/export-modal.tsx @@ -17,7 +17,7 @@ import { spacing, createElectronFileInputBackend, } from '@mongodb-js/compass-components'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; import { closeExport, @@ -153,8 +153,7 @@ function ExportModal({ } }, [isOpen], - undefined, - React + undefined ); const onClickBack = useCallback(() => { diff --git a/packages/compass-import-export/src/components/import-modal.tsx b/packages/compass-import-export/src/components/import-modal.tsx index fc669218c10..f8c75257285 100644 --- a/packages/compass-import-export/src/components/import-modal.tsx +++ b/packages/compass-import-export/src/components/import-modal.tsx @@ -16,7 +16,7 @@ import { palette, useDarkMode, } from '@mongodb-js/compass-components'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; import { FINISHED_STATUSES, STARTED } from '../constants/process-status'; import type { ProcessStatus } from '../constants/process-status'; @@ -162,8 +162,7 @@ function ImportModal({ } }, [isOpen], - undefined, - React + undefined ); if (isOpen && !fileName && errors.length === 0) { diff --git a/packages/compass-import-export/src/components/import-toast.tsx b/packages/compass-import-export/src/components/import-toast.tsx index 9120c0ad4da..5eaaf7653be 100644 --- a/packages/compass-import-export/src/components/import-toast.tsx +++ b/packages/compass-import-export/src/components/import-toast.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Body, css, openToast } from '@mongodb-js/compass-components'; import path from 'path'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { ToastBody } from './toast-body'; import { openFile } from '../utils/open-file'; diff --git a/packages/compass-import-export/src/modules/export.ts b/packages/compass-import-export/src/modules/export.ts index 923e8e61461..b9295aa1707 100644 --- a/packages/compass-import-export/src/modules/export.ts +++ b/packages/compass-import-export/src/modules/export.ts @@ -1,7 +1,7 @@ import type { Action, AnyAction, Reducer } from 'redux'; import { combineReducers } from 'redux'; import type { ThunkAction } from 'redux-thunk'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import fs from 'fs'; import _ from 'lodash'; diff --git a/packages/compass-import-export/src/modules/import.ts b/packages/compass-import-export/src/modules/import.ts index 628ecfb6341..49864582e88 100644 --- a/packages/compass-import-export/src/modules/import.ts +++ b/packages/compass-import-export/src/modules/import.ts @@ -11,7 +11,7 @@ import path from 'path'; import { combineReducers } from 'redux'; import type { Action, AnyAction, Reducer } from 'redux'; import type { ThunkAction, ThunkDispatch } from 'redux-thunk'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import PROCESS_STATUS from '../constants/process-status'; import FILE_TYPES from '../constants/file-types'; diff --git a/packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx b/packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx index b6f5cacf2fb..80e8d246943 100644 --- a/packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx +++ b/packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import { connect } from 'react-redux'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; import { Modal, ModalFooter, @@ -59,8 +59,7 @@ function CreateIndexModal({ } }, [isVisible], - undefined, - React + undefined ); return ( diff --git a/packages/compass-indexes/src/components/drop-index-modal/index.tsx b/packages/compass-indexes/src/components/drop-index-modal/index.tsx index 746857afdd8..82eeffb60e3 100644 --- a/packages/compass-indexes/src/components/drop-index-modal/index.tsx +++ b/packages/compass-indexes/src/components/drop-index-modal/index.tsx @@ -20,7 +20,7 @@ import { dropIndex } from '../../modules/drop-index'; import { resetForm } from '../../modules/reset-form'; import type { RootState } from '../../modules/drop-index'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; const messageStyles = css({ display: 'flex', @@ -127,8 +127,7 @@ export function DropIndexModal({ } }, [isVisible], - undefined, - React + undefined ); return ( diff --git a/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.tsx b/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.tsx index 63c92bf0623..27d089ec2ef 100644 --- a/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.tsx +++ b/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.tsx @@ -32,7 +32,7 @@ import { import type { EditorRef } from '@mongodb-js/compass-editor'; import _parseShellBSON, { ParseMode } from 'ejson-shell-parser'; import type { Document } from 'mongodb'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; import { SearchIndexTemplateDropdown } from '../search-index-template-dropdown'; import type { SearchTemplate } from '@mongodb-js/mongodb-constants'; import type { Field } from '../../modules/fields'; @@ -160,8 +160,7 @@ export const BaseSearchIndexModal: React.FunctionComponent< } }, [isModalOpen, mode], - undefined, - React + undefined ); useEffect(() => { diff --git a/packages/compass-logging/package.json b/packages/compass-logging/package.json index 446d3f818c0..37ae4496859 100644 --- a/packages/compass-logging/package.json +++ b/packages/compass-logging/package.json @@ -19,16 +19,22 @@ "url": "https://github.com/mongodb-js/compass.git" }, "files": [ - "dist" + "dist", + "provier.js" ], "license": "SSPL", "peerDependencies": { "hadron-ipc": "^3.2.4" }, "main": "dist/index.js", + "exports": { + ".": "./dist/index.js", + "./provider": "./dist/provider.js" + }, "compass:main": "src/index.ts", "compass:exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./provider": "./src/provider.ts" }, "types": "./dist/index.d.ts", "scripts": { @@ -50,7 +56,8 @@ "dependencies": { "debug": "^4.3.4", "is-electron-renderer": "^2.0.1", - "mongodb-log-writer": "^1.3.0" + "mongodb-log-writer": "^1.3.0", + "react": "^17.0.2" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.0.11", diff --git a/packages/compass-logging/provider.d.ts b/packages/compass-logging/provider.d.ts new file mode 100644 index 00000000000..6084d76ab26 --- /dev/null +++ b/packages/compass-logging/provider.d.ts @@ -0,0 +1 @@ +export * from './dist/provider.d'; diff --git a/packages/compass-logging/src/index.spec.ts b/packages/compass-logging/src/index.spec.ts index 2e0e2aa7c10..a87a59bf310 100644 --- a/packages/compass-logging/src/index.spec.ts +++ b/packages/compass-logging/src/index.spec.ts @@ -1,4 +1,4 @@ -import createLoggerAndTelemetry from './'; +import { createLoggerAndTelemetry } from './'; import { once } from 'events'; import { expect } from 'chai'; diff --git a/packages/compass-logging/src/index.ts b/packages/compass-logging/src/index.ts index 6486a3216ff..da53760fa07 100644 --- a/packages/compass-logging/src/index.ts +++ b/packages/compass-logging/src/index.ts @@ -1,11 +1,5 @@ -export { createLoggerAndTelemetry } from './logger'; -export { createLoggerAndTelemetry as default } from './logger'; +export { createLoggerAndTelemetry } from './ipc-logger'; export type { LoggerAndTelemetry } from './logger'; -export { - useLoggerAndTelemetry, - useTrackOnChange, - withLoggerAndTelemetry, -} from './react'; export { mongoLogId } from 'mongodb-log-writer'; import createDebug from 'debug'; export const debug = createDebug('mongodb-compass'); diff --git a/packages/compass-logging/src/ipc-logger.ts b/packages/compass-logging/src/ipc-logger.ts new file mode 100644 index 00000000000..1c548d7e378 --- /dev/null +++ b/packages/compass-logging/src/ipc-logger.ts @@ -0,0 +1,25 @@ +import isElectronRenderer from 'is-electron-renderer'; +import type { HadronIpcRenderer } from 'hadron-ipc'; +import { createGenericLoggerAndTelemetry } from './logger'; + +function emit( + ipc: HadronIpcRenderer | null | undefined, + event: string, + data: Record +): void { + // We use ipc.callQuiet instead of ipc.call because we already + // print debugging messages below + void ipc?.callQuiet?.(event, data); + if (typeof process !== 'undefined' && typeof process.emit === 'function') { + (process as any).emit(event, data); + } +} + +export function createLoggerAndTelemetry(component: string) { + // This application may not be running in an Node.js/Electron context. + const ipc: HadronIpcRenderer | null | undefined = isElectronRenderer + ? // eslint-disable-next-line @typescript-eslint/no-var-requires + require('hadron-ipc').ipcRenderer + : null; + return createGenericLoggerAndTelemetry(component, emit.bind(null, ipc)); +} diff --git a/packages/compass-logging/src/logger.ts b/packages/compass-logging/src/logger.ts index 57ecaef9119..0d4cddd3b49 100644 --- a/packages/compass-logging/src/logger.ts +++ b/packages/compass-logging/src/logger.ts @@ -1,9 +1,7 @@ import type { MongoLogEntry } from 'mongodb-log-writer'; import { MongoLogWriter, mongoLogId } from 'mongodb-log-writer'; -import isElectronRenderer from 'is-electron-renderer'; import createDebug from 'debug'; import type { Writable } from 'stream'; -import type { HadronIpcRenderer } from 'hadron-ipc'; let preferences: { getPreferences(): { trackUsageStatistics: boolean }; @@ -12,19 +10,6 @@ let preferences: { type TrackProps = Record | (() => Record); type TrackFunction = (event: string, properties?: TrackProps) => void; -function emit( - ipc: HadronIpcRenderer | null | undefined, - event: string, - data: Record -): void { - // We use ipc.callQuiet instead of ipc.call because we already - // print debugging messages below - void ipc?.callQuiet?.(event, data); - if (typeof process !== 'undefined' && typeof process.emit === 'function') { - (process as any).emit(event, data); - } -} - export type LoggerAndTelemetry = { log: ReturnType; mongoLogId: typeof mongoLogId; @@ -32,22 +17,17 @@ export type LoggerAndTelemetry = { track: TrackFunction; }; -export function createLoggerAndTelemetry( - component: string +export function createGenericLoggerAndTelemetry( + component: string, + emit: (event: string, arg: any) => void ): LoggerAndTelemetry { - // This application may not be running in an Node.js/Electron context. - const ipc: HadronIpcRenderer | null | undefined = isElectronRenderer - ? // eslint-disable-next-line @typescript-eslint/no-var-requires - require('hadron-ipc').ipcRenderer - : null; - // Do not create an actual Writable stream here, since the callback // logic in Node.js streams would mean that two writes from the // same event loop tick would not be written synchronously, // allowing another logger's write to be written out-of-order. const target = { write(line: string, callback: () => void) { - emit(ipc, 'compass:log', { line }); + emit('compass:log', { line }); callback(); }, end(callback: () => void) { @@ -99,7 +79,7 @@ export function createLoggerAndTelemetry( // for instance if we can't fetch host information, // we track a new error indicating the failure. // This is so that we are aware of which events might be misreported. - emit(ipc, 'compass:track', { + emit('compass:track', { event: 'Error Fetching Attributes', properties: { event_name: event, @@ -118,7 +98,7 @@ export function createLoggerAndTelemetry( return; } } - emit(ipc, 'compass:track', data); + emit('compass:track', data); }; const debug = createDebug(`mongodb-compass:${component.toLowerCase()}`); @@ -136,5 +116,3 @@ export function createLoggerAndTelemetry( track, }; } - -export default createLoggerAndTelemetry; diff --git a/packages/compass-logging/src/provider.ts b/packages/compass-logging/src/provider.ts new file mode 100644 index 00000000000..2836f6295a5 --- /dev/null +++ b/packages/compass-logging/src/provider.ts @@ -0,0 +1,71 @@ +import React from 'react'; +import type { LoggerAndTelemetry } from './logger'; +export type { LoggerAndTelemetry } from './logger'; + +function defaultCreateLoggerAndTelemetry(component: string) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('./logger').createGenericLoggerAndTelemetry(component, () => { + /* ignore */ + }); +} + +const LoggerAndTelemetryContext = React.createContext< + (component: string) => LoggerAndTelemetry +>(defaultCreateLoggerAndTelemetry); + +export const LoggerAndTelemetryProvider = LoggerAndTelemetryContext.Provider; + +export function createLoggerAndTelemetryLocator(component: string) { + return useLoggerAndTelemetry.bind(null, component); +} + +export function useLoggerAndTelemetry(component: string): LoggerAndTelemetry { + const createLoggerAndTelemetry = React.useContext(LoggerAndTelemetryContext); + if (!createLoggerAndTelemetry) { + throw new Error('LoggerAndTelemetry service is missing from React context'); + } + const loggerRef = React.createRef(); + if (!loggerRef.current) { + (loggerRef as any).current = createLoggerAndTelemetry(component); + } + return loggerRef.current!; +} + +export function useTrackOnChange( + component: string, + onChange: (track: LoggerAndTelemetry['track']) => void, + dependencies: unknown[], + options: { skipOnMount: boolean } = { skipOnMount: false } +) { + const onChangeRef = React.useRef(onChange); + onChangeRef.current = onChange; + const { track } = useLoggerAndTelemetry(component); + let initial = true; + React.useEffect(() => { + if (options.skipOnMount && initial) { + initial = false; + return; + } + onChangeRef.current(track); + }, [...dependencies, track]); +} + +type FirstArgument = F extends (...args: [infer A, ...any]) => any + ? A + : F extends { new (...args: [infer A, ...any]): any } + ? A + : never; +export function withLoggerAndTelemetry< + T extends ((...args: any[]) => any) | { new (...args: any[]): any } +>( + ReactComponent: T, + component: string +): React.FunctionComponent, 'logger'>> { + const WithLoggerAndTelemetry = ( + props: Omit, 'logger'> & React.Attributes + ) => { + const logger = useLoggerAndTelemetry(component); + return React.createElement(ReactComponent, { ...props, logger }); + }; + return WithLoggerAndTelemetry; +} diff --git a/packages/compass-logging/src/react.ts b/packages/compass-logging/src/react.ts deleted file mode 100644 index 63eafc30a9f..00000000000 --- a/packages/compass-logging/src/react.ts +++ /dev/null @@ -1,59 +0,0 @@ -import createLoggerAndTelemetry from './logger'; -import type { LoggerAndTelemetry } from './logger'; - -export function useLoggerAndTelemetry( - component: string, - React: { useRef: any } -): LoggerAndTelemetry { - const loggerRef = React.useRef(); - if (!loggerRef.current) { - loggerRef.current = createLoggerAndTelemetry(component); - } - return loggerRef.current as LoggerAndTelemetry; -} - -export function useTrackOnChange( - component: string, - onChange: (track: LoggerAndTelemetry['track']) => void, - dependencies: unknown[], - options: { skipOnMount: boolean } = { skipOnMount: false }, - React: { useRef: any; useEffect: any } -) { - const onChangeRef = React.useRef(onChange); - onChangeRef.current = onChange; - const { track } = useLoggerAndTelemetry(component, React); - let initial = true; - React.useEffect(() => { - if (options.skipOnMount && initial) { - initial = false; - return; - } - onChangeRef.current(track); - }, [...dependencies, track]); -} - -type ComponentProps = T extends (props: infer P) => any - ? P - : T extends { new (props: infer P): any } - ? P - : never; - -type ComponentReturnType = T extends (...args: any[]) => infer R - ? R - : T extends { new (...args: any[]): { render(...args: any[]): infer R } } - ? R - : never; - -export function withLoggerAndTelemetry( - ReactComponent: T, - component: string, - React: any -) { - const WithLoggerAndTelemetry = ( - props: Omit, 'logger'> - ): ComponentReturnType => { - const logger = useLoggerAndTelemetry(component, React); - return React.createElement(ReactComponent, { ...props, logger }); - }; - return WithLoggerAndTelemetry; -} diff --git a/packages/compass-query-bar/src/components/query-ai.tsx b/packages/compass-query-bar/src/components/query-ai.tsx index 289620d9ffa..70ccca4a936 100644 --- a/packages/compass-query-bar/src/components/query-ai.tsx +++ b/packages/compass-query-bar/src/components/query-ai.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { openToast } from '@mongodb-js/compass-components'; import { GenerativeAIInput } from '@mongodb-js/compass-generative-ai'; import { connect } from 'react-redux'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { usePreference } from 'compass-preferences-model'; import type { RootState } from '../stores/query-bar-store'; diff --git a/packages/compass-query-bar/src/components/query-history-button-popover.tsx b/packages/compass-query-bar/src/components/query-history-button-popover.tsx index 2c7320051ce..7ddadcdcb8e 100644 --- a/packages/compass-query-bar/src/components/query-history-button-popover.tsx +++ b/packages/compass-query-bar/src/components/query-history-button-popover.tsx @@ -7,7 +7,7 @@ import { focusRing, spacing, } from '@mongodb-js/compass-components'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; import QueryHistory from './query-history'; import { fetchSavedQueries } from '../stores/query-bar-reducer'; @@ -51,8 +51,7 @@ const QueryHistoryButtonPopover = ({ } }, [isOpen], - undefined, - React + undefined ); const setOpen = useCallback( diff --git a/packages/compass-query-bar/src/components/query-history/index.tsx b/packages/compass-query-bar/src/components/query-history/index.tsx index 7b26b68e321..d89304f16fd 100644 --- a/packages/compass-query-bar/src/components/query-history/index.tsx +++ b/packages/compass-query-bar/src/components/query-history/index.tsx @@ -7,7 +7,7 @@ import FavoriteList from './favorite-list'; import { connect } from 'react-redux'; import type { RootState } from '../../stores/query-bar-store'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; const containerStyle = css({ display: 'flex', @@ -42,8 +42,7 @@ const QueryHistory = ({ namespace }: QueryHistoryProps) => { } }, [tab], - undefined, - React + undefined ); return ( diff --git a/packages/compass-query-bar/src/stores/ai-query-reducer.ts b/packages/compass-query-bar/src/stores/ai-query-reducer.ts index aae74f7fda3..e04579268f1 100644 --- a/packages/compass-query-bar/src/stores/ai-query-reducer.ts +++ b/packages/compass-query-bar/src/stores/ai-query-reducer.ts @@ -1,5 +1,5 @@ import type { Reducer } from 'redux'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { getSimplifiedSchema } from 'mongodb-schema'; import toNS from 'mongodb-ns'; import preferences from 'compass-preferences-model'; diff --git a/packages/compass-schema/src/modules/schema-analysis.ts b/packages/compass-schema/src/modules/schema-analysis.ts index 18d19db31b1..48f7f3f2563 100644 --- a/packages/compass-schema/src/modules/schema-analysis.ts +++ b/packages/compass-schema/src/modules/schema-analysis.ts @@ -1,6 +1,6 @@ import { isInternalFieldPath } from 'hadron-document'; import type { AggregateOptions, Filter, Document } from 'mongodb'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import type { DataService } from 'mongodb-data-service'; import mongodbSchema from 'mongodb-schema'; import type { diff --git a/packages/compass-settings/src/components/index.tsx b/packages/compass-settings/src/components/index.tsx index cd7526f7e5d..60573fbce09 100644 --- a/packages/compass-settings/src/components/index.tsx +++ b/packages/compass-settings/src/components/index.tsx @@ -1,16 +1,14 @@ import React from 'react'; import SettingsModal from './modal'; -import { Provider } from 'react-redux'; -import store from '../stores'; import { ConfirmationModalArea } from '@mongodb-js/compass-components'; const SettingsModalWithStore: React.FunctionComponent = () => { return ( - + <> - + ); }; diff --git a/packages/compass-settings/src/components/modal.spec.tsx b/packages/compass-settings/src/components/modal.spec.tsx index a08c9660101..4ae832b0b88 100644 --- a/packages/compass-settings/src/components/modal.spec.tsx +++ b/packages/compass-settings/src/components/modal.spec.tsx @@ -13,7 +13,7 @@ import { expect } from 'chai'; import { Provider } from 'react-redux'; import userEvent from '@testing-library/user-event'; -import store from '../stores'; +import { configureStore } from '../stores'; import { SettingsModal } from './modal'; describe('SettingsModal', function () { @@ -29,6 +29,7 @@ describe('SettingsModal', function () { fetchSettingsSpy = stub().resolves(); onSaveSpy = spy(); + const store = configureStore({ logger: stub() as any }); renderSettingsModal = ( props: Partial> = {} ) => { diff --git a/packages/compass-settings/src/components/settings/general.spec.tsx b/packages/compass-settings/src/components/settings/general.spec.tsx index acb0c83c8c1..6f9b99bc4ca 100644 --- a/packages/compass-settings/src/components/settings/general.spec.tsx +++ b/packages/compass-settings/src/components/settings/general.spec.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { cleanup, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect } from 'chai'; +import { stub } from 'sinon'; import { Provider } from 'react-redux'; import { GeneralSettings } from './general'; import { configureStore } from '../../stores'; @@ -16,7 +17,7 @@ describe('GeneralSettings', function () { } beforeEach(async function () { - store = configureStore(); + store = configureStore({ logger: stub() as any }); await store.dispatch(fetchSettings()); const component = () => ( diff --git a/packages/compass-settings/src/components/settings/oidc-settings.spec.tsx b/packages/compass-settings/src/components/settings/oidc-settings.spec.tsx index 5e983fc1aec..72af1f46310 100644 --- a/packages/compass-settings/src/components/settings/oidc-settings.spec.tsx +++ b/packages/compass-settings/src/components/settings/oidc-settings.spec.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { cleanup, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect } from 'chai'; +import { stub } from 'sinon'; import { Provider } from 'react-redux'; import { OIDCSettings } from './oidc-settings'; import { configureStore } from '../../stores'; @@ -16,7 +17,7 @@ describe('OIDCSettings', function () { } beforeEach(async function () { - store = configureStore(); + store = configureStore({ logger: stub() as any }); await store.dispatch(fetchSettings()); const component = () => ( diff --git a/packages/compass-settings/src/components/settings/privacy.spec.tsx b/packages/compass-settings/src/components/settings/privacy.spec.tsx index ab97991ff99..a4772d4be33 100644 --- a/packages/compass-settings/src/components/settings/privacy.spec.tsx +++ b/packages/compass-settings/src/components/settings/privacy.spec.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { cleanup, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect } from 'chai'; +import { stub } from 'sinon'; import { Provider } from 'react-redux'; import { PrivacySettings } from './privacy'; import { configureStore } from '../../stores'; @@ -29,7 +30,7 @@ describe('PrivacySettings', function () { } beforeEach(async function () { - store = configureStore(); + store = configureStore({ logger: stub() as any }); await store.dispatch(fetchSettings()); }); diff --git a/packages/compass-settings/src/index.ts b/packages/compass-settings/src/index.ts index c68341a6817..2225ee39bfb 100644 --- a/packages/compass-settings/src/index.ts +++ b/packages/compass-settings/src/index.ts @@ -1,21 +1,24 @@ -import type AppRegistry from 'hadron-app-registry'; +import { registerHadronPlugin } from 'hadron-app-registry'; +import { createLoggerAndTelemetryLocator } from '@mongodb-js/compass-logging/provider'; import SettingsPlugin from './components/index'; -import SettingsStore from './stores'; +import { onActivated } from './stores'; -const ROLE = { - name: 'SettingsModal', - component: SettingsPlugin, -}; - -function activate(appRegistry: AppRegistry): void { - appRegistry.registerRole('Global.Modal', ROLE); - appRegistry.registerStore('Settings.Store', SettingsStore); +function activate(): void { + // noop } -function deactivate(appRegistry: AppRegistry): void { - appRegistry.deregisterRole('Global.Modal', ROLE); - appRegistry.deregisterStore('Settings.Store'); +function deactivate(): void { + // noop } +export const CompassSettingsPlugin = registerHadronPlugin( + { + name: 'CompassSettings', + component: SettingsPlugin, + activate: onActivated, + }, + { logger: createLoggerAndTelemetryLocator('COMPASS-SETTINGS') } +); + export { activate, deactivate }; export { default as metadata } from '../package.json'; diff --git a/packages/compass-settings/src/stores/index.ts b/packages/compass-settings/src/stores/index.ts index 02992afd2f6..578674f6da5 100644 --- a/packages/compass-settings/src/stores/index.ts +++ b/packages/compass-settings/src/stores/index.ts @@ -13,18 +13,22 @@ import atlasLoginReducer, { atlasServiceTokenRefreshFailed, atlasServiceUserConfigChanged, } from './atlas-login'; +import type { LoggerAndTelemetry } from '@mongodb-js/compass-logging/provider'; export type Public = { [K in keyof T]: T[K] }; -export function configureStore({ - preferencesSandbox, - atlasService, -}: { - preferencesSandbox?: Public; - atlasService?: Public; -} = {}) { - preferencesSandbox ??= new PreferencesSandbox(); - atlasService ??= new AtlasService(); +type ThunkExtraArg = { + preferencesSandbox: Public; + atlasService: Public; + logger: LoggerAndTelemetry; +}; + +export function configureStore( + options: Pick & Partial +) { + const preferencesSandbox = + options?.preferencesSandbox ?? new PreferencesSandbox(); + const atlasService = options?.atlasService ?? new AtlasService(); const store = createStore( combineReducers({ @@ -35,7 +39,11 @@ export function configureStore({ atlasLogin: ReturnType; }>, // combineReducers CombinedState return type is broken, have to remove the EmptyObject from the union that it returns applyMiddleware( - thunk.withExtraArgument({ preferencesSandbox, atlasService }) + thunk.withExtraArgument({ + preferencesSandbox, + atlasService, + logger: options.logger, + }) ) ); @@ -58,32 +66,38 @@ export function configureStore({ return store; } -const store = configureStore(); - -export type RootState = ReturnType; +export type RootState = ReturnType< + ReturnType['getState'] +>; export type SettingsThunkAction< R, A extends AnyAction = AnyAction -> = ThunkAction< - R, - RootState, +> = ThunkAction; + +const onActivated = ( + _: unknown, { - preferencesSandbox: Public; - atlasService: Public; - }, - A ->; + globalAppRegistry, + logger, + }: { globalAppRegistry: AppRegistry; logger: LoggerAndTelemetry } +) => { + const store = configureStore({ logger }); -(store as any).onActivated = (appRegistry: AppRegistry) => { - appRegistry.on('open-compass-settings', () => { + const onOpenSettings = () => { void store.dispatch(openModal()); - }); - ipcRenderer?.on('window:show-settings', () => { - void store.dispatch(openModal()); - }); -}; + }; + + globalAppRegistry.on('open-compass-settings', onOpenSettings); + ipcRenderer?.on('window:show-settings', onOpenSettings); -export default store as typeof store & { - onActivated(appRegistry: AppRegistry): void; + return { + store, + deactivate() { + globalAppRegistry.removeListener('open-compass-settings', onOpenSettings); + ipcRenderer?.removeListener('window:show-settings', onOpenSettings); + }, + }; }; + +export { onActivated }; diff --git a/packages/compass-settings/src/stores/settings.ts b/packages/compass-settings/src/stores/settings.ts index ff986877077..d48b7ce3d0a 100644 --- a/packages/compass-settings/src/stores/settings.ts +++ b/packages/compass-settings/src/stores/settings.ts @@ -1,14 +1,11 @@ import type { Reducer } from 'redux'; import type { SettingsThunkAction } from '.'; -import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import type { PreferenceStateInformation, UserConfigurablePreferences, } from 'compass-preferences-model'; import { cancelAtlasLoginAttempt } from './atlas-login'; -const { log, mongoLogId } = createLoggerAndTelemetry('COMPASS-SETTINGS'); - export type State = { isModalOpen: boolean } & ( | { loadingState: 'loading'; @@ -123,15 +120,15 @@ export const fetchSettings = (): SettingsThunkAction> => { return async ( dispatch, _getState, - { preferencesSandbox: sandbox } + { preferencesSandbox: sandbox, logger } ): Promise => { try { dispatch({ type: ActionTypes.SettingsFetchedStart }); await sandbox.setupSandbox(); await dispatch(syncSandboxStateToStore()); } catch (e) { - log.warn( - mongoLogId(1_001_000_145), + logger.log.warn( + logger.mongoLogId(1_001_000_145), 'Settings', 'Failed to fetch settings', { message: (e as Error).message } @@ -147,7 +144,7 @@ export const changeFieldValue = ( return async ( dispatch, getState, - { preferencesSandbox: sandbox } + { preferencesSandbox: sandbox, logger } ): Promise => { const { loadingState } = getState().settings; if (loadingState === 'loading') { @@ -163,8 +160,8 @@ export const changeFieldValue = ( // can fail if user input doesn't pass validation. await sandbox.updateField(field, value); } catch (err) { - log.error( - mongoLogId(1_001_000_223), + logger.log.error( + logger.mongoLogId(1_001_000_223), 'Settings', 'Failed to change settings value', { error: (err as Error).stack } @@ -194,7 +191,7 @@ export const saveSettings = (): SettingsThunkAction> => { return async ( dispatch, getState, - { preferencesSandbox: sandbox } + { preferencesSandbox: sandbox, logger } ): Promise => { const { loadingState } = getState().settings; if (loadingState === 'loading') { @@ -208,8 +205,8 @@ export const saveSettings = (): SettingsThunkAction> => { type: ActionTypes.SettingsSaved, }); } catch (e) { - log.warn( - mongoLogId(1_001_000_146), + logger.log.warn( + logger.mongoLogId(1_001_000_146), 'Settings', 'Failed to update settings', { message: (e as Error).message } diff --git a/packages/compass-shell/package.json b/packages/compass-shell/package.json index d608a546fca..d5b0376c027 100644 --- a/packages/compass-shell/package.json +++ b/packages/compass-shell/package.json @@ -86,7 +86,6 @@ "enzyme": "^3.11.0", "eslint": "^7.25.0", "hadron-app-registry": "^9.0.13", - "hadron-ipc": "^3.2.4", "mocha": "^10.2.0", "nyc": "^15.1.0", "prop-types": "^15.7.2", diff --git a/packages/compass-shell/src/components/shell-info-modal/shell-info-modal.jsx b/packages/compass-shell/src/components/shell-info-modal/shell-info-modal.jsx index c009ebc32ab..05fd7a2e233 100644 --- a/packages/compass-shell/src/components/shell-info-modal/shell-info-modal.jsx +++ b/packages/compass-shell/src/components/shell-info-modal/shell-info-modal.jsx @@ -8,7 +8,7 @@ import { Subtitle, spacing, } from '@mongodb-js/compass-components'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; import { KeyboardShortcutsTable } from './keyboard-shortcuts-table'; diff --git a/packages/compass-shell/src/stores/store.js b/packages/compass-shell/src/stores/store.js index c724c65aa54..29d7744d6d9 100644 --- a/packages/compass-shell/src/stores/store.js +++ b/packages/compass-shell/src/stores/store.js @@ -3,7 +3,7 @@ import reducer from '../modules'; import { changeEnableShell, setupRuntime } from '../modules/runtime'; import { globalAppRegistryActivated } from '@mongodb-js/mongodb-redux-common/app-registry'; import { setupLoggerAndTelemetry } from '@mongosh/logging'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import preferences from 'compass-preferences-model'; const { log, debug, track } = createLoggerAndTelemetry('COMPASS-SHELL'); diff --git a/packages/compass-sidebar/src/components/csfle-connection-modal.tsx b/packages/compass-sidebar/src/components/csfle-connection-modal.tsx index 1474c8e50bc..bc5aac62599 100644 --- a/packages/compass-sidebar/src/components/csfle-connection-modal.tsx +++ b/packages/compass-sidebar/src/components/csfle-connection-modal.tsx @@ -10,7 +10,7 @@ import { Toggle, InfoModal, } from '@mongodb-js/compass-components'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; const toggleStyles = css({ marginTop: spacing[3], @@ -57,8 +57,7 @@ export default function CSFLEConnectionModal({ } }, [open], - undefined, - React + undefined ); return ( diff --git a/packages/compass-sidebar/src/components/non-genuine-warning-modal.tsx b/packages/compass-sidebar/src/components/non-genuine-warning-modal.tsx index ce1afe4c5a2..dbf72ee1034 100644 --- a/packages/compass-sidebar/src/components/non-genuine-warning-modal.tsx +++ b/packages/compass-sidebar/src/components/non-genuine-warning-modal.tsx @@ -8,7 +8,7 @@ import { Body, BannerVariant, } from '@mongodb-js/compass-components'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; const modalBodyStyles = css({ marginTop: spacing[3], @@ -43,8 +43,7 @@ function NonGenuineWarningModal({ } }, [isVisible], - undefined, - React + undefined ); return ( diff --git a/packages/compass/src/app/index.jsx b/packages/compass/src/app/index.jsx index 84615e460fa..a947eb03bbe 100644 --- a/packages/compass/src/app/index.jsx +++ b/packages/compass/src/app/index.jsx @@ -4,12 +4,15 @@ import '../setup-hadron-distribution'; import dns from 'dns'; import { ipcRenderer } from 'hadron-ipc'; import * as remote from '@electron/remote'; - +import { AppRegistryProvider, globalAppRegistry } from 'hadron-app-registry'; import preferences, { getActiveUser } from 'compass-preferences-model'; +import { CompassHomePlugin } from '@mongodb-js/compass-home'; // https://github.com/nodejs/node/issues/40537 dns.setDefaultResultOrder('ipv4first'); +app.appRegistry = globalAppRegistry; + // this is so sub-processes (ie. the shell) will do the same process.env.NODE_OPTIONS ??= ''; if (!process.env.NODE_OPTIONS.includes('--dns-result-order')) { @@ -65,6 +68,7 @@ import { setupTheme } from './theme'; import { setupIntercom } from './intercom'; +import { LoggerAndTelemetryProvider } from '@mongodb-js/compass-logging/provider'; import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; const { log, mongoLogId, track } = createLoggerAndTelemetry('COMPASS-APP'); @@ -174,19 +178,16 @@ const Application = View.extend({ this.el = document.querySelector('#application'); this.renderWithTemplate(this); - const HomeComponent = app.appRegistry.getComponent('Home.Home'); - - if (!HomeComponent) { - throw new Error("Can't find Home plugin in appRegistry"); - } - ReactDOM.render( - + + + + + , this.queryByHook('layout-container') ); diff --git a/packages/compass/src/app/intercom/intercom-script.ts b/packages/compass/src/app/intercom/intercom-script.ts index 8291c066e1a..8069f691a4a 100644 --- a/packages/compass/src/app/intercom/intercom-script.ts +++ b/packages/compass/src/app/intercom/intercom-script.ts @@ -1,4 +1,4 @@ -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; const { debug, mongoLogId, log } = createLoggerAndTelemetry('COMPASS-INTERCOM'); const INTERCOM_SCRIPT_ELEM_ID = 'intercom-script'; diff --git a/packages/compass/src/app/intercom/setup-intercom.ts b/packages/compass/src/app/intercom/setup-intercom.ts index 279b517a97e..a67943715e4 100644 --- a/packages/compass/src/app/intercom/setup-intercom.ts +++ b/packages/compass/src/app/intercom/setup-intercom.ts @@ -1,4 +1,4 @@ -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import type { IntercomMetadata } from './intercom-script'; import { IntercomScript, buildIntercomScriptUrl } from './intercom-script'; diff --git a/packages/compass/src/main/application.ts b/packages/compass/src/main/application.ts index 8dd1ba31041..a15a1195d6d 100644 --- a/packages/compass/src/main/application.ts +++ b/packages/compass/src/main/application.ts @@ -16,7 +16,7 @@ import preferences, { } from 'compass-preferences-model'; import { AtlasService } from '@mongodb-js/atlas-service/main'; import { defaultsDeep } from 'lodash'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { setupTheme } from './theme'; import { setupProtocolHandlers } from './protocol-handling'; import { ConnectionStorage } from '@mongodb-js/connection-storage/main'; diff --git a/packages/compass/src/main/protocol-handling.ts b/packages/compass/src/main/protocol-handling.ts index fb4486333ad..9e6a3ddb60d 100644 --- a/packages/compass/src/main/protocol-handling.ts +++ b/packages/compass/src/main/protocol-handling.ts @@ -2,7 +2,7 @@ import preferencesAccess from 'compass-preferences-model'; import { promisify } from 'util'; import { app as electronApp } from 'electron'; import path from 'path'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { mongoLogId } from 'mongodb-log-writer'; import type { RegistryItem } from 'winreg-ts'; import { Registry } from 'winreg-ts'; diff --git a/packages/connection-storage/src/connection-storage.ts b/packages/connection-storage/src/connection-storage.ts index b6e9be83f4d..af38b1a664e 100644 --- a/packages/connection-storage/src/connection-storage.ts +++ b/packages/connection-storage/src/connection-storage.ts @@ -3,7 +3,7 @@ import { ipcMain } from 'hadron-ipc'; import keytar from 'keytar'; import type { ConnectionInfo } from './connection-info'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { type ConnectionSecrets, mergeSecrets, diff --git a/packages/connection-storage/src/import-export-connection.ts b/packages/connection-storage/src/import-export-connection.ts index a841c202d0f..3a4fc8533fd 100644 --- a/packages/connection-storage/src/import-export-connection.ts +++ b/packages/connection-storage/src/import-export-connection.ts @@ -4,7 +4,7 @@ import type { ConnectionInfo } from './connection-info'; import type { ConnectionSecrets } from './connection-secrets'; import { extractSecrets, mergeSecrets } from './connection-secrets'; import { Decrypter, Encrypter } from './encrypt'; -import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; const { log, mongoLogId, track } = createLoggerAndTelemetry( 'COMPASS-CONNECTION-IMPORT-EXPORT' diff --git a/packages/databases-collections/package.json b/packages/databases-collections/package.json index 0cbd3b6fe95..7c6f22cf760 100644 --- a/packages/databases-collections/package.json +++ b/packages/databases-collections/package.json @@ -69,7 +69,6 @@ "eslint": "^7.25.0", "hadron-app": "^5.15.0", "hadron-app-registry": "^9.0.13", - "hadron-ipc": "^3.2.4", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.0.0", diff --git a/packages/databases-collections/src/components/drop-collection-modal.tsx b/packages/databases-collections/src/components/drop-collection-modal.tsx index 904bfe4b4bd..880c8d44a04 100644 --- a/packages/databases-collections/src/components/drop-collection-modal.tsx +++ b/packages/databases-collections/src/components/drop-collection-modal.tsx @@ -14,7 +14,7 @@ import { import { dropCollection } from '../modules/drop-collection/drop-collection'; import { toggleIsVisible } from '../modules/is-visible'; import type { RootState } from '../modules/drop-collection/drop-collection'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; const progressContainerStyles = css({ display: 'flex', @@ -66,8 +66,7 @@ function DropCollectionModal({ } }, [isVisible], - undefined, - React + undefined ); return ( diff --git a/packages/databases-collections/src/components/drop-database-modal.tsx b/packages/databases-collections/src/components/drop-database-modal.tsx index af3a880da46..6292cd07961 100644 --- a/packages/databases-collections/src/components/drop-database-modal.tsx +++ b/packages/databases-collections/src/components/drop-database-modal.tsx @@ -14,7 +14,7 @@ import { import { dropDatabase } from '../modules/drop-database/drop-database'; import { toggleIsVisible } from '../modules/is-visible'; import type { RootState } from '../modules/drop-database/drop-database'; -import { useTrackOnChange } from '@mongodb-js/compass-logging'; +import { useTrackOnChange } from '@mongodb-js/compass-logging/provider'; const progressContainerStyles = css({ display: 'flex', @@ -68,8 +68,7 @@ function DropDatabaseModal({ } }, [isVisible], - undefined, - React + undefined ); return ( diff --git a/packages/hadron-app-registry/.mocharc.js b/packages/hadron-app-registry/.mocharc.js index 7e473d17b76..30aecfb78c3 100644 --- a/packages/hadron-app-registry/.mocharc.js +++ b/packages/hadron-app-registry/.mocharc.js @@ -1 +1 @@ -module.exports = require('@mongodb-js/mocha-config-compass'); +module.exports = require('@mongodb-js/mocha-config-compass/react'); diff --git a/packages/hadron-app-registry/package.json b/packages/hadron-app-registry/package.json index 0da8b54c032..be710b12b93 100644 --- a/packages/hadron-app-registry/package.json +++ b/packages/hadron-app-registry/package.json @@ -45,13 +45,17 @@ "dependencies": { "debug": "^4.2.0", "eventemitter3": "^4.0.0", - "reflux": "^0.4.1" + "reflux": "^0.4.1", + "redux": "^4.2.1", + "react": "^17.0.2", + "react-redux": "^8.0.5" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.0.11", "@mongodb-js/mocha-config-compass": "^1.3.2", "@mongodb-js/prettier-config-compass": "^1.0.1", "@mongodb-js/tsconfig-compass": "^1.0.3", + "@testing-library/react": "^12.1.4", "@types/chai": "^4.2.21", "@types/mocha": "^9.0.0", "@types/reflux": "^6.4.3", @@ -61,6 +65,7 @@ "eslint-config-mongodb-js": "^5.0.3", "mocha": "^10.2.0", "prettier": "^2.7.1", + "reflux-state-mixin": "github:mongodb-js/reflux-state-mixin", "sinon": "^9.0.0", "typescript": "^5.0.4" } diff --git a/packages/hadron-app-registry/src/app-registry.ts b/packages/hadron-app-registry/src/app-registry.ts index 2b9a49190d8..027456e6caf 100644 --- a/packages/hadron-app-registry/src/app-registry.ts +++ b/packages/hadron-app-registry/src/app-registry.ts @@ -1,4 +1,5 @@ import type { Store as RefluxStore } from 'reflux'; +import type { Store as ReduxStore } from 'redux'; import EventEmitter from 'eventemitter3'; import { Actions } from './actions'; @@ -17,14 +18,25 @@ interface Role { storeName?: string; configureStore?: (storeSetup: any) => any; order?: number; - hasQueryHistory?: boolean; } -type Store = Partial< - RefluxStore & { - onActivated?: (appRegistry: AppRegistry) => void; - } ->; +export type Store = (ReduxStore | Partial) & { + onActivated?: (appRegistry: AppRegistry) => void; +}; + +export function isReduxStore(store: Store): store is ReduxStore { + return ( + store && + typeof store === 'object' && + Object.prototype.hasOwnProperty.call(store, 'dispatch') + ); +} + +export interface Plugin { + store: Store; + actions?: Record; + deactivate?: () => void; +} /** * Is a registry for all user interface components, stores, and actions @@ -36,6 +48,7 @@ export class AppRegistry { components: Record>; stores: Record; roles: Record; + plugins: Record; storeMisses: Record; /** @@ -47,6 +60,7 @@ export class AppRegistry { this.components = {}; this.stores = {}; this.roles = {}; + this.plugins = {}; this.storeMisses = {}; } @@ -114,6 +128,11 @@ export class AppRegistry { return this; } + deregisterPlugin(name: string): this { + delete this.plugins[name]; + return this; + } + /** * Get an action for the name. * @@ -158,6 +177,10 @@ export class AppRegistry { return this.stores[name]; } + getPlugin(name: string): Plugin | undefined { + return this.plugins[name]; + } + /** * Calls onActivated on all the stores in the registry. * @@ -253,6 +276,20 @@ export class AppRegistry { return this; } + registerPlugin(name: string, plugin: Plugin): this { + this.plugins[name] = plugin; + return this; + } + + deactivate() { + for (const plugin of Object.values(this.plugins)) { + plugin.deactivate?.(); + } + for (const event of this.eventNames()) { + this.removeAllListeners(event); + } + } + /** * Adds a listener for the event name to the underlying event emitter. * diff --git a/packages/hadron-app-registry/src/index.ts b/packages/hadron-app-registry/src/index.ts index 4022f5359cb..497b41d2e2a 100644 --- a/packages/hadron-app-registry/src/index.ts +++ b/packages/hadron-app-registry/src/index.ts @@ -2,4 +2,16 @@ import { AppRegistry, globalAppRegistry } from './app-registry'; import type { Role } from './app-registry'; export { AppRegistry, globalAppRegistry }; export type { Role }; +export { + AppRegistryProvider, + useGlobalAppRegistry, + useLocalAppRegistry, + useAppRegistryComponent, + useAppRegistryRole, +} from './react-context'; +export { + registerHadronPlugin, + HadronPluginComponent, + HadronPluginConfig, +} from './register-plugin'; export default AppRegistry; diff --git a/packages/hadron-app-registry/src/react-context.tsx b/packages/hadron-app-registry/src/react-context.tsx new file mode 100644 index 00000000000..6eebdcbc776 --- /dev/null +++ b/packages/hadron-app-registry/src/react-context.tsx @@ -0,0 +1,142 @@ +import React, { + createContext, + useEffect, + useRef, + useContext, + useState, +} from 'react'; +import { globalAppRegistry, AppRegistry } from './app-registry'; +import createDebug from 'debug'; +const debug = createDebug('hadron-app-registry:react'); + +const GlobalAppRegistryContext = createContext(globalAppRegistry); +const LocalAppRegistryContext = createContext(null); + +type AppRegistryProviderProps = + | { + localAppRegistry?: never; + deactivateOnUnmount?: never; + children: React.ReactNode; + } + | { + /** + * localAppRegistry to be set in React context. By default will be created + * when this component renders. Can be used to preserve appRegistry state even + * if AppRegistryProvider is unmounted + * + * @example + * function CollectionTab({ id }) { + * return ( + * + * ... + * + * ) + * } + */ + localAppRegistry: AppRegistry; + + /** + * Deactivates all active plugins and remove all event listeners from the app + * registry when provider unmounts. Default is `true` + */ + deactivateOnUnmount?: boolean; + children: React.ReactNode; + }; + +export function AppRegistryProvider({ + children, + ...props +}: AppRegistryProviderProps) { + const initialPropsRef = useRef(props); + const { + localAppRegistry: initialLocalAppRegistry, + deactivateOnUnmount = true, + } = initialPropsRef.current; + + const globalAppRegistry = useGlobalAppRegistry(); + const isTopLevelProvider = useContext(LocalAppRegistryContext) === null; + const [localAppRegistry] = useState(() => { + return ( + initialLocalAppRegistry ?? + (isTopLevelProvider ? globalAppRegistry : new AppRegistry()) + ); + }); + + useEffect(() => { + // For cases where localAppRegistry was provided by the parent, we allow + // parent to also take control over the cleanup lifecycle by disabling + // deactivate call with the `deactivateOnUnmount` prop. Otherwise if + // localAppRegistry was created by the provider, it will always clean up on + // unmount + const shouldDeactivate = initialLocalAppRegistry + ? deactivateOnUnmount + : true; + return () => { + if (shouldDeactivate) { + localAppRegistry.deactivate(); + } + }; + }, [localAppRegistry, initialLocalAppRegistry, deactivateOnUnmount]); + + return ( + + + {children} + + + ); +} + +export function useGlobalAppRegistry(): AppRegistry { + return useContext(GlobalAppRegistryContext); +} + +export function useLocalAppRegistry(): AppRegistry { + const appRegistry = useContext(LocalAppRegistryContext); + if (!appRegistry) { + throw new Error(`No local AppRegistry registered within this context`); + } + return appRegistry; +} + +/** @deprecated prefer using plugins or direct references instead */ +export function useAppRegistryComponent( + componentName: string +): React.JSXElementConstructor | null { + const appRegistry = useLocalAppRegistry(); + + const [component] = useState(() => { + const newComponent = appRegistry.getComponent(componentName); + if (!newComponent) { + debug( + `home plugin loading component, but ${String(componentName)} is NULL` + ); + } + return newComponent; + }); + + return component ? component : null; +} + +/** @deprecated prefer using plugins or direct references instead */ +export function useAppRegistryRole(roleName: string): + | { + component: React.JSXElementConstructor; + name: string; + }[] + | null { + const appRegistry = useLocalAppRegistry(); + + const [role] = useState(() => { + const newRole = appRegistry.getRole(roleName); + if (!newRole) { + debug(`home plugin loading role, but ${String(roleName)} is NULL`); + } + return newRole; + }); + + return role ? role : null; +} diff --git a/packages/hadron-app-registry/src/register-plugin.spec.tsx b/packages/hadron-app-registry/src/register-plugin.spec.tsx new file mode 100644 index 00000000000..efac8a2406f --- /dev/null +++ b/packages/hadron-app-registry/src/register-plugin.spec.tsx @@ -0,0 +1,134 @@ +import React, { createContext, useContext } from 'react'; +import { cleanup, render } from '@testing-library/react'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { createStore as createRefluxStore } from 'reflux'; +import StateMixin from 'reflux-state-mixin'; +import { AppRegistryProvider, registerHadronPlugin } from './'; +import { createStore } from 'redux'; +import { connect } from 'react-redux'; + +describe('registerHadronPlugin', function () { + afterEach(cleanup); + + it('allows registering plugins with a reflux-ish store', function () { + const component = sinon.stub().callsFake(() => <>); + const activate = sinon.stub().returns({ store: { state: { foo: 'bar' } } }); + const Plugin = registerHadronPlugin({ + name: 'refluxish', + component, + activate, + }); + expect(Plugin.displayName).to.equal('refluxish'); + render( + + + + ); + expect(activate).to.have.been.calledOnce; + expect(activate.firstCall.args[0]).to.deep.equal({}); + expect(activate.firstCall.args[1]).to.have.property('localAppRegistry'); + expect(activate.firstCall.args[1]).to.have.property('globalAppRegistry'); + expect(component).to.have.been.calledOnceWith({ + store: { state: { foo: 'bar' } }, + actions: undefined, + foo: 'bar', + }); + }); + + it('allows registering plugins with a proper reflux store', function () { + const component = sinon.stub().callsFake(() => <>); + const store = createRefluxStore({ + mixins: [StateMixin.store], + getInitialState() { + return { foo: 'bar' }; + }, + }); + const activate = sinon.stub().returns({ store }); + const Plugin = registerHadronPlugin({ + name: 'reflux', + component, + activate, + }); + expect(Plugin.displayName).to.equal('reflux'); + render( + + + + ); + expect(activate).to.have.been.calledOnce; + expect(activate.firstCall.args[0]).to.deep.equal({}); + expect(activate.firstCall.args[1]).to.have.property('localAppRegistry'); + expect(activate.firstCall.args[1]).to.have.property('globalAppRegistry'); + expect(component).to.have.been.calledOnceWith({ + store, + actions: undefined, + foo: 'bar', + }); + }); + + it('allows registering plugins with a redux store', function () { + const connector = connect(({ counter }) => ({ counter })); + const component = sinon.stub().callsFake(() => <>); + const store = createStore( + (state: { counter: number } | undefined, action: { type: 'inc' }) => { + state ??= { counter: 0 }; + if (action.type === 'inc') return { counter: state.counter + 1 }; + return state; + } + ); + const activate = sinon.stub().returns({ store }); + const Plugin = registerHadronPlugin({ + name: 'redux', + component: connector(component), + activate, + }); + expect(Plugin.displayName).to.equal('redux'); + render( + + + + ); + expect(activate).to.have.been.calledOnce; + expect(activate.firstCall.args[0]).to.deep.equal({}); + expect(activate.firstCall.args[1]).to.have.property('localAppRegistry'); + expect(activate.firstCall.args[1]).to.have.property('globalAppRegistry'); + expect(component).to.have.been.calledWith({ + counter: 0, + dispatch: store.dispatch, + }); + store.dispatch({ type: 'inc' }); + expect(component).to.have.been.calledWith({ + counter: 1, + dispatch: store.dispatch, + }); + }); + + it('allows registering a plugin with external services dependencies', function () { + const dummy = { value: 'blah' }; + const blahContext = createContext(dummy); + const useBlah = () => useContext(blahContext); + + const connector = connect(); + const component = sinon.stub().callsFake(() => <>); + const store = createStore(() => ({})); + const activate = sinon.stub().returns({ store }); + const Plugin = registerHadronPlugin( + { + name: 'service1', + component: connector(component), + activate, + }, + { + blah: useBlah, + } + ); + expect(Plugin.displayName).to.equal('service1'); + render( + + + + ); + expect(activate.firstCall.args[1]).to.have.property('blah', dummy); + }); +}); diff --git a/packages/hadron-app-registry/src/register-plugin.tsx b/packages/hadron-app-registry/src/register-plugin.tsx new file mode 100644 index 00000000000..d7c0c0946c7 --- /dev/null +++ b/packages/hadron-app-registry/src/register-plugin.tsx @@ -0,0 +1,200 @@ +import React, { useRef, useState } from 'react'; +import type { Store as RefluxStore } from 'reflux'; +import { Provider as ReduxStoreProvider } from 'react-redux'; +import type { Actions } from './actions'; +import { type Store, type AppRegistry, isReduxStore } from './app-registry'; +import { useGlobalAppRegistry, useLocalAppRegistry } from './react-context'; + +function LegacyRefluxProvider({ + store, + actions, + children, +}: { + store: Partial; + actions?: Partial; + children: React.ReactElement; +}) { + const storeRef = useRef(store); + const [state, setState] = React.useState(() => { + return storeRef.current.state; + }); + + React.useEffect(() => { + const unsubscribe = storeRef.current.listen?.(setState, null); + return () => unsubscribe?.(); + }, []); + + return React.cloneElement( + // There is a ton of issues with cloning children like that, + // this is a legacy piece of code that we know works in our cases and so we + // can ignore ts errors instead of handling all corner-cases + children, + // There is no single pattern to how reflux is used by plugins, sometime + // store is passed directly, sometimes only state, sometimes actions are + // passed as a single prop, sometimes spreaded, sometimes all the approaches + // are mixed and used like that in the plugins. Reflux is legacy that we + // prefer to not spend time cleaning up so we're just trying to cover all + // the cases here as much as possible + { store, actions, ...actions, ...state } + ); +} + +type Registries = { + globalAppRegistry: AppRegistry; + localAppRegistry: AppRegistry; +}; + +type Services unknown>> = { + [SvcName in keyof S]: ReturnType; +}; + +export type HadronPluginConfig unknown>> = { + name: string; + component: React.ComponentType; + /** + * Plugin activation method, will receive any props passed to the component, + * and global and local app registry instances to subscribe to any relevant + * events. Should return plugin store and an optional deactivate method to + * clean up subscriptions or any other store-related state + */ + activate: ( + options: T, + services: Registries & Services + ) => { + /** + * Redux or reflux store that will be automatically passed to a + * corresponding provider + */ + store: Store; + /** + * Optional, only relevant for plugins still using reflux + */ + actions?: typeof Actions; + /** + * Will be called to clean up plugin subscriptions when it is deactivated by + * app registry scope + */ + deactivate: () => void; + }; +}; + +export type HadronPluginComponent = React.FunctionComponent & { + displayName: string; +}; + +/** + * Creates a hadron plugin that will be automatically activated on first render + * and cleaned up when localAppRegistry unmounts + * + * @param config Hadron plugin configuration + * @param services Map of service locator functions that plugin depends on + * + * @returns Hadron plugin component + * + * @example + * const CreateCollectionPlugin = registerHadronPlugin({ + * name: 'CreateCollection', + * component: CreateCollectionModal, + * activate(opts, { globalAppRegistry }) { + * const store = configureStore(...); + * const openCreateCollectionModal = (ns) => { + * store.dispatch(openModal(ns)); + * } + * globalAppRegistry.on('create-collection', openCreateCollectionModal); + * return { + * store, + * deactivate() { + * globalAppRegistry.removeEventListener( + * 'create-collection', + * openCreateCollectionModal + * ); + * } + * } + * } + * }); + * + * @example + * // app.js + * import CompassLogging from '@mongodb-js/compass-logging'; + * import { LoggingProvider } from '@mongodb-js/compass-logging/provider'; + * + * ReactDOM.render( + * + * + * + * ) + * + * // plugin.js + * import { logging } from '@mongodb-js/compass-logging/provider' + * + * const PluginWithLogger = registerHadronPlugin({ + * name: 'LoggingPlugin', + * component: () => null, + * activate(opts, { logging }) { + * loggging.log('Plugin activated!'); + * } + * }, { logging }) + */ +export function registerHadronPlugin< + T, + S extends Record unknown> +>(config: HadronPluginConfig, services?: S): HadronPluginComponent { + const Component = config.component; + const registryName = `${config.name}.Plugin`; + + return Object.assign( + (props: React.PropsWithChildren) => { + const propsRef = useRef(props); + const globalAppRegistry = useGlobalAppRegistry(); + const localAppRegistry = useLocalAppRegistry(); + + const serviceImpls = Object.fromEntries( + Object.entries(services ?? {}).map(([key, service]) => { + try { + return [key, service()]; + } 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; + + const [{ store, actions }] = useState( + () => + localAppRegistry.getPlugin(registryName) ?? + (() => { + const plugin = config.activate(propsRef.current, { + globalAppRegistry, + localAppRegistry, + ...serviceImpls, + }); + localAppRegistry.registerPlugin(registryName, plugin); + return plugin; + })() + ); + + if (isReduxStore(store)) { + return ( + + + + ); + } + + return ( + + + + ); + }, + { + displayName: config.name, + } + ); +} diff --git a/packages/hadron-app-registry/tsconfig.json b/packages/hadron-app-registry/tsconfig.json index ecd0a14474a..79bc84584ce 100644 --- a/packages/hadron-app-registry/tsconfig.json +++ b/packages/hadron-app-registry/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@mongodb-js/tsconfig-compass/tsconfig.common.json", + "extends": "@mongodb-js/tsconfig-compass/tsconfig.react.json", "compilerOptions": { "outDir": "dist" },