=> {
+ assert(message.command === 'THEME_CHANGED');
+ assert(message.darkMode === true);
+ if (++callsSoFar === 1) {
+ // This should be fine since we catch the rejection and proceed ahead silently
+ throw new Error('BAM');
+ }
+ if (++callsSoFar === totalExpectedPostMessageCalls) {
+ done();
+ }
+ },
+ onDidReceiveMessage: (): void => {},
+ asWebviewUri: sandbox.fake.returns(''),
+ };
+ const fakeVSCodeCreateWebviewPanel = sandbox.fake.returns({
+ webview: fakeWebview,
+ onDidDispose: sandbox.fake.returns(''),
+ });
+
+ sandbox.replace(
+ vscode.window,
+ 'createWebviewPanel',
+ fakeVSCodeCreateWebviewPanel
+ );
+
+ const testWebviewController = new WebviewController({
+ connectionController: testConnectionController,
+ storageController: testStorageController,
+ telemetryService: testTelemetryService,
+ });
+
+ void testWebviewController.openWebview(
+ mdbTestExtension.extensionContextStub
+ );
+
+ void testWebviewController.openWebview(
+ mdbTestExtension.extensionContextStub
+ );
+
+ void testWebviewController.openWebview(
+ mdbTestExtension.extensionContextStub
+ );
+
+ // Mock a theme change
+ void testWebviewController.onThemeChanged({
+ kind: vscode.ColorThemeKind.Dark,
+ });
+ });
+
suite('with a rendered webview', () => {
const extensionContextStub = new ExtensionContextStub();
const testStorageController = new StorageController(extensionContextStub);
@@ -816,6 +896,7 @@ suite('Webview Test Suite', () => {
const fakeVSCodeCreateWebviewPanel = sandbox.fake.returns({
webview: fakeWebview,
+ onDidDispose: sandbox.fake.returns(''),
});
sandbox.replace(
vscode.window,
diff --git a/src/views/webview-app/app.tsx b/src/views/webview-app/app.tsx
index c4c76aa4e..ecbc7af33 100644
--- a/src/views/webview-app/app.tsx
+++ b/src/views/webview-app/app.tsx
@@ -1,10 +1,17 @@
import React from 'react';
import { getFeatureFlag } from '../../featureFlags';
import LegacyApp from './legacy/app-with-store';
+import OverviewPage from './overview-page';
+import { LeafyGreenProvider } from '@mongodb-js/compass-components';
+import { useDetectVsCodeDarkMode } from './use-detect-vscode-dark-mode';
const App: React.FC = () => {
+ const darkMode = useDetectVsCodeDarkMode();
+
return getFeatureFlag('useNewConnectionForm') ? (
- <>Silence is golden>
+
+
+
) : (
);
diff --git a/src/views/webview-app/atlas-cta.tsx b/src/views/webview-app/atlas-cta.tsx
new file mode 100644
index 000000000..36423fb0f
--- /dev/null
+++ b/src/views/webview-app/atlas-cta.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import {
+ Body,
+ Button,
+ css,
+ cx,
+ palette,
+ spacing,
+ useDarkMode,
+} from '@mongodb-js/compass-components';
+import LINKS from '../../utils/links';
+import AtlasLogo from './atlas-logo';
+import { openTrustedLink, trackExtensionLinkClicked } from './vscode-api';
+import { VSCODE_EXTENSION_SEGMENT_ANONYMOUS_ID } from './extension-app-message-constants';
+
+const ctaContainerStyles = css({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spacing[2],
+ marginTop: spacing[4],
+ alignItems: 'center',
+ '@media(min-width: 600px)': {
+ '&': {
+ flexDirection: 'row',
+ gap: spacing[4],
+ },
+ },
+});
+
+const atlasTxtLogoContainerStyles = css({
+ display: 'flex',
+ alignItems: 'center',
+ gap: spacing[2],
+});
+
+const txtStyles = css({
+ textAlign: 'left',
+});
+
+const linkDarkModeStyles = css({
+ color: palette.green.base,
+ '&:hover': {
+ color: palette.green.base,
+ },
+});
+
+const linkLightModeStyles = css({
+ color: palette.green.dark2,
+ '&:hover': {
+ color: palette.green.dark2,
+ },
+});
+
+const AtlasCta: React.FC = () => {
+ const isDarkMode = useDarkMode();
+
+ const handleAtlasCTAClicked = () => {
+ const telemetryUserId = window[VSCODE_EXTENSION_SEGMENT_ANONYMOUS_ID];
+ const atlasLink = LINKS.createAtlasCluster(telemetryUserId);
+ openTrustedLink(atlasLink);
+ trackExtensionLinkClicked('overviewPage', 'freeClusterCTA');
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export default AtlasCta;
diff --git a/src/views/webview-app/atlas-logo.tsx b/src/views/webview-app/atlas-logo.tsx
new file mode 100644
index 000000000..802354cee
--- /dev/null
+++ b/src/views/webview-app/atlas-logo.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { palette, useDarkMode } from '@mongodb-js/compass-components';
+
+const AtlasLogo: React.FC = () => {
+ const isDarkMode = useDarkMode();
+ const mongodbGreen = isDarkMode ? palette.green.base : palette.green.dark2;
+ return (
+
+ );
+};
+
+export default AtlasLogo;
diff --git a/src/views/webview-app/connect-helper.tsx b/src/views/webview-app/connect-helper.tsx
new file mode 100644
index 000000000..a5f997ca2
--- /dev/null
+++ b/src/views/webview-app/connect-helper.tsx
@@ -0,0 +1,108 @@
+import React from 'react';
+import {
+ css,
+ spacing,
+ Button,
+ Body,
+ palette,
+ cx,
+} from '@mongodb-js/compass-components';
+import { connectWithConnectionString } from './vscode-api';
+
+const containerStyles = css({
+ display: 'flex',
+ alignItems: 'center',
+ flexDirection: 'column',
+ gap: spacing[2],
+ marginTop: spacing[4],
+});
+
+const cardContainerStyles = css({
+ width: '400px',
+ height: '200px',
+ borderRadius: '6px',
+ overflow: 'hidden',
+});
+
+const inlineCardStyles = css({
+ width: '50%',
+ height: '100%',
+ display: 'inline-flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: spacing[4],
+});
+
+const leftCardStyles = cx(
+ inlineCardStyles,
+ css({
+ background: palette.gray.light2,
+ })
+);
+
+const rightCardStyles = cx(
+ inlineCardStyles,
+ css({
+ background: palette.gray.dark4,
+ })
+);
+
+const getOSCommandShortcutName = (): string => {
+ if (navigator.userAgent.includes('Win')) {
+ return 'Ctrl';
+ }
+
+ return 'Cmd';
+};
+
+const ConnectHelper: React.FC = () => {
+ return (
+
+
+
+
+
+ Connect with
+
+
+ Connection String
+
+
+
+
+
+
+
+ Advanced
+
+
+ Connection Settings
+
+
+
+
+
+
+
{getOSCommandShortcutName()} + Shift + P for all
+ MongoDB Command Palette options
+
+
+ );
+};
+
+export default ConnectHelper;
diff --git a/src/views/webview-app/connection-status.tsx b/src/views/webview-app/connection-status.tsx
new file mode 100644
index 000000000..eca136fb8
--- /dev/null
+++ b/src/views/webview-app/connection-status.tsx
@@ -0,0 +1,176 @@
+import React from 'react';
+import {
+ Body,
+ Button,
+ Icon,
+ IconButton,
+ css,
+ cx,
+ palette,
+ spacing,
+ useDarkMode,
+} from '@mongodb-js/compass-components';
+import { CONNECTION_STATUS } from './extension-app-message-constants';
+import LINKS from '../../utils/links';
+import useConnectionStatus from './use-connection-status';
+import { createNewPlayground, renameActiveConnection } from './vscode-api';
+
+const connectedContainerStyles = css({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spacing[4],
+});
+
+const statusDotStyles = css({
+ width: spacing[2],
+ height: spacing[2],
+ borderRadius: '50%',
+ pointerEvents: 'none',
+ display: 'inline-block',
+ marginRight: spacing[2],
+});
+
+const connectedStatusDotStyles = cx(
+ statusDotStyles,
+ css({
+ backgroundColor: palette.green.base,
+ })
+);
+
+const connectedStatusDotLightModeStyles = css({
+ backgroundColor: palette.green.dark2,
+});
+
+const disconnectedStatusDotStyles = cx(
+ statusDotStyles,
+ css({
+ backgroundColor: palette.red.base,
+ })
+);
+
+const statusContainerStyles = css({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spacing[1],
+ alignItems: 'center',
+ justifyContent: 'center',
+ '@media(min-width: 500px)': {
+ '&': {
+ flexDirection: 'row',
+ gap: spacing[2],
+ },
+ },
+});
+
+const connectionNameStyles = css({
+ display: 'flex',
+ alignItems: 'center',
+});
+
+const playgroundCtaContainerStyles = css({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spacing[3],
+ alignItems: 'center',
+ '@media(min-width: 500px)': {
+ '&': {
+ gap: spacing[5],
+ justifyContent: 'center',
+ flexDirection: 'row',
+ },
+ },
+});
+
+const textContainerStyles = css({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spacing[1],
+});
+
+const ctaTextStyles = css({
+ fontSize: '18px',
+ display: 'flex',
+ alignItems: 'center',
+});
+
+const ConnectionStatusConnected: React.FC<{ connectionName: string }> = ({
+ connectionName,
+}) => {
+ const isDarkMode = useDarkMode();
+ return (
+
+
+
+
+ Connected to:
+
+
+ {connectionName}
+
+
+
+
+
+
+
+
All set. Ready to start?
+
+ Create a playground.
+
+
+
+
+
+
+
+
+ );
+};
+
+// eslint-disable-next-line react/no-multi-comp
+const ConnectionStatus: React.FC = () => {
+ const { connectionStatus, connectionName } = useConnectionStatus();
+ return (
+ <>
+ {connectionStatus === CONNECTION_STATUS.CONNECTED && (
+
+ )}
+ {connectionStatus === CONNECTION_STATUS.DISCONNECTED && (
+
+
+ Not connected.
+
+ )}
+ {connectionStatus === CONNECTION_STATUS.LOADING && (
+ Loading...
+ )}
+ {connectionStatus === CONNECTION_STATUS.CONNECTING && (
+ Connecting...
+ )}
+ {connectionStatus === CONNECTION_STATUS.DISCONNECTING && (
+ Disconnecting...
+ )}
+ >
+ );
+};
+
+export default ConnectionStatus;
diff --git a/src/views/webview-app/extension-app-message-constants.ts b/src/views/webview-app/extension-app-message-constants.ts
index 75e13f14b..6b9a88cdf 100644
--- a/src/views/webview-app/extension-app-message-constants.ts
+++ b/src/views/webview-app/extension-app-message-constants.ts
@@ -24,6 +24,7 @@ export enum MESSAGE_TYPES {
OPEN_FILE_PICKER = 'OPEN_FILE_PICKER',
OPEN_TRUSTED_LINK = 'OPEN_TRUSTED_LINK',
RENAME_ACTIVE_CONNECTION = 'RENAME_ACTIVE_CONNECTION',
+ THEME_CHANGED = 'THEME_CHANGED',
}
interface BasicWebviewMessage {
@@ -89,6 +90,11 @@ export interface RenameConnectionMessage extends BasicWebviewMessage {
command: MESSAGE_TYPES.RENAME_ACTIVE_CONNECTION;
}
+export interface ThemeChangedMessage extends BasicWebviewMessage {
+ command: MESSAGE_TYPES.THEME_CHANGED;
+ darkMode: boolean;
+}
+
export type MESSAGE_FROM_WEBVIEW_TO_EXTENSION =
| ConnectMessage
| CreateNewPlaygroundMessage
@@ -102,4 +108,5 @@ export type MESSAGE_FROM_WEBVIEW_TO_EXTENSION =
export type MESSAGE_FROM_EXTENSION_TO_WEBVIEW =
| ConnectResultsMessage
| FilePickerResultsMessage
- | ConnectionStatusMessage;
+ | ConnectionStatusMessage
+ | ThemeChangedMessage;
diff --git a/src/views/webview-app/legacy/store/store.ts b/src/views/webview-app/legacy/store/store.ts
index 4444a52e8..738af1233 100644
--- a/src/views/webview-app/legacy/store/store.ts
+++ b/src/views/webview-app/legacy/store/store.ts
@@ -12,7 +12,7 @@ import {
MESSAGE_TYPES,
} from '../../extension-app-message-constants';
import { CONNECTION_FORM_TABS } from './constants';
-import { vscode } from '../../vscode-api';
+import vscode from '../../vscode-api';
export interface AppState {
activeConnectionName: string;
diff --git a/src/views/webview-app/overview-header.tsx b/src/views/webview-app/overview-header.tsx
new file mode 100644
index 000000000..a56615140
--- /dev/null
+++ b/src/views/webview-app/overview-header.tsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import {
+ Body,
+ css,
+ focusRing,
+ Icon,
+ MongoDBLogo,
+ spacing,
+} from '@mongodb-js/compass-components';
+
+const headerContainerStyles = css({
+ width: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ gap: spacing[2],
+ position: 'relative',
+});
+
+const headerTextStyles = css({
+ textAlign: 'center',
+ maxWidth: '60%',
+});
+
+const resourcesBtnContainer = css({
+ position: 'absolute',
+ top: spacing[3],
+ right: spacing[3],
+});
+
+const resourcesBtnStyles = css(
+ {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ background: 'none',
+ border: 'none',
+ color: 'inherit',
+ '&:hover': {
+ cursor: 'pointer',
+ },
+ },
+ focusRing
+);
+
+const OverviewHeader: React.FC<{ onResourcesClick: () => void }> = ({
+ onResourcesClick,
+}) => {
+ return (
+
+
+
+ Navigate your databases and collections, use playgrounds for exploring
+ and transforming your data
+
+
+
+
+
+ );
+};
+
+export default OverviewHeader;
diff --git a/src/views/webview-app/overview-page.tsx b/src/views/webview-app/overview-page.tsx
new file mode 100644
index 000000000..8ebe9cb56
--- /dev/null
+++ b/src/views/webview-app/overview-page.tsx
@@ -0,0 +1,46 @@
+import React, { useCallback, useState } from 'react';
+import { HorizontalRule, css, spacing } from '@mongodb-js/compass-components';
+import OverviewHeader from './overview-header';
+import ConnectionStatus from './connection-status';
+import ConnectHelper from './connect-helper';
+import AtlasCta from './atlas-cta';
+import ResourcesPanel from './resources-panel/panel';
+
+const pageStyles = css({
+ width: '90%',
+ minWidth: '500px',
+ margin: '0 auto',
+ display: 'flex',
+ flexDirection: 'column',
+ padding: spacing[3],
+ gap: spacing[4],
+ alignItems: 'center',
+ textAlign: 'center',
+ fontSize: '14px',
+});
+
+const OverviewPage: React.FC = () => {
+ const [showResourcesPanel, setShowResourcesPanel] = useState(false);
+ const handleResourcesPanelClose = useCallback(
+ () => setShowResourcesPanel(false),
+ []
+ );
+ const handleResourcesClick = useCallback(
+ () => setShowResourcesPanel(true),
+ []
+ );
+ return (
+
+ {showResourcesPanel && (
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default OverviewPage;
diff --git a/src/views/webview-app/resources-panel/footer.tsx b/src/views/webview-app/resources-panel/footer.tsx
new file mode 100644
index 000000000..e61301e96
--- /dev/null
+++ b/src/views/webview-app/resources-panel/footer.tsx
@@ -0,0 +1,130 @@
+import React from 'react';
+import { css, cx, palette, useDarkMode } from '@mongodb-js/compass-components';
+import LINKS from '../../../utils/links';
+import { trackExtensionLinkClicked } from '../vscode-api';
+import { TELEMETRY_SCREEN_ID } from './panel';
+
+const FooterFeatures = [
+ {
+ title: 'Navigate databases',
+ linkId: 'navigateDatabaseInfo',
+ url: LINKS.extensionDocs('databases-collections'),
+ },
+ {
+ title: 'Perform CRUD operations',
+ linkId: 'crudInfo',
+ url: LINKS.extensionDocs('crud-ops'),
+ },
+ {
+ title: 'Run aggregation pipelines',
+ linkId: 'aggPipelineInfo',
+ url: LINKS.extensionDocs('run-agg-pipelines'),
+ },
+ {
+ title: 'Playgrounds',
+ linkId: 'playgroundsInfo',
+ url: LINKS.extensionDocs('playgrounds'),
+ },
+] as const;
+
+const FooterLinks = [
+ {
+ title: 'Github',
+ linkId: 'github',
+ url: LINKS.github,
+ },
+ {
+ title: 'Suggest a feature',
+ linkId: 'feedback',
+ url: LINKS.feedback,
+ },
+ {
+ title: 'Report a bug',
+ linkId: 'reportABug',
+ url: LINKS.reportBug,
+ },
+] as const;
+
+const footerStyles = css({
+ marginTop: '40px',
+ display: 'flex',
+ flexDirection: 'row',
+ textAlign: 'left',
+});
+
+const footerItemStyles = css({
+ width: '50%',
+ display: 'inline-block',
+ '&:last-child': {
+ paddingLeft: '10px',
+ },
+});
+
+const footerItemTitleStyles = css({
+ margin: '10px 0px',
+ fontWeight: 'bold',
+});
+
+const footerItemLinkStyles = css({
+ margin: '5px 0px',
+ textDecoration: 'none',
+ display: 'block',
+ color: 'var(--vscode-editor-foreground)',
+ '&:hover': {
+ color: palette.green.base,
+ },
+});
+
+const footerItemLinkLightModeStyles = css({
+ '&:hover': {
+ color: palette.green.dark2,
+ },
+});
+
+const ResourcesPanelFooter: React.FC = () => {
+ const isDarkMode = useDarkMode();
+ const itemLinkStyles = cx(footerItemLinkStyles, {
+ [footerItemLinkLightModeStyles]: !isDarkMode,
+ });
+ return (
+
+ );
+};
+
+export default ResourcesPanelFooter;
diff --git a/src/views/webview-app/resources-panel/header.tsx b/src/views/webview-app/resources-panel/header.tsx
new file mode 100644
index 000000000..6f9220a8b
--- /dev/null
+++ b/src/views/webview-app/resources-panel/header.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import {
+ css,
+ spacing,
+ palette,
+ IconButton,
+ Body,
+ Icon,
+ useDarkMode,
+ cx,
+} from '@mongodb-js/compass-components';
+
+const headerTextStyles = css({
+ fontSize: spacing[4],
+ color: palette.green.base,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ gap: spacing[2],
+});
+
+const headerTextLightModeStyles = css({
+ color: palette.green.dark2,
+});
+
+const ResourcesPanelHeader: React.FC<{ onCloseClick: () => void }> = ({
+ onCloseClick,
+}) => {
+ const isDarkMode = useDarkMode();
+ return (
+ <>
+
+
+
+
+
+
+
+
+ MongoDB resources
+
+
+ >
+ );
+};
+
+export default ResourcesPanelHeader;
diff --git a/src/views/webview-app/resources-panel/links.tsx b/src/views/webview-app/resources-panel/links.tsx
new file mode 100644
index 000000000..927683aad
--- /dev/null
+++ b/src/views/webview-app/resources-panel/links.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import {
+ Icon,
+ css,
+ cx,
+ palette,
+ spacing,
+ useDarkMode,
+} from '@mongodb-js/compass-components';
+import LINKS from '../../../utils/links';
+import { trackExtensionLinkClicked } from '../vscode-api';
+import { TELEMETRY_SCREEN_ID } from './panel';
+
+const ResourceLinks = [
+ {
+ title: 'Product overview',
+ description: 'Get an overview on MongoDB',
+ linkId: 'productOverview',
+ url: LINKS.docs,
+ },
+ {
+ title: 'Extension documentation',
+ description: 'Check the documentation about the extension',
+ linkId: 'extensionDocumentation',
+ url: LINKS.extensionDocs(),
+ },
+ {
+ title: 'Connect to your database',
+ description: 'Connect in just a few steps',
+ linkId: 'connectInfo',
+ url: LINKS.extensionDocs('connect'),
+ },
+ {
+ title: 'Interact with your data',
+ description: 'Play with your data, create queries and aggregations',
+ linkId: 'interactWithYourData',
+ url: LINKS.extensionDocs('playgrounds'),
+ },
+] as const;
+
+const linksContainerStyles = css({
+ marginTop: spacing[6],
+});
+
+const linkStyles = css({
+ marginTop: spacing[2],
+ padding: `10px ${spacing[3]}px`,
+ textAlign: 'left',
+ textDecoration: 'none',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ position: 'relative',
+ backgroundColor: 'var(--vscode-sideBar-background, rgba(50, 50, 50, 0.25))',
+ color: 'var(--vscode-editor-foreground)',
+ '&:hover': {
+ color: palette.green.base,
+ },
+});
+
+const linkLightModeStyles = css({
+ '&:hover': {
+ color: palette.green.dark2,
+ },
+});
+
+const linkBlockStyles = css({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spacing[1],
+});
+
+const ResourcesPanelLinks: React.FC = () => {
+ const isDarkMode = useDarkMode();
+ return (
+
+ );
+};
+
+export default ResourcesPanelLinks;
diff --git a/src/views/webview-app/resources-panel/panel.tsx b/src/views/webview-app/resources-panel/panel.tsx
new file mode 100644
index 000000000..abafed07f
--- /dev/null
+++ b/src/views/webview-app/resources-panel/panel.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import { css, keyframes } from '@mongodb-js/compass-components';
+import ResourcesPanelHeader from './header';
+import ResourcesPanelLinks from './links';
+import ResourcesPanelFooter from './footer';
+
+const panelActualWidth = 428;
+const panelWidthWithMargin = 468;
+
+const resourcesPanelStyles = css({
+ position: 'fixed',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ zIndex: 10,
+});
+
+const bgDarkenAnimation = keyframes({
+ from: {
+ backgroundColor: 'rgba(0, 0, 0, 0.1)',
+ },
+ to: {
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ },
+});
+
+const resourcesPanelBackgroundStyles = css({
+ position: 'fixed',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ zIndex: -1,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ animationDuration: '250ms',
+ animationDirection: 'forwards',
+ animationName: bgDarkenAnimation,
+});
+
+const panelOpenAnimation = keyframes({
+ from: {
+ left: '100%',
+ },
+ to: {
+ left: `calc(100% - ${panelWidthWithMargin}px)`,
+ },
+});
+
+const resourcePanelContentStyles = css({
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: `calc(100% - ${panelWidthWithMargin}px)`,
+ width: `${panelActualWidth}px`,
+ overflow: 'auto',
+ padding: '24px',
+ paddingBottom: '48px',
+ background: 'var(--vscode-editor-background)',
+ animationDuration: '250ms',
+ animationDirection: 'forwards',
+ animationName: panelOpenAnimation,
+ animationTimingFunction: 'cubic-bezier(0, 1.3, 0.7, 1)',
+ boxShadow: '-4px 0px 5px rgba(0, 0, 0, 0.25)',
+});
+
+const ResourcesPanel: React.FC<{ onClose: () => void }> = ({ onClose }) => {
+ return (
+
+ );
+};
+
+export const TELEMETRY_SCREEN_ID = 'overviewResourcesPanel';
+
+export default ResourcesPanel;
diff --git a/src/views/webview-app/use-connection-status.ts b/src/views/webview-app/use-connection-status.ts
new file mode 100644
index 000000000..920a20a67
--- /dev/null
+++ b/src/views/webview-app/use-connection-status.ts
@@ -0,0 +1,50 @@
+import { useState, useEffect } from 'react';
+import {
+ CONNECTION_STATUS,
+ type MESSAGE_FROM_EXTENSION_TO_WEBVIEW,
+ MESSAGE_TYPES,
+} from './extension-app-message-constants';
+import vscode from './vscode-api';
+
+const CONNECTION_STATUS_POLLING_FREQ_MS = 1000;
+
+const useConnectionStatus = () => {
+ const [connectionStatus, setConnectionStatus] = useState(
+ CONNECTION_STATUS.LOADING
+ );
+ const [connectionName, setConnectionName] = useState('');
+ useEffect(() => {
+ const handleConnectionStatusResponse = (event) => {
+ const message: MESSAGE_FROM_EXTENSION_TO_WEBVIEW = event.data;
+ if (message.command === MESSAGE_TYPES.CONNECTION_STATUS_MESSAGE) {
+ setConnectionStatus(message.connectionStatus);
+ setConnectionName(message.activeConnectionName);
+ }
+ // TODO(VSCODE-489): Also listen on the new connected event whenever that is
+ // implemented
+ };
+ window.addEventListener('message', handleConnectionStatusResponse);
+
+ const requestConnectionStatus = () =>
+ vscode.postMessage({
+ command: MESSAGE_TYPES.GET_CONNECTION_STATUS,
+ });
+
+ requestConnectionStatus();
+ const pollingInterval = setInterval(
+ requestConnectionStatus,
+ CONNECTION_STATUS_POLLING_FREQ_MS
+ );
+ return () => {
+ window.removeEventListener('message', handleConnectionStatusResponse);
+ clearInterval(pollingInterval);
+ };
+ }, []);
+
+ return {
+ connectionStatus,
+ connectionName,
+ };
+};
+
+export default useConnectionStatus;
diff --git a/src/views/webview-app/use-detect-vscode-dark-mode.tsx b/src/views/webview-app/use-detect-vscode-dark-mode.tsx
new file mode 100644
index 000000000..1e62eedfc
--- /dev/null
+++ b/src/views/webview-app/use-detect-vscode-dark-mode.tsx
@@ -0,0 +1,24 @@
+import { useEffect, useState } from 'react';
+import {
+ type MESSAGE_FROM_EXTENSION_TO_WEBVIEW,
+ MESSAGE_TYPES,
+} from './extension-app-message-constants';
+
+export const useDetectVsCodeDarkMode = () => {
+ const [darkModeDetected, setDarkModeDetected] = useState(
+ globalThis.document.body.classList.contains('vscode-dark') ||
+ globalThis.document.body.classList.contains('vscode-high-contrast')
+ );
+ useEffect(() => {
+ const onThemeChanged = (event) => {
+ const message: MESSAGE_FROM_EXTENSION_TO_WEBVIEW = event.data;
+ if (message.command === MESSAGE_TYPES.THEME_CHANGED) {
+ setDarkModeDetected(message.darkMode);
+ }
+ };
+ window.addEventListener('message', onThemeChanged);
+ return () => window.removeEventListener('message', onThemeChanged);
+ }, []);
+
+ return darkModeDetected;
+};
diff --git a/src/views/webview-app/vscode-api.ts b/src/views/webview-app/vscode-api.ts
index 79b7d69d9..cf664f4aa 100644
--- a/src/views/webview-app/vscode-api.ts
+++ b/src/views/webview-app/vscode-api.ts
@@ -1,8 +1,46 @@
-import type { MESSAGE_FROM_WEBVIEW_TO_EXTENSION } from './extension-app-message-constants';
+import {
+ MESSAGE_TYPES,
+ type MESSAGE_FROM_WEBVIEW_TO_EXTENSION,
+} from './extension-app-message-constants';
interface VSCodeApi {
postMessage: (message: MESSAGE_FROM_WEBVIEW_TO_EXTENSION) => void;
}
declare const acquireVsCodeApi: () => VSCodeApi;
-export const vscode = acquireVsCodeApi();
+const vscode = acquireVsCodeApi();
+
+export const renameActiveConnection = () => {
+ vscode.postMessage({
+ command: MESSAGE_TYPES.RENAME_ACTIVE_CONNECTION,
+ });
+};
+
+export const createNewPlayground = () => {
+ vscode.postMessage({
+ command: MESSAGE_TYPES.CREATE_NEW_PLAYGROUND,
+ });
+};
+
+export const connectWithConnectionString = () => {
+ vscode.postMessage({
+ command: MESSAGE_TYPES.OPEN_CONNECTION_STRING_INPUT,
+ });
+};
+
+export const trackExtensionLinkClicked = (screen: string, linkId: string) => {
+ vscode.postMessage({
+ command: MESSAGE_TYPES.EXTENSION_LINK_CLICKED,
+ screen,
+ linkId,
+ });
+};
+
+export const openTrustedLink = (linkTo: string) => {
+ vscode.postMessage({
+ command: MESSAGE_TYPES.OPEN_TRUSTED_LINK,
+ linkTo,
+ });
+};
+
+export default vscode;
diff --git a/src/views/webviewController.ts b/src/views/webviewController.ts
index c821dff3c..7e89d12a1 100644
--- a/src/views/webviewController.ts
+++ b/src/views/webviewController.ts
@@ -85,6 +85,8 @@ export default class WebviewController {
_connectionController: ConnectionController;
_storageController: StorageController;
_telemetryService: TelemetryService;
+ _activeWebviewPanels: vscode.WebviewPanel[] = [];
+ _themeChangedSubscription: vscode.Disposable;
constructor({
connectionController,
@@ -98,6 +100,13 @@ export default class WebviewController {
this._connectionController = connectionController;
this._storageController = storageController;
this._telemetryService = telemetryService;
+ this._themeChangedSubscription = vscode.window.onDidChangeActiveColorTheme(
+ this.onThemeChanged
+ );
+ }
+
+ deactivate(): void {
+ this._themeChangedSubscription?.dispose();
}
handleWebviewConnectAttempt = async (
@@ -232,6 +241,31 @@ export default class WebviewController {
}
};
+ onWebviewPanelClosed = (disposedPanel: vscode.WebviewPanel) => {
+ this._activeWebviewPanels = this._activeWebviewPanels.filter(
+ (panel) => panel !== disposedPanel
+ );
+ };
+
+ onThemeChanged = (theme: vscode.ColorTheme) => {
+ const darkModeDetected =
+ theme.kind === vscode.ColorThemeKind.Dark ||
+ theme.kind === vscode.ColorThemeKind.HighContrast;
+ for (const panel of this._activeWebviewPanels) {
+ void panel.webview
+ .postMessage({
+ command: MESSAGE_TYPES.THEME_CHANGED,
+ darkMode: darkModeDetected,
+ })
+ .then(undefined, (error) => {
+ log.warn(
+ 'Could not post THEME_CHANGED to webview, most like already disposed',
+ error
+ );
+ });
+ }
+ };
+
openWebview(context: vscode.ExtensionContext): Promise {
log.info('Opening webview...');
const extensionPath = context.extensionPath;
@@ -251,6 +285,9 @@ export default class WebviewController {
}
);
+ panel.onDidDispose(() => this.onWebviewPanelClosed(panel));
+ this._activeWebviewPanels.push(panel);
+
panel.iconPath = vscode.Uri.file(
path.join(
extensionPath,
diff --git a/webpack.config.js b/webpack.config.js
index 5d0f45fb7..23bfb5c9c 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -5,6 +5,7 @@ const webpack = require('webpack');
const autoprefixer = require('autoprefixer');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const { merge } = require('webpack-merge');
+const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const { WebpackDependenciesPlugin } = require('@mongodb-js/sbom-tools');
@@ -154,6 +155,8 @@ module.exports = (env, argv) => {
fallback: {
stream: require.resolve('stream-browserify'),
buffer: require.resolve('buffer'),
+ crypto: require.resolve('crypto-browserify'),
+ path: require.resolve('path-browserify'),
},
},
module: {
@@ -195,6 +198,10 @@ module.exports = (env, argv) => {
],
},
plugins: [
+ // This plugin has been added to avoid Out of memory crashes of webpack on
+ // our Github runners. It does so by moving the type checking to a
+ // separate process.
+ new ForkTsCheckerWebpackPlugin(),
// This is here to deal with some node.js code brought in
// by @leafygreen/logo via @emotion/server:
new webpack.ProvidePlugin({