From 2d5c012c1ca27e0e2adcd94886f92a89bb00c228 Mon Sep 17 00:00:00 2001 From: williamsyang-work Date: Mon, 22 Jan 2024 17:28:37 +0100 Subject: [PATCH] Add Server Offline Landing to Placeholder Widget Adds a prompt to start the server when the front-end detects that there is no server currently running. This provides a clearer application flow for the user. Signed-off-by: Will Yang --- .../components/trace-context-component.tsx | 32 ++++++---- .../trace-explorer-placeholder-widget.tsx | 26 +++++--- .../react-components/style/trace-viewer.css | 20 ++++++ ...heia-trace-explorer-placeholder-widget.tsx | 33 +++++++++- .../trace-explorer-server-status-widget.tsx | 21 +++++-- .../trace-explorer/trace-explorer-widget.tsx | 29 ++++++--- ...ce-server-connection-status-client-impl.ts | 33 ++++++---- .../trace-viewer/trace-viewer-contribution.ts | 12 ++-- .../src/browser/trace-viewer/trace-viewer.tsx | 62 +++++++++++++------ .../common/trace-server-connection-status.ts | 18 +++++- 10 files changed, 211 insertions(+), 75 deletions(-) diff --git a/packages/react-components/src/components/trace-context-component.tsx b/packages/react-components/src/components/trace-context-component.tsx index 2136e8a96..0d289a319 100644 --- a/packages/react-components/src/components/trace-context-component.tsx +++ b/packages/react-components/src/components/trace-context-component.tsx @@ -48,6 +48,7 @@ export interface TraceContextProps { removeResizeHandler: (handler: () => void) => void; backgroundTheme: string; persistedState?: PersistedState; + serverStatus?: boolean; } export interface TraceContextState { @@ -491,20 +492,29 @@ export class TraceContextComponent extends React.Component 0 && (this.props.outputs.length || this.props.overviewDescriptor); return ( -
this.onContextMenu(event)} - onKeyDown={event => this.onKeyDown(event)} - onKeyUp={event => this.onKeyUp(event)} - ref={this.traceContextContainer} - > - - - {shouldRenderOutputs ? this.renderOutputs() : this.renderPlaceHolder()} -
+ <> + {/* Render the grey-out overlay if the server is down */} + {serverStatus === false && ( +
+
Please reconnect to resume using the application.
+
+ )} +
this.onContextMenu(event)} + onKeyDown={event => this.onKeyDown(event)} + onKeyUp={event => this.onKeyUp(event)} + ref={this.traceContextContainer} + > + + + {shouldRenderOutputs ? this.renderOutputs() : this.renderPlaceHolder()} +
+ ); } diff --git a/packages/react-components/src/trace-explorer/trace-explorer-placeholder-widget.tsx b/packages/react-components/src/trace-explorer/trace-explorer-placeholder-widget.tsx index 44bd1ffd0..2211de0ec 100644 --- a/packages/react-components/src/trace-explorer/trace-explorer-placeholder-widget.tsx +++ b/packages/react-components/src/trace-explorer/trace-explorer-placeholder-widget.tsx @@ -4,28 +4,38 @@ import * as React from 'react'; export interface ReactPlaceholderWidgetProps { loading: boolean; + serverOn: boolean; + tracesOpen: boolean; handleOpenTrace: () => void; + handleStartServer: () => void; } -export class ReactExplorerPlaceholderWidget extends React.Component { +export class ReactExplorerPlaceholderWidget extends React.Component { constructor(props: ReactPlaceholderWidgetProps) { super(props); } render(): React.ReactNode { + const { loading, serverOn, handleOpenTrace, handleStartServer } = this.props; + const onClick = serverOn ? handleOpenTrace : handleStartServer; + const infoText = serverOn + ? 'You have not yet opened a trace.' + : 'No trace server instance is currently running.'; + const buttonText = serverOn ? 'Open Trace' : 'Start Trace Server'; + return (
-
{'You have not yet opened a trace.'}
+
{infoText}
diff --git a/packages/react-components/style/trace-viewer.css b/packages/react-components/style/trace-viewer.css index 00f0b8067..b9d795379 100644 --- a/packages/react-components/style/trace-viewer.css +++ b/packages/react-components/style/trace-viewer.css @@ -13,4 +13,24 @@ div { .trace-viewer-container { margin: 0px 5px 0px 5px; height: 100%; +} + +/* Grey out container */ +.overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */ + display: flex; + justify-content: center; + align-items: center; + z-index: 999; /* Ensure the overlay appears on top */ +} + +.warning-text { + color: white; /* Color of the warning text */ + text-align: center; + font-size: 24px; } \ No newline at end of file diff --git a/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/theia-trace-explorer-placeholder-widget.tsx b/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/theia-trace-explorer-placeholder-widget.tsx index 9ef03f1bb..1a0a23f18 100644 --- a/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/theia-trace-explorer-placeholder-widget.tsx +++ b/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/theia-trace-explorer-placeholder-widget.tsx @@ -2,8 +2,9 @@ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify' import { ReactWidget } from '@theia/core/lib/browser'; import * as React from 'react'; import { CommandService } from '@theia/core'; -import { OpenTraceCommand } from '../../trace-viewer/trace-viewer-commands'; +import { OpenTraceCommand, StartServerCommand } from '../../trace-viewer/trace-viewer-commands'; import { ReactExplorerPlaceholderWidget } from 'traceviewer-react-components/lib/trace-explorer/trace-explorer-placeholder-widget'; +import { TraceServerConnectionStatusClient } from '../../../common/trace-server-connection-status'; @injectable() export class TraceExplorerPlaceholderWidget extends ReactWidget { @@ -11,10 +12,14 @@ export class TraceExplorerPlaceholderWidget extends ReactWidget { static LABEL = 'Trace Explorer Placeholder Widget'; state = { - loading: false + loading: false, + serverStatus: false, + tracesOpened: false }; @inject(CommandService) protected readonly commandService!: CommandService; + @inject(TraceServerConnectionStatusClient) + protected traceServerConnectionStatusProxy: TraceServerConnectionStatusClient; @postConstruct() protected init(): void { @@ -23,10 +28,17 @@ export class TraceExplorerPlaceholderWidget extends ReactWidget { this.update(); } + dispose(): void { + super.dispose(); + } + render(): React.ReactNode { - const { loading } = this.state; + const { loading, serverStatus, tracesOpened } = this.state; return ( @@ -42,4 +54,19 @@ export class TraceExplorerPlaceholderWidget extends ReactWidget { this.state.loading = false; this.update(); } + + protected handleStartServer = async (): Promise => this.doHandleStartServer(); + + private async doHandleStartServer() { + this.state.loading = true; + this.update(); + await this.commandService.executeCommand(StartServerCommand.id); + this.state.loading = false; + this.update(); + } + + public setStateAndShow(newState: { serverStatus: boolean; tracesOpened: boolean }): void { + this.state = { ...newState, ...this.state }; + this.show(); + } } diff --git a/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/trace-explorer-server-status-widget.tsx b/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/trace-explorer-server-status-widget.tsx index 4c83746d6..058602533 100644 --- a/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/trace-explorer-server-status-widget.tsx +++ b/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-sub-widgets/trace-explorer-server-status-widget.tsx @@ -7,6 +7,7 @@ import { CommandService } from '@theia/core'; export class TraceExplorerServerStatusWidget extends ReactWidget { static ID = 'trace-explorer-server-status-widget'; static LABEL = 'Trace Explorer Server Status Widget'; + private serverOn = false; @inject(CommandService) protected readonly commandService!: CommandService; @@ -17,16 +18,24 @@ export class TraceExplorerServerStatusWidget extends ReactWidget { this.update(); } + public updateStatus = (status: boolean): void => { + this.serverOn = status; + this.update(); + }; + render(): React.ReactNode { + const className = this.serverOn ? 'fa fa-check-circle-o fa-lg' : 'fa fa-times-circle-o fa-lg'; + + const title = this.serverOn + ? 'Server health and latency are good. No known issues' + : 'Trace Viewer Critical Error: Trace Server Offline'; + + const color = this.serverOn ? 'green' : 'red'; + return (
Server Status - +
); } diff --git a/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-widget.tsx b/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-widget.tsx index 91e06f7b9..da765d6ef 100644 --- a/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-widget.tsx +++ b/theia-extensions/viewer-prototype/src/browser/trace-explorer/trace-explorer-widget.tsx @@ -83,12 +83,16 @@ export class TraceExplorerWidget extends BaseWidget { layout.addWidget(this.traceViewsContainer); this.node.tabIndex = 0; signalManager().on(Signals.OPENED_TRACES_UPDATED, this.onUpdateSignal); + // TODO - Should we be using the backend, since we're on the backend? + this.connectionStatusClient.addServerStatusChangeListener(this.onServerStatusChange); this.update(); } dispose(): void { super.dispose(); signalManager().off(Signals.OPENED_TRACES_UPDATED, this.onUpdateSignal); + // TODO - Should we be using the backend, since we're on the backend? + this.connectionStatusClient.removeServerStatusChangeListener(this.onServerStatusChange); } protected onUpdateSignal = (payload: OpenedTracesUpdatedSignalPayload): void => @@ -100,12 +104,17 @@ export class TraceExplorerWidget extends BaseWidget { protected onUpdateRequest(msg: Message): void { super.onUpdateRequest(msg); - if (this._numberOfOpenedTraces > 0) { - this.traceViewsContainer.show(); - this.placeholderWidget.hide(); - } else { + + const serverStatus = this.connectionStatusClient.status; + const tracesOpened = this._numberOfOpenedTraces > 0; + const shouldShowPlaceholder = serverStatus === false || tracesOpened === false; + + if (shouldShowPlaceholder) { + this.placeholderWidget.setStateAndShow({ serverStatus, tracesOpened }); this.traceViewsContainer.hide(); - this.placeholderWidget.show(); + } else { + this.placeholderWidget.hide(); + this.traceViewsContainer.show(); } } @@ -115,12 +124,18 @@ export class TraceExplorerWidget extends BaseWidget { } protected async onAfterShow(): Promise { - this.connectionStatusClient.addConnectionStatusListener(); + this.connectionStatusClient.activate(); const status = await this.traceServerConnectionStatusProxy.getStatus(); this.connectionStatusClient.updateStatus(status); } protected onAfterHide(): void { - this.connectionStatusClient.removeConnectionStatusListener(); + this.connectionStatusClient.deactivate(); + } + + protected onServerStatusChange = (status: boolean): void => this.doHandleOnServerStatusChange(status); + + protected doHandleOnServerStatusChange(status: boolean): void { + this.serverStatusWidget.updateStatus(status); } } diff --git a/theia-extensions/viewer-prototype/src/browser/trace-server-connection-status-client-impl.ts b/theia-extensions/viewer-prototype/src/browser/trace-server-connection-status-client-impl.ts index 8ecfd297c..be2184e60 100644 --- a/theia-extensions/viewer-prototype/src/browser/trace-server-connection-status-client-impl.ts +++ b/theia-extensions/viewer-prototype/src/browser/trace-server-connection-status-client-impl.ts @@ -2,33 +2,40 @@ import { injectable } from 'inversify'; import { TraceServerConnectionStatusClient } from '../common/trace-server-connection-status'; +type Listener = (serverStatus: boolean) => void; @injectable() export class TraceServerConnectionStatusClientImpl implements TraceServerConnectionStatusClient { protected active = false; + protected _status = false; + protected listeners: Listener[] = []; - updateStatus(status: boolean): void { + public updateStatus(status: boolean): void { + this._status = status; if (this.active) { - TraceServerConnectionStatusClientImpl.renderStatus(status); + this.listeners.forEach(fn => fn(status)); } } - addConnectionStatusListener(): void { + public activate(): void { this.active = true; } - removeConnectionStatusListener(): void { + public deactivate(): void { this.active = false; } - static renderStatus(status: boolean): void { - if (document.getElementById('server-status-id')) { - document.getElementById('server-status-id')!.className = status - ? 'fa fa-check-circle-o fa-lg' - : 'fa fa-times-circle-o fa-lg'; - document.getElementById('server-status-id')!.title = status - ? 'Server health and latency are good. No known issues' - : 'Trace Viewer Critical Error: Trace Server Offline'; - document.getElementById('server-status-id')!.style.color = status ? 'green' : 'red'; + public addServerStatusChangeListener(fn: Listener): void { + this.listeners.push(fn); + } + + public removeServerStatusChangeListener(fn: Listener): void { + const index = this.listeners.indexOf(fn); + if (index) { + this.listeners.splice(index, 1); } } + + get status(): boolean { + return this._status; + } } diff --git a/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-contribution.ts b/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-contribution.ts index 96d4d420a..d4971ff74 100644 --- a/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-contribution.ts +++ b/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer-contribution.ts @@ -24,9 +24,9 @@ import { TracePreferences, TRACE_PATH, TRACE_ARGS } from '../trace-server-prefer import { TspClientProvider } from '../tsp-client-provider-impl'; import { ChartShortcutsDialog } from '../trace-explorer/trace-explorer-sub-widgets/charts-cheatsheet-component'; import { signalManager } from 'traceviewer-base/lib/signals/signal-manager'; -import { TraceServerConnectionStatusClientImpl } from '../trace-server-connection-status-client-impl'; import { FileStat } from '@theia/filesystem/lib/common/files'; import { ITspClient } from 'tsp-typescript-client'; +import { TraceServerConnectionStatusClient } from '../../common/trace-server-connection-status'; interface TraceViewerWidgetOpenerOptions extends WidgetOpenerOptions { traceUUID: string; @@ -50,6 +50,8 @@ export class TraceViewerContribution @inject(TracePreferences) protected tracePreferences: TracePreferences; @inject(TraceServerConfigService) protected readonly traceServerConfigService: TraceServerConfigService; @inject(MessageService) protected readonly messageService: MessageService; + @inject(TraceServerConnectionStatusClient) + protected readonly serverStatusService: TraceServerConnectionStatusClient; readonly id = TraceViewerWidget.ID; readonly label = 'Trace Viewer'; @@ -94,7 +96,7 @@ export class TraceViewerContribution progress.report({ message: 'Trace server started.', work: { done: 100, total: 100 } }); } progress.cancel(); - TraceServerConnectionStatusClientImpl.renderStatus(true); + this.serverStatusService.updateStatus(true); signalManager().fireTraceServerStartedSignal(); this.openDialog(rootPath); } @@ -163,7 +165,7 @@ export class TraceViewerContribution } else { progress.report({ message: 'Trace server started.', work: { done: 100, total: 100 } }); } - TraceServerConnectionStatusClientImpl.renderStatus(true); + this.serverStatusService.updateStatus(true); signalManager().fireTraceServerStartedSignal(); return super.open(traceURI, options); } @@ -230,7 +232,7 @@ export class TraceViewerContribution } else { progress.report({ message: 'Trace server started.', work: { done: 100, total: 100 } }); } - TraceServerConnectionStatusClientImpl.renderStatus(true); + this.serverStatusService.updateStatus(true); signalManager().fireTraceServerStartedSignal(); return; } @@ -261,7 +263,7 @@ export class TraceViewerContribution try { await this.traceServerConfigService.stopTraceServer(); this.messageService.info('Trace server terminated successfully.'); - TraceServerConnectionStatusClientImpl.renderStatus(false); + this.serverStatusService.updateStatus(false); } catch (err) { this.messageService.error('Failed to stop the trace server.'); } diff --git a/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer.tsx b/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer.tsx index 416ebcf59..b1f1c8b5d 100644 --- a/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer.tsx +++ b/theia-extensions/viewer-prototype/src/browser/trace-viewer/trace-viewer.tsx @@ -1,4 +1,4 @@ -import { Disposable, DisposableCollection, MessageService, Path, URI } from '@theia/core'; +import { CommandService, Disposable, DisposableCollection, MessageService, Path, URI } from '@theia/core'; import { ApplicationShell, Message, StatusBar, WidgetManager, StatefulWidget } from '@theia/core/lib/browser'; import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; @@ -11,6 +11,7 @@ import { TraceContextComponent, PersistedState } from 'traceviewer-react-components/lib/components/trace-context-component'; +import { TraceServerConnectionStatusClient } from '../../common/trace-server-connection-status'; import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; import { TheiaMessageManager } from '../theia-message-manager'; import { ThemeService } from '@theia/core/lib/browser/theming'; @@ -56,6 +57,8 @@ export class TraceViewerWidget extends ReactWidget implements StatefulWidget { protected traceContextComponent: React.RefObject; protected persistedState?: PersistedState; protected loadTraceOverview = true; + protected serverStatus: boolean; + protected experimentLoaded: boolean; protected resizeHandlers: (() => void)[] = []; protected readonly addResizeHandler = (h: () => void): void => { @@ -101,10 +104,13 @@ export class TraceViewerWidget extends ReactWidget implements StatefulWidget { @inject(TheiaMessageManager) protected readonly _signalHandler: TheiaMessageManager; @inject(MessageService) protected readonly messageService: MessageService; @inject(TraceExplorerContribution) protected readonly traceExplorerContribution: TraceExplorerContribution; + @inject(CommandService) protected readonly commandService!: CommandService; @inject(WidgetManager) protected readonly widgetManager!: WidgetManager; @inject(ThemeService) protected readonly themeService: ThemeService; @inject(OverviewPreferences) protected overviewPreferences: OverviewPreferences; @inject(FileDialogService) protected readonly fileDialogService: FileDialogService; + @inject(TraceServerConnectionStatusClient) + protected readonly connectionStatusService: TraceServerConnectionStatusClient; @postConstruct() protected init(): void { @@ -118,6 +124,7 @@ export class TraceViewerWidget extends ReactWidget implements StatefulWidget { this.title.closable = true; this.addClass('theia-trace-open'); this.backgroundTheme = this.themeService.getCurrentTheme().type; + this.serverStatus = this.connectionStatusService.status; this.toDispose.push(this.themeService.onDidColorThemeChange(() => this.updateBackgroundTheme())); if (!this.options.traceUUID) { this.initialize(); @@ -136,21 +143,7 @@ export class TraceViewerWidget extends ReactWidget implements StatefulWidget { }) ); - if (this.options.traceUUID) { - const experiment = await this.experimentManager.updateExperiment(this.options.traceUUID); - if (experiment) { - this.openedExperiment = experiment; - this.title.label = 'Trace: ' + experiment.name; - this.id = experiment.UUID; - this.experimentManager.addExperiment(experiment); - signalManager().fireExperimentOpenedSignal(experiment); - if (this.isVisible) { - signalManager().fireTraceViewerTabActivatedSignal(experiment); - } - this.fetchMarkerSets(experiment.UUID); - } - this.update(); - } + this.experimentLoaded = await this.loadExperiment(); this.subscribeToEvents(); this.toDispose.push(this.toDisposeOnNewExplorer); // Make node focusable so it can achieve focus on activate (avoid warning); @@ -173,6 +166,7 @@ export class TraceViewerWidget extends ReactWidget implements StatefulWidget { signalManager().on(Signals.OPEN_OVERVIEW_OUTPUT, this.onTraceOverviewOpened); signalManager().on(Signals.OVERVIEW_OUTPUT_SELECTED, this.onTraceOverviewOutputSelected); signalManager().on(Signals.SAVE_AS_CSV, this.onSaveAsCSV); + this.connectionStatusService.addServerStatusChangeListener(this.onServerStatusChange); } protected updateBackgroundTheme(): void { @@ -188,6 +182,27 @@ export class TraceViewerWidget extends ReactWidget implements StatefulWidget { signalManager().off(Signals.OPEN_OVERVIEW_OUTPUT, this.onTraceOverviewOpened); signalManager().off(Signals.OVERVIEW_OUTPUT_SELECTED, this.onTraceOverviewOutputSelected); signalManager().off(Signals.SAVE_AS_CSV, this.onSaveAsCSV); + this.connectionStatusService.removeServerStatusChangeListener(this.onServerStatusChange); + } + + protected async loadExperiment(): Promise { + if (!this.options.traceUUID) { + return false; + } + const experiment = await this.experimentManager.updateExperiment(this.options.traceUUID); + if (!experiment) { + return false; + } + this.openedExperiment = experiment; + this.title.label = 'Trace: ' + experiment.name; + this.id = experiment.UUID; + this.experimentManager.addExperiment(experiment); + signalManager().fireExperimentOpenedSignal(experiment); + if (this.isVisible) { + signalManager().fireTraceViewerTabActivatedSignal(experiment); + } + this.fetchMarkerSets(experiment.UUID); + return true; } async initialize(): Promise { @@ -317,10 +332,6 @@ export class TraceViewerWidget extends ReactWidget implements StatefulWidget { } restoreState(persistedState: PersistedState): void { - /* - TODO - BigInt support for restoring state in outputs/outputDescriptors - Identify what values need to be BigInt and convert. - */ if (persistedState.outputs.length > 0 || persistedState.storedOverviewOutput) { this.persistedState = persistedState; @@ -336,6 +347,14 @@ export class TraceViewerWidget extends ReactWidget implements StatefulWidget { this.loadTraceOverview = false; } + private onServerStatusChange = async (status: boolean): Promise => { + this.serverStatus = status; + if (!this.experimentLoaded) { + await this.loadExperiment(); + } + this.update(); + }; + private async fetchMarkerSets(expUUID: string) { const markers = await this.tspClient.fetchMarkerSets(expUUID); const markersResponse = markers.getModel(); @@ -390,7 +409,10 @@ export class TraceViewerWidget extends ReactWidget implements StatefulWidget { backgroundTheme={this.backgroundTheme} persistedState={this.persistedState} messageManager={this._signalHandler} + serverStatus={this.serverStatus} /> + ) : !this.serverStatus ? ( + 'Server offline. Waiting to load...' ) : ( 'Trace is loading...' )} diff --git a/theia-extensions/viewer-prototype/src/common/trace-server-connection-status.ts b/theia-extensions/viewer-prototype/src/common/trace-server-connection-status.ts index 831a14d2f..8c856fc8b 100644 --- a/theia-extensions/viewer-prototype/src/common/trace-server-connection-status.ts +++ b/theia-extensions/viewer-prototype/src/common/trace-server-connection-status.ts @@ -31,9 +31,23 @@ export interface TraceServerConnectionStatusClient { /** * Subscribe this client to the connection status */ - addConnectionStatusListener(): void; + activate(): void; /** * Unsubscribe this client from the connection status */ - removeConnectionStatusListener(): void; + deactivate(): void; + + /** + * Adds event listener for server status change + * @param fn event listener + */ + addServerStatusChangeListener(fn: (status: boolean) => void): void; + + /** + * Removes event listener for server status change. + * @param fn event listener to be removed + */ + removeServerStatusChangeListener(fn: (status: boolean) => void): void; + + status: boolean; }