Skip to content

Commit 1e4eeed

Browse files
authored
feat(workspaces): move tab rendering to the plugins COMPASS-9413 (#6997)
1 parent 7f7bc82 commit 1e4eeed

40 files changed

+885
-471
lines changed
Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import React from 'react';
12
import CollectionTab from './components/collection-tab';
23
import { activatePlugin as activateCollectionTabPlugin } from './stores/collection-tab';
34
import { registerHadronPlugin } from 'hadron-app-registry';
@@ -7,27 +8,32 @@ import {
78
type DataService,
89
} from '@mongodb-js/compass-connections/provider';
910
import { collectionModelLocator } from '@mongodb-js/compass-app-stores/provider';
10-
import type { WorkspaceComponent } from '@mongodb-js/compass-workspaces';
11+
import type { WorkspacePlugin } from '@mongodb-js/compass-workspaces';
1112
import { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider';
13+
import {
14+
CollectionWorkspaceTitle,
15+
CollectionPluginTitleComponent,
16+
} from './plugin-tab-title';
1217

13-
export const CollectionTabPlugin = registerHadronPlugin(
14-
{
15-
name: 'CollectionTab',
16-
component: CollectionTab,
17-
activate: activateCollectionTabPlugin,
18-
},
19-
{
20-
dataService: dataServiceLocator as DataServiceLocator<keyof DataService>,
21-
collection: collectionModelLocator,
22-
workspaces: workspacesServiceLocator,
23-
}
24-
);
25-
26-
export const WorkspaceTab: WorkspaceComponent<'Collection'> = {
27-
name: 'Collection' as const,
28-
component: CollectionTabPlugin,
18+
export const WorkspaceTab: WorkspacePlugin<typeof CollectionWorkspaceTitle> = {
19+
name: CollectionWorkspaceTitle,
20+
provider: registerHadronPlugin(
21+
{
22+
name: CollectionWorkspaceTitle,
23+
component: function CollectionProvider({ children }) {
24+
return React.createElement(React.Fragment, null, children);
25+
},
26+
activate: activateCollectionTabPlugin,
27+
},
28+
{
29+
dataService: dataServiceLocator as DataServiceLocator<keyof DataService>,
30+
collection: collectionModelLocator,
31+
workspaces: workspacesServiceLocator,
32+
}
33+
),
34+
content: CollectionTab,
35+
header: CollectionPluginTitleComponent,
2936
};
3037

31-
export default CollectionTabPlugin;
3238
export type { CollectionTabPluginMetadata } from './modules/collection-tab';
3339
export { CollectionTabsProvider } from './components/collection-tab-provider';
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React from 'react';
2+
import { connect } from 'react-redux';
3+
import toNS from 'mongodb-ns';
4+
import {
5+
useConnectionInfo,
6+
useConnectionsListRef,
7+
useTabConnectionTheme,
8+
} from '@mongodb-js/compass-connections/provider';
9+
import {
10+
WorkspaceTab,
11+
type WorkspaceTabCoreProps,
12+
} from '@mongodb-js/compass-components';
13+
import type { WorkspacePluginProps } from '@mongodb-js/compass-workspaces';
14+
15+
import { type CollectionState } from './modules/collection-tab';
16+
17+
export const CollectionWorkspaceTitle = 'Collection' as const;
18+
19+
type PluginTitleProps = {
20+
isTimeSeries?: boolean;
21+
isReadonly?: boolean;
22+
sourceName?: string | null;
23+
} & WorkspaceTabCoreProps &
24+
WorkspacePluginProps<typeof CollectionWorkspaceTitle>;
25+
26+
function _PluginTitle({
27+
editViewName,
28+
isNonExistent,
29+
isReadonly,
30+
isTimeSeries,
31+
sourceName,
32+
namespace,
33+
...tabProps
34+
}: PluginTitleProps) {
35+
const { getThemeOf } = useTabConnectionTheme();
36+
const { getConnectionById } = useConnectionsListRef();
37+
const { id: connectionId } = useConnectionInfo();
38+
39+
const { database, collection, ns } = toNS(namespace);
40+
const connectionName = getConnectionById(connectionId)?.title || '';
41+
const collectionType = isTimeSeries
42+
? 'timeseries'
43+
: isReadonly
44+
? 'view'
45+
: 'collection';
46+
// Similar to what we have in the collection breadcrumbs.
47+
const tooltip: [string, string][] = [
48+
['Connection', connectionName || ''],
49+
['Database', database],
50+
];
51+
if (sourceName) {
52+
tooltip.push(['View', collection]);
53+
tooltip.push(['Derived from', toNS(sourceName).collection]);
54+
} else if (editViewName) {
55+
tooltip.push(['View', toNS(editViewName).collection]);
56+
tooltip.push(['Derived from', collection]);
57+
} else {
58+
tooltip.push(['Collection', collection]);
59+
}
60+
61+
return (
62+
<WorkspaceTab
63+
{...tabProps}
64+
connectionName={connectionName}
65+
type={CollectionWorkspaceTitle}
66+
title={collection}
67+
tooltip={tooltip}
68+
iconGlyph={
69+
collectionType === 'view'
70+
? 'Visibility'
71+
: collectionType === 'timeseries'
72+
? 'TimeSeries'
73+
: isNonExistent
74+
? 'EmptyFolder'
75+
: 'Folder'
76+
}
77+
data-namespace={ns}
78+
tabTheme={getThemeOf(connectionId)}
79+
isNonExistent={isNonExistent}
80+
/>
81+
);
82+
}
83+
84+
export const CollectionPluginTitleComponent = connect(
85+
(state: CollectionState) => ({
86+
isTimeSeries: state.metadata?.isTimeSeries,
87+
isReadonly: state.metadata?.isReadonly,
88+
sourceName: state.metadata?.sourceName,
89+
})
90+
)(_PluginTitle);

packages/compass-components/src/components/workspace-tabs/tab.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { spacing } from '@leafygreen-ui/tokens';
55
import type { GlyphName } from '@leafygreen-ui/icon';
66
import { useSortable } from '@dnd-kit/sortable';
77
import { CSS as cssDndKit } from '@dnd-kit/utilities';
8+
import { useId } from '@react-aria/utils';
89
import { useDarkMode } from '../../hooks/use-theme';
910
import { Icon, IconButton } from '../leafygreen';
1011
import { mergeProps } from '../../utils/merge-props';
@@ -149,6 +150,10 @@ const draggingTabStyles = css({
149150
cursor: 'grabbing !important',
150151
});
151152

153+
const nonExistentStyles = css({
154+
color: palette.gray.base,
155+
});
156+
152157
const tabIconStyles = css({
153158
color: 'currentColor',
154159
marginLeft: spacing[300],
@@ -185,25 +190,34 @@ const workspaceTabTooltipStyles = css({
185190
textWrap: 'wrap',
186191
});
187192

188-
type TabProps = {
193+
// The plugins provide these essential props use to render the tab.
194+
// The workspace-tabs component provides the other parts of TabProps.
195+
export type WorkspaceTabPluginProps = {
189196
connectionName?: string;
190197
type: string;
191-
title: string;
198+
title: React.ReactNode;
199+
isNonExistent?: boolean;
200+
iconGlyph: GlyphName | 'Logo' | 'Server';
201+
tooltip?: [string, string][];
202+
tabTheme?: Partial<TabTheme>;
203+
};
204+
205+
export type WorkspaceTabCoreProps = {
192206
isSelected: boolean;
193207
isDragging: boolean;
194208
onSelect: () => void;
195209
onClose: () => void;
196-
iconGlyph: GlyphName | 'Logo' | 'Server';
197210
tabContentId: string;
198-
tooltip?: [string, string][];
199-
tabTheme?: Partial<TabTheme>;
200211
};
201212

213+
type TabProps = WorkspaceTabCoreProps & WorkspaceTabPluginProps;
214+
202215
function Tab({
203216
connectionName,
204217
type,
205218
title,
206219
tooltip,
220+
isNonExistent,
207221
isSelected,
208222
isDragging,
209223
onSelect,
@@ -213,7 +227,7 @@ function Tab({
213227
tabTheme,
214228
className: tabClassName,
215229
...props
216-
}: TabProps & React.HTMLProps<HTMLDivElement>) {
230+
}: TabProps & Omit<React.HTMLProps<HTMLDivElement>, 'title'>) {
217231
const darkMode = useDarkMode();
218232
const defaultActionProps = useDefaultAction(onSelect);
219233
const { listeners, setNodeRef, transform, transition } = useSortable({
@@ -240,6 +254,8 @@ function Tab({
240254
cursor: 'grabbing !important',
241255
};
242256

257+
const tabId = useId();
258+
243259
return (
244260
<Tooltip
245261
enabled={!!tooltip}
@@ -254,6 +270,7 @@ function Tab({
254270
className={cx(
255271
tabStyles,
256272
themeClass,
273+
isNonExistent && nonExistentStyles,
257274
isSelected && selectedTabStyles,
258275
isSelected && tabTheme && selectedThemedTabStyles,
259276
isDragging && draggingTabStyles,
@@ -267,6 +284,7 @@ function Tab({
267284
data-testid="workspace-tab-button"
268285
data-connection-name={connectionName}
269286
data-type={type}
287+
id={tabId}
270288
{...tabProps}
271289
>
272290
{iconGlyph === 'Logo' && (

packages/compass-components/src/components/workspace-tabs/workspace-tabs.spec.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,23 @@ import { expect } from 'chai';
99
import sinon from 'sinon';
1010

1111
import { WorkspaceTabs } from './workspace-tabs';
12-
import type { TabProps } from './workspace-tabs';
12+
import { Tab, type WorkspaceTabCoreProps } from './tab';
1313

14-
function mockTab(tabId: number): TabProps {
14+
function mockTab(tabId: number): {
15+
id: string;
16+
renderTab: (tabProps: WorkspaceTabCoreProps) => ReturnType<typeof Tab>;
17+
} {
1518
return {
16-
type: 'Documents',
17-
title: `mock-tab-${tabId}`,
1819
id: `${tabId}-content`,
19-
iconGlyph: 'Folder',
20+
renderTab: (tabProps: WorkspaceTabCoreProps) => (
21+
<Tab
22+
{...tabProps}
23+
type="Documents"
24+
title={`mock-tab-${tabId}`}
25+
id={`${tabId}-content`}
26+
iconGlyph="Folder"
27+
/>
28+
),
2029
};
2130
}
2231

packages/compass-components/src/components/workspace-tabs/workspace-tabs.tsx

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import React, {
88
import { css, cx } from '@leafygreen-ui/emotion';
99
import { palette } from '@leafygreen-ui/palette';
1010
import { spacing } from '@leafygreen-ui/tokens';
11-
import type { GlyphName } from '@leafygreen-ui/icon';
1211
import { rgba } from 'polished';
1312

1413
import {
@@ -28,7 +27,8 @@ import { useDarkMode } from '../../hooks/use-theme';
2827
import { FocusState, useFocusState } from '../../hooks/use-focus-hover';
2928
import { Icon, IconButton } from '../leafygreen';
3029
import { mergeProps } from '../../utils/merge-props';
31-
import { Tab } from './tab';
30+
import type { Tab } from './tab';
31+
import type { WorkspaceTabCoreProps } from './tab';
3232
import { useHotkeys } from '../../hooks/use-hotkeys';
3333

3434
export const scrollbarThumbLightTheme = rgba(palette.gray.base, 0.65);
@@ -139,8 +139,13 @@ function useTabListKeyboardNavigation<HTMLDivElement>({
139139
return [{ onKeyDown }];
140140
}
141141

142+
type TabItem = {
143+
id: string;
144+
renderTab: (props: WorkspaceTabCoreProps) => ReturnType<typeof Tab>;
145+
};
146+
142147
type SortableItemProps = {
143-
tab: TabProps;
148+
tab: TabItem;
144149
index: number;
145150
selectedTabIndex: number;
146151
activeId: UniqueIdentifier | null;
@@ -149,7 +154,7 @@ type SortableItemProps = {
149154
};
150155

151156
type SortableListProps = {
152-
tabs: TabProps[];
157+
tabs: TabItem[];
153158
selectedTabIndex: number;
154159
onMove: (oldTabIndex: number, newTabIndex: number) => void;
155160
onSelect: (tabIndex: number) => void;
@@ -164,19 +169,10 @@ type WorkspaceTabsProps = {
164169
onSelectPrevTab: () => void;
165170
onCloseTab: (tabIndex: number) => void;
166171
onMoveTab: (oldTabIndex: number, newTabIndex: number) => void;
167-
tabs: TabProps[];
172+
tabs: TabItem[];
168173
selectedTabIndex: number;
169174
};
170175

171-
export type TabProps = {
172-
id: string;
173-
type: string;
174-
title: string;
175-
tooltip?: [string, string][];
176-
connectionId?: string;
177-
iconGlyph: GlyphName | 'Logo' | 'Server';
178-
} & Omit<React.HTMLProps<HTMLDivElement>, 'id' | 'title'>;
179-
180176
export function useRovingTabIndex<T extends HTMLElement = HTMLElement>({
181177
currentTabbable,
182178
}: {
@@ -263,7 +259,7 @@ const SortableList = ({
263259
>
264260
<SortableContext items={items} strategy={horizontalListSortingStrategy}>
265261
<div className={sortableItemContainerStyles}>
266-
{tabs.map((tab: TabProps, index: number) => (
262+
{tabs.map((tab: TabItem, index: number) => (
267263
<SortableItem
268264
key={tab.id}
269265
index={index}
@@ -281,15 +277,13 @@ const SortableList = ({
281277
};
282278

283279
const SortableItem = ({
284-
tab: tabProps,
280+
tab: { id: tabId, renderTab },
285281
index,
286282
selectedTabIndex,
287283
activeId,
288284
onSelect,
289285
onClose,
290286
}: SortableItemProps) => {
291-
const { id: tabId } = tabProps;
292-
293287
const onTabSelected = useCallback(() => {
294288
onSelect(index);
295289
}, [onSelect, index]);
@@ -305,16 +299,13 @@ const SortableItem = ({
305299

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

308-
return (
309-
<Tab
310-
{...tabProps}
311-
isSelected={isSelected}
312-
isDragging={isDragging}
313-
tabContentId={tabId}
314-
onSelect={onTabSelected}
315-
onClose={onTabClosed}
316-
/>
317-
);
302+
return renderTab({
303+
isSelected,
304+
isDragging,
305+
tabContentId: tabId,
306+
onSelect: onTabSelected,
307+
onClose: onTabClosed,
308+
});
318309
};
319310

320311
function WorkspaceTabs({

packages/compass-components/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export {
3838
import { ResizeHandle, ResizeDirection } from './components/resize-handle';
3939
import { Accordion } from './components/accordion';
4040
import { CollapsibleFieldSet } from './components/collapsible-field-set';
41-
export { type TabTheme } from './components/workspace-tabs/tab';
41+
export {
42+
Tab as WorkspaceTab,
43+
type TabTheme,
44+
type WorkspaceTabCoreProps,
45+
} from './components/workspace-tabs/tab';
4246
import { WorkspaceTabs } from './components/workspace-tabs/workspace-tabs';
4347
import ResizableSidebar, {
4448
defaultSidebarWidth,

0 commit comments

Comments
 (0)