Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React, { useCallback, useEffect, useRef } from 'react';
import {
getLocalAppRegistryForTab,
type WorkspaceTab,
} from '../stores/workspaces';
import { NamespaceProvider } from '@mongodb-js/compass-app-stores/provider';
import { ConnectionInfoProvider } from '@mongodb-js/compass-connections/provider';
import { rafraf } from '@mongodb-js/compass-components';
import { useOnTabReplace } from './workspace-close-handler';
import {
useTabState,
WorkspaceTabStateProvider,
} from './workspace-tab-state-provider';
import { AppRegistryProvider } from 'hadron-app-registry';

function getInitialPropsForWorkspace(tab: WorkspaceTab) {
switch (tab.type) {
case 'Welcome':
case 'My Queries':
case 'Data Modeling':
case 'Performance':
case 'Databases':
return null;
case 'Shell':
return {
initialEvaluate: tab.initialEvaluate,
initialInput: tab.initialInput,
};
case 'Collections':
return { namespace: tab.namespace };
case 'Collection': {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, type, connectionId, ...collectionMetadata } = tab;
return { tabId: id, ...collectionMetadata };
}
}
}

const TabCloseHandler: React.FunctionComponent = ({ children }) => {
const mountedRef = useRef(false);
const [hasInteractedOnce, setHasInteractedOnce] = useTabState(
'hasInteractedOnce',
false
);

useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
});

const markAsInteracted = useCallback(() => {
// Make sure we don't count clicking on buttons that actually cause the
// workspace to change, like using breadcrumbs or clicking on an item in the
// Databases / Collections list. There are certain corner-cases this doesn't
// handle, but it's good enough to prevent most cases where users can lose
// content by accident
rafraf(() => {
if (mountedRef.current) {
setHasInteractedOnce(true);
}
});
}, [setHasInteractedOnce]);

useOnTabReplace(() => {
return !hasInteractedOnce;
});

return (
// We're not using these for actual user interactions, just to capture the
// interacted state
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
style={{ display: 'contents' }}
onKeyDown={markAsInteracted}
onClickCapture={markAsInteracted}
>
{children}
</div>
);
};

const WorkspaceTabContextProvider: React.FunctionComponent<{
tab: WorkspaceTab;
sectionType: 'tab-content' | 'tab-title';
onNamespaceNotFound?: (
tab: Extract<WorkspaceTab, { namespace: string }>,
fallbackNamespace: string | null
) => void;
children: React.JSX.Element;
}> = ({ tab, onNamespaceNotFound, sectionType: type, children }) => {
const initialProps = getInitialPropsForWorkspace(tab);

if (initialProps) {
children = React.cloneElement(children, initialProps);
}

if ('namespace' in tab) {
children = (
<NamespaceProvider
namespace={tab.namespace}
onNamespaceFallbackSelect={(ns) => {
onNamespaceNotFound?.(tab, ns);
}}
>
{children}
</NamespaceProvider>
);
}

if ('connectionId' in tab) {
children = (
<ConnectionInfoProvider connectionInfoId={tab.connectionId}>
{children}
</ConnectionInfoProvider>
);
}

if (type === 'tab-content') {
children = <TabCloseHandler>{children}</TabCloseHandler>;
}

return (
<WorkspaceTabStateProvider id={tab.id}>
<AppRegistryProvider
key={tab.id}
scopeName="Workspace Tab"
localAppRegistry={getLocalAppRegistryForTab(tab.id)}
deactivateOnUnmount={false}
>
{children}
</AppRegistryProvider>
</WorkspaceTabStateProvider>
);
};

export { WorkspaceTabContextProvider };
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export const useWorkspacePlugins = () => {
hasWorkspacePlugin: <T extends AnyWorkspace['type']>(name: T) => {
return workspaces.some((ws) => ws.name === name);
},
getWorkspacePluginByName: <T extends AnyWorkspace['type']>(name: T) => {
getWorkspacePluginByName: <T extends AnyWorkspace['type']>(name?: T) => {
if (!name) {
return null;
}
const plugin = workspaces.find((ws) => ws.name === name);
if (!plugin) {
throw new Error(
Expand Down
171 changes: 23 additions & 148 deletions packages/compass-workspaces/src/components/workspaces.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { AppRegistryProvider } from 'hadron-app-registry';
import React, { useCallback, useMemo } from 'react';
import {
ErrorBoundary,
MongoDBLogoMark,
WorkspaceTabs,
css,
palette,
rafraf,
spacing,
useDarkMode,
} from '@mongodb-js/compass-components';
Expand All @@ -20,7 +18,6 @@ import type {
import {
closeTab,
getActiveTab,
getLocalAppRegistryForTab,
moveTab,
openFallbackWorkspace,
openTabFromCurrent,
Expand All @@ -32,17 +29,9 @@ import { useWorkspacePlugins } from './workspaces-provider';
import toNS from 'mongodb-ns';
import { useLogger } from '@mongodb-js/compass-logging/provider';
import { connect } from '../stores/context';
import {
WorkspaceTabStateProvider,
useTabState,
} from './workspace-tab-state-provider';
import { useOnTabReplace } from './workspace-close-handler';
import { NamespaceProvider } from '@mongodb-js/compass-app-stores/provider';
import {
ConnectionInfoProvider,
useTabConnectionTheme,
} from '@mongodb-js/compass-connections/provider';
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][];

Expand All @@ -64,51 +53,6 @@ const EmptyWorkspaceContent = () => {
);
};

const ActiveTabCloseHandler: React.FunctionComponent = ({ children }) => {
const mountedRef = useRef(false);
const [hasInteractedOnce, setHasInteractedOnce] = useTabState(
'hasInteractedOnce',
false
);

useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
});

const markAsInteracted = useCallback(() => {
// Make sure we don't count clicking on buttons that actually cause the
// workspace to change, like using breadcrumbs or clicking on an item in the
// Databases / Collections list. There are certain corner-cases this doesn't
// handle, but it's good enough to prevent most cases where users can lose
// content by accident
rafraf(() => {
if (mountedRef.current) {
setHasInteractedOnce(true);
}
});
}, [setHasInteractedOnce]);

useOnTabReplace(() => {
return !hasInteractedOnce;
});

return (
// We're not using these for actual user interactions, just to capture the
// interacted state
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
style={{ display: 'contents' }}
onKeyDown={markAsInteracted}
onClickCapture={markAsInteracted}
>
{children}
</div>
);
};

const workspacesContainerStyles = css({
display: 'grid',
width: '100%',
Expand Down Expand Up @@ -309,71 +253,7 @@ const CompassWorkspaces: React.FunctionComponent<CompassWorkspacesProps> = ({
}, [tabs, collectionInfo, databaseInfo, getThemeOf, getConnectionById]);

const activeTabIndex = tabs.findIndex((tab) => tab === activeTab);

const activeWorkspaceElement = useMemo(() => {
switch (activeTab?.type) {
case 'Welcome':
case 'My Queries':
case 'Data Modeling': {
const Component = getWorkspacePluginByName(activeTab.type);
return <Component></Component>;
}
case 'Shell': {
const Component = getWorkspacePluginByName(activeTab.type);
return (
<ConnectionInfoProvider connectionInfoId={activeTab.connectionId}>
<Component
initialEvaluate={activeTab.initialEvaluate}
initialInput={activeTab.initialInput}
></Component>
</ConnectionInfoProvider>
);
}
case 'Performance':
case 'Databases': {
const Component = getWorkspacePluginByName(activeTab.type);
return (
<ConnectionInfoProvider connectionInfoId={activeTab.connectionId}>
<Component></Component>
</ConnectionInfoProvider>
);
}
case 'Collections': {
const Component = getWorkspacePluginByName(activeTab.type);
return (
<ConnectionInfoProvider connectionInfoId={activeTab.connectionId}>
<NamespaceProvider
namespace={activeTab.namespace}
onNamespaceFallbackSelect={(ns) => {
onNamespaceNotFound(activeTab, ns);
}}
>
<Component namespace={activeTab.namespace}></Component>
</NamespaceProvider>
</ConnectionInfoProvider>
);
}
case 'Collection': {
const Component = getWorkspacePluginByName(activeTab.type);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, type, connectionId, ...collectionMetadata } = activeTab;
return (
<ConnectionInfoProvider connectionInfoId={activeTab.connectionId}>
<NamespaceProvider
namespace={activeTab.namespace}
onNamespaceFallbackSelect={(ns) => {
onNamespaceNotFound(activeTab, ns);
}}
>
<Component tabId={id} {...collectionMetadata}></Component>
</NamespaceProvider>
</ConnectionInfoProvider>
);
}
default:
return null;
}
}, [activeTab, getWorkspacePluginByName, onNamespaceNotFound]);
const WorkspaceComponent = getWorkspacePluginByName(activeTab?.type);

const onCreateNewTab = useCallback(() => {
onCreateTab(openOnEmptyWorkspace);
Expand All @@ -397,31 +277,26 @@ const CompassWorkspaces: React.FunctionComponent<CompassWorkspacesProps> = ({
></WorkspaceTabs>

<div className={workspacesContentStyles}>
{activeTab && activeWorkspaceElement ? (
<WorkspaceTabStateProvider id={activeTab.id}>
<AppRegistryProvider
key={activeTab.id}
scopeName="Workspace Tab"
localAppRegistry={getLocalAppRegistryForTab(activeTab.id)}
deactivateOnUnmount={false}
{activeTab && WorkspaceComponent ? (
<ErrorBoundary
displayName={activeTab.type}
onError={(error, errorInfo) => {
log.error(
mongoLogId(1_001_000_277),
'Workspace',
'Rendering workspace tab failed',
{ name: activeTab.type, error: error.message, errorInfo }
);
}}
>
<WorkspaceTabContextProvider
tab={activeTab}
sectionType="tab-content"
onNamespaceNotFound={onNamespaceNotFound}
>
<ActiveTabCloseHandler>
<ErrorBoundary
displayName={activeTab.type}
onError={(error, errorInfo) => {
log.error(
mongoLogId(1_001_000_277),
'Workspace',
'Rendering workspace tab failed',
{ name: activeTab.type, error: error.message, errorInfo }
);
}}
>
{activeWorkspaceElement}
</ErrorBoundary>
</ActiveTabCloseHandler>
</AppRegistryProvider>
</WorkspaceTabStateProvider>
<WorkspaceComponent></WorkspaceComponent>
</WorkspaceTabContextProvider>
</ErrorBoundary>
) : (
<EmptyWorkspaceContent></EmptyWorkspaceContent>
)}
Expand Down
Loading