Skip to content
42 changes: 24 additions & 18 deletions packages/compass-collection/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<keyof DataService>,
collection: collectionModelLocator,
workspaces: workspacesServiceLocator,
}
);

export const WorkspaceTab: WorkspaceComponent<'Collection'> = {
name: 'Collection' as const,
component: CollectionTabPlugin,
export const WorkspaceTab: WorkspacePlugin<typeof CollectionWorkspaceTitle> = {
name: CollectionWorkspaceTitle,
provider: registerHadronPlugin(
{
name: CollectionWorkspaceTitle,
component: function CollectionProvider({ children }) {
return React.createElement(React.Fragment, null, children);
},
activate: activateCollectionTabPlugin,
},
{
dataService: dataServiceLocator as DataServiceLocator<keyof DataService>,
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';
109 changes: 109 additions & 0 deletions packages/compass-collection/src/plugin-tab-title.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react';
import { connect } from 'react-redux';
import toNS from 'mongodb-ns';
import {
useConnectionsListRef,
useTabConnectionTheme,
} from '@mongodb-js/compass-connections/provider';
import {
WorkspaceTab,
type WorkspaceTabCoreProps,
} from '@mongodb-js/compass-components';
import type { CollectionSubtab } from '@mongodb-js/compass-workspaces';

import { type CollectionState } from './modules/collection-tab';

type WorkspaceProps = {
id: string;
connectionId: string;
namespace: string;
subTab: CollectionSubtab;
initialQuery?: unknown;
initialPipeline?: unknown[];
initialPipelineText?: string;
initialAggregation?: unknown;
editViewName?: string;
isNonExistent: boolean;
};

function _PluginTitle({
isTimeSeries,
isReadonly,
sourceName,
tabProps,
workspaceProps,
}: {
isTimeSeries?: boolean;
isReadonly?: boolean;
sourceName?: string | null;
tabProps: WorkspaceTabCoreProps;
workspaceProps: WorkspaceProps;
}) {
const { getThemeOf } = useTabConnectionTheme();
const { getConnectionById } = useConnectionsListRef();

const { database, collection, ns } = toNS(workspaceProps.namespace);
const connectionName =
getConnectionById(workspaceProps.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 (workspaceProps.editViewName) {
tooltip.push(['View', toNS(workspaceProps.editViewName).collection]);
tooltip.push(['Derived from', collection]);
} else {
tooltip.push(['Collection', collection]);
}

return (
<WorkspaceTab
{...tabProps}
id={workspaceProps.id}
connectionName={connectionName}
type={CollectionWorkspaceTitle}
title={collection}
tooltip={tooltip}
iconGlyph={
collectionType === 'view'
? 'Visibility'
: collectionType === 'timeseries'
? 'TimeSeries'
: workspaceProps.isNonExistent
? 'EmptyFolder'
: 'Folder'
}
data-namespace={ns}
tabTheme={getThemeOf(workspaceProps.connectionId)}
isNonExistent={workspaceProps.isNonExistent}
/>
);
}

const ConnectedPluginTitle = connect((state: CollectionState) => ({
isTimeSeries: state.metadata?.isTimeSeries,
isReadonly: state.metadata?.isReadonly,
sourceName: state.metadata?.sourceName,
}))(_PluginTitle);

export const CollectionWorkspaceTitle = 'Collection' as const;
export function CollectionPluginTitleComponent({
tabProps,
workspaceProps,
}: {
tabProps: WorkspaceTabCoreProps;
workspaceProps: WorkspaceProps;
}) {
return (
<ConnectedPluginTitle tabProps={tabProps} workspaceProps={workspaceProps} />
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ const draggingTabStyles = css({
cursor: 'grabbing !important',
});

const nonExistentStyles = css({
color: palette.gray.base,
});

const tabIconStyles = css({
color: 'currentColor',
marginLeft: spacing[300],
Expand Down Expand Up @@ -185,25 +189,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<TabTheme>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could get rid of this prop and instead use a provider.
Here's a branch with that provider: main...tab-theme-context-provider-example
I didn't include it in these changes, as it is something we could do as a follow up.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me, and then we can also add it to all the shared workspace providers. Then we can also use these colors inside the workspaces UI easily if we need to

};

export type WorkspaceTabCoreProps = {
isSelected: boolean;
isDragging: boolean;
onSelect: () => void;
onClose: () => void;
iconGlyph: GlyphName | 'Logo' | 'Server';
tabContentId: string;
tooltip?: [string, string][];
tabTheme?: Partial<TabTheme>;
};

type TabProps = WorkspaceTabCoreProps & WorkspaceTabPluginProps;

function Tab({
connectionName,
type,
title,
tooltip,
isNonExistent,
isSelected,
isDragging,
onSelect,
Expand All @@ -213,7 +226,7 @@ function Tab({
tabTheme,
className: tabClassName,
...props
}: TabProps & React.HTMLProps<HTMLDivElement>) {
}: TabProps & Omit<React.HTMLProps<HTMLDivElement>, 'title'>) {
const darkMode = useDarkMode();
const defaultActionProps = useDefaultAction(onSelect);
const { listeners, setNodeRef, transform, transition } = useSortable({
Expand Down Expand Up @@ -254,6 +267,7 @@ function Tab({
className={cx(
tabStyles,
themeClass,
isNonExistent && nonExistentStyles,
isSelected && selectedTabStyles,
isSelected && tabTheme && selectedThemedTabStyles,
isDragging && draggingTabStyles,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Tab>;
} {
return {
type: 'Documents',
title: `mock-tab-${tabId}`,
id: `${tabId}-content`,
iconGlyph: 'Folder',
renderTab: (tabProps: WorkspaceTabCoreProps) => (
<Tab
{...tabProps}
type="Documents"
title={`mock-tab-${tabId}`}
id={`${tabId}-content`}
iconGlyph="Folder"
/>
),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -139,8 +139,13 @@ function useTabListKeyboardNavigation<HTMLDivElement>({
return [{ onKeyDown }];
}

type TabItem = {
id: string;
renderTab: (props: WorkspaceTabCoreProps) => ReturnType<typeof Tab>;
};

type SortableItemProps = {
tab: TabProps;
tab: TabItem;
index: number;
selectedTabIndex: number;
activeId: UniqueIdentifier | null;
Expand All @@ -149,7 +154,7 @@ type SortableItemProps = {
};

type SortableListProps = {
tabs: TabProps[];
tabs: TabItem[];
selectedTabIndex: number;
onMove: (oldTabIndex: number, newTabIndex: number) => void;
onSelect: (tabIndex: number) => void;
Expand All @@ -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<React.HTMLProps<HTMLDivElement>, 'id' | 'title'>;

export function useRovingTabIndex<T extends HTMLElement = HTMLElement>({
currentTabbable,
}: {
Expand Down Expand Up @@ -263,7 +259,7 @@ const SortableList = ({
>
<SortableContext items={items} strategy={horizontalListSortingStrategy}>
<div className={sortableItemContainerStyles}>
{tabs.map((tab: TabProps, index: number) => (
{tabs.map((tab: TabItem, index: number) => (
<SortableItem
key={tab.id}
index={index}
Expand All @@ -281,15 +277,13 @@ const SortableList = ({
};

const SortableItem = ({
tab: tabProps,
tab: { id: tabId, renderTab },
index,
selectedTabIndex,
activeId,
onSelect,
onClose,
}: SortableItemProps) => {
const { id: tabId } = tabProps;

const onTabSelected = useCallback(() => {
onSelect(index);
}, [onSelect, index]);
Expand All @@ -305,16 +299,13 @@ const SortableItem = ({

const isDragging = useMemo(() => tabId === activeId, [tabId, activeId]);

return (
<Tab
{...tabProps}
isSelected={isSelected}
isDragging={isDragging}
tabContentId={tabId}
onSelect={onTabSelected}
onClose={onTabClosed}
/>
);
return renderTab({
isSelected,
isDragging,
tabContentId: tabId,
onSelect: onTabSelected,
onClose: onTabClosed,
});
};

function WorkspaceTabs({
Expand Down
6 changes: 5 additions & 1 deletion packages/compass-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading