diff --git a/packages/compass-collection/src/index.ts b/packages/compass-collection/src/index.ts index 966639c078c..4f754f76b0b 100644 --- a/packages/compass-collection/src/index.ts +++ b/packages/compass-collection/src/index.ts @@ -1,3 +1,4 @@ +import React from 'react'; import CollectionTab from './components/collection-tab'; import { activatePlugin as activateCollectionTabPlugin } from './stores/collection-tab'; import { registerHadronPlugin } from 'hadron-app-registry'; @@ -7,27 +8,32 @@ import { type DataService, } from '@mongodb-js/compass-connections/provider'; import { collectionModelLocator } from '@mongodb-js/compass-app-stores/provider'; -import type { WorkspaceComponent } from '@mongodb-js/compass-workspaces'; +import type { WorkspacePlugin } from '@mongodb-js/compass-workspaces'; import { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider'; +import { + CollectionWorkspaceTitle, + CollectionPluginTitleComponent, +} from './plugin-tab-title'; -export const CollectionTabPlugin = registerHadronPlugin( - { - name: 'CollectionTab', - component: CollectionTab, - activate: activateCollectionTabPlugin, - }, - { - dataService: dataServiceLocator as DataServiceLocator, - collection: collectionModelLocator, - workspaces: workspacesServiceLocator, - } -); - -export const WorkspaceTab: WorkspaceComponent<'Collection'> = { - name: 'Collection' as const, - component: CollectionTabPlugin, +export const WorkspaceTab: WorkspacePlugin = { + name: CollectionWorkspaceTitle, + provider: registerHadronPlugin( + { + name: CollectionWorkspaceTitle, + component: function CollectionProvider({ children }) { + return React.createElement(React.Fragment, null, children); + }, + activate: activateCollectionTabPlugin, + }, + { + dataService: dataServiceLocator as DataServiceLocator, + collection: collectionModelLocator, + workspaces: workspacesServiceLocator, + } + ), + content: CollectionTab, + header: CollectionPluginTitleComponent, }; -export default CollectionTabPlugin; export type { CollectionTabPluginMetadata } from './modules/collection-tab'; export { CollectionTabsProvider } from './components/collection-tab-provider'; diff --git a/packages/compass-collection/src/plugin-tab-title.tsx b/packages/compass-collection/src/plugin-tab-title.tsx new file mode 100644 index 00000000000..c0e29b8efcd --- /dev/null +++ b/packages/compass-collection/src/plugin-tab-title.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import toNS from 'mongodb-ns'; +import { + useConnectionInfo, + useConnectionsListRef, + useTabConnectionTheme, +} from '@mongodb-js/compass-connections/provider'; +import { + WorkspaceTab, + type WorkspaceTabCoreProps, +} from '@mongodb-js/compass-components'; +import type { WorkspacePluginProps } from '@mongodb-js/compass-workspaces'; + +import { type CollectionState } from './modules/collection-tab'; + +export const CollectionWorkspaceTitle = 'Collection' as const; + +type PluginTitleProps = { + isTimeSeries?: boolean; + isReadonly?: boolean; + sourceName?: string | null; +} & WorkspaceTabCoreProps & + WorkspacePluginProps; + +function _PluginTitle({ + editViewName, + isNonExistent, + isReadonly, + isTimeSeries, + sourceName, + namespace, + ...tabProps +}: PluginTitleProps) { + const { getThemeOf } = useTabConnectionTheme(); + const { getConnectionById } = useConnectionsListRef(); + const { id: connectionId } = useConnectionInfo(); + + const { database, collection, ns } = toNS(namespace); + const connectionName = getConnectionById(connectionId)?.title || ''; + const collectionType = isTimeSeries + ? 'timeseries' + : isReadonly + ? 'view' + : 'collection'; + // Similar to what we have in the collection breadcrumbs. + const tooltip: [string, string][] = [ + ['Connection', connectionName || ''], + ['Database', database], + ]; + if (sourceName) { + tooltip.push(['View', collection]); + tooltip.push(['Derived from', toNS(sourceName).collection]); + } else if (editViewName) { + tooltip.push(['View', toNS(editViewName).collection]); + tooltip.push(['Derived from', collection]); + } else { + tooltip.push(['Collection', collection]); + } + + return ( + + ); +} + +export const CollectionPluginTitleComponent = connect( + (state: CollectionState) => ({ + isTimeSeries: state.metadata?.isTimeSeries, + isReadonly: state.metadata?.isReadonly, + sourceName: state.metadata?.sourceName, + }) +)(_PluginTitle); diff --git a/packages/compass-components/src/components/workspace-tabs/tab.tsx b/packages/compass-components/src/components/workspace-tabs/tab.tsx index 14892ec71b2..227eee98332 100644 --- a/packages/compass-components/src/components/workspace-tabs/tab.tsx +++ b/packages/compass-components/src/components/workspace-tabs/tab.tsx @@ -5,6 +5,7 @@ import { spacing } from '@leafygreen-ui/tokens'; import type { GlyphName } from '@leafygreen-ui/icon'; import { useSortable } from '@dnd-kit/sortable'; import { CSS as cssDndKit } from '@dnd-kit/utilities'; +import { useId } from '@react-aria/utils'; import { useDarkMode } from '../../hooks/use-theme'; import { Icon, IconButton } from '../leafygreen'; import { mergeProps } from '../../utils/merge-props'; @@ -149,6 +150,10 @@ const draggingTabStyles = css({ cursor: 'grabbing !important', }); +const nonExistentStyles = css({ + color: palette.gray.base, +}); + const tabIconStyles = css({ color: 'currentColor', marginLeft: spacing[300], @@ -185,25 +190,34 @@ const workspaceTabTooltipStyles = css({ textWrap: 'wrap', }); -type TabProps = { +// The plugins provide these essential props use to render the tab. +// The workspace-tabs component provides the other parts of TabProps. +export type WorkspaceTabPluginProps = { connectionName?: string; type: string; - title: string; + title: React.ReactNode; + isNonExistent?: boolean; + iconGlyph: GlyphName | 'Logo' | 'Server'; + tooltip?: [string, string][]; + tabTheme?: Partial; +}; + +export type WorkspaceTabCoreProps = { isSelected: boolean; isDragging: boolean; onSelect: () => void; onClose: () => void; - iconGlyph: GlyphName | 'Logo' | 'Server'; tabContentId: string; - tooltip?: [string, string][]; - tabTheme?: Partial; }; +type TabProps = WorkspaceTabCoreProps & WorkspaceTabPluginProps; + function Tab({ connectionName, type, title, tooltip, + isNonExistent, isSelected, isDragging, onSelect, @@ -213,7 +227,7 @@ function Tab({ tabTheme, className: tabClassName, ...props -}: TabProps & React.HTMLProps) { +}: TabProps & Omit, 'title'>) { const darkMode = useDarkMode(); const defaultActionProps = useDefaultAction(onSelect); const { listeners, setNodeRef, transform, transition } = useSortable({ @@ -240,6 +254,8 @@ function Tab({ cursor: 'grabbing !important', }; + const tabId = useId(); + return ( {iconGlyph === 'Logo' && ( diff --git a/packages/compass-components/src/components/workspace-tabs/workspace-tabs.spec.tsx b/packages/compass-components/src/components/workspace-tabs/workspace-tabs.spec.tsx index ec0868042c2..b882fd1e025 100644 --- a/packages/compass-components/src/components/workspace-tabs/workspace-tabs.spec.tsx +++ b/packages/compass-components/src/components/workspace-tabs/workspace-tabs.spec.tsx @@ -9,14 +9,23 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { WorkspaceTabs } from './workspace-tabs'; -import type { TabProps } from './workspace-tabs'; +import { Tab, type WorkspaceTabCoreProps } from './tab'; -function mockTab(tabId: number): TabProps { +function mockTab(tabId: number): { + id: string; + renderTab: (tabProps: WorkspaceTabCoreProps) => ReturnType; +} { return { - type: 'Documents', - title: `mock-tab-${tabId}`, id: `${tabId}-content`, - iconGlyph: 'Folder', + renderTab: (tabProps: WorkspaceTabCoreProps) => ( + + ), }; } diff --git a/packages/compass-components/src/components/workspace-tabs/workspace-tabs.tsx b/packages/compass-components/src/components/workspace-tabs/workspace-tabs.tsx index d85852a4a04..5d48e3f4388 100644 --- a/packages/compass-components/src/components/workspace-tabs/workspace-tabs.tsx +++ b/packages/compass-components/src/components/workspace-tabs/workspace-tabs.tsx @@ -8,7 +8,6 @@ import React, { import { css, cx } from '@leafygreen-ui/emotion'; import { palette } from '@leafygreen-ui/palette'; import { spacing } from '@leafygreen-ui/tokens'; -import type { GlyphName } from '@leafygreen-ui/icon'; import { rgba } from 'polished'; import { @@ -28,7 +27,8 @@ import { useDarkMode } from '../../hooks/use-theme'; import { FocusState, useFocusState } from '../../hooks/use-focus-hover'; import { Icon, IconButton } from '../leafygreen'; import { mergeProps } from '../../utils/merge-props'; -import { Tab } from './tab'; +import type { Tab } from './tab'; +import type { WorkspaceTabCoreProps } from './tab'; import { useHotkeys } from '../../hooks/use-hotkeys'; export const scrollbarThumbLightTheme = rgba(palette.gray.base, 0.65); @@ -139,8 +139,13 @@ function useTabListKeyboardNavigation({ return [{ onKeyDown }]; } +type TabItem = { + id: string; + renderTab: (props: WorkspaceTabCoreProps) => ReturnType; +}; + type SortableItemProps = { - tab: TabProps; + tab: TabItem; index: number; selectedTabIndex: number; activeId: UniqueIdentifier | null; @@ -149,7 +154,7 @@ type SortableItemProps = { }; type SortableListProps = { - tabs: TabProps[]; + tabs: TabItem[]; selectedTabIndex: number; onMove: (oldTabIndex: number, newTabIndex: number) => void; onSelect: (tabIndex: number) => void; @@ -164,19 +169,10 @@ type WorkspaceTabsProps = { onSelectPrevTab: () => void; onCloseTab: (tabIndex: number) => void; onMoveTab: (oldTabIndex: number, newTabIndex: number) => void; - tabs: TabProps[]; + tabs: TabItem[]; selectedTabIndex: number; }; -export type TabProps = { - id: string; - type: string; - title: string; - tooltip?: [string, string][]; - connectionId?: string; - iconGlyph: GlyphName | 'Logo' | 'Server'; -} & Omit, 'id' | 'title'>; - export function useRovingTabIndex({ currentTabbable, }: { @@ -263,7 +259,7 @@ const SortableList = ({ >
- {tabs.map((tab: TabProps, index: number) => ( + {tabs.map((tab: TabItem, index: number) => ( { - const { id: tabId } = tabProps; - const onTabSelected = useCallback(() => { onSelect(index); }, [onSelect, index]); @@ -305,16 +299,13 @@ const SortableItem = ({ const isDragging = useMemo(() => tabId === activeId, [tabId, activeId]); - return ( - - ); + return renderTab({ + isSelected, + isDragging, + tabContentId: tabId, + onSelect: onTabSelected, + onClose: onTabClosed, + }); }; function WorkspaceTabs({ diff --git a/packages/compass-components/src/index.ts b/packages/compass-components/src/index.ts index d577dd2bf13..5f208e4fdc0 100644 --- a/packages/compass-components/src/index.ts +++ b/packages/compass-components/src/index.ts @@ -38,7 +38,11 @@ export { import { ResizeHandle, ResizeDirection } from './components/resize-handle'; import { Accordion } from './components/accordion'; import { CollapsibleFieldSet } from './components/collapsible-field-set'; -export { type TabTheme } from './components/workspace-tabs/tab'; +export { + Tab as WorkspaceTab, + type TabTheme, + type WorkspaceTabCoreProps, +} from './components/workspace-tabs/tab'; import { WorkspaceTabs } from './components/workspace-tabs/workspace-tabs'; import ResizableSidebar, { defaultSidebarWidth, diff --git a/packages/compass-data-modeling/src/index.spec.tsx b/packages/compass-data-modeling/src/index.spec.tsx index 3c3cf401c05..3aabdc09919 100644 --- a/packages/compass-data-modeling/src/index.spec.tsx +++ b/packages/compass-data-modeling/src/index.spec.tsx @@ -1,12 +1,18 @@ import React from 'react'; import { expect } from 'chai'; import { render } from '@mongodb-js/testing-library-compass'; -import CompassPlugin from './index'; +import { WorkspaceTab } from './index'; describe('Compass Plugin', function () { - const Plugin = CompassPlugin.withMockServices({}); + const Plugin = WorkspaceTab.provider.withMockServices({}); it('renders a Plugin', function () { - expect(() => render()).to.not.throw(); + expect(() => + render( + + + + ) + ).to.not.throw(); }); }); diff --git a/packages/compass-data-modeling/src/index.ts b/packages/compass-data-modeling/src/index.ts index a9f5f0c5fda..25196f4e70e 100644 --- a/packages/compass-data-modeling/src/index.ts +++ b/packages/compass-data-modeling/src/index.ts @@ -1,33 +1,35 @@ +import React from 'react'; import { registerHadronPlugin } from 'hadron-app-registry'; import { preferencesLocator } from 'compass-preferences-model/provider'; import { connectionsLocator } from '@mongodb-js/compass-connections/provider'; import { telemetryLocator } from '@mongodb-js/compass-telemetry/provider'; import { createLoggerLocator } from '@mongodb-js/compass-logging/provider'; -import type { WorkspaceComponent } from '@mongodb-js/compass-workspaces'; +import type { WorkspacePlugin } from '@mongodb-js/compass-workspaces'; import DataModelingComponent from './components/data-modeling'; import { mongoDBInstancesManagerLocator } from '@mongodb-js/compass-app-stores/provider'; import { dataModelStorageServiceLocator } from './provider'; import { activateDataModelingStore } from './store'; +import { PluginTabTitleComponent, WorkspaceName } from './plugin-tab-title'; -const DataModelingPlugin = registerHadronPlugin( - { - name: 'DataModeling', - component: DataModelingComponent, - activate: activateDataModelingStore, - }, - { - preferences: preferencesLocator, - connections: connectionsLocator, - instanceManager: mongoDBInstancesManagerLocator, - dataModelStorage: dataModelStorageServiceLocator, - track: telemetryLocator, - logger: createLoggerLocator('COMPASS-DATA-MODELING'), - } -); - -export const WorkspaceTab: WorkspaceComponent<'Data Modeling'> = { - name: 'Data Modeling', - component: DataModelingPlugin, +export const WorkspaceTab: WorkspacePlugin = { + name: WorkspaceName, + provider: registerHadronPlugin( + { + name: 'DataModeling', + component: function DataModelingProvider({ children }) { + return React.createElement(React.Fragment, null, children); + }, + activate: activateDataModelingStore, + }, + { + preferences: preferencesLocator, + connections: connectionsLocator, + instanceManager: mongoDBInstancesManagerLocator, + dataModelStorage: dataModelStorageServiceLocator, + track: telemetryLocator, + logger: createLoggerLocator('COMPASS-DATA-MODELING'), + } + ), + content: DataModelingComponent, + header: PluginTabTitleComponent, }; - -export default DataModelingPlugin; diff --git a/packages/compass-data-modeling/src/plugin-tab-title.tsx b/packages/compass-data-modeling/src/plugin-tab-title.tsx new file mode 100644 index 00000000000..40aac13afbf --- /dev/null +++ b/packages/compass-data-modeling/src/plugin-tab-title.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import { + WorkspaceTab, + type WorkspaceTabCoreProps, +} from '@mongodb-js/compass-components'; +import type { DataModelingState } from './store/reducer'; +import type { WorkspacePluginProps } from '@mongodb-js/compass-workspaces'; + +export const WorkspaceName = 'Data Modeling' as const; + +type WorkspaceProps = WorkspacePluginProps; +type PluginTabTitleProps = { + tabTitle: string; +} & WorkspaceTabCoreProps & + WorkspaceProps; + +function _TabTitle({ tabTitle, ...props }: PluginTabTitleProps) { + return ( + + ); +} + +export const PluginTabTitleComponent = connect((state: DataModelingState) => { + return { + tabTitle: + state.step === 'NO_DIAGRAM_SELECTED' + ? WorkspaceName + : state.diagram?.name ?? WorkspaceName, + }; +})(_TabTitle); diff --git a/packages/compass-saved-aggregations-queries/src/index.spec.tsx b/packages/compass-saved-aggregations-queries/src/index.spec.tsx index 82ed378a06c..eb1f7d1686d 100644 --- a/packages/compass-saved-aggregations-queries/src/index.spec.tsx +++ b/packages/compass-saved-aggregations-queries/src/index.spec.tsx @@ -2,7 +2,7 @@ import React from 'react'; import Sinon from 'sinon'; import { expect } from 'chai'; import { queries, pipelines } from '../test/fixtures'; -import { MyQueriesPlugin } from '.'; +import { WorkspaceTab } from '.'; import type { PipelineStorage, FavoriteQueryStorage, @@ -81,7 +81,7 @@ describe('AggregationsAndQueriesAndUpdatemanyList', function () { let connectionsStore: RenderWithConnectionsResult['connectionsStore']; const renderPlugin = () => { - const PluginWithMocks = MyQueriesPlugin.withMockServices({ + const PluginWithMocks = WorkspaceTab.provider.withMockServices({ instancesManager, favoriteQueryStorageAccess: { getStorage() { @@ -91,9 +91,17 @@ describe('AggregationsAndQueriesAndUpdatemanyList', function () { pipelineStorage: pipelineStorage, workspaces, }); - const result = renderWithConnections(, { - connections: [connectionOne.connectionInfo, connectionTwo.connectionInfo], - }); + const result = renderWithConnections( + + + , + { + connections: [ + connectionOne.connectionInfo, + connectionTwo.connectionInfo, + ], + } + ); connectionsStore = result.connectionsStore; }; diff --git a/packages/compass-saved-aggregations-queries/src/index.ts b/packages/compass-saved-aggregations-queries/src/index.ts index b760ec05565..71d96b26d27 100644 --- a/packages/compass-saved-aggregations-queries/src/index.ts +++ b/packages/compass-saved-aggregations-queries/src/index.ts @@ -1,10 +1,11 @@ +import React from 'react'; import { registerHadronPlugin } from 'hadron-app-registry'; import { mongoDBInstancesManagerLocator } from '@mongodb-js/compass-app-stores/provider'; import { createLoggerLocator } from '@mongodb-js/compass-logging/provider'; import { telemetryLocator } from '@mongodb-js/compass-telemetry/provider'; import { activatePlugin } from './stores'; import AggregationsQueriesList from './components/aggregations-queries-list'; -import type { WorkspaceComponent } from '@mongodb-js/compass-workspaces'; +import type { WorkspacePlugin } from '@mongodb-js/compass-workspaces'; import { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider'; import { pipelineStorageLocator, @@ -12,30 +13,29 @@ import { } from '@mongodb-js/my-queries-storage/provider'; import { preferencesLocator } from 'compass-preferences-model/provider'; import { connectionsLocator } from '@mongodb-js/compass-connections/provider'; +import { PluginTabTitleComponent, WorkspaceName } from './plugin-tab-title'; -const serviceLocators = { - connections: connectionsLocator, - instancesManager: mongoDBInstancesManagerLocator, - preferencesAccess: preferencesLocator, - logger: createLoggerLocator('COMPASS-MY-QUERIES-UI'), - track: telemetryLocator, - workspaces: workspacesServiceLocator, - pipelineStorage: pipelineStorageLocator, - favoriteQueryStorageAccess: favoriteQueryStorageAccessLocator, +export const WorkspaceTab: WorkspacePlugin = { + name: WorkspaceName, + provider: registerHadronPlugin( + { + name: WorkspaceName, + component: function MyQueriesProvider({ children }): any { + return React.createElement(React.Fragment, null, children); + }, + activate: activatePlugin, + }, + { + connections: connectionsLocator, + instancesManager: mongoDBInstancesManagerLocator, + preferencesAccess: preferencesLocator, + logger: createLoggerLocator('COMPASS-MY-QUERIES-UI'), + track: telemetryLocator, + workspaces: workspacesServiceLocator, + pipelineStorage: pipelineStorageLocator, + favoriteQueryStorageAccess: favoriteQueryStorageAccessLocator, + } + ), + content: AggregationsQueriesList, + header: PluginTabTitleComponent, }; - -export const MyQueriesPlugin = registerHadronPlugin( - { - name: 'MyQueries', - component: AggregationsQueriesList, - activate: activatePlugin, - }, - serviceLocators -); - -export const WorkspaceTab: WorkspaceComponent<'My Queries'> = { - name: 'My Queries' as const, - component: MyQueriesPlugin, -}; - -export default MyQueriesPlugin; diff --git a/packages/compass-saved-aggregations-queries/src/plugin-tab-title.tsx b/packages/compass-saved-aggregations-queries/src/plugin-tab-title.tsx new file mode 100644 index 00000000000..53114bee352 --- /dev/null +++ b/packages/compass-saved-aggregations-queries/src/plugin-tab-title.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { + WorkspaceTab, + type WorkspaceTabCoreProps, +} from '@mongodb-js/compass-components'; +import type { WorkspacePluginProps } from '@mongodb-js/compass-workspaces'; + +export const WorkspaceName = 'My Queries' as const; + +type PluginTabTitleProps = WorkspaceTabCoreProps & + WorkspacePluginProps; + +export function PluginTabTitleComponent(props: PluginTabTitleProps) { + return ( + + ); +} diff --git a/packages/compass-saved-aggregations-queries/src/stores/index.ts b/packages/compass-saved-aggregations-queries/src/stores/index.ts index d03e4b7d696..781678c2b3e 100644 --- a/packages/compass-saved-aggregations-queries/src/stores/index.ts +++ b/packages/compass-saved-aggregations-queries/src/stores/index.ts @@ -79,8 +79,12 @@ export type SavedQueryAggregationThunkAction< A extends Action = AnyAction > = ThunkAction; +type SavedQueryAggregationPluginProps = { + children?: React.ReactNode; +}; + export function activatePlugin( - _: Record, + _initialProps: SavedQueryAggregationPluginProps, services: MyQueriesServices ) { const store = configureStore(services); diff --git a/packages/compass-serverstats/src/index.ts b/packages/compass-serverstats/src/index.ts index 5b10bced541..a2de4a71407 100644 --- a/packages/compass-serverstats/src/index.ts +++ b/packages/compass-serverstats/src/index.ts @@ -1,3 +1,4 @@ +import React from 'react'; import { PerformanceComponent } from './components'; import { registerHadronPlugin } from 'hadron-app-registry'; import { @@ -9,38 +10,48 @@ import { mongoDBInstanceLocator } from '@mongodb-js/compass-app-stores/provider' import CurrentOpStore from './stores/current-op-store'; import ServerStatsStore from './stores/server-stats-graphs-store'; import TopStore from './stores/top-store'; -import type { WorkspaceComponent } from '@mongodb-js/compass-workspaces'; +import type { WorkspacePlugin } from '@mongodb-js/compass-workspaces'; +import { + WorkspaceName, + ServerStatsPluginTitleComponent, +} from './plugin-tab-title'; -const PerformancePlugin = registerHadronPlugin( - { - name: 'Performance', - component: PerformanceComponent, - activate(_initialProps: Record, { dataService, instance }) { - CurrentOpStore.onActivated(dataService); - ServerStatsStore.onActivated(dataService); - TopStore.onActivated(dataService, instance); +type PerformancePluginInitialProps = Record; - // TODO(COMPASS-7416): no stores or subscriptions are returned here, we'd - // need to refactor the stores of this package - return { - store: {}, - deactivate() { - // noop - }, - }; - }, - }, - { - dataService: dataServiceLocator as DataServiceLocator, - instance: mongoDBInstanceLocator, - } -); +const WorkspaceTab: WorkspacePlugin = { + name: WorkspaceName, + provider: registerHadronPlugin( + { + name: WorkspaceName, + component: function PerformanceProvider({ children }) { + return React.createElement(React.Fragment, null, children); + }, + activate( + _initialProps: PerformancePluginInitialProps, + { dataService, instance } + ) { + CurrentOpStore.onActivated(dataService); + ServerStatsStore.onActivated(dataService); + TopStore.onActivated(dataService, instance); -const WorkspaceTab: WorkspaceComponent<'Performance'> = { - name: 'Performance' as const, - component: PerformancePlugin, + // TODO(COMPASS-7416): no stores or subscriptions are returned here, we'd + // need to refactor the stores of this package + return { + store: {}, + deactivate() { + // noop + }, + }; + }, + }, + { + dataService: dataServiceLocator as DataServiceLocator, + instance: mongoDBInstanceLocator, + } + ), + content: PerformanceComponent, + header: ServerStatsPluginTitleComponent, }; -export default PerformancePlugin; export { WorkspaceTab }; export { default as d3 } from './d3'; diff --git a/packages/compass-serverstats/src/plugin-tab-title.tsx b/packages/compass-serverstats/src/plugin-tab-title.tsx new file mode 100644 index 00000000000..9c0742b94a9 --- /dev/null +++ b/packages/compass-serverstats/src/plugin-tab-title.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { + useConnectionInfo, + useConnectionsListRef, + useTabConnectionTheme, +} from '@mongodb-js/compass-connections/provider'; +import { + WorkspaceTab, + type WorkspaceTabCoreProps, +} from '@mongodb-js/compass-components'; +import type { WorkspacePluginProps } from '@mongodb-js/compass-workspaces'; + +export const WorkspaceName = 'Performance' as const; + +type PluginTitleComponentProps = WorkspaceTabCoreProps & + WorkspacePluginProps; + +export function ServerStatsPluginTitleComponent( + props: PluginTitleComponentProps +) { + const { getConnectionById } = useConnectionsListRef(); + const { id: connectionId } = useConnectionInfo(); + const connectionName = getConnectionById(connectionId)?.title || ''; + + const { getThemeOf } = useTabConnectionTheme(); + + return ( + + ); +} diff --git a/packages/compass-shell/src/index.ts b/packages/compass-shell/src/index.ts index 2b73e217df9..ac829daea94 100644 --- a/packages/compass-shell/src/index.ts +++ b/packages/compass-shell/src/index.ts @@ -1,32 +1,36 @@ +import React from 'react'; import { createLoggerLocator } from '@mongodb-js/compass-logging/provider'; import { telemetryLocator } from '@mongodb-js/compass-telemetry/provider'; import { ShellPlugin, onActivated } from './plugin'; import { registerHadronPlugin } from 'hadron-app-registry'; import { preferencesLocator } from 'compass-preferences-model/provider'; -import { type WorkspaceComponent } from '@mongodb-js/compass-workspaces'; +import type { WorkspacePlugin } from '@mongodb-js/compass-workspaces'; import { dataServiceLocator, type DataService, connectionInfoRefLocator, type DataServiceLocator, } from '@mongodb-js/compass-connections/provider'; +import { WorkspaceName, ShellPluginTitleComponent } from './plugin-tab-title'; -export const CompassShellPlugin = registerHadronPlugin( - { - name: 'CompassShell', - component: ShellPlugin, - activate: onActivated, - }, - { - logger: createLoggerLocator('COMPASS-SHELL'), - track: telemetryLocator, - dataService: dataServiceLocator as DataServiceLocator, - connectionInfo: connectionInfoRefLocator, - preferences: preferencesLocator, - } -); - -export const WorkspaceTab: WorkspaceComponent<'Shell'> = { - name: 'Shell' as const, - component: CompassShellPlugin, +export const WorkspaceTab: WorkspacePlugin = { + name: WorkspaceName, + provider: registerHadronPlugin( + { + name: WorkspaceName, + component: function ShellProvider({ children }) { + return React.createElement(React.Fragment, null, children); + }, + activate: onActivated, + }, + { + logger: createLoggerLocator('COMPASS-SHELL'), + track: telemetryLocator, + dataService: dataServiceLocator as DataServiceLocator, + connectionInfo: connectionInfoRefLocator, + preferences: preferencesLocator, + } + ), + content: ShellPlugin, + header: ShellPluginTitleComponent, }; diff --git a/packages/compass-shell/src/plugin-tab-title.tsx b/packages/compass-shell/src/plugin-tab-title.tsx new file mode 100644 index 00000000000..4d18ddf7194 --- /dev/null +++ b/packages/compass-shell/src/plugin-tab-title.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { + useConnectionInfo, + useConnectionsListRef, + useTabConnectionTheme, +} from '@mongodb-js/compass-connections/provider'; +import { + WorkspaceTab, + type WorkspaceTabCoreProps, +} from '@mongodb-js/compass-components'; +import type { WorkspacePluginProps } from '@mongodb-js/compass-workspaces'; + +export const WorkspaceName = 'Shell' as const; + +type PluginTitleProps = WorkspaceTabCoreProps & + WorkspacePluginProps; + +export function ShellPluginTitleComponent(tabProps: PluginTitleProps) { + const { getThemeOf } = useTabConnectionTheme(); + const { getConnectionById } = useConnectionsListRef(); + const { id: connectionId } = useConnectionInfo(); + + const connectionName = getConnectionById(connectionId)?.title || ''; + return ( + + ); +} diff --git a/packages/compass-shell/src/plugin.spec.tsx b/packages/compass-shell/src/plugin.spec.tsx index 55913229890..b54c65569c8 100644 --- a/packages/compass-shell/src/plugin.spec.tsx +++ b/packages/compass-shell/src/plugin.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { expect } from 'chai'; import { EventEmitter } from 'events'; -import { CompassShellPlugin } from './index'; +import { WorkspaceTab } from './index'; import { renderWithActiveConnection, screen, @@ -9,7 +9,7 @@ import { } from '@mongodb-js/testing-library-compass'; import { RuntimeMap } from './stores/store'; -describe('CompassShellPlugin', function () { +describe('CompassShellPlugin WorkspaceTab', function () { it('returns a renderable plugin', async function () { RuntimeMap.set('test', { eventEmitter: new EventEmitter(), @@ -19,7 +19,12 @@ describe('CompassShellPlugin', function () { }, } as any); - await renderWithActiveConnection(); + const ShellContentComponent = WorkspaceTab.content; + await renderWithActiveConnection( + + + + ); await waitFor(() => { expect(screen.getByTestId('shell-section')).to.exist; diff --git a/packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx b/packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx index 8b054331613..62f0b0fe7ff 100644 --- a/packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx +++ b/packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx @@ -106,8 +106,18 @@ describe('Multiple Connections Sidebar Component', function () { null }, - { name: 'Performance', component: () => null }, + { + name: 'My Queries', + content: () => null, + header: () => null as any, + provider: (() => null) as any, + }, + { + name: 'Performance', + content: () => null, + header: () => null as any, + provider: (() => null) as any, + }, ]} > diff --git a/packages/compass-welcome/src/index.ts b/packages/compass-welcome/src/index.ts index 21822f46a58..026f8f1bf83 100644 --- a/packages/compass-welcome/src/index.ts +++ b/packages/compass-welcome/src/index.ts @@ -1,10 +1,12 @@ +import React from 'react'; import { registerHadronPlugin } from 'hadron-app-registry'; import { createLoggerLocator } from '@mongodb-js/compass-logging/provider'; import { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider'; -import type { WorkspaceComponent } from '@mongodb-js/compass-workspaces'; +import type { WorkspacePlugin } from '@mongodb-js/compass-workspaces'; import { WelcomeModal, DesktopWelcomeTab, WebWelcomeTab } from './components'; import { activatePlugin } from './stores'; import { telemetryLocator } from '@mongodb-js/compass-telemetry/provider'; +import { PluginTabTitleComponent, WorkspaceName } from './plugin-tab-title'; const serviceLocators = { logger: createLoggerLocator('COMPASS-MY-QUERIES-UI'), @@ -12,28 +14,36 @@ const serviceLocators = { workspaces: workspacesServiceLocator, }; -export const DesktopWorkspaceTab: WorkspaceComponent<'Welcome'> = { - name: 'Welcome' as const, - component: registerHadronPlugin( +export const DesktopWorkspaceTab: WorkspacePlugin = { + name: WorkspaceName, + provider: registerHadronPlugin( { - name: 'Welcome', - component: DesktopWelcomeTab, + name: WorkspaceName, + component: function WelcomeProvider({ children }) { + return React.createElement(React.Fragment, null, children); + }, activate: activatePlugin, }, serviceLocators ), + content: DesktopWelcomeTab, + header: PluginTabTitleComponent, }; -export const WebWorkspaceTab: WorkspaceComponent<'Welcome'> = { - name: 'Welcome' as const, - component: registerHadronPlugin( +export const WebWorkspaceTab: WorkspacePlugin = { + name: WorkspaceName, + provider: registerHadronPlugin( { - name: 'Welcome', - component: WebWelcomeTab, + name: WorkspaceName, + component: function WelcomeProvider({ children }) { + return React.createElement(React.Fragment, null, children); + }, activate: activatePlugin, }, serviceLocators ), + content: WebWelcomeTab, + header: PluginTabTitleComponent, }; export { WelcomeModal }; diff --git a/packages/compass-welcome/src/plugin-tab-title.tsx b/packages/compass-welcome/src/plugin-tab-title.tsx new file mode 100644 index 00000000000..ce564c7159b --- /dev/null +++ b/packages/compass-welcome/src/plugin-tab-title.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { + WorkspaceTab, + type WorkspaceTabCoreProps, +} from '@mongodb-js/compass-components'; +import type { WorkspacePluginProps } from '@mongodb-js/compass-workspaces'; + +export const WorkspaceName = 'Welcome' as const; + +type PluginTitleComponentProps = WorkspaceTabCoreProps & + WorkspacePluginProps; + +export function PluginTabTitleComponent(props: PluginTitleComponentProps) { + return ( + + ); +} diff --git a/packages/compass-workspaces/src/components/index.tsx b/packages/compass-workspaces/src/components/index.tsx index 35593bce19f..490192d6135 100644 --- a/packages/compass-workspaces/src/components/index.tsx +++ b/packages/compass-workspaces/src/components/index.tsx @@ -4,9 +4,9 @@ import type { CollectionTabInfo } from '../stores/workspaces'; import { getActiveTab, type OpenWorkspaceOptions, - type WorkspaceTab, type WorkspacesState, } from '../stores/workspaces'; +import type { WorkspaceTab } from '../types'; import Workspaces from './workspaces'; import { connect } from '../stores/context'; import { WorkspacesServiceProvider } from '../provider'; diff --git a/packages/compass-workspaces/src/components/workspace-close-handler.tsx b/packages/compass-workspaces/src/components/workspace-close-handler.tsx index 96b2cd9bcf1..e254cf94aff 100644 --- a/packages/compass-workspaces/src/components/workspace-close-handler.tsx +++ b/packages/compass-workspaces/src/components/workspace-close-handler.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import type { WorkspaceTab } from '../stores/workspaces'; +import type { WorkspaceTab } from '../types'; import { useWorkspaceTabId } from './workspace-tab-state-provider'; export type WorkspaceDestroyHandler = () => boolean; diff --git a/packages/compass-workspaces/src/components/workspace-tab-context-provider.tsx b/packages/compass-workspaces/src/components/workspace-tab-context-provider.tsx index 6a3a1d8dade..587e4822b2b 100644 --- a/packages/compass-workspaces/src/components/workspace-tab-context-provider.tsx +++ b/packages/compass-workspaces/src/components/workspace-tab-context-provider.tsx @@ -1,8 +1,6 @@ import React, { useCallback, useEffect, useRef } from 'react'; -import { - getLocalAppRegistryForTab, - type WorkspaceTab, -} from '../stores/workspaces'; +import { getLocalAppRegistryForTab } from '../stores/workspaces'; +import type { WorkspaceTab } from '../types'; import { NamespaceProvider } from '@mongodb-js/compass-app-stores/provider'; import { ConnectionInfoProvider } from '@mongodb-js/compass-connections/provider'; import { rafraf } from '@mongodb-js/compass-components'; @@ -12,6 +10,7 @@ import { WorkspaceTabStateProvider, } from './workspace-tab-state-provider'; import { AppRegistryProvider } from 'hadron-app-registry'; +import { useWorkspacePlugins } from './workspaces-provider'; function getInitialPropsForWorkspace(tab: WorkspaceTab) { switch (tab.type) { @@ -81,21 +80,42 @@ const TabCloseHandler: React.FunctionComponent = ({ children }) => { ); }; -const WorkspaceTabContextProvider: React.FunctionComponent<{ +type WorkspaceTabContextProviderProps = { tab: WorkspaceTab; - sectionType: 'tab-content' | 'tab-title'; - onNamespaceNotFound?: ( - tab: Extract, - fallbackNamespace: string | null - ) => void; children: React.JSX.Element; -}> = ({ tab, onNamespaceNotFound, sectionType: type, children }) => { +} & ( + | { + sectionType: 'tab-content'; + onNamespaceNotFound: ( + tab: Extract, + fallbackNamespace: string | null + ) => void; + } + | { + sectionType: 'tab-title'; + onNamespaceNotFound?: undefined; + } +); + +const WorkspaceTabContextProvider: React.FunctionComponent< + WorkspaceTabContextProviderProps +> = ({ tab, onNamespaceNotFound, sectionType: type, children }) => { const initialProps = getInitialPropsForWorkspace(tab); + const { getWorkspacePluginByName } = useWorkspacePlugins(); + + const { provider: WorkspaceProvider } = getWorkspacePluginByName(tab.type); if (initialProps) { children = React.cloneElement(children, initialProps); } + // The ordering of the these providers is important, + // the workspace provider needs access to the + // connection info and namespace providers. + children = ( + {children} + ); + if ('namespace' in tab) { children = ( - | WorkspaceComponent<'My Queries'> - | WorkspaceComponent<'Data Modeling'> - | WorkspaceComponent<'Shell'> - | WorkspaceComponent<'Performance'> - | WorkspaceComponent<'Databases'> - | WorkspaceComponent<'Collections'> - | WorkspaceComponent<'Collection'>; +export type AnyWorkspacePlugin = + | WorkspacePlugin<'Welcome'> + | WorkspacePlugin<'My Queries'> + | WorkspacePlugin<'Data Modeling'> + | WorkspacePlugin<'Shell'> + | WorkspacePlugin<'Performance'> + | WorkspacePlugin<'Databases'> + | WorkspacePlugin<'Collections'> + | WorkspacePlugin<'Collection'>; -const WorkspacesContext = React.createContext([]); +const WorkspacesContext = React.createContext([]); export const WorkspacesProvider: React.FunctionComponent<{ - value: AnyWorkspaceComponent[]; + value: AnyWorkspacePlugin[]; }> = ({ value, children }) => { const valueRef = useRef(value); return ( @@ -30,17 +31,17 @@ export const useWorkspacePlugins = () => { hasWorkspacePlugin: (name: T) => { return workspaces.some((ws) => ws.name === name); }, - getWorkspacePluginByName: (name?: T) => { - if (!name) { - return null; - } + getWorkspacePlugins: (): AnyWorkspacePlugin[] => { + return workspaces; + }, + getWorkspacePluginByName: (name: T) => { const plugin = workspaces.find((ws) => ws.name === name); if (!plugin) { throw new Error( `Component for workspace "${name}" is missing in context. Did you forget to set up WorkspacesProvider?` ); } - return plugin.component as unknown as WorkspaceComponent['component']; + return plugin as unknown as WorkspacePlugin; }, }); return workspacePlugins.current; diff --git a/packages/compass-workspaces/src/components/workspaces.tsx b/packages/compass-workspaces/src/components/workspaces.tsx index ad873735703..61a2e5de1e1 100644 --- a/packages/compass-workspaces/src/components/workspaces.tsx +++ b/packages/compass-workspaces/src/components/workspaces.tsx @@ -4,15 +4,14 @@ import { MongoDBLogoMark, WorkspaceTabs, css, - palette, spacing, useDarkMode, + type WorkspaceTabCoreProps, } from '@mongodb-js/compass-components'; import type { CollectionTabInfo, DatabaseTabInfo, OpenWorkspaceOptions, - WorkspaceTab, WorkspacesState, } from '../stores/workspaces'; import { @@ -29,11 +28,8 @@ import { useWorkspacePlugins } from './workspaces-provider'; import toNS from 'mongodb-ns'; import { useLogger } from '@mongodb-js/compass-logging/provider'; import { connect } from '../stores/context'; -import { useTabConnectionTheme } from '@mongodb-js/compass-connections/provider'; -import { useConnectionsListRef } from '@mongodb-js/compass-connections/provider'; import { WorkspaceTabContextProvider } from './workspace-tab-context-provider'; - -type Tooltip = [string, string][]; +import type { WorkspaceTab } from '../types'; const emptyWorkspaceStyles = css({ margin: '0 auto', @@ -86,10 +82,6 @@ type CompassWorkspacesProps = { ): void; }; -const nonExistantStyles = css({ - color: palette.gray.base, -}); - const CompassWorkspaces: React.FunctionComponent = ({ tabs, activeTab, @@ -106,159 +98,98 @@ const CompassWorkspaces: React.FunctionComponent = ({ }) => { const { log, mongoLogId } = useLogger('COMPASS-WORKSPACES'); const { getWorkspacePluginByName } = useWorkspacePlugins(); - const { getThemeOf } = useTabConnectionTheme(); - const { getConnectionById } = useConnectionsListRef(); - - const tabDescriptions = useMemo(() => { - return tabs.map((tab) => { - switch (tab.type) { - case 'Welcome': - return { - id: tab.id, - type: tab.type, - title: tab.type, - iconGlyph: 'Logo', - } as const; - case 'My Queries': - return { - id: tab.id, - type: tab.type, - title: tab.type, - iconGlyph: 'CurlyBraces', - } as const; - case 'Data Modeling': - return { - id: tab.id, - type: tab.type, - title: tab.type, - iconGlyph: 'Diagram' as const, - }; - case 'Shell': { - const connectionName = - getConnectionById(tab.connectionId)?.title || ''; - const tooltip: Tooltip = []; - if (connectionName) { - tooltip.push(['mongosh', connectionName || '']); - } - return { - id: tab.id, - connectionName, - type: tab.type, - title: connectionName - ? `mongosh: ${connectionName}` - : 'MongoDB Shell', - tooltip, - iconGlyph: 'Shell', - tabTheme: getThemeOf(tab.connectionId), - } as const; - } - case 'Databases': { - const connectionName = - getConnectionById(tab.connectionId)?.title || ''; - return { - id: tab.id, - connectionName, - type: tab.type, - title: connectionName, - tooltip: [['Connection', connectionName || '']] as Tooltip, - iconGlyph: 'Server', - tabTheme: getThemeOf(tab.connectionId), - } as const; - } - case 'Performance': { - const connectionName = - getConnectionById(tab.connectionId)?.title || ''; - return { - id: tab.id, - connectionName, - type: tab.type, - title: `Performance: ${connectionName}`, - tooltip: [['Performance', connectionName || '']] as Tooltip, - iconGlyph: 'Gauge', - tabTheme: getThemeOf(tab.connectionId), - } as const; - } - case 'Collections': { - const connectionName = - getConnectionById(tab.connectionId)?.title || ''; - const database = tab.namespace; - const namespaceId = `${tab.connectionId}.${database}`; - const { isNonExistent } = databaseInfo[namespaceId] ?? {}; - return { - id: tab.id, - connectionName, - type: tab.type, - title: database, - tooltip: [ - ['Connection', connectionName || ''], - ['Database', database], - ] as Tooltip, - iconGlyph: isNonExistent ? 'EmptyDatabase' : 'Database', - 'data-namespace': tab.namespace, - tabTheme: getThemeOf(tab.connectionId), - ...(isNonExistent && { - className: nonExistantStyles, - }), - } as const; - } - case 'Collection': { - const { database, collection, ns } = toNS(tab.namespace); - const namespaceId = `${tab.connectionId}.${ns}`; - const info = collectionInfo[namespaceId] ?? {}; - const { isTimeSeries, isReadonly, sourceName, isNonExistent } = info; - const connectionName = - getConnectionById(tab.connectionId)?.title || ''; - const collectionType = isTimeSeries - ? 'timeseries' - : isReadonly - ? 'view' - : 'collection'; - // Similar to what we have in the collection breadcrumbs. - const tooltip: Tooltip = [ - ['Connection', connectionName || ''], - ['Database', database], - ]; - if (sourceName) { - tooltip.push(['View', collection]); - tooltip.push(['Derived from', toNS(sourceName).collection]); - } else if (tab.editViewName) { - tooltip.push(['View', toNS(tab.editViewName).collection]); - tooltip.push(['Derived from', collection]); - } else { - tooltip.push(['Collection', collection]); - } - return { - id: tab.id, - connectionName, - type: tab.type, - title: collection, - tooltip, - iconGlyph: - collectionType === 'view' - ? 'Visibility' - : collectionType === 'timeseries' - ? 'TimeSeries' - : isNonExistent - ? 'EmptyFolder' - : 'Folder', - 'data-namespace': ns, - tabTheme: getThemeOf(tab.connectionId), - ...(isNonExistent && { - className: nonExistantStyles, - }), - } as const; - } - } - }); - }, [tabs, collectionInfo, databaseInfo, getThemeOf, getConnectionById]); const activeTabIndex = tabs.findIndex((tab) => tab === activeTab); - const WorkspaceComponent = getWorkspacePluginByName(activeTab?.type); const onCreateNewTab = useCallback(() => { onCreateTab(openOnEmptyWorkspace); }, [onCreateTab, openOnEmptyWorkspace]); + const workspaceTabs = useMemo(() => { + return tabs.map((tab) => { + const plugin = getWorkspacePluginByName(tab.type); + if (!plugin) { + throw new Error( + `Content component for workspace "${tab.type}" is missing in context. Did you forget to set up WorkspacesProvider?` + ); + } + const { content: WorkspaceTabContent, header: WorkspaceTabTitle } = + plugin; + + let isNonExistent: boolean | undefined; + if (tab.type === 'Collections') { + // TODO(COMPASS-9456): Move this logic and `isNonExistent` setting to the plugin. + const database = tab.namespace; + const namespaceId = `${tab.connectionId}.${database}`; + const { isNonExistent: databaseDoesNotExist } = + databaseInfo[namespaceId] ?? {}; + isNonExistent = databaseDoesNotExist; + } else if (tab.type === 'Collection') { + // TODO(COMPASS-9456): Move this logic and `isNonExistent` setting to the plugin. + const { ns } = toNS(tab.namespace); + const namespaceId = `${tab.connectionId}.${ns}`; + const { isNonExistent: collectionDoesNotExist } = + collectionInfo[namespaceId] ?? {}; + isNonExistent = collectionDoesNotExist; + } + + return { + id: tab.id, + renderTab: (workspaceTabCoreProps: WorkspaceTabCoreProps) => ( + { + log.error( + mongoLogId(1_001_000_360), + 'Workspace', + 'Rendering workspace tab header failed', + { name: tab.type, error: error.message, errorInfo } + ); + }} + > + + + + + ), + content: ( + { + log.error( + mongoLogId(1_001_000_277), + 'Workspace', + 'Rendering workspace tab content failed', + { name: tab.type, error: error.message, errorInfo } + ); + }} + > + + + + + ), + }; + }); + }, [ + getWorkspacePluginByName, + tabs, + log, + collectionInfo, + databaseInfo, + mongoLogId, + onNamespaceNotFound, + ]); + + const workspaceTabContent = workspaceTabs[activeTabIndex]?.content ?? null; + return (
= ({ onMoveTab={onMoveTab} onCreateNewTab={onCreateNewTab} onCloseTab={onCloseTab} - tabs={tabDescriptions} + tabs={workspaceTabs} selectedTabIndex={activeTabIndex} >
- {activeTab && WorkspaceComponent ? ( - { - log.error( - mongoLogId(1_001_000_277), - 'Workspace', - 'Rendering workspace tab failed', - { name: activeTab.type, error: error.message, errorInfo } - ); - }} - > - - - - + {activeTab && workspaceTabContent ? ( + workspaceTabContent ) : ( )} diff --git a/packages/compass-workspaces/src/index.spec.tsx b/packages/compass-workspaces/src/index.spec.tsx index 7ee10abb955..7bc542d8fc7 100644 --- a/packages/compass-workspaces/src/index.spec.tsx +++ b/packages/compass-workspaces/src/index.spec.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { expect } from 'chai'; import WorkspacesPlugin, { WorkspacesProvider } from './index'; import Sinon from 'sinon'; -import type { AnyWorkspaceComponent } from './components/workspaces-provider'; +import type { AnyWorkspacePlugin } from './components/workspaces-provider'; import { useOpenWorkspace } from './provider'; import { renderWithConnections, @@ -12,14 +12,35 @@ import { userEvent, } from '@mongodb-js/testing-library-compass'; import { TestMongoDBInstanceManager } from '@mongodb-js/compass-app-stores/provider'; +import { WorkspaceTab } from '@mongodb-js/compass-components'; function mockWorkspace(name: string) { return { name, - component: function Component() { + provider: function Component({ + children, + props, + }: { + children?: React.ReactNode; + props?: any; + }) { + return
{children}
; + }, + content: function Component() { return <>{name}; }, - } as unknown as AnyWorkspaceComponent; + header: function Component(props: any) { + return ( + + ); + }, + } as unknown as AnyWorkspacePlugin; } const TEST_CONNECTION_INFO = { @@ -71,10 +92,33 @@ describe('WorkspacesPlugin', function () { connectFn() { return { listDatabases() { - return Promise.resolve([]); + return Promise.resolve([ + // Mock the databases and collections so we don't trigger the onNamespaceNotFound + // fallback handler which redirects collections to the databases view. + { + _id: 'db', + name: 'db', + is_non_existent: false, + collection_count: 0, + document_count: 0, + index_count: 0, + storage_size: 0, + data_size: 0, + index_size: 0, + }, + ]); }, listCollections() { - return Promise.resolve([]); + return Promise.resolve( + Array.from({ + length: 3, + }).map((_, index) => ({ + _id: `db.coll${index}`, + name: `coll${index}`, + database: 'db', + type: 'collection', + })) as any + ); }, }; }, @@ -90,16 +134,12 @@ describe('WorkspacesPlugin', function () { cleanup(); }); - const connectionName = TEST_CONNECTION_INFO.favorite.name; const tabs = [ ['My Queries', () => openFns.openMyQueriesWorkspace()], - [connectionName, () => openFns.openDatabasesWorkspace('1')], // Databases - [ - `Performance: ${connectionName}`, - () => openFns.openPerformanceWorkspace('1'), - ], + ['Databases', () => openFns.openDatabasesWorkspace('1')], + ['Performance', () => openFns.openPerformanceWorkspace('1')], ['db', () => openFns.openCollectionsWorkspace('1', 'db')], - ['coll', () => openFns.openCollectionWorkspace('1', 'db.coll')], + ['db.coll0', () => openFns.openCollectionWorkspace('1', 'db.coll0')], ] as const; for (const suite of tabs) { @@ -118,23 +158,25 @@ describe('WorkspacesPlugin', function () { expect(onTabChangeSpy).to.have.been.calledWith(null); + openFns.openCollectionWorkspace('1', 'db.coll0', { newTab: true }); openFns.openCollectionWorkspace('1', 'db.coll1', { newTab: true }); openFns.openCollectionWorkspace('1', 'db.coll2', { newTab: true }); - openFns.openCollectionWorkspace('1', 'db.coll3', { newTab: true }); - expect(screen.getByRole('tab', { name: 'coll3' })).to.have.attribute( - 'aria-selected', - 'true' - ); + await waitFor(() => { + expect(screen.getByRole('tab', { name: 'db.coll2' })).to.have.attribute( + 'aria-selected', + 'true' + ); + }); - userEvent.click(screen.getByRole('tab', { name: 'coll1' })); + userEvent.click(screen.getByRole('tab', { name: 'db.coll0' })); await waitFor(() => { - expect(screen.getByRole('tab', { name: /coll3/i })).to.have.attribute( + expect(screen.getByRole('tab', { name: 'db.coll2' })).to.have.attribute( 'aria-selected', 'false' ); - expect(screen.getByRole('tab', { name: /coll1/i })).to.have.attribute( + expect(screen.getByRole('tab', { name: 'db.coll0' })).to.have.attribute( 'aria-selected', 'true' ); diff --git a/packages/compass-workspaces/src/index.ts b/packages/compass-workspaces/src/index.ts index 92a90f7b2be..61450e48a0c 100644 --- a/packages/compass-workspaces/src/index.ts +++ b/packages/compass-workspaces/src/index.ts @@ -3,7 +3,6 @@ import type { ActivateHelpers } from 'hadron-app-registry'; import { registerHadronPlugin } from 'hadron-app-registry'; import type { OpenWorkspaceOptions, - WorkspaceTab, CollectionTabInfo, } from './stores/workspaces'; import workspacesReducer, { @@ -238,7 +237,7 @@ const WorkspacesPlugin = registerHadronPlugin( export default WorkspacesPlugin; export { WorkspacesProvider } from './components/workspaces-provider'; -export type { OpenWorkspaceOptions, WorkspaceTab, CollectionTabInfo }; +export type { OpenWorkspaceOptions, CollectionTabInfo }; export type { WelcomeWorkspace, MyQueriesWorkspace, @@ -250,7 +249,8 @@ export type { CollectionWorkspace, AnyWorkspace, Workspace, - WorkspacePluginProps, - WorkspaceComponent, + WorkspacePlugin, + WorkspaceTab, CollectionSubtab, + WorkspacePluginProps, } from './types'; diff --git a/packages/compass-workspaces/src/provider.tsx b/packages/compass-workspaces/src/provider.tsx index edd627abb84..a8f8354dc78 100644 --- a/packages/compass-workspaces/src/provider.tsx +++ b/packages/compass-workspaces/src/provider.tsx @@ -1,17 +1,13 @@ import React, { useContext, useRef } from 'react'; import { useSelector, useStore } from './stores/context'; -import type { - OpenWorkspaceOptions, - TabOptions, - WorkspaceTab, -} from './stores/workspaces'; +import type { OpenWorkspaceOptions, TabOptions } from './stores/workspaces'; import { collectionSubtabSelected, getActiveTab, openWorkspace as openWorkspaceAction, } from './stores/workspaces'; import { createServiceLocator } from 'hadron-app-registry'; -import type { CollectionSubtab } from './types'; +import type { CollectionSubtab, WorkspaceTab } from './types'; import type { WorkspaceDestroyHandler } from './components/workspace-close-handler'; import { useRegisterTabDestroyHandler } from './components/workspace-close-handler'; diff --git a/packages/compass-workspaces/src/stores/workspaces.spec.ts b/packages/compass-workspaces/src/stores/workspaces.spec.ts index 86bc9c4f0aa..86105e40f28 100644 --- a/packages/compass-workspaces/src/stores/workspaces.spec.ts +++ b/packages/compass-workspaces/src/stores/workspaces.spec.ts @@ -6,7 +6,7 @@ import * as workspacesSlice from './workspaces'; import { _bulkTabsClose } from './workspaces'; import { TestMongoDBInstanceManager } from '@mongodb-js/compass-app-stores/provider'; import type { ConnectionInfo } from '../../../connection-info/dist'; -import type { WorkspaceTab } from '../stores/workspaces'; +import type { WorkspaceTab } from '../types'; import { setTabDestroyHandler } from '../components/workspace-close-handler'; import { createPluginTestHelpers } from '@mongodb-js/testing-library-compass'; diff --git a/packages/compass-workspaces/src/stores/workspaces.ts b/packages/compass-workspaces/src/stores/workspaces.ts index 214165c51ae..bbfcb438f80 100644 --- a/packages/compass-workspaces/src/stores/workspaces.ts +++ b/packages/compass-workspaces/src/stores/workspaces.ts @@ -3,19 +3,8 @@ import type { ThunkAction } from 'redux-thunk'; import { ObjectId } from 'bson'; import AppRegistry from 'hadron-app-registry'; import toNS from 'mongodb-ns'; -import type { - CollectionWorkspace, - CollectionsWorkspace, - DatabasesWorkspace, - MyQueriesWorkspace, - DataModelingWorkspace, - ShellWorkspace, - ServerStatsWorkspace, - WelcomeWorkspace, - Workspace, - WorkspacesServices, - CollectionSubtab, -} from '..'; +import type { Workspace, WorkspacesServices, CollectionSubtab } from '..'; +import type { WorkspaceTab, WorkspaceTabProps } from '../types'; import { isEqual } from 'lodash'; import { cleanupTabState } from '../components/workspace-tab-state-provider'; import { @@ -83,22 +72,6 @@ function isAction( return action.type === type; } -type WorkspaceTabProps = - | Omit - | Omit - | Omit - | Omit - | Omit - | Omit - | Omit - | (Omit & { - subTab: CollectionSubtab; - }); - -export type WorkspaceTab = { - id: string; -} & WorkspaceTabProps; - export type CollectionTabInfo = { isTimeSeries: boolean; isReadonly: boolean; diff --git a/packages/compass-workspaces/src/types.ts b/packages/compass-workspaces/src/types.ts index a744c060e61..f3bf71aff2b 100644 --- a/packages/compass-workspaces/src/types.ts +++ b/packages/compass-workspaces/src/types.ts @@ -1,3 +1,6 @@ +import type { HadronPluginComponent } from 'hadron-app-registry'; +import type { WorkspaceTabCoreProps } from '@mongodb-js/compass-components'; + export type CollectionSubtab = | 'Documents' | 'Aggregations' @@ -39,6 +42,8 @@ export type CollectionsWorkspace = { type: 'Collections'; connectionId: string; namespace: string; + // TODO(COMPASS-9456): Remove the `isNonExistent` field here. + isNonExistent?: boolean; }; export type CollectionWorkspace = { @@ -56,8 +61,26 @@ export type CollectionWorkspace = { initialPipelineText?: string; initialAggregation?: unknown; editViewName?: string; + // TODO(COMPASS-9456): Remove the `isNonExistent` field here. + isNonExistent?: boolean; }; +export type WorkspaceTabProps = + | WelcomeWorkspace + | MyQueriesWorkspace + | DataModelingWorkspace + | ShellWorkspace + | ServerStatsWorkspace + | DatabasesWorkspace + | CollectionsWorkspace + | (Omit & { + subTab: CollectionSubtab; + }); + +export type WorkspaceTab = { + id: string; +} & WorkspaceTabProps; + export type AnyWorkspace = | WelcomeWorkspace | MyQueriesWorkspace @@ -78,9 +101,12 @@ export type WorkspacePluginProps = Omit< 'type' | 'connectionId' >; -export type WorkspaceComponent = { +export type PluginHeaderProps = + WorkspaceTabCoreProps & WorkspacePluginProps; + +export type WorkspacePlugin = { name: T; - component: - | React.ComponentClass> - | ((props: WorkspacePluginProps) => React.ReactElement | null); + provider: HadronPluginComponent; + content: (props: WorkspacePluginProps) => React.ReactElement | null; + header: (props: PluginHeaderProps) => React.ReactElement | null; }; diff --git a/packages/databases-collections/src/collections-plugin-title.tsx b/packages/databases-collections/src/collections-plugin-title.tsx new file mode 100644 index 00000000000..4b4bdb6f854 --- /dev/null +++ b/packages/databases-collections/src/collections-plugin-title.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { + useConnectionInfo, + useConnectionsListRef, + useTabConnectionTheme, +} from '@mongodb-js/compass-connections/provider'; +import { + WorkspaceTab, + type WorkspaceTabCoreProps, +} from '@mongodb-js/compass-components'; +import type { WorkspacePluginProps } from '@mongodb-js/compass-workspaces'; + +import { CollectionsWorkspaceName } from './collections-plugin'; + +type PluginTitleProps = WorkspaceTabCoreProps & + WorkspacePluginProps; + +export function CollectionsPluginTitleComponent(props: PluginTitleProps) { + const { id: connectionId } = useConnectionInfo(); + const { getConnectionById } = useConnectionsListRef(); + const { getThemeOf } = useTabConnectionTheme(); + + const connectionName = getConnectionById(connectionId)?.title || ''; + const database = props.namespace; + + return ( + + ); +} diff --git a/packages/databases-collections/src/collections-plugin.spec.tsx b/packages/databases-collections/src/collections-plugin.spec.tsx index 703516a844d..ce4eb6bc1f9 100644 --- a/packages/databases-collections/src/collections-plugin.spec.tsx +++ b/packages/databases-collections/src/collections-plugin.spec.tsx @@ -9,7 +9,7 @@ import { userEvent, } from '@mongodb-js/testing-library-compass'; import { expect } from 'chai'; -import { CollectionsPlugin } from './collections-plugin'; +import { CollectionsWorkspaceTab } from './'; import Sinon from 'sinon'; import { type PreferencesAccess, @@ -61,13 +61,17 @@ describe('Collections [Plugin]', function () { describe('with loaded collections', function () { beforeEach(async function () { - const Plugin = CollectionsPlugin.withMockServices({ + const Plugin = CollectionsWorkspaceTab.provider.withMockServices({ instance: mongodbInstance, database: mongodbInstance.databases.get('foo'), dataService, }); - const { globalAppRegistry } = render(); + const { globalAppRegistry } = render( + + + + ); appRegistry = Sinon.spy(globalAppRegistry); await waitFor(() => { diff --git a/packages/databases-collections/src/collections-plugin.tsx b/packages/databases-collections/src/collections-plugin.tsx index b0d5e172032..e88ac113913 100644 --- a/packages/databases-collections/src/collections-plugin.tsx +++ b/packages/databases-collections/src/collections-plugin.tsx @@ -1,8 +1,8 @@ +import React from 'react'; import { databaseModelLocator, mongoDBInstanceLocator, } from '@mongodb-js/compass-app-stores/provider'; -import CollectionsList from './components/collections'; import { activatePlugin as activateCollectionsTabPlugin } from './stores/collections-store'; import { registerHadronPlugin } from 'hadron-app-registry'; import { @@ -11,10 +11,14 @@ import { type DataService, } from '@mongodb-js/compass-connections/provider'; +export const CollectionsWorkspaceName = 'Collections' as const; + export const CollectionsPlugin = registerHadronPlugin( { - name: 'Collections', - component: CollectionsList, + name: 'Collections' as const, + component: function CollectionsProvider({ children }) { + return React.createElement(React.Fragment, null, children); + }, activate: activateCollectionsTabPlugin, }, { diff --git a/packages/databases-collections/src/databases-plugin-title.tsx b/packages/databases-collections/src/databases-plugin-title.tsx new file mode 100644 index 00000000000..062faa7049c --- /dev/null +++ b/packages/databases-collections/src/databases-plugin-title.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { + WorkspaceTab, + type WorkspaceTabCoreProps, +} from '@mongodb-js/compass-components'; +import { + useConnectionInfo, + useConnectionsListRef, + useTabConnectionTheme, +} from '@mongodb-js/compass-connections/provider'; +import type { WorkspacePluginProps } from '@mongodb-js/compass-workspaces'; + +import { DatabasesWorkspaceName } from './databases-plugin'; + +type PluginTitleProps = WorkspaceTabCoreProps & + WorkspacePluginProps; + +export function DatabasesPluginTitleComponent(props: PluginTitleProps) { + const { id: connectionId } = useConnectionInfo(); + const { getConnectionById } = useConnectionsListRef(); + const { getThemeOf } = useTabConnectionTheme(); + + const connectionName = getConnectionById(connectionId)?.title || ''; + return ( + + ); +} diff --git a/packages/databases-collections/src/databases-plugin.spec.tsx b/packages/databases-collections/src/databases-plugin.spec.tsx index fd9c07c1c57..32fcf190833 100644 --- a/packages/databases-collections/src/databases-plugin.spec.tsx +++ b/packages/databases-collections/src/databases-plugin.spec.tsx @@ -9,7 +9,7 @@ import { userEvent, } from '@mongodb-js/testing-library-compass'; import { expect } from 'chai'; -import { DatabasesPlugin } from './databases-plugin'; +import { DatabasesWorkspaceTab } from './'; import Sinon from 'sinon'; import { createSandboxFromDefaultPreferences, @@ -49,12 +49,16 @@ describe('Databasees [Plugin]', function () { }, }; - const Plugin = DatabasesPlugin.withMockServices({ + const Plugin = DatabasesWorkspaceTab.provider.withMockServices({ instance: mongodbInstance, dataService, }); - const { globalAppRegistry } = render(); + const { globalAppRegistry } = render( + + + + ); appRegistry = Sinon.spy(globalAppRegistry); diff --git a/packages/databases-collections/src/databases-plugin.tsx b/packages/databases-collections/src/databases-plugin.tsx index 23ab01032a7..a44875c72fc 100644 --- a/packages/databases-collections/src/databases-plugin.tsx +++ b/packages/databases-collections/src/databases-plugin.tsx @@ -1,5 +1,5 @@ +import React from 'react'; import { mongoDBInstanceLocator } from '@mongodb-js/compass-app-stores/provider'; -import Databases from './components/databases'; import { activatePlugin as activateDatabasesTabPlugin } from './stores/databases-store'; import { registerHadronPlugin } from 'hadron-app-registry'; import { @@ -8,10 +8,14 @@ import { type DataService, } from '@mongodb-js/compass-connections/provider'; +export const DatabasesWorkspaceName = 'Databases' as const; + export const DatabasesPlugin = registerHadronPlugin( { - name: 'Databases', - component: Databases, + name: 'Databases' as const, + component: function DatabasesProvider({ children }) { + return React.createElement(React.Fragment, null, children); + }, activate: activateDatabasesTabPlugin, }, { diff --git a/packages/databases-collections/src/index.ts b/packages/databases-collections/src/index.ts index e45a71de468..9ed127b5f5b 100644 --- a/packages/databases-collections/src/index.ts +++ b/packages/databases-collections/src/index.ts @@ -3,31 +3,46 @@ import { createLoggerLocator } from '@mongodb-js/compass-logging/provider'; import { telemetryLocator } from '@mongodb-js/compass-telemetry/provider'; import { connectionsLocator } from '@mongodb-js/compass-connections/provider'; import { mongoDBInstancesManagerLocator } from '@mongodb-js/compass-app-stores/provider'; -import { CollectionsPlugin } from './collections-plugin'; +import { + CollectionsPlugin, + CollectionsWorkspaceName, +} from './collections-plugin'; import { DropNamespaceComponent, activatePlugin as activateDropNamespacePlugin, } from './stores/drop-namespace'; import CreateNamespaceModal from './components/create-namespace-modal'; import { activatePlugin as activateCreateNamespacePlugin } from './stores/create-namespace'; -import { DatabasesPlugin } from './databases-plugin'; +import { DatabasesPlugin, DatabasesWorkspaceName } from './databases-plugin'; import MappedRenameCollectionModal from './components/rename-collection-modal/rename-collection-modal'; import { activateRenameCollectionPlugin } from './stores/rename-collection'; -import type { WorkspaceComponent } from '@mongodb-js/compass-workspaces'; +import type { WorkspacePlugin } from '@mongodb-js/compass-workspaces'; import { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider'; import { favoriteQueryStorageAccessLocator, pipelineStorageLocator, } from '@mongodb-js/my-queries-storage/provider'; +import Databases from './components/databases'; +import CollectionsList from './components/collections'; +import { DatabasesPluginTitleComponent } from './databases-plugin-title'; +import { CollectionsPluginTitleComponent } from './collections-plugin-title'; -export const CollectionsWorkspaceTab: WorkspaceComponent<'Collections'> = { - name: 'Collections' as const, - component: CollectionsPlugin, +export const CollectionsWorkspaceTab: WorkspacePlugin< + typeof CollectionsWorkspaceName +> = { + name: CollectionsWorkspaceName, + provider: CollectionsPlugin, + content: CollectionsList, + header: CollectionsPluginTitleComponent, }; -export const DatabasesWorkspaceTab: WorkspaceComponent<'Databases'> = { - name: 'Databases' as const, - component: DatabasesPlugin, +export const DatabasesWorkspaceTab: WorkspacePlugin< + typeof DatabasesWorkspaceName +> = { + name: DatabasesWorkspaceName, + provider: DatabasesPlugin, + content: Databases, + header: DatabasesPluginTitleComponent, }; export const CreateNamespacePlugin = registerHadronPlugin( diff --git a/packages/databases-collections/src/stores/databases-store.ts b/packages/databases-collections/src/stores/databases-store.ts index 7cca099fd12..73d515fdbcd 100644 --- a/packages/databases-collections/src/stores/databases-store.ts +++ b/packages/databases-collections/src/stores/databases-store.ts @@ -16,8 +16,11 @@ type DatabasesTabServices = { dataService: DataService; }; +type DatabasesPluginInitialProps = { + children?: React.ReactNode; +}; export function activatePlugin( - _initialProps: Record, + _initialProps: DatabasesPluginInitialProps, { globalAppRegistry, instance, dataService }: DatabasesTabServices, { on, cleanup, addCleanup }: ActivateHelpers ) {