diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx index f08e334c9b..3eb433e969 100644 --- a/packages/code-studio/src/main/AppMainContainer.tsx +++ b/packages/code-studio/src/main/AppMainContainer.tsx @@ -123,7 +123,7 @@ interface AppMainContainerProps { match: { params: { notebookPath: string }; }; - connection: IdeConnection; + connection?: IdeConnection; session?: IdeSession; sessionConfig?: SessionConfig; setActiveTool: (tool: string) => void; @@ -255,6 +255,10 @@ export class AppMainContainer extends Component< initWidgets(): void { const { connection } = this.props; + if (connection == null) { + return; + } + if (connection.subscribeToFieldUpdates == null) { log.warn( 'subscribeToFieldUpdates not supported, not initializing widgets' @@ -614,6 +618,10 @@ export class AppMainContainer extends Component< startListeningForDisconnect(): void { const { connection } = this.props; + if (connection == null) { + return; + } + connection.addEventListener( dh.IdeConnection.EVENT_DISCONNECT, this.handleDisconnect @@ -630,6 +638,10 @@ export class AppMainContainer extends Component< stopListeningForDisconnect(): void { const { connection } = this.props; + if (connection == null) { + return; + } + connection.removeEventListener( dh.IdeConnection.EVENT_DISCONNECT, this.handleDisconnect @@ -650,6 +662,7 @@ export class AppMainContainer extends Component< ): DehydratedDashboardPanelProps & { fetch?: () => Promise } { const { connection } = this.props; const { metadata } = props; + if ( metadata?.type != null && (metadata?.id != null || metadata?.name != null) @@ -666,12 +679,14 @@ export class AppMainContainer extends Component< name: metadata.name, title: metadata.name, }; + return { - fetch: () => connection.getObject(widget), + fetch: async () => connection?.getObject(widget), ...props, localDashboardId: id, }; } + return DashboardUtils.hydrate(props, id); } @@ -684,7 +699,7 @@ export class AppMainContainer extends Component< const { connection } = this.props; this.emitLayoutEvent(PanelEvent.OPEN, { dragEvent, - fetch: () => connection.getObject(widget), + fetch: async () => connection?.getObject(widget), widget, }); } diff --git a/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx b/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx index 866d041e38..66159d6e3f 100644 --- a/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx +++ b/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx @@ -1,12 +1,16 @@ import React from 'react'; import { render } from '@testing-library/react'; import { CommandHistoryStorage } from '@deephaven/console'; -import type { Container } from '@deephaven/golden-layout'; +import type { Container, EventEmitter } from '@deephaven/golden-layout'; import type { IdeConnection, IdeSession } from '@deephaven/jsapi-types'; +import { dh } from '@deephaven/jsapi-shim'; import { SessionConfig, SessionWrapper } from '@deephaven/jsapi-utils'; +import { TestUtils } from '@deephaven/utils'; import { ConsolePanel } from './ConsolePanel'; -const mockConsole = jest.fn(() => null); +type IdeSessionConstructor = new (language: string) => IdeSession; + +const mockConsole = jest.fn((_props: unknown) => null); jest.mock('@deephaven/console', () => ({ ...(jest.requireActual('@deephaven/console') as Record), Console: props => mockConsole(props), @@ -14,7 +18,7 @@ jest.mock('@deephaven/console', () => ({ })); function makeSession(language = 'TEST_LANG'): IdeSession { - return new dh.IdeSession(language) as unknown as IdeSession; + return new (dh.IdeSession as unknown as IdeSessionConstructor)(language); } function makeConnection({ @@ -42,31 +46,15 @@ function makeSessionWrapper({ return { session, connection, config, dh }; } -function makeEventHub() { - return { - emit: jest.fn(), - on: jest.fn(), - off: jest.fn(), - trigger: jest.fn(), - unbind: jest.fn(), - }; -} - -function makeGlContainer(): Container { - return { - emit: jest.fn(), - on: jest.fn(), - off: jest.fn(), - } as unknown as Container; -} - function makeCommandHistoryStorage(): CommandHistoryStorage { return {} as CommandHistoryStorage; } function renderConsolePanel({ - eventHub = makeEventHub(), - container = makeGlContainer(), + eventHub = TestUtils.createMockProxy(), + container = TestUtils.createMockProxy({ + tab: undefined, + }), commandHistoryStorage = makeCommandHistoryStorage(), timeZone = 'MockTimeZone', sessionWrapper = makeSessionWrapper(), @@ -78,11 +66,18 @@ function renderConsolePanel({ commandHistoryStorage={commandHistoryStorage} timeZone={timeZone} sessionWrapper={sessionWrapper} + localDashboardId="mock-localDashboardId" + plugins={new Map()} /> ); } beforeEach(() => { + // Mocking the Console component causes it to be treated as a functional + // component which causes React to log an error about passing refs. Disable + // logging to supress this + TestUtils.disableConsoleOutput('error'); + mockConsole.mockClear(); }); diff --git a/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx b/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx index c8d9becfb5..d1f2257060 100644 --- a/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx @@ -63,7 +63,7 @@ interface ConsolePanelProps extends DashboardPanelProps { panelState?: PanelState; - sessionWrapper: SessionWrapper; + sessionWrapper?: SessionWrapper; timeZone: string; unzip?: (file: File) => Promise; @@ -159,6 +159,10 @@ export class ConsolePanel extends PureComponent< subscribeToFieldUpdates(): void { const { sessionWrapper } = this.props; + if (sessionWrapper == null) { + return; + } + const { session } = sessionWrapper; this.objectSubscriptionCleanup = session.subscribeToFieldUpdates( @@ -244,6 +248,9 @@ export class ConsolePanel extends PureComponent< handleOpenObject(object: VariableDefinition, forceOpen = true): void { const { sessionWrapper } = this.props; + if (sessionWrapper == null) { + return; + } const { session } = sessionWrapper; const { root } = this.context; const oldPanelId = @@ -359,7 +366,7 @@ export class ConsolePanel extends PureComponent< return ; } - render(): ReactElement { + render(): ReactElement | null { const { commandHistoryStorage, glContainer, @@ -368,6 +375,11 @@ export class ConsolePanel extends PureComponent< timeZone, unzip, } = this.props; + + if (sessionWrapper == null) { + return null; + } + const { consoleSettings, error, objectMap } = this.state; const { config, session, connection, details = {}, dh } = sessionWrapper; const { workerName, processInfoId } = details; diff --git a/packages/dashboard-core-plugins/src/panels/FileExplorerPanel.tsx b/packages/dashboard-core-plugins/src/panels/FileExplorerPanel.tsx index 1b07cd85be..e1c46d555e 100644 --- a/packages/dashboard-core-plugins/src/panels/FileExplorerPanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/FileExplorerPanel.tsx @@ -21,7 +21,7 @@ const log = Log.module('FileExplorerPanel'); type StateProps = { fileStorage: FileStorage; - language: string; + language?: string; session?: IdeSession; }; diff --git a/packages/dashboard-core-plugins/src/panels/LogPanel.tsx b/packages/dashboard-core-plugins/src/panels/LogPanel.tsx index 1ad84167d9..4e1ce3e66c 100644 --- a/packages/dashboard-core-plugins/src/panels/LogPanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/LogPanel.tsx @@ -14,11 +14,11 @@ import { getDashboardSessionWrapper } from '../redux'; const log = Log.module('LogPanel'); interface LogPanelProps extends DashboardPanelProps { - session: IdeSession; + session?: IdeSession; } interface LogPanelState { - session: IdeSession; + session?: IdeSession; } class LogPanel extends PureComponent { diff --git a/packages/dashboard-core-plugins/src/panels/NotebookPanel.tsx b/packages/dashboard-core-plugins/src/panels/NotebookPanel.tsx index 3f5e306317..05cf51859f 100644 --- a/packages/dashboard-core-plugins/src/panels/NotebookPanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/NotebookPanel.tsx @@ -64,7 +64,7 @@ interface Metadata extends PanelMetadata { id: string; } interface NotebookSetting { - isMinimapEnabled: boolean; + isMinimapEnabled?: boolean; } interface FileMetadata { @@ -78,16 +78,21 @@ interface PanelState { fileMetadata: FileMetadata | null; } -interface NotebookPanelProps extends DashboardPanelProps { +interface NotebookPanelMappedProps { + defaultNotebookSettings: NotebookSetting; fileStorage: FileStorage; + session?: IdeSession; + sessionLanguage?: string; +} + +interface NotebookPanelProps + extends DashboardPanelProps, + NotebookPanelMappedProps { isDashboardActive: boolean; isPreview: boolean; metadata: Metadata; - session: IdeSession; - sessionLanguage: string; panelState: PanelState; notebooksUrl: string; - defaultNotebookSettings: NotebookSetting; updateSettings: (settings: Partial) => void; } @@ -790,7 +795,7 @@ class NotebookPanel extends Component { const { defaultNotebookSettings, updateSettings } = this.props; const newSettings = { defaultNotebookSettings: { - isMinimapEnabled: !defaultNotebookSettings.isMinimapEnabled, + isMinimapEnabled: !(defaultNotebookSettings.isMinimapEnabled ?? false), }, }; updateSettings(newSettings); @@ -1176,10 +1181,10 @@ class NotebookPanel extends Component { const { defaultNotebookSettings } = this.props; const { settings: initialSettings } = this.state; return this.getOverflowActions( - defaultNotebookSettings.isMinimapEnabled, + defaultNotebookSettings.isMinimapEnabled ?? false, this.getSettings( initialSettings, - defaultNotebookSettings.isMinimapEnabled + defaultNotebookSettings.isMinimapEnabled ?? false ).wordWrap === 'on' ); } @@ -1216,7 +1221,7 @@ class NotebookPanel extends Component { const isExistingItem = fileMetadata?.id != null; const settings = this.getSettings( initialSettings, - defaultNotebookSettings.isMinimapEnabled + defaultNotebookSettings.isMinimapEnabled ?? false ); const isSessionConnected = session != null; const isLanguageMatching = sessionLanguage === settings.language; @@ -1428,10 +1433,7 @@ class NotebookPanel extends Component { const mapStateToProps = ( state: RootState, ownProps: { localDashboardId: string } -): Pick< - NotebookPanelProps, - 'defaultNotebookSettings' | 'fileStorage' | 'session' | 'sessionLanguage' -> => { +): NotebookPanelMappedProps => { const fileStorage = getFileStorage(state); const defaultNotebookSettings = getDefaultNotebookSettings(state); const sessionWrapper = getDashboardSessionWrapper( @@ -1443,7 +1445,7 @@ const mapStateToProps = ( const { type: sessionLanguage } = sessionConfig ?? {}; return { fileStorage, - defaultNotebookSettings: defaultNotebookSettings as NotebookSetting, + defaultNotebookSettings, session, sessionLanguage, }; diff --git a/packages/dashboard-core-plugins/src/redux/selectors.ts b/packages/dashboard-core-plugins/src/redux/selectors.ts index 12806932e0..597b16b311 100644 --- a/packages/dashboard-core-plugins/src/redux/selectors.ts +++ b/packages/dashboard-core-plugins/src/redux/selectors.ts @@ -7,6 +7,8 @@ import { Link } from '../linker/LinkerUtils'; import { FilterSet } from '../panels'; import { ColumnSelectionValidator } from '../linker/ColumnSelectionValidator'; +const EMPTY_OBJECT = Object.freeze({}); + const EMPTY_MAP = new Map(); const EMPTY_ARRAY = Object.freeze([]); @@ -107,10 +109,8 @@ export const getDashboardConsoleSettings = ( store: RootState, dashboardId: string ): Record => - getDashboardData(store, dashboardId).consoleSettings as Record< - string, - unknown - >; + (getDashboardData(store, dashboardId).consoleSettings ?? + EMPTY_OBJECT) as Record; /** * @@ -121,8 +121,8 @@ export const getDashboardConsoleSettings = ( export const getDashboardConnection = ( store: RootState, dashboardId: string -): IdeConnection => - getDashboardData(store, dashboardId).connection as IdeConnection; +): IdeConnection | undefined => + getDashboardData(store, dashboardId).connection as IdeConnection | undefined; /** * @@ -133,5 +133,7 @@ export const getDashboardConnection = ( export const getDashboardSessionWrapper = ( store: RootState, dashboardId: string -): SessionWrapper => - getDashboardData(store, dashboardId).sessionWrapper as SessionWrapper; +): SessionWrapper | undefined => + getDashboardData(store, dashboardId).sessionWrapper as + | SessionWrapper + | undefined; diff --git a/packages/react-hooks/src/useAsyncInterval.ts b/packages/react-hooks/src/useAsyncInterval.ts index c5f27156ee..84a772dd1d 100644 --- a/packages/react-hooks/src/useAsyncInterval.ts +++ b/packages/react-hooks/src/useAsyncInterval.ts @@ -1,9 +1,6 @@ import { useCallback, useEffect, useRef } from 'react'; -import Log from '@deephaven/log'; import { useIsMountedRef } from './useIsMountedRef'; -const log = Log.module('useAsyncInterval'); - /** * Calls the given async callback at a target interval. * @@ -41,12 +38,6 @@ export function useAsyncInterval( ? targetIntervalMs : now - trackingStartedRef.current; - log.debug( - `tick #${trackingCountRef.current}.`, - elapsedSinceLastTick, - 'ms elapsed since last tick.' - ); - trackingStartedRef.current = now; await callback(); @@ -62,21 +53,10 @@ export function useAsyncInterval( const nextTickInterval = Math.max(0, targetIntervalMs - overage); - log.debug( - 'Next tick target:', - targetIntervalMs, - ', overage', - overage, - ', adjusted:', - nextTickInterval - ); - setTimeoutRef.current = window.setTimeout(tick, nextTickInterval); }, [callback, isMountedRef, targetIntervalMs]); useEffect(() => { - log.debug('Setting target interval:', targetIntervalMs); - trackingStartedRef.current = null; tick();