diff --git a/package-lock.json b/package-lock.json index d72d2d5584a..3bdd08b6221 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43585,6 +43585,7 @@ "@mongodb-js/compass-connections": "^1.52.0", "@mongodb-js/compass-logging": "^1.6.8", "@mongodb-js/connection-info": "^0.11.9", + "compass-preferences-model": "^2.33.8", "hadron-app-registry": "^9.4.8", "mongodb-collection-model": "^5.25.8", "mongodb-database-model": "^2.25.8", @@ -45959,6 +45960,7 @@ "version": "2.34.0", "license": "SSPL", "dependencies": { + "@mongodb-js/compass-components": "^1.34.7", "@mongodb-js/compass-logging": "^1.6.8", "@mongodb-js/compass-user-data": "^0.5.8", "@mongodb-js/devtools-proxy-support": "^0.4.2", @@ -47325,6 +47327,7 @@ "@mongodb-js/compass-connections": "^1.52.0", "@mongodb-js/compass-logging": "^1.6.8", "bson": "^6.10.3", + "compass-preferences-model": "^2.33.8", "hadron-app-registry": "^9.4.8", "lodash": "^4.17.21", "mongodb-collection-model": "^5.25.8", @@ -49454,6 +49457,7 @@ "license": "SSPL", "dependencies": { "ampersand-model": "^8.0.1", + "compass-preferences-model": "^2.33.8", "mongodb-collection-model": "^5.25.8", "mongodb-data-service": "^22.25.8", "mongodb-database-model": "^2.25.8" @@ -56072,6 +56076,7 @@ "@types/mocha": "^9.0.0", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", + "compass-preferences-model": "^2.33.8", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "hadron-app-registry": "^9.4.8", @@ -59021,6 +59026,7 @@ "@types/sinon-chai": "^3.2.5", "bson": "^6.10.3", "chai": "^4.3.6", + "compass-preferences-model": "^2.33.8", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "hadron-app-registry": "^9.4.8", @@ -68210,6 +68216,7 @@ "compass-preferences-model": { "version": "file:packages/compass-preferences-model", "requires": { + "@mongodb-js/compass-components": "^1.34.7", "@mongodb-js/compass-logging": "^1.6.8", "@mongodb-js/compass-user-data": "^0.5.8", "@mongodb-js/devtools-proxy-support": "^0.4.2", @@ -79874,6 +79881,7 @@ "@mongodb-js/prettier-config-compass": "^1.2.8", "ampersand-model": "^8.0.1", "chai": "^4.3.4", + "compass-preferences-model": "^2.33.8", "depcheck": "^1.4.1", "mocha": "^10.2.0", "mongodb-collection-model": "^5.25.8", diff --git a/packages/collection-model/index.d.ts b/packages/collection-model/index.d.ts index 482d9232617..847d2f0990c 100644 --- a/packages/collection-model/index.d.ts +++ b/packages/collection-model/index.d.ts @@ -64,18 +64,18 @@ interface CollectionProps { specialish: boolean; normal: boolean; readonly: boolean; - view_on: string; + view_on: string | null; collation: unknown; pipeline: unknown[]; validation: unknown; - is_capped: boolean; - document_count: number; - document_size: number; - avg_document_size: number; - storage_size: number; - free_storage_size: number; - index_count: number; - index_size: number; + is_capped: boolean | undefined; + document_count: number | undefined; + document_size: number | undefined; + avg_document_size: number | undefined; + storage_size: number | undefined; + free_storage_size: number | undefined; + index_count: number | undefined; + index_size: number | undefined; isTimeSeries: boolean; isView: boolean; /** Only relevant for a view and identifies collection/view from which this view was created. */ @@ -85,7 +85,13 @@ interface CollectionProps { is_non_existent: boolean; } -type CollectionDataService = Pick; +type CollectionDataService = Pick< + DataService, + | 'collectionStats' + | 'collectionInfo' + | 'listCollections' + | 'isListSearchIndexesSupported' +>; interface Collection extends CollectionProps { fetch(opts: { @@ -106,7 +112,10 @@ interface Collection extends CollectionProps { } interface CollectionCollection extends Array { - fetch(opts: { dataService: CollectionDataService; fetchInfo?: boolean }): Promise; + fetch(opts: { + dataService: CollectionDataService; + fetchInfo?: boolean; + }): Promise; toJSON(opts?: { derived: boolean }): Array; at(index: number): Collection | undefined; get(id: string, key?: '_id' | 'name'): Collection | undefined; diff --git a/packages/collection-model/lib/model.js b/packages/collection-model/lib/model.js index 90cc0636a8d..a3d9b7c66bc 100644 --- a/packages/collection-model/lib/model.js +++ b/packages/collection-model/lib/model.js @@ -104,7 +104,17 @@ function pickCollectionInfo({ fle2, is_non_existent, }) { - return { type, readonly, view_on, collation, pipeline, validation, clustered, fle2, is_non_existent }; + return { + type, + readonly, + view_on, + collation, + pipeline, + validation, + clustered, + fle2, + is_non_existent, + }; } /** @@ -232,18 +242,30 @@ const CollectionModel = AmpersandModel.extend(debounceActions(['fetch']), { }, /** - * @param {{ dataService: import('mongodb-data-service').DataService }} dataService + * @param {{ + * dataService: import('mongodb-data-service').DataService, + * fetchInfo: boolean, + * force: boolean + * }} options * @returns */ async fetch({ dataService, fetchInfo = true, force = false }) { if (!shouldFetch(this.status, force)) { return; } + + const shouldFetchDbAndCollStats = getParentByType( + this, + 'Instance' + ).shouldFetchDbAndCollStats; + try { const newStatus = this.status === 'initial' ? 'fetching' : 'refreshing'; this.set({ status: newStatus }); const [collStats, collectionInfo] = await Promise.all([ - dataService.collectionStats(this.database, this.name), + shouldFetchDbAndCollStats + ? dataService.collectionStats(this.database, this.name) + : null, fetchInfo ? dataService.collectionInfo(this.database, this.name) : null, ]); this.set({ @@ -255,7 +277,7 @@ const CollectionModel = AmpersandModel.extend(debounceActions(['fetch']), { // If the collection is not unprovisioned `is_non_existent` anymore, // let's update the parent database model to reflect the change. // This happens when a user tries to insert first document into a - // collection that doesn't exist yet or creates a new collection + // collection that doesn't exist yet or creates a new collection // for an unprovisioned database. if (!this.is_non_existent) { getParentByType(this, 'Database').set({ @@ -271,7 +293,9 @@ const CollectionModel = AmpersandModel.extend(debounceActions(['fetch']), { /** * Fetches collection info and returns a special format of collection metadata * that events like open-in-new-tab, select-namespace, edit-view require - * @param {{ dataService: import('mongodb-data-service').DataService }} dataService + * @param {{ + * dataService: import('mongodb-data-service').DataService, + * }} options */ async fetchMetadata({ dataService }) { try { diff --git a/packages/compass-aggregations/src/modules/aggregation.ts b/packages/compass-aggregations/src/modules/aggregation.ts index ed9b9e39930..d8210612e5a 100644 --- a/packages/compass-aggregations/src/modules/aggregation.ts +++ b/packages/compass-aggregations/src/modules/aggregation.ts @@ -27,6 +27,7 @@ import { runPipelineConfirmationDescription } from '../utils/modal-descriptions' import type { MongoDBInstance } from 'mongodb-instance-model'; import type { DataService } from '../modules/data-service'; import toNS from 'mongodb-ns'; +import type { PreferencesAccess } from 'compass-preferences-model'; const WRITE_STAGE_LINK = { $merge: @@ -225,12 +226,18 @@ const reducer: Reducer = (state = INITIAL_STATE, action) => { return state; }; -const confirmWriteOperationIfNeeded = async ( - instance: MongoDBInstance, - dataService: DataService, - namespace: string, - pipeline: Document[] -) => { +const confirmWriteOperationIfNeeded = async ({ + instance, + dataService, + namespace, + pipeline, +}: { + instance: MongoDBInstance; + dataService: DataService; + namespace: string; + pipeline: Document[]; + preferences: PreferencesAccess; +}) => { const lastStageOperator = getStageOperator(pipeline[pipeline.length - 1]); let typeOfWrite; @@ -289,17 +296,25 @@ export const runAggregation = (): PipelineBuilderThunkAction> => { return async ( dispatch, getState, - { pipelineBuilder, instance, dataService, track, connectionInfoRef } + { + pipelineBuilder, + instance, + dataService, + track, + connectionInfoRef, + preferences, + } ) => { const pipeline = getPipelineFromBuilderState(getState(), pipelineBuilder); if ( - !(await confirmWriteOperationIfNeeded( + !(await confirmWriteOperationIfNeeded({ instance, dataService, - getState().namespace, - pipeline - )) + namespace: getState().namespace, + pipeline, + preferences, + })) ) { return; } diff --git a/packages/compass-app-stores/package.json b/packages/compass-app-stores/package.json index daf70948632..9fb002a03ec 100644 --- a/packages/compass-app-stores/package.json +++ b/packages/compass-app-stores/package.json @@ -79,6 +79,7 @@ "mongodb-collection-model": "^5.25.8", "mongodb-database-model": "^2.25.8", "mongodb-instance-model": "^12.26.8", + "compass-preferences-model": "^2.33.8", "mongodb-ns": "^2.4.2", "react": "^17.0.2" }, diff --git a/packages/compass-app-stores/src/instances-manager.spec.ts b/packages/compass-app-stores/src/instances-manager.spec.ts index 90b4065af26..bc619216d37 100644 --- a/packages/compass-app-stores/src/instances-manager.spec.ts +++ b/packages/compass-app-stores/src/instances-manager.spec.ts @@ -6,13 +6,19 @@ import { } from './instances-manager'; import { MongoDBInstance } from 'mongodb-instance-model'; import { createDefaultConnectionInfo } from '@mongodb-js/testing-library-compass'; +import { + type PreferencesAccess, + createSandboxFromDefaultPreferences, +} from 'compass-preferences-model'; const TEST_CONNECTION_INFO = createDefaultConnectionInfo(); describe('InstancesManager', function () { let instancesManager: MongoDBInstancesManager; - beforeEach(function () { + let preferences: PreferencesAccess; + beforeEach(async function () { instancesManager = new MongoDBInstancesManager(); + preferences = await createSandboxFromDefaultPreferences(); }); it('should be able to create and return a MongoDB instance', function () { @@ -27,6 +33,7 @@ describe('InstancesManager', function () { servers: [], setName: '', }, + preferences, } ); expect(instance).to.be.instanceOf(MongoDBInstance); @@ -44,6 +51,7 @@ describe('InstancesManager', function () { servers: [], setName: '', }, + preferences, } ); expect(instancesManager.listMongoDBInstances()).to.have.lengthOf(1); @@ -66,6 +74,7 @@ describe('InstancesManager', function () { servers: [], setName: '', }, + preferences, } ); expect(onInstanceCreatedStub).to.be.calledOnceWithExactly( @@ -89,6 +98,7 @@ describe('InstancesManager', function () { servers: [], setName: '', }, + preferences, } ); expect(() => @@ -108,6 +118,7 @@ describe('InstancesManager', function () { servers: [], setName: '', }, + preferences, } ); expect(() => @@ -138,6 +149,7 @@ describe('InstancesManager', function () { servers: [], setName: '', }, + preferences, } ); instancesManager.removeMongoDBInstanceForConnection( diff --git a/packages/compass-app-stores/src/plugin.tsx b/packages/compass-app-stores/src/plugin.tsx index 47790d4320f..2a8b12d599a 100644 --- a/packages/compass-app-stores/src/plugin.tsx +++ b/packages/compass-app-stores/src/plugin.tsx @@ -9,6 +9,8 @@ import { createInstancesStore } from './stores'; import type { ConnectionsService } from '@mongodb-js/compass-connections/provider'; import { connectionsLocator } from '@mongodb-js/compass-connections/provider'; import { type MongoDBInstancesManager } from './instances-manager'; +import type { PreferencesAccess } from 'compass-preferences-model'; +import { preferencesLocator } from 'compass-preferences-model/provider'; interface MongoDBInstancesProviderProps { children?: React.ReactNode; @@ -37,10 +39,12 @@ export const CompassInstanceStorePlugin = registerHadronPlugin( { connections, logger, + preferences, globalAppRegistry, }: { connections: ConnectionsService; logger: Logger; + preferences: PreferencesAccess; globalAppRegistry: AppRegistry; }, helpers: ActivateHelpers @@ -49,6 +53,7 @@ export const CompassInstanceStorePlugin = registerHadronPlugin( { connections, logger, + preferences, globalAppRegistry, }, helpers @@ -63,6 +68,7 @@ export const CompassInstanceStorePlugin = registerHadronPlugin( }, { logger: createLoggerLocator('COMPASS-INSTANCE-STORE'), + preferences: preferencesLocator, connections: connectionsLocator, } ); diff --git a/packages/compass-app-stores/src/provider.spec.tsx b/packages/compass-app-stores/src/provider.spec.tsx index a0226fb5a2d..e979c2ddcd3 100644 --- a/packages/compass-app-stores/src/provider.spec.tsx +++ b/packages/compass-app-stores/src/provider.spec.tsx @@ -12,9 +12,19 @@ import { cleanup, waitFor, } from '@mongodb-js/testing-library-compass'; +import { + createSandboxFromDefaultPreferences, + type PreferencesAccess, +} from 'compass-preferences-model'; +import { PreferencesProvider } from 'compass-preferences-model/provider'; describe('NamespaceProvider', function () { const sandbox = Sinon.createSandbox(); + let preferences: PreferencesAccess; + + beforeEach(async function () { + preferences = await createSandboxFromDefaultPreferences(); + }); afterEach(function () { cleanup(); @@ -26,9 +36,11 @@ describe('NamespaceProvider', function () { databases: [{ _id: 'foo' }] as any, }); await renderWithActiveConnection( - - hello - + + + hello + + ); expect(screen.getByText('hello')).to.exist; }); @@ -38,9 +50,11 @@ describe('NamespaceProvider', function () { databases: [{ _id: 'foo', collections: [{ _id: 'foo.bar' }] }] as any, }); await renderWithActiveConnection( - - hello - + + + hello + + ); expect(screen.getByText('hello')).to.exist; }); @@ -48,9 +62,11 @@ describe('NamespaceProvider', function () { it("should not render content when namespace doesn't exist", async function () { const instanceManager = new TestMongoDBInstanceManager(); await renderWithActiveConnection( - - hello - + + + hello + + ); expect(screen.queryByText('hello')).to.not.exist; }); @@ -64,9 +80,11 @@ describe('NamespaceProvider', function () { }); await renderWithActiveConnection( - - hello - + + + hello + + ); expect(screen.queryByText('hello')).to.not.exist; @@ -82,14 +100,16 @@ describe('NamespaceProvider', function () { databases: [{ _id: 'foo' }] as any, }); await renderWithActiveConnection( - - - hello - - + + + + hello + + + ); await waitFor(() => { expect(onNamespaceFallbackSelect).to.be.calledOnceWithExactly('foo'); @@ -100,14 +120,16 @@ describe('NamespaceProvider', function () { const onNamespaceFallbackSelect = sandbox.spy(); const instanceManager = new TestMongoDBInstanceManager(); await renderWithActiveConnection( - - - hello - - + + + + hello + + + ); await waitFor(() => { expect(onNamespaceFallbackSelect).to.be.calledOnceWithExactly(null); diff --git a/packages/compass-app-stores/src/provider.tsx b/packages/compass-app-stores/src/provider.tsx index fdce751a2fb..7b0cf82f7d6 100644 --- a/packages/compass-app-stores/src/provider.tsx +++ b/packages/compass-app-stores/src/provider.tsx @@ -20,6 +20,10 @@ import { MongoDBInstancesManager } from './instances-manager'; import toNS from 'mongodb-ns'; import type Collection from 'mongodb-collection-model'; import type Database from 'mongodb-database-model'; +import type { + AllPreferences, + PreferencesAccess, +} from 'compass-preferences-model'; export { MongoDBInstancesManagerEvents, @@ -36,6 +40,15 @@ export class TestMongoDBInstanceManager extends MongoDBInstancesManager { private _instance: MongoDBInstance; constructor(instanceProps = {} as Partial) { super(); + if (!instanceProps.preferences) { + instanceProps.preferences = { + getPreferences: () => ({} as AllPreferences), + setPreferences: () => Promise.resolve(), + onPreferenceValueChanged: () => () => { + /* no-op */ + }, + } as unknown as PreferencesAccess; + } this._instance = new MongoDBInstance(instanceProps as MongoDBInstanceProps); } getMongoDBInstanceForConnection() { @@ -46,6 +59,8 @@ export class TestMongoDBInstanceManager extends MongoDBInstancesManager { } } +// We need to create the context with a proper mock for testing +// that includes default preferences export const MongoDBInstancesManagerContext = createContext( process.env.NODE_ENV === 'test' diff --git a/packages/compass-app-stores/src/stores/instance-store.spec.ts b/packages/compass-app-stores/src/stores/instance-store.spec.ts index a0446572229..b33c7355007 100644 --- a/packages/compass-app-stores/src/stores/instance-store.spec.ts +++ b/packages/compass-app-stores/src/stores/instance-store.spec.ts @@ -9,6 +9,10 @@ import { createPluginTestHelpers, cleanup, } from '@mongodb-js/testing-library-compass'; +import { + createSandboxFromDefaultPreferences, + type PreferencesAccess, +} from 'compass-preferences-model'; const mockConnections = [ createDefaultConnectionInfo(), @@ -39,6 +43,7 @@ describe('InstanceStore [Store]', function () { let sandbox: sinon.SinonSandbox; let getDataService: any; let connectionsStore: any; + let preferences: PreferencesAccess; function waitForInstanceRefresh(instance: MongoDBInstance): Promise { return new Promise((resolve) => { @@ -57,15 +62,22 @@ describe('InstanceStore [Store]', function () { CompassInstanceStorePlugin ); - beforeEach(function () { - const result = activatePluginWithConnections(undefined, { - connectFn() { - return createDataService(); + beforeEach(async function () { + preferences = await createSandboxFromDefaultPreferences(); + const result = activatePluginWithConnections( + { + preferences, }, - }); + { + connectFn() { + return createDataService(); + }, + } + ); connectionsStore = result.connectionsStore; getDataService = result.getDataServiceForConnection; globalAppRegistry = result.globalAppRegistry; + preferences = result.preferences; sandbox = sinon.createSandbox(); instancesManager = result.plugin.store.getState().instancesManager; }); diff --git a/packages/compass-app-stores/src/stores/instance-store.ts b/packages/compass-app-stores/src/stores/instance-store.ts index d0d8288d103..e91ee2f3b67 100644 --- a/packages/compass-app-stores/src/stores/instance-store.ts +++ b/packages/compass-app-stores/src/stores/instance-store.ts @@ -9,6 +9,7 @@ import type { ActivateHelpers, AppRegistry } from 'hadron-app-registry'; import type { Logger } from '@mongodb-js/compass-logging/provider'; import { openToast } from '@mongodb-js/compass-components'; import { MongoDBInstancesManager } from '../instances-manager'; +import type { PreferencesAccess } from 'compass-preferences-model'; function serversArray( serversMap: NonNullable< @@ -44,10 +45,12 @@ export function createInstancesStore( globalAppRegistry, connections, logger: { log, mongoLogId }, + preferences, }: { connections: ConnectionsService; logger: Logger; globalAppRegistry: AppRegistry; + preferences: PreferencesAccess; }, { on, cleanup, addCleanup }: ActivateHelpers ) { @@ -88,7 +91,7 @@ export function createInstancesStore( const refreshInstance = async ( refreshOptions: Omit< Parameters[0], - 'dataService' + 'dataService' | 'preferences' > = {}, { connectionId }: { connectionId?: string } = {} ) => { @@ -102,7 +105,10 @@ export function createInstancesStore( instancesManager.getMongoDBInstanceForConnection(connectionId); const dataService = connections.getDataServiceForConnection(connectionId); isFirstRun = instance.status === 'initial'; - await instance.refresh({ dataService, ...refreshOptions }); + await instance.refresh({ + dataService, + ...refreshOptions, + }); } catch (err: any) { log.warn( mongoLogId(1_001_000_295), @@ -255,6 +261,28 @@ export function createInstancesStore( } }; + preferences.onPreferenceValueChanged( + 'enableDbAndCollStats', + (enableDbAndCollStats) => { + if (enableDbAndCollStats) { + const connectedConnectionIds = Array.from( + instancesManager.listMongoDBInstances().keys() + ); + connectedConnectionIds.forEach( + (connectionId) => + void refreshInstance( + { + fetchDbStats: true, + }, + { + connectionId, + } + ) + ); + } + } + ); + on(connections, 'disconnected', function (connectionInfoId: string) { try { const instance = @@ -288,6 +316,7 @@ export function createInstancesStore( topologyDescription: getTopologyDescription( dataService.getLastSeenTopology() ), + preferences, }; const instance = instancesManager.createMongoDBInstanceForConnection( instanceConnectionId, diff --git a/packages/compass-crud/src/plugin-title.tsx b/packages/compass-crud/src/plugin-title.tsx index 06ecc1e6e23..8d5f8665404 100644 --- a/packages/compass-crud/src/plugin-title.tsx +++ b/packages/compass-crud/src/plugin-title.tsx @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import numeral from 'numeral'; import { css, Tooltip, Badge, spacing } from '@mongodb-js/compass-components'; import type { CrudStore } from './stores/crud-store'; +import { usePreference } from 'compass-preferences-model/provider'; const tooltipContentStyles = css({ listStyleType: 'none', @@ -88,6 +89,8 @@ export const CrudTabTitle = ({ avgDocumentSize: format(avg_document_size, 'b'), }; }, [collectionStats]); + const enableDbAndCollStats = usePreference('enableDbAndCollStats'); + const details = [ `Documents: ${documentCount}`, `Storage Size: ${storageSize}`, @@ -97,7 +100,9 @@ export const CrudTabTitle = ({ return (
Documents - + {enableDbAndCollStats && ( + + )}
); }; diff --git a/packages/compass-crud/src/stores/crud-store.spec.ts b/packages/compass-crud/src/stores/crud-store.spec.ts index 626340340ed..54000017a64 100644 --- a/packages/compass-crud/src/stores/crud-store.spec.ts +++ b/packages/compass-crud/src/stores/crud-store.spec.ts @@ -249,6 +249,7 @@ describe('store', function () { dataLake: { isDataLake: false, }, + preferences, } as any); sinon.restore(); diff --git a/packages/compass-indexes/src/plugin-title.tsx b/packages/compass-indexes/src/plugin-title.tsx index d19a1cc2381..8cc0cd433d9 100644 --- a/packages/compass-indexes/src/plugin-title.tsx +++ b/packages/compass-indexes/src/plugin-title.tsx @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import type { RootState } from './modules'; import { Badge, css, spacing, Tooltip } from '@mongodb-js/compass-components'; import numeral from 'numeral'; +import { usePreference } from 'compass-preferences-model/provider'; const containerStyles = css({ display: 'flex', @@ -90,6 +91,8 @@ const TabTitle = ({ }; }, [collectionStats]); + const enableDbAndCollStats = usePreference('enableDbAndCollStats'); + const details = [ `Indexes: ${indexCount}`, `Total Size: ${totalIndexSize}`, @@ -99,7 +102,9 @@ const TabTitle = ({ return (
Indexes - + {enableDbAndCollStats && ( + + )}
); }; diff --git a/packages/compass-preferences-model/package.json b/packages/compass-preferences-model/package.json index e1892ec9ec9..8b4297e3208 100644 --- a/packages/compass-preferences-model/package.json +++ b/packages/compass-preferences-model/package.json @@ -52,6 +52,7 @@ "@mongodb-js/compass-logging": "^1.6.8", "@mongodb-js/compass-user-data": "^0.5.8", "@mongodb-js/devtools-proxy-support": "^0.4.2", + "@mongodb-js/compass-components": "^1.34.7", "bson": "^6.10.3", "hadron-app-registry": "^9.4.8", "hadron-ipc": "^3.4.8", diff --git a/packages/compass-preferences-model/src/preferences-schema.ts b/packages/compass-preferences-model/src/preferences-schema.tsx similarity index 96% rename from packages/compass-preferences-model/src/preferences-schema.ts rename to packages/compass-preferences-model/src/preferences-schema.tsx index 1d97aa1025f..9acd7a873a7 100644 --- a/packages/compass-preferences-model/src/preferences-schema.ts +++ b/packages/compass-preferences-model/src/preferences-schema.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { z } from 'zod'; import { type FeatureFlagDefinition, @@ -13,10 +14,27 @@ import { proxyOptionsToProxyPreference, proxyPreferenceToProxyOptions, } from './utils'; +import { Link } from '@mongodb-js/compass-components'; export const THEMES_VALUES = ['DARK', 'LIGHT', 'OS_THEME'] as const; export type THEMES = typeof THEMES_VALUES[number]; +const enableDbAndCollStatsDescription: React.ReactNode = ( + <> + The{' '} + + dbStats + + and{' '} + + collStats + {' '} + command return storage statistics for a given database or collection. + Disabling this setting can help reduce Compass' overhead on your + MongoDB deployments. + +); + export const SORT_ORDER_VALUES = [ '', '{ $natural: -1 }', @@ -43,6 +61,7 @@ export type UserConfigurablePreferences = PermanentFeatureFlags & networkTraffic: boolean; readOnly: boolean; enableShell: boolean; + enableDbAndCollStats: boolean; protectConnectionStrings?: boolean; forceConnectionOptions?: [key: string, value: string][]; showKerberosPasswordField: boolean; @@ -203,6 +222,7 @@ type PreferenceDefinition = { : { short: string; long?: string; + longReact?: React.ReactNode; options?: AllPreferences[K] extends string ? { [k in AllPreferences[K]]: { label: string; description: string } } : never; @@ -482,6 +502,21 @@ export const storedUserPreferencesProps: Required<{ validator: z.boolean().default(true), type: 'boolean', }, + /** + * Switch to enable/disable dbStats and collStats calls. + */ + enableDbAndCollStats: { + ui: true, + cli: true, + global: true, + description: { + short: 'Show Database and Collection Statistics', + long: "The dbStats and collStats command returns storage statistics for a given database or collection. Disabling this setting can help reduce Compass' overhead on your MongoDB deployments.", + longReact: enableDbAndCollStatsDescription, + }, + validator: z.boolean().default(true), + type: 'boolean', + }, /** * Switch to enable/disable maps rendering. */ diff --git a/packages/compass-preferences-model/tsconfig.json b/packages/compass-preferences-model/tsconfig.json index b2d3c80179f..6ab6fc5c42a 100644 --- a/packages/compass-preferences-model/tsconfig.json +++ b/packages/compass-preferences-model/tsconfig.json @@ -1,8 +1,10 @@ { - "extends": "@mongodb-js/tsconfig-compass/tsconfig.common.json", + "extends": "@mongodb-js/tsconfig-compass/tsconfig.react.json", "compilerOptions": { "outDir": "dist", - "allowJs": true + "allowJs": true, + "moduleResolution": "node16", + "module": "Node16" }, "include": ["src/**/*"], "exclude": ["./src/**/*.spec.*"] diff --git a/packages/compass-schema-validation/src/stores/store.spec.ts b/packages/compass-schema-validation/src/stores/store.spec.ts index 1357dfec014..785b26469c5 100644 --- a/packages/compass-schema-validation/src/stores/store.spec.ts +++ b/packages/compass-schema-validation/src/stores/store.spec.ts @@ -34,13 +34,7 @@ const topologyDescription = { servers: [{ type: 'Unknown' }], }; -const fakeInstance = new MongoDBInstance({ - _id: '123', - topologyDescription, - build: { - version: '6.0.0', - }, -} as any); +let fakeInstance: MongoDBInstance; const fakeDataService = { collectionInfo: () => @@ -61,6 +55,16 @@ const getMockedStore = async (analyzeSchema: any) => { const connectionInfoRef = { current: {}, } as ConnectionInfoRef; + const preferences = await createSandboxFromDefaultPreferences(); + + fakeInstance = new MongoDBInstance({ + _id: '123', + topologyDescription, + build: { + version: '6.0.0', + }, + preferences, + } as any); const activateResult = onActivated( { namespace: 'test.test' } as any, { @@ -68,7 +72,7 @@ const getMockedStore = async (analyzeSchema: any) => { dataService: fakeDataService, instance: fakeInstance, workspaces: fakeWorkspaces, - preferences: await createSandboxFromDefaultPreferences(), + preferences, logger: createNoopLogger(), track: createNoopTrack(), connectionInfoRef, diff --git a/packages/compass-settings/src/components/settings/general.tsx b/packages/compass-settings/src/components/settings/general.tsx index 98ec03a3d18..92516f7e073 100644 --- a/packages/compass-settings/src/components/settings/general.tsx +++ b/packages/compass-settings/src/components/settings/general.tsx @@ -13,6 +13,7 @@ const generalFields = [ ? (['installURLHandlers'] as const) : []), 'enableShowDialogOnQuit', + 'enableDbAndCollStats', ] as const; export const GeneralSettings: React.FunctionComponent = () => { diff --git a/packages/compass-settings/src/components/settings/settings-list.tsx b/packages/compass-settings/src/components/settings/settings-list.tsx index b00377d9d26..5dc3357ec7f 100644 --- a/packages/compass-settings/src/components/settings/settings-list.tsx +++ b/packages/compass-settings/src/components/settings/settings-list.tsx @@ -71,7 +71,7 @@ export type SettingsListProps = { }; function SettingLabel({ name }: { name: SupportedPreferences }) { - const { short, long } = getSettingDescription(name).description; + const { short, long, longReact } = getSettingDescription(name).description; return ( <> - {long && {long}} + {(longReact || long) && {longReact ?? long}} ); } diff --git a/packages/compass-sidebar/src/modules/databases.spec.ts b/packages/compass-sidebar/src/modules/databases.spec.ts index 9648e703a44..7f351ce6325 100644 --- a/packages/compass-sidebar/src/modules/databases.spec.ts +++ b/packages/compass-sidebar/src/modules/databases.spec.ts @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; import databasesReducer, { INITIAL_STATE, changeDatabases } from './databases'; @@ -6,13 +7,16 @@ import { createInstance } from '../../test/helpers'; const CONNECTION_ID = 'webscale'; -function createDatabases(dbs: any[] = []) { - const data = createInstance(dbs).databases.map((db) => { - return { - ...db.toJSON(), - collections: db.collections.toJSON(), - }; - }); +async function createDatabases(dbs: any[] = []) { + const preferences = await createSandboxFromDefaultPreferences(); + const data = createInstance(dbs, undefined, preferences).databases.map( + (db) => { + return { + ...db.toJSON(), + collections: db.collections.toJSON(), + }; + } + ); return data.map(({ is_non_existent, collections, ...rest }) => ({ ...rest, isNonExistent: is_non_existent, @@ -26,8 +30,8 @@ function createDatabases(dbs: any[] = []) { describe('sidebar databases', function () { describe('#reducer', function () { context('when changing databases', function () { - it('sets databases as-is', function () { - const dbs = createDatabases([{ _id: 'foo' }, { _id: 'bar' }]); + it('sets databases as-is', async function () { + const dbs = await createDatabases([{ _id: 'foo' }, { _id: 'bar' }]); expect( databasesReducer(undefined, changeDatabases(CONNECTION_ID, dbs)) diff --git a/packages/compass-sidebar/src/modules/instance.spec.ts b/packages/compass-sidebar/src/modules/instance.spec.ts index 634381d747a..f48f8b16047 100644 --- a/packages/compass-sidebar/src/modules/instance.spec.ts +++ b/packages/compass-sidebar/src/modules/instance.spec.ts @@ -6,11 +6,15 @@ import { setupInstance } from './instance'; import type { RootState } from '.'; import type AppRegistry from 'hadron-app-registry'; import type { Logger } from '@mongodb-js/compass-logging'; -import type { MongoDBInstancesManager } from '@mongodb-js/compass-app-stores/provider'; +import type { + MongoDBInstance, + MongoDBInstancesManager, +} from '@mongodb-js/compass-app-stores/provider'; +import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; describe('sidebar instance', function () { - const instance = createInstance(); let instanceOnSpy: SinonSpy; + let instance: MongoDBInstance; const globalAppRegistry = {} as any as AppRegistry; const connectionsService = { getDataServiceForConnection() { @@ -31,7 +35,9 @@ describe('sidebar instance', function () { let logger: Logger; let listMongoDBInstancesStub: SinonStub; - beforeEach(function () { + beforeEach(async function () { + const preferences = await createSandboxFromDefaultPreferences(); + instance = createInstance(undefined, undefined, preferences); instanceOnSpy = spy(); instance.on = instanceOnSpy; instancesManager = { diff --git a/packages/compass-sidebar/src/stores/store.spec.ts b/packages/compass-sidebar/src/stores/store.spec.ts index 8385f5bc953..6a90dbfc466 100644 --- a/packages/compass-sidebar/src/stores/store.spec.ts +++ b/packages/compass-sidebar/src/stores/store.spec.ts @@ -6,9 +6,11 @@ import { createInstance } from '../../test/helpers'; import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; import type { DataService } from '@mongodb-js/compass-connections/provider'; import { + type MongoDBInstance, MongoDBInstancesManagerEvents, type MongoDBInstancesManager, } from '@mongodb-js/compass-app-stores/provider'; +import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; const CONNECTION_ID = 'webscale'; const ALL_EVENTS = [ @@ -31,7 +33,7 @@ const ALL_EVENTS = [ ]; describe('SidebarStore [Store]', function () { - const instance = createInstance(); + let instance: MongoDBInstance; const globalAppRegistry = {} as any; let instanceOnSpy: SinonSpy; let instanceOffSpy: SinonSpy; @@ -40,7 +42,9 @@ describe('SidebarStore [Store]', function () { let listMongoDBInstancesStub: SinonStub; let instancesManager: MongoDBInstancesManager; - beforeEach(function () { + beforeEach(async function () { + const preferences = await createSandboxFromDefaultPreferences(); + instance = createInstance(undefined, undefined, preferences); instanceOnSpy = spy(); instanceOffSpy = spy(); instance.on = instanceOnSpy; @@ -89,11 +93,12 @@ describe('SidebarStore [Store]', function () { } describe('when a new instance is created', function () { - beforeEach(function () { + beforeEach(async function () { + const preferences = await createSandboxFromDefaultPreferences(); instancesManager.emit( MongoDBInstancesManagerEvents.InstanceCreated, 'newConnection', - createInstance() + createInstance(undefined, undefined, preferences) ); }); diff --git a/packages/compass-sidebar/test/helpers.ts b/packages/compass-sidebar/test/helpers.ts index e679b782f36..7219d48af7d 100644 --- a/packages/compass-sidebar/test/helpers.ts +++ b/packages/compass-sidebar/test/helpers.ts @@ -1,3 +1,4 @@ +import { type PreferencesAccess } from 'compass-preferences-model'; import { MongoDBInstance } from 'mongodb-instance-model'; export function createInstance( @@ -9,7 +10,8 @@ export function createInstance( type: 'Unknown', servers: [], setName: 'foo', - } + }, + preferences: PreferencesAccess ) { return new MongoDBInstance({ _id: '123', @@ -26,5 +28,6 @@ export function createInstance( }; }), topologyDescription, + preferences, } as any); } diff --git a/packages/compass-workspaces/package.json b/packages/compass-workspaces/package.json index cfa9faea714..9f26d573e81 100644 --- a/packages/compass-workspaces/package.json +++ b/packages/compass-workspaces/package.json @@ -57,6 +57,7 @@ "@mongodb-js/compass-logging": "^1.6.8", "bson": "^6.10.3", "hadron-app-registry": "^9.4.8", + "compass-preferences-model": "^2.33.8", "lodash": "^4.17.21", "mongodb-collection-model": "^5.25.8", "mongodb-database-model": "^2.25.8", diff --git a/packages/compass-workspaces/src/index.ts b/packages/compass-workspaces/src/index.ts index f64d9732d09..92a90f7b2be 100644 --- a/packages/compass-workspaces/src/index.ts +++ b/packages/compass-workspaces/src/index.ts @@ -37,12 +37,15 @@ import { type MongoDBInstancesManager, MongoDBInstancesManagerEvents, } from '@mongodb-js/compass-app-stores/provider'; +import type { PreferencesAccess } from 'compass-preferences-model/provider'; +import { preferencesLocator } from 'compass-preferences-model/provider'; export type WorkspacesServices = { globalAppRegistry: AppRegistry; instancesManager: MongoDBInstancesManager; connections: ConnectionsService; logger: Logger; + preferences: PreferencesAccess; }; export function configureStore( @@ -79,6 +82,7 @@ export function activateWorkspacePlugin( instancesManager, connections, logger, + preferences, }: WorkspacesServices, { on, cleanup, addCleanup }: ActivateHelpers ) { @@ -87,6 +91,7 @@ export function activateWorkspacePlugin( instancesManager, connections, logger, + preferences, }); addCleanup(cleanupLocalAppRegistries); @@ -227,6 +232,7 @@ const WorkspacesPlugin = registerHadronPlugin( instancesManager: mongoDBInstancesManagerLocator, connections: connectionsLocator, logger: createLoggerLocator('COMPASS-WORKSPACES-UI'), + preferences: preferencesLocator, } ); diff --git a/packages/database-model/index.d.ts b/packages/database-model/index.d.ts index 5593cc622d4..ac5bdf4250b 100644 --- a/packages/database-model/index.d.ts +++ b/packages/database-model/index.d.ts @@ -8,12 +8,12 @@ interface DatabaseProps { statusError: string | null; collectionsStatus: 'initial' | 'fetching' | 'refreshing' | 'ready' | 'error'; collectionsStatusError: string | null; - collection_count: number; - document_count: number; - storage_size: number; - data_size: number; - index_count: number; - index_size: number; + collection_count: number | undefined; + document_count: number | undefined; + storage_size: number | undefined; + data_size: number | undefined; + index_count: number | undefined; + index_size: number | undefined; collectionsLength: number; collections: CollectionCollection; is_non_existent: boolean; diff --git a/packages/database-model/lib/model.js b/packages/database-model/lib/model.js index 00d8e4a0e1b..cbe596434a1 100644 --- a/packages/database-model/lib/model.js +++ b/packages/database-model/lib/model.js @@ -131,19 +131,37 @@ const DatabaseModel = AmpersandModel.extend( collections: MongoDbCollectionCollection, }, /** - * @param {{ dataService: import('mongodb-data-service').DataService }} dataService + * @param {{ + * dataService: import('mongodb-data-service').DataService, + * force: boolean + * }} options + * @param force * @returns {Promise} */ async fetch({ dataService, force = false }) { + const shouldFetchDbAndCollStats = getParentByType( + this, + 'Instance' + ).shouldFetchDbAndCollStats; + if (!shouldFetch(this.status, force)) { return; } + if (!shouldFetchDbAndCollStats) { + this.set({ status: 'ready' }); + return; + } + try { const newStatus = this.status === 'initial' ? 'fetching' : 'refreshing'; this.set({ status: newStatus }); const stats = await dataService.databaseStats(this.getId()); - this.set({ status: 'ready', statusError: null, ...stats }); + this.set({ + status: 'ready', + statusError: null, + ...stats, + }); } catch (err) { this.set({ status: 'error', statusError: err.message }); throw err; @@ -151,7 +169,10 @@ const DatabaseModel = AmpersandModel.extend( }, /** - * @param {{ dataService: import('mongodb-data-service').DataService }} dataService + * @param {{ + * dataService: import('mongodb-data-service').DataService, + * force: boolean + * }} options * @returns {Promise} */ async fetchCollections({ dataService, force = false }) { @@ -196,7 +217,7 @@ const DatabaseModel = AmpersandModel.extend( dataService, // We already fetched it with fetchCollections fetchInfo: false, - force + force, }); }) ); @@ -233,10 +254,16 @@ const DatabaseCollection = AmpersandCollection.extend( const dbs = await dataService.listDatabases({ nameOnly: true, privileges: instanceModel.auth.privileges, - roles: instanceModel.auth.roles + roles: instanceModel.auth.roles, }); - this.set(dbs.map(({ _id, name, is_non_existent }) => ({ _id, name, is_non_existent }))); + this.set( + dbs.map(({ _id, name, is_non_existent }) => ({ + _id, + name, + is_non_existent, + })) + ); }, toJSON(opts = { derived: true }) { diff --git a/packages/databases-collections-list/src/collections.tsx b/packages/databases-collections-list/src/collections.tsx index 69199320e06..886583c43f8 100644 --- a/packages/databases-collections-list/src/collections.tsx +++ b/packages/databases-collections-list/src/collections.tsx @@ -5,12 +5,16 @@ import type { BadgeProp } from './namespace-card'; import { NamespaceItemCard } from './namespace-card'; import { ItemsGrid } from './items-grid'; import type { CollectionProps } from 'mongodb-collection-model'; +import { usePreference } from 'compass-preferences-model/provider'; const COLLECTION_CARD_WIDTH = spacing[1600] * 4; const COLLECTION_CARD_HEIGHT = 238; +const COLLECTION_CARD_WITHOUT_STATS_HEIGHT = COLLECTION_CARD_HEIGHT - 150; const COLLECTION_CARD_LIST_HEIGHT = 118; +const COLLECTION_CARD_LIST_WITHOUT_STATS_HEIGHT = + COLLECTION_CARD_LIST_HEIGHT - 50; function collectionPropertyToBadge({ id, @@ -80,6 +84,7 @@ const CollectionsList: React.FunctionComponent<{ onDeleteCollectionClick, onRefreshClick, }) => { + const enableDbAndCollStats = usePreference('enableDbAndCollStats'); return (
{ + const enableDbAndCollStats = usePreference('enableDbAndCollStats'); return ( = 10_000 ? PerformanceSignals.get('too-many-collections') @@ -76,7 +96,10 @@ const DatabasesList: React.FunctionComponent<{ }, { label: 'Indexes', - value: compactNumber(db.index_count), + value: + enableDbAndCollStats && db.index_count !== undefined + ? compactNumber(db.index_count) + : 'N/A', }, ]} onItemClick={onItemClick} diff --git a/packages/databases-collections-list/src/index.spec.tsx b/packages/databases-collections-list/src/index.spec.tsx index 591028c30e6..f4514118406 100644 --- a/packages/databases-collections-list/src/index.spec.tsx +++ b/packages/databases-collections-list/src/index.spec.tsx @@ -8,16 +8,30 @@ import { import { expect } from 'chai'; import { DatabasesList, CollectionsList } from './index'; import Sinon from 'sinon'; +import { + type PreferencesAccess, + PreferencesProvider, +} from 'compass-preferences-model/provider'; +import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; function createDatabase(name) { return { _id: name, name: name, status: 'ready' as const, - storage_size: 0, - data_size: 0, - index_count: 0, - collectionsLength: 0, + statusError: null, + collectionsLength: 35, + collectionsStatus: 'ready' as const, + collectionsStatusError: null, + collection_count: 1, + collections: [] as any, + is_non_existent: false, + // dbStats + document_count: 10, + storage_size: 1500, + data_size: 1000, + index_count: 25, + index_size: 100, }; } @@ -27,31 +41,43 @@ function createCollection(name) { name: name, type: 'collection' as const, status: 'ready' as const, - document_count: 0, - document_size: 0, - avg_document_size: 0, - storage_size: 0, - free_storage_size: 0, - index_count: 0, - index_size: 0, + statusError: null, + ns: `db.${name}`, + database: 'db', + system: true, + oplog: true, + command: true, + special: false, + specialish: false, + normal: false, + readonly: false, + view_on: null, + collation: '', + pipeline: [], + validation: '', properties: [], + is_capped: false, + isTimeSeries: false, + isView: false, + is_non_existent: false, + /** Only relevant for a view and identifies collection/view from which this view was created. */ + sourceName: null, + source: {} as any, + // collStats + document_count: 10, + document_size: 11, + avg_document_size: 150, + storage_size: 2500, + free_storage_size: 1000, + index_count: 15, + index_size: 16, }; } function createTimeSeries(name) { return { - _id: name, - name: name, + ...createCollection(name), type: 'timeseries' as const, - status: 'ready' as const, - document_count: 0, - document_size: 0, - avg_document_size: 0, - storage_size: 0, - free_storage_size: 0, - index_count: 0, - index_size: 0, - properties: [], }; } @@ -71,17 +97,26 @@ const colls = [ describe('databases and collections list', function () { describe('DatabasesList', function () { + let preferences: PreferencesAccess; + + beforeEach(async function () { + preferences = await createSandboxFromDefaultPreferences(); + }); + afterEach(cleanup); + const renderDatabasesList = (props) => { + render( + + + + ); + }; + it('should render databases in a list', function () { const clickSpy = Sinon.spy(); - render( - - ); + renderDatabasesList({ databases: dbs, onDatabaseClick: clickSpy }); expect(screen.getByTestId('database-grid')).to.exist; @@ -95,21 +130,75 @@ describe('databases and collections list', function () { expect(clickSpy).to.be.calledWith('foo'); }); + + it('should render database with statistics when dbStats are enabled', async function () { + const clickSpy = Sinon.spy(); + + const db = createDatabase('foo'); + await preferences.savePreferences({ enableDbAndCollStats: true }); + + renderDatabasesList({ databases: [db], onDatabaseClick: clickSpy }); + + expect(screen.getByTestId('database-grid')).to.exist; + + expect(screen.getAllByTestId('database-grid-item')).to.have.lengthOf(1); + expect(screen.getByText('foo')).to.exist; + + expect(screen.getByText(/Storage size/)).to.exist; + expect(screen.getByText('1.50 kB')).to.exist; + expect(screen.getByText(/Collections/)).to.exist; + expect(screen.getByText('35')).to.exist; + expect(screen.getByText(/Indexes/)).to.exist; + expect(screen.getByText('25')).to.exist; + }); + + it('should render database without statistics when dbStats are disabled', async function () { + const clickSpy = Sinon.spy(); + + const db = createDatabase('foo'); + await preferences.savePreferences({ enableDbAndCollStats: false }); + + renderDatabasesList({ databases: [db], onDatabaseClick: clickSpy }); + + expect(screen.getByTestId('database-grid')).to.exist; + + expect(screen.getAllByTestId('database-grid-item')).to.have.lengthOf(1); + expect(screen.getByText('foo')).to.exist; + + expect(screen.queryByText(/Storage size/)).not.to.exist; + expect(screen.queryByText('1.50 kB')).not.to.exist; + expect(screen.queryByText(/Collections/)).not.to.exist; + expect(screen.queryByText('35')).not.to.exist; + expect(screen.queryByText(/Indexes/)).not.to.exist; + expect(screen.queryByText('25')).not.to.exist; + }); }); describe('CollectionsList', function () { + let preferences: PreferencesAccess; + + beforeEach(async function () { + preferences = await createSandboxFromDefaultPreferences(); + }); + afterEach(cleanup); + const renderCollectionsList = (props) => { + render( + + + + ); + }; + it('should render collections in a list', function () { const clickSpy = Sinon.spy(); - render( - - ); + renderCollectionsList({ + namespace: 'db', + collections: colls, + onCollectionClick: clickSpy, + }); expect(screen.getByTestId('collection-grid')).to.exist; @@ -125,13 +214,11 @@ describe('databases and collections list', function () { }); it('should not display statistics (except storage size) on timeseries collection card', function () { - render( - {}} - > - ); + renderCollectionsList({ + namespace: 'db', + collections: colls, + onCollectionClick: () => {}, + }); const timeseriesCard = screen .getByText('bat.bat') @@ -143,5 +230,51 @@ describe('databases and collections list', function () { expect(timeseriesCard).to.not.contain.text('Indexes:'); expect(timeseriesCard).to.not.contain.text('Total index size:'); }); + + it('should display statistics when collStats are enabled', async function () { + await preferences.savePreferences({ enableDbAndCollStats: true }); + + const coll = createCollection('bar'); + + renderCollectionsList({ + namespace: 'db', + collections: [coll], + onCollectionClick: () => {}, + }); + + expect(screen.getByText(/Storage size/)).to.exist; + expect(screen.getByText('1.50 kB')).to.exist; + expect(screen.getByText(/Documents/)).to.exist; + expect(screen.getByText('10')).to.exist; + expect(screen.getByText(/Avg. document size/)).to.exist; + expect(screen.getByText('150.00 B')).to.exist; + expect(screen.getByText(/Indexes/)).to.exist; + expect(screen.getByText('15')).to.exist; + expect(screen.getByText(/Total index size/)).to.exist; + expect(screen.getByText('16.00 B')).to.exist; + }); + + it('should not display statistics when collStats are disabled', async function () { + await preferences.savePreferences({ enableDbAndCollStats: false }); + + const coll = createCollection('bar'); + + renderCollectionsList({ + namespace: 'db', + collections: [coll], + onCollectionClick: () => {}, + }); + + expect(screen.queryByText(/Storage size/)).not.to.exist; + expect(screen.queryByText('1.50 kB')).not.to.exist; + expect(screen.queryByText(/Documents/)).not.to.exist; + expect(screen.queryByText('10')).not.to.exist; + expect(screen.queryByText(/Avg. document size/)).not.to.exist; + expect(screen.queryByText('150.00 B')).not.to.exist; + expect(screen.queryByText(/Indexes/)).not.to.exist; + expect(screen.queryByText('15')).not.to.exist; + expect(screen.queryByText(/Total index size/)).not.to.exist; + expect(screen.queryByText('16.00 B')).not.to.exist; + }); }); }); diff --git a/packages/databases-collections-list/src/namespace-card.tsx b/packages/databases-collections-list/src/namespace-card.tsx index 10bf1638d5d..90263412ada 100644 --- a/packages/databases-collections-list/src/namespace-card.tsx +++ b/packages/databases-collections-list/src/namespace-card.tsx @@ -26,7 +26,7 @@ import type { } from '@mongodb-js/compass-components'; import { NamespaceParam } from './namespace-param'; import type { ViewType } from './use-view-type'; -import { usePreference } from 'compass-preferences-model/provider'; +import { usePreferences } from 'compass-preferences-model/provider'; const cardTitleGroup = css({ display: 'flex', @@ -225,7 +225,10 @@ export const NamespaceItemCard: React.FunctionComponent< isNonExistent, ...props }) => { - const readOnly = usePreference('readOnly'); + const { readOnly, enableDbAndCollStats } = usePreferences([ + 'readOnly', + 'enableDbAndCollStats', + ]); const darkMode = useDarkMode(); const [hoverProps, isHovered] = useHoverState(); const [focusProps, focusState] = useFocusState(); @@ -330,21 +333,23 @@ export const NamespaceItemCard: React.FunctionComponent< {viewType === 'grid' && badgesGroup} -
- {data.map(({ label, value, hint, insights }, idx) => { - return ( - - ); - })} -
+ {enableDbAndCollStats && ( +
+ {data.map(({ label, value, hint, insights }, idx) => { + return ( + + ); + })} +
+ )} ); }; diff --git a/packages/databases-collections-list/src/namespace-param.tsx b/packages/databases-collections-list/src/namespace-param.tsx index dcd1426628b..fbbeaf188f6 100644 --- a/packages/databases-collections-list/src/namespace-param.tsx +++ b/packages/databases-collections-list/src/namespace-param.tsx @@ -120,6 +120,7 @@ export const NamespaceParam: React.FunctionComponent<{ status === 'refreshing' && namespaceParamValueRefreshing, shouldAnimate && fadeIn )} + data-testid="namespace-param-value" > {missingValue ? '—' : value} diff --git a/packages/databases-collections/src/collections-plugin.spec.tsx b/packages/databases-collections/src/collections-plugin.spec.tsx index 4e35039402f..703516a844d 100644 --- a/packages/databases-collections/src/collections-plugin.spec.tsx +++ b/packages/databases-collections/src/collections-plugin.spec.tsx @@ -11,6 +11,10 @@ import { import { expect } from 'chai'; import { CollectionsPlugin } from './collections-plugin'; import Sinon from 'sinon'; +import { + type PreferencesAccess, + createSandboxFromDefaultPreferences, +} from 'compass-preferences-model'; describe('Collections [Plugin]', function () { let dataService: any; @@ -18,8 +22,10 @@ describe('Collections [Plugin]', function () { let appRegistry: Sinon.SinonSpiedInstance< RenderWithConnectionsResult['globalAppRegistry'] >; + let preferences: PreferencesAccess; - beforeEach(function () { + beforeEach(async function () { + preferences = await createSandboxFromDefaultPreferences(); mongodbInstance = Sinon.spy( new MongoDBInstance({ databases: [ @@ -30,6 +36,7 @@ describe('Collections [Plugin]', function () { }, ], topologyDescription: { type: 'ReplicaSetWithPrimary' }, + preferences, } as any) ); for (const db of mongodbInstance.databases) { diff --git a/packages/databases-collections/src/databases-plugin.spec.tsx b/packages/databases-collections/src/databases-plugin.spec.tsx index 09a1cc5755a..fd9c07c1c57 100644 --- a/packages/databases-collections/src/databases-plugin.spec.tsx +++ b/packages/databases-collections/src/databases-plugin.spec.tsx @@ -11,10 +11,15 @@ import { import { expect } from 'chai'; import { DatabasesPlugin } from './databases-plugin'; import Sinon from 'sinon'; +import { + createSandboxFromDefaultPreferences, + type PreferencesAccess, +} from 'compass-preferences-model'; describe('Databasees [Plugin]', function () { let dataService: any; let mongodbInstance: Sinon.SinonSpiedInstance; + let preferences: PreferencesAccess; let appRegistry: Sinon.SinonSpiedInstance< RenderWithConnectionsResult['globalAppRegistry'] >; @@ -26,10 +31,12 @@ describe('Databasees [Plugin]', function () { describe('with loaded databases', function () { beforeEach(async function () { + preferences = await createSandboxFromDefaultPreferences(); mongodbInstance = Sinon.spy( new MongoDBInstance({ databases: [], topologyDescription: { type: 'ReplicaSetWithPrimary' }, + preferences, } as any) ); diff --git a/packages/databases-collections/src/modules/collections.ts b/packages/databases-collections/src/modules/collections.ts index 268e43decc2..89bacd28ffc 100644 --- a/packages/databases-collections/src/modules/collections.ts +++ b/packages/databases-collections/src/modules/collections.ts @@ -106,7 +106,10 @@ const reducer: Reducer = ( export const refreshCollections = (): CollectionsThunkAction => { return (_dispatch, _getState, { database, dataService }) => { - void database.fetchCollectionsDetails({ dataService, force: true }); + void database.fetchCollectionsDetails({ + dataService, + force: true, + }); }; }; diff --git a/packages/databases-collections/src/stores/collections-store.ts b/packages/databases-collections/src/stores/collections-store.ts index bddf6bdd1e4..5a974a85a35 100644 --- a/packages/databases-collections/src/stores/collections-store.ts +++ b/packages/databases-collections/src/stores/collections-store.ts @@ -43,7 +43,11 @@ export function activatePlugin( }, }, applyMiddleware( - thunk.withExtraArgument({ globalAppRegistry, database, dataService }) + thunk.withExtraArgument({ + globalAppRegistry, + database, + dataService, + }) ) ); diff --git a/packages/instance-model/index.d.ts b/packages/instance-model/index.d.ts index 4ccc05a08e4..629a07d1123 100644 --- a/packages/instance-model/index.d.ts +++ b/packages/instance-model/index.d.ts @@ -3,6 +3,7 @@ import type { DataService } from 'mongodb-data-service'; import type { Collection as DatabaseCollection } from 'mongodb-database-model'; import Database from 'mongodb-database-model'; import { CollectionCollection } from 'mongodb-collection-model'; +import type { PreferencesAccess } from 'compass-preferences-model'; declare const ServerType: { humanize(serverType: string): string; @@ -105,6 +106,7 @@ declare class MongoDBInstanceProps { databases: DatabaseCollection; csfleMode: 'enabled' | 'disabled' | 'unavailable'; topologyDescription: TopologyDescription; + preferences: PreferencesAccess; } declare class MongoDBInstance extends MongoDBInstanceProps { diff --git a/packages/instance-model/lib/model.js b/packages/instance-model/lib/model.js index 39eebf95688..213c578f546 100644 --- a/packages/instance-model/lib/model.js +++ b/packages/instance-model/lib/model.js @@ -135,6 +135,24 @@ const InstanceModel = AmpersandModel.extend( isSearchIndexesSupported: 'boolean', atlasVersion: { type: 'string', default: '' }, csfleMode: { type: 'string', default: 'unavailable' }, + shouldFetchDbAndCollStats: { type: 'boolean', default: false }, + }, + initialize: function ({ preferences, ...props }) { + // Initialize the property directly from preferences + this.set({ + shouldFetchDbAndCollStats: + preferences.getPreferences().enableDbAndCollStats, + }); + + // Listen to preference changes using the preferences API + this._preferenceUnsubscribe = preferences.onPreferenceValueChanged( + 'enableDbAndCollStats', + (value) => { + this.set({ shouldFetchDbAndCollStats: value }); + } + ); + + AmpersandModel.prototype.initialize.call(this, props); }, derived: { isRefreshing: { @@ -146,8 +164,8 @@ const InstanceModel = AmpersandModel.extend( isTopologyWritable: { deps: ['topologyDescription.type'], fn() { - return TopologyType.isWritable(this.topologyDescription.type) - } + return TopologyType.isWritable(this.topologyDescription.type); + }, }, singleServerType: { deps: ['topologyDescription.type', 'topologyDescription.servers'], @@ -156,16 +174,23 @@ const InstanceModel = AmpersandModel.extend( return this.topologyDescription.servers[0].type; } return null; - } + }, }, isServerWritable: { deps: ['singleServerType'], fn() { - return this.singleServerType !== null && ServerType.isWritable(this.singleServerType); - } + return ( + this.singleServerType !== null && + ServerType.isWritable(this.singleServerType) + ); + }, }, isWritable: { - deps: ['topologyDescription.type', 'isTopologyWritable', 'isServerWritable'], + deps: [ + 'topologyDescription.type', + 'isTopologyWritable', + 'isServerWritable', + ], fn() { if (this.isTopologyWritable) { if (this.topologyDescription.type === TopologyType.SINGLE) { @@ -176,22 +201,35 @@ const InstanceModel = AmpersandModel.extend( } else { return false; } - } + }, }, description: { - deps: ['topologyDescription.type', 'isTopologyWritable', 'isServerWritable', 'singleServerType'], + deps: [ + 'topologyDescription.type', + 'isTopologyWritable', + 'isServerWritable', + 'singleServerType', + ], fn() { const topologyType = this.topologyDescription.type; if (this.isTopologyWritable) { if (topologyType === TopologyType.SINGLE) { - const message = this.isServerWritable ? 'is writable' : 'is not writable'; - return `Single connection to server type: ${ServerType.humanize(this.singleServerType)} ${message}`; + const message = this.isServerWritable + ? 'is writable' + : 'is not writable'; + return `Single connection to server type: ${ServerType.humanize( + this.singleServerType + )} ${message}`; } - return `Topology type: ${TopologyType.humanize(topologyType)} is writable`; + return `Topology type: ${TopologyType.humanize( + topologyType + )} is writable`; } - return `Topology type: ${TopologyType.humanize(topologyType)} is not writable`; - } + return `Topology type: ${TopologyType.humanize( + topologyType + )} is not writable`; + }, }, env: { deps: ['isAtlas', 'isLocalAtlas', 'dataLake'], @@ -203,8 +241,8 @@ const InstanceModel = AmpersandModel.extend( return Environment.ATLAS; } return Environment.ON_PREM; - } - } + }, + }, }, children: { host: HostInfo, @@ -363,7 +401,11 @@ const InstanceModel = AmpersandModel.extend( }, removeAllListeners() { - InstanceModel.removeAllListeners(this) + // Clean up preference listeners + if (this._preferenceUnsubscribe) { + this._preferenceUnsubscribe(); + } + InstanceModel.removeAllListeners(this); }, toJSON(opts = { derived: true }) { @@ -376,4 +418,4 @@ module.exports = Object.assign(InstanceModel, { removeAllListeners(model) { removeListenersRec(model); }, -}); \ No newline at end of file +}); diff --git a/packages/instance-model/package.json b/packages/instance-model/package.json index b6b5f41f596..0431125e544 100644 --- a/packages/instance-model/package.json +++ b/packages/instance-model/package.json @@ -31,7 +31,8 @@ "ampersand-model": "^8.0.1", "mongodb-collection-model": "^5.25.8", "mongodb-data-service": "^22.25.8", - "mongodb-database-model": "^2.25.8" + "mongodb-database-model": "^2.25.8", + "compass-preferences-model": "^2.33.8" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.3.8", diff --git a/packages/instance-model/test/index.test.js b/packages/instance-model/test/index.test.js index 83276fa851b..9dd8f78cf86 100644 --- a/packages/instance-model/test/index.test.js +++ b/packages/instance-model/test/index.test.js @@ -1,15 +1,34 @@ 'use strict'; const { expect } = require('chai'); const { MongoDBInstance } = require('../'); +const { + createSandboxFromDefaultPreferences, +} = require('compass-preferences-model'); describe('mongodb-instance-model', function () { + let preferences; + + beforeEach(async function () { + preferences = await createSandboxFromDefaultPreferences(); + }); + it('should be in initial state when created', function () { - const instance = new MongoDBInstance({ _id: 'abc' }); + const instance = new MongoDBInstance({ _id: 'abc', preferences }); expect(instance).to.have.property('status', 'initial'); expect(instance.build.toJSON()).to.be.an('object').that.is.empty; expect(instance.host.toJSON()).to.be.an('object').that.is.empty; }); + it('should answer shouldFetchDbAndCollStats based on preferences', async function () { + const instance = new MongoDBInstance({ _id: 'abc', preferences }); + + await preferences.savePreferences({ enableDbAndCollStats: true }); + expect(instance.shouldFetchDbAndCollStats).to.equal(true); + + await preferences.savePreferences({ enableDbAndCollStats: false }); + expect(instance.shouldFetchDbAndCollStats).to.equal(false); + }); + context('with mocked dataService', function () { const dataService = { instance() { @@ -24,7 +43,7 @@ describe('mongodb-instance-model', function () { }; it('should fetch and populate instance info when fetch called', async function () { - const instance = new MongoDBInstance({ _id: 'abc' }); + const instance = new MongoDBInstance({ _id: 'abc', preferences }); await instance.fetch({ dataService }); @@ -61,6 +80,7 @@ describe('mongodb-instance-model', function () { _id: 'foo', hostname: 'foo.com', port: 1234, + preferences, topologyDescription: getTopologyDescription(), });