From 4bf2666c3c75b06e3938404ea13476581ba3dadf Mon Sep 17 00:00:00 2001 From: Sergey Petushkov Date: Thu, 14 Dec 2023 15:51:08 +0100 Subject: [PATCH] feat(sidebar): disable clicking on the performance tab when unsupported in Atlas (#5235) * feat(sidebar): disallow clicking on the performance tab when unsupported * chore(sidebar): use the same behavior for adf; fix tests --- package-lock.json | 2 + packages/compass-home/src/index.spec.tsx | 2 + packages/compass-sidebar/package.json | 1 + .../src/components/navigation-items.spec.tsx | 15 +- .../src/components/navigation-items.tsx | 152 ++++++++++++------ packages/compass-sidebar/src/index.ts | 7 + packages/compass-sidebar/src/modules/index.ts | 10 +- .../modules/is-performance-tab-supported.ts | 32 ++++ .../compass-sidebar/src/stores/store.spec.ts | 3 + packages/compass-sidebar/src/stores/store.ts | 25 +++ 10 files changed, 197 insertions(+), 52 deletions(-) create mode 100644 packages/compass-sidebar/src/modules/is-performance-tab-supported.ts diff --git a/package-lock.json b/package-lock.json index c6a9d658233..ee79d4aa29a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47570,6 +47570,7 @@ "eslint": "^7.25.0", "lodash": "^4.17.21", "mocha": "^10.2.0", + "mongodb": "^6.3.0", "mongodb-ns": "^2.4.0", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -60945,6 +60946,7 @@ "hadron-app-registry": "^9.1.1", "lodash": "^4.17.21", "mocha": "^10.2.0", + "mongodb": "^6.3.0", "mongodb-data-service": "^22.16.2", "mongodb-instance-model": "^12.16.4", "mongodb-ns": "^2.4.0", diff --git a/packages/compass-home/src/index.spec.tsx b/packages/compass-home/src/index.spec.tsx index 9e3fe669f43..b9bd54d5850 100644 --- a/packages/compass-home/src/index.spec.tsx +++ b/packages/compass-home/src/index.spec.tsx @@ -27,6 +27,8 @@ const createDataService = () => ({ configuredKMSProviders() { return []; }, + currentOp() {}, + top() {}, on() {}, off() {}, removeListener() {}, diff --git a/packages/compass-sidebar/package.json b/packages/compass-sidebar/package.json index 21d9dbb8457..de0508591c4 100644 --- a/packages/compass-sidebar/package.json +++ b/packages/compass-sidebar/package.json @@ -107,6 +107,7 @@ "eslint": "^7.25.0", "lodash": "^4.17.21", "mocha": "^10.2.0", + "mongodb": "^6.3.0", "mongodb-ns": "^2.4.0", "nyc": "^15.1.0", "prettier": "^2.7.1", diff --git a/packages/compass-sidebar/src/components/navigation-items.spec.tsx b/packages/compass-sidebar/src/components/navigation-items.spec.tsx index d169c3c9dc8..8b0e43b95b7 100644 --- a/packages/compass-sidebar/src/components/navigation-items.spec.tsx +++ b/packages/compass-sidebar/src/components/navigation-items.spec.tsx @@ -20,8 +20,8 @@ function renderNavigationItems( onAction={() => { /* noop */ }} - showPerformanceItem={false} showCreateDatabaseAction={true} + isPerformanceTabSupported={true} onFilterChange={() => { /* noop */ }} @@ -59,4 +59,17 @@ describe('NavigationItems [Component]', function () { expect(screen.queryByLabelText(createDatabaseText)).to.not.exist; }); }); + + describe('when performance tab is not supported', function () { + it('renders disabled "Performance" navigation item', function () { + renderNavigationItems({ + isPerformanceTabSupported: false, + }); + + expect(screen.getByRole('button', { name: 'Performance' })).to.exist; + expect( + screen.getByRole('button', { name: 'Performance' }) + ).to.have.attribute('aria-disabled', 'true'); + }); + }); }); diff --git a/packages/compass-sidebar/src/components/navigation-items.tsx b/packages/compass-sidebar/src/components/navigation-items.tsx index 22a90ecf35a..b24ec66a76f 100644 --- a/packages/compass-sidebar/src/components/navigation-items.tsx +++ b/packages/compass-sidebar/src/components/navigation-items.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { connect } from 'react-redux'; import { useHoverState, @@ -14,6 +14,9 @@ import { PerformanceSignals, Placeholder, ContentWithFallback, + palette, + useDarkMode, + Tooltip, } from '@mongodb-js/compass-components'; import { usePreference, withPreferences } from 'compass-preferences-model'; import type { ItemAction } from '@mongodb-js/compass-components'; @@ -117,17 +120,31 @@ const navigationItemLabel = css({ marginLeft: spacing[2], }); +const navigationItemDisabledDarkModeStyles = css({ + '--item-color': palette.gray.dark1, + '--item-color-active': palette.gray.dark1, + '--item-bg-color-hover': 'var(--item-bg-color)', +}); + +const navigationItemDisabledLightModeStyles = css({ + '--item-color': palette.gray.base, + '--item-color-active': palette.gray.base, + '--item-bg-color-hover': 'var(--item-bg-color)', +}); + const navigationItemActionIcons = css({ color: 'inherit' }); export function NavigationItem({ isExpanded, onAction, - onClick, + onClick: onButtonClick, glyph, label, actions, isActive, showTooManyCollectionsInsight, + disabled: isButtonDisabled = false, + disabledMessage: buttonDisabledMessage, }: { isExpanded?: boolean; onAction(actionName: Actions, ...rest: any[]): void; @@ -137,17 +154,35 @@ export function NavigationItem({ actions?: ItemAction[]; isActive: boolean; showTooManyCollectionsInsight?: boolean; + disabled?: boolean; + disabledMessage?: string; }) { + const darkMode = useDarkMode(); const showInsights = usePreference('showInsights', React); + const onClick = useCallback(() => { + if (isButtonDisabled) { + return; + } + onButtonClick(); + }, [isButtonDisabled, onButtonClick]); const [hoverProps] = useHoverState(); const focusRingProps = useFocusRing(); const defaultActionProps = useDefaultAction(onClick); const navigationItemProps = mergeProps( { - className: cx(navigationItem, isActive && activeNavigationItem), + className: cx( + navigationItem, + isActive && activeNavigationItem, + isButtonDisabled && + (darkMode + ? navigationItemDisabledDarkModeStyles + : navigationItemDisabledLightModeStyles) + ), + role: 'button', ['aria-label']: label, ['aria-current']: isActive, + ['aria-disabled']: isButtonDisabled, tabIndex: 0, }, hoverProps, @@ -156,37 +191,52 @@ export function NavigationItem({ ) as React.HTMLProps; return ( -
-
-
- - {isExpanded && {label}} -
- {showInsights && isExpanded && showTooManyCollectionsInsight && ( -
- + { + const props = mergeProps(triggerProps, navigationItemProps); + return ( +
+
+
+ + {isExpanded && ( + {label} + )} + {tooltip} +
+ {showInsights && isExpanded && showTooManyCollectionsInsight && ( +
+ +
+ )} + {!isButtonDisabled && isExpanded && actions && ( + + iconSize="small" + onAction={onAction} + data-testid="sidebar-navigation-item-actions" + actions={actions} + // This is what renders the "create database" action, + // the icons here should always be clearly visible, + // so we let the icon to inherit the foreground color of + // the text + isVisible={true} + iconClassName={navigationItemActionIcons} + collapseToMenuThreshold={3} + > + )} +
+
- )} - {isExpanded && actions && ( - - iconSize="small" - onAction={onAction} - data-testid="sidebar-navigation-item-actions" - actions={actions} - // This is what renders the "create database" action, - // the icons here should always be clearly visible, - // so we let the icon to inherit the foreground color of - // the text - isVisible={true} - iconClassName={navigationItemActionIcons} - collapseToMenuThreshold={3} - > - )} -
-
-
+ ); + }} + > + {buttonDisabledMessage} +
); } @@ -201,8 +251,8 @@ const PlaceholderItem = ({ forLabel }: { forLabel: string }) => { export function NavigationItems({ isReady, isExpanded, - showCreateDatabaseAction = false, - showPerformanceItem = false, + showCreateDatabaseAction, + isPerformanceTabSupported, onFilterChange, onAction, currentLocation, @@ -211,8 +261,8 @@ export function NavigationItems({ }: { isReady?: boolean; isExpanded?: boolean; - showCreateDatabaseAction?: boolean; - showPerformanceItem?: boolean; + showCreateDatabaseAction: boolean; + isPerformanceTabSupported: boolean; onFilterChange(regex: RegExp | null): void; onAction(actionName: string, ...rest: any[]): void; currentLocation: string | null; @@ -272,16 +322,16 @@ export function NavigationItems({ label="My Queries" isActive={currentLocation === 'My Queries'} /> - {showPerformanceItem && ( - - isExpanded={isExpanded} - onAction={onAction} - onClick={openPerformanceWorkspace} - glyph="Gauge" - label="Performance" - isActive={currentLocation === 'Performance'} - /> - )} + + isExpanded={isExpanded} + onAction={onAction} + onClick={openPerformanceWorkspace} + glyph="Gauge" + label="Performance" + isActive={currentLocation === 'Performance'} + disabled={!isPerformanceTabSupported} + disabledMessage="Performance metrics are not available for your deployment or to your database user" + /> isExpanded={isExpanded} onAction={onAction} @@ -321,9 +371,10 @@ const mapStateToProps = ( 0 ); - const isReady = ['ready', 'refreshing'].includes( - state.instance?.status ?? '' - ); + const isReady = + ['ready', 'refreshing'].includes(state.instance?.status ?? '') && + state.isPerformanceTabSupported !== null; + const isDataLake = state.instance?.dataLake.isDataLake ?? false; const isWritable = state.instance?.isWritable ?? false; @@ -332,6 +383,7 @@ const mapStateToProps = ( showPerformanceItem: !isDataLake, showCreateDatabaseAction: !isDataLake && isWritable && !preferencesReadOnly, showTooManyCollectionsInsight: totalCollectionsCount > 10_000, + isPerformanceTabSupported: !isDataLake && !!state.isPerformanceTabSupported, }; }; diff --git a/packages/compass-sidebar/src/index.ts b/packages/compass-sidebar/src/index.ts index 2ca8a7c2e5e..0dc73f2681d 100644 --- a/packages/compass-sidebar/src/index.ts +++ b/packages/compass-sidebar/src/index.ts @@ -7,6 +7,8 @@ import { mongoDBInstanceLocator } from '@mongodb-js/compass-app-stores/provider' import { dataServiceLocator } from 'mongodb-data-service/provider'; import type { DataService } from 'mongodb-data-service'; import type { MongoDBInstance } from 'mongodb-instance-model'; +import type { LoggerAndTelemetry } from '@mongodb-js/compass-logging/provider'; +import { createLoggerAndTelemetryLocator } from '@mongodb-js/compass-logging/provider'; function activate() { // noop @@ -21,6 +23,7 @@ export const CompassSidebarPlugin = registerHadronPlugin< { instance: () => MongoDBInstance; dataService: () => DataService; + logger: () => LoggerAndTelemetry; } >( { @@ -32,10 +35,12 @@ export const CompassSidebarPlugin = registerHadronPlugin< globalAppRegistry, instance, dataService, + logger, }: { globalAppRegistry: AppRegistry; instance: MongoDBInstance; dataService: DataService; + logger: LoggerAndTelemetry; }, helpers: ActivateHelpers ) { @@ -45,6 +50,7 @@ export const CompassSidebarPlugin = registerHadronPlugin< instance, dataService, connectionInfo: initialConnectionInfo, + logger, }, helpers ); @@ -57,6 +63,7 @@ export const CompassSidebarPlugin = registerHadronPlugin< { instance: mongoDBInstanceLocator, dataService: dataServiceLocator, + logger: createLoggerAndTelemetryLocator('COMPASS-SIDEBAR-UI'), } ); diff --git a/packages/compass-sidebar/src/modules/index.ts b/packages/compass-sidebar/src/modules/index.ts index bf844b18f0b..6722858bc94 100644 --- a/packages/compass-sidebar/src/modules/index.ts +++ b/packages/compass-sidebar/src/modules/index.ts @@ -30,6 +30,11 @@ import isExpanded from './is-expanded'; import type { AppRegistry } from 'hadron-app-registry'; import type { DataServiceAction, DataServiceState } from './data-service'; import dataService from './data-service'; +import type { + IsPerformanceTabSupportedState, + SetIsPerformanceTabSupportedAction, +} from './is-performance-tab-supported'; +import isPerformanceTabSupported from './is-performance-tab-supported'; export interface RootState { appRegistry: { @@ -44,6 +49,7 @@ export interface RootState { isDetailsExpanded: IsDetailsExpandedState; isGenuineMongoDBVisible: IsGenuineMongoDBVisibleState; isExpanded: IsExpandedState; + isPerformanceTabSupported: IsPerformanceTabSupportedState; } export type RootAction = @@ -54,7 +60,8 @@ export type RootAction = | ToggleIsDetailsExpandedAction | IsGenuineMongoDBVisibleAction | IsExpandedAction - | DataServiceAction; + | DataServiceAction + | SetIsPerformanceTabSupportedAction; /** * The reducer. @@ -69,6 +76,7 @@ const reducer = combineReducers({ isDetailsExpanded, isGenuineMongoDBVisible, isExpanded, + isPerformanceTabSupported, }); export default reducer; diff --git a/packages/compass-sidebar/src/modules/is-performance-tab-supported.ts b/packages/compass-sidebar/src/modules/is-performance-tab-supported.ts new file mode 100644 index 00000000000..cdda2179d81 --- /dev/null +++ b/packages/compass-sidebar/src/modules/is-performance-tab-supported.ts @@ -0,0 +1,32 @@ +import type { RootAction } from '.'; + +const SET_IS_PERFORMANCE_TAB_SUPPORTED = + 'compass-sidebar/SET_IS_PERFORMANCE_TAB_SUPPORTED'; + +export type SetIsPerformanceTabSupportedAction = { + type: typeof SET_IS_PERFORMANCE_TAB_SUPPORTED; + isSupported: boolean; +}; + +export function setIsPerformanceTabSupported( + isSupported: boolean +): SetIsPerformanceTabSupportedAction { + return { + type: SET_IS_PERFORMANCE_TAB_SUPPORTED, + isSupported, + }; +} + +export type IsPerformanceTabSupportedState = boolean | null; + +const reducer = ( + state: IsPerformanceTabSupportedState = null, + action: RootAction +): IsPerformanceTabSupportedState => { + if (action.type === SET_IS_PERFORMANCE_TAB_SUPPORTED) { + return action.isSupported; + } + return state; +}; + +export default reducer; diff --git a/packages/compass-sidebar/src/stores/store.spec.ts b/packages/compass-sidebar/src/stores/store.spec.ts index dcc640716a9..ad0e93dc545 100644 --- a/packages/compass-sidebar/src/stores/store.spec.ts +++ b/packages/compass-sidebar/src/stores/store.spec.ts @@ -40,8 +40,11 @@ describe('SidebarStore [Store]', function () { getConnectionOptions() { return {}; }, + currentOp() {}, + top() {}, }, instance, + logger: { log: { warn() {} }, mongoLogId() {} }, } as any, { on() {}, cleanup() {}, addCleanup() {} } )); diff --git a/packages/compass-sidebar/src/stores/store.ts b/packages/compass-sidebar/src/stores/store.ts index 9d21f7f3d6a..7054b16ac5a 100644 --- a/packages/compass-sidebar/src/stores/store.ts +++ b/packages/compass-sidebar/src/stores/store.ts @@ -17,6 +17,9 @@ import type { ActivateHelpers, AppRegistry } from 'hadron-app-registry'; import type { MongoDBInstance } from 'mongodb-instance-model'; import type { DataService } from 'mongodb-data-service'; import type { ConnectionInfo } from '@mongodb-js/connection-storage/renderer'; +import { setIsPerformanceTabSupported } from '../modules/is-performance-tab-supported'; +import type { MongoServerError } from 'mongodb'; +import type { LoggerAndTelemetry } from '@mongodb-js/compass-logging/provider'; export function createSidebarStore( { @@ -24,11 +27,13 @@ export function createSidebarStore( instance, dataService, connectionInfo, + logger: { log, mongoLogId }, }: { globalAppRegistry: AppRegistry; instance: MongoDBInstance; dataService: DataService; connectionInfo: ConnectionInfo | null | undefined; + logger: LoggerAndTelemetry; }, { on, cleanup, addCleanup }: ActivateHelpers ): { @@ -165,6 +170,26 @@ export function createSidebarStore( store.dispatch(toggleSidebar()); }); + // Checking if "Performance" tab is supported by running commands required for + // the "Performance" tab to function + void Promise.all([dataService.currentOp(), dataService.top()]).then( + () => { + store.dispatch(setIsPerformanceTabSupported(true)); + }, + (err) => { + log.info( + mongoLogId(1_001_000_278), + 'Sidebar', + 'Performance tab requied commands failed', + { error: (err as Error).message } + ); + // Only disable performance tab if encountered Atlas error + const isSupported = + (err as MongoServerError).codeName === 'AtlasError' ? false : true; + store.dispatch(setIsPerformanceTabSupported(isSupported)); + } + ); + return { store, deactivate: cleanup,