Skip to content

Commit ed107db

Browse files
committed
feat(workspaces): move tab rendering to the plugins COMPASS-9413
1 parent 715ce44 commit ed107db

File tree

33 files changed

+834
-421
lines changed

33 files changed

+834
-421
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+
CollectionPluginTitle,
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: CollectionPluginTitle,
2936
};
3037

31-
export default CollectionTabPlugin;
3238
export type { CollectionTabPluginMetadata } from './modules/collection-tab';
3339
export { CollectionTabsProvider } from './components/collection-tab-provider';
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React from 'react';
2+
import { connect } from 'react-redux';
3+
import toNS from 'mongodb-ns';
4+
import {
5+
useConnectionsListRef,
6+
useTabConnectionTheme,
7+
} from '@mongodb-js/compass-connections/provider';
8+
import {
9+
WorkspaceTab,
10+
type WorkspaceTabCoreProps,
11+
} from '@mongodb-js/compass-components';
12+
import type { CollectionSubtab } from '@mongodb-js/compass-workspaces';
13+
14+
import { type CollectionState } from './modules/collection-tab';
15+
16+
type WorkspaceProps = {
17+
id: string;
18+
connectionId: string;
19+
namespace: string;
20+
subTab: CollectionSubtab;
21+
initialQuery?: unknown;
22+
initialPipeline?: unknown[];
23+
initialPipelineText?: string;
24+
initialAggregation?: unknown;
25+
editViewName?: string;
26+
isNonExistent: boolean;
27+
};
28+
29+
function _PluginTitle({
30+
isTimeSeries,
31+
isReadonly,
32+
sourceName,
33+
tabProps,
34+
workspaceProps,
35+
}: {
36+
isTimeSeries?: boolean;
37+
isReadonly?: boolean;
38+
sourceName?: string | null;
39+
tabProps: WorkspaceTabCoreProps;
40+
workspaceProps: WorkspaceProps;
41+
}) {
42+
const { getThemeOf } = useTabConnectionTheme();
43+
const { getConnectionById } = useConnectionsListRef();
44+
45+
const { database, collection, ns } = toNS(workspaceProps.namespace);
46+
const namespaceId = `${workspaceProps.connectionId}.${ns}`;
47+
const connectionName =
48+
getConnectionById(workspaceProps.connectionId)?.title || '';
49+
const collectionType = isTimeSeries
50+
? 'timeseries'
51+
: isReadonly
52+
? 'view'
53+
: 'collection';
54+
// Similar to what we have in the collection breadcrumbs.
55+
const tooltip: [string, string][] = [
56+
['Connection', connectionName || ''],
57+
['Database', database],
58+
];
59+
if (sourceName) {
60+
tooltip.push(['View', collection]);
61+
tooltip.push(['Derived from', toNS(sourceName).collection]);
62+
} else if (workspaceProps.editViewName) {
63+
tooltip.push(['View', toNS(workspaceProps.editViewName).collection]);
64+
tooltip.push(['Derived from', collection]);
65+
} else {
66+
tooltip.push(['Collection', collection]);
67+
}
68+
69+
return (
70+
<WorkspaceTab
71+
{...tabProps}
72+
id={workspaceProps.id}
73+
connectionName={connectionName}
74+
type={CollectionWorkspaceTitle}
75+
title={collection}
76+
tooltip={tooltip}
77+
iconGlyph={
78+
collectionType === 'view'
79+
? 'Visibility'
80+
: collectionType === 'timeseries'
81+
? 'TimeSeries'
82+
: workspaceProps.isNonExistent
83+
? 'EmptyFolder'
84+
: 'Folder'
85+
}
86+
data-namespace={ns}
87+
tabTheme={getThemeOf(workspaceProps.connectionId)}
88+
isNonExistent={workspaceProps.isNonExistent}
89+
/>
90+
);
91+
}
92+
93+
const ConnectedPluginTitle = connect((state: CollectionState) => ({
94+
// TODO: Need to check the implications of moving this metadata here
95+
// instead of having it passed from the workspaces store.
96+
isTimeSeries: state.metadata?.isTimeSeries,
97+
isReadonly: state.metadata?.isReadonly,
98+
sourceName: state.metadata?.sourceName,
99+
}))(_PluginTitle);
100+
101+
export const CollectionWorkspaceTitle = 'Collection' as const;
102+
export function CollectionPluginTitle(workspaceProps: WorkspaceProps) {
103+
return (tabProps: WorkspaceTabCoreProps) => {
104+
return (
105+
<ConnectedPluginTitle
106+
tabProps={tabProps}
107+
workspaceProps={workspaceProps}
108+
/>
109+
);
110+
};
111+
}

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ const draggingTabStyles = css({
149149
cursor: 'grabbing !important',
150150
});
151151

152+
const nonExistentStyles = css({
153+
color: palette.gray.base,
154+
});
155+
152156
const tabIconStyles = css({
153157
color: 'currentColor',
154158
marginLeft: spacing[300],
@@ -185,25 +189,34 @@ const workspaceTabTooltipStyles = css({
185189
textWrap: 'wrap',
186190
});
187191

188-
type TabProps = {
192+
// The plugins provide these essential props use to render the tab.
193+
// The workspace-tabs component provides the other parts of TabProps.
194+
export type WorkspaceTabPluginProps = {
189195
connectionName?: string;
190196
type: string;
191-
title: string;
197+
title: React.ReactNode;
198+
isNonExistent?: boolean;
199+
iconGlyph: GlyphName | 'Logo' | 'Server';
200+
tooltip?: [string, string][];
201+
tabTheme?: Partial<TabTheme>;
202+
};
203+
204+
export type WorkspaceTabCoreProps = {
192205
isSelected: boolean;
193206
isDragging: boolean;
194207
onSelect: () => void;
195208
onClose: () => void;
196-
iconGlyph: GlyphName | 'Logo' | 'Server';
197209
tabContentId: string;
198-
tooltip?: [string, string][];
199-
tabTheme?: Partial<TabTheme>;
200210
};
201211

212+
type TabProps = WorkspaceTabCoreProps & WorkspaceTabPluginProps;
213+
202214
function Tab({
203215
connectionName,
204216
type,
205217
title,
206218
tooltip,
219+
isNonExistent,
207220
isSelected,
208221
isDragging,
209222
onSelect,
@@ -213,7 +226,7 @@ function Tab({
213226
tabTheme,
214227
className: tabClassName,
215228
...props
216-
}: TabProps & React.HTMLProps<HTMLDivElement>) {
229+
}: TabProps & Omit<React.HTMLProps<HTMLDivElement>, 'title'>) {
217230
const darkMode = useDarkMode();
218231
const defaultActionProps = useDefaultAction(onSelect);
219232
const { listeners, setNodeRef, transform, transition } = useSortable({
@@ -254,6 +267,7 @@ function Tab({
254267
className={cx(
255268
tabStyles,
256269
themeClass,
270+
isNonExistent && nonExistentStyles,
257271
isSelected && selectedTabStyles,
258272
isSelected && tabTheme && selectedThemedTabStyles,
259273
isDragging && draggingTabStyles,

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: 37 additions & 27 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 {
@@ -29,6 +28,7 @@ import { FocusState, useFocusState } from '../../hooks/use-focus-hover';
2928
import { Icon, IconButton } from '../leafygreen';
3029
import { mergeProps } from '../../utils/merge-props';
3130
import { 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,32 @@ 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+
const tab = useMemo(
303+
() =>
304+
renderTab({
305+
// ...tabProps,
306+
// TODO: id here?
307+
isSelected,
308+
isDragging,
309+
tabContentId: tabId,
310+
onSelect: onTabSelected,
311+
onClose: onTabClosed,
312+
}),
313+
[renderTab]
314+
); // TODO: Add the rest of the tab props. Maybe no useMemo.
315+
316+
return tab;
317+
318+
// return tab;(
319+
// <Tab
320+
// {...tabProps}
321+
// isSelected={isSelected}
322+
// isDragging={isDragging}
323+
// tabContentId={tabId}
324+
// onSelect={onTabSelected}
325+
// onClose={onTabClosed}
326+
// />
327+
// );
318328
};
319329

320330
function WorkspaceTabs({

0 commit comments

Comments
 (0)