Skip to content

Commit

Permalink
Display the full stack trace for errors in a panel
Browse files Browse the repository at this point in the history
- TODO: Fix the Reload button. Doesn't quite seem to be working.
  • Loading branch information
mofojed committed Apr 25, 2024
1 parent 636052f commit 2669a9e
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from jsonrpc import JSONRPCResponseManager, Dispatcher
import logging
import threading
import traceback
from enum import Enum
from queue import Queue
from typing import Any, Callable
Expand Down Expand Up @@ -162,7 +163,8 @@ def _render(self) -> None:
logger.exception("Error rendering %s", self._element.name)
# Try and send the error to the client
# There's a possibility this will error out too, but we can't do much about that
self._send_document_error(e)
stack_trace = traceback.format_exc()
self._send_document_error(e, stack_trace)
raise e

def _process_callable_queue(self) -> None:
Expand Down Expand Up @@ -358,7 +360,7 @@ def _send_document_update(
self._dispatcher = dispatcher
self._connection.on_data(payload.encode(), new_objects)

def _send_document_error(self, error: Exception) -> None:
def _send_document_error(self, error: Exception, stack_trace: str) -> None:
"""
Send an error to the client. This is called when an error occurs during rendering.
Expand All @@ -371,6 +373,7 @@ def _send_document_error(self, error: Exception) -> None:
{
"message": str(error),
"type": type(error).__name__,
"stack": stack_trace,
"code": ErrorCode.DOCUMENT_ERROR.value,
}
),
Expand Down
59 changes: 45 additions & 14 deletions plugins/ui/src/js/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,32 +31,63 @@
}
}

.error-viewer {
.widget-error-view {
display: flex;
flex-direction: column;
gap: $spacer-1;
flex-grow: 1;

.widget-error-view-footer {
display: flex;
justify-content: flex-end;
align-items: center;
gap: $spacer-1;
flex-wrap: wrap;
}
}

.error-view {
position: relative;
color: $danger;
border-radius: $border-radius;
background-color: negative-opacity($exception-transparency);
display: flex;
flex-direction: column;
flex-grow: 0;
&.expanded {
flex-grow: 1;
}
transition: flex-grow $transition ease-in-out;

font-family: $font-family-monospace;

label {
display: block;
padding: $spacer;
font-weight: bold;
}

textarea {
width: 100%;
color: $danger;
border-radius: $border-radius;
background-color: negative-opacity($exception-transparency);
padding: $spacer;
color: $danger;
background-color: transparent;
border: 0;
resize: none;
outline: none;
height: 100px;
min-height: 100px;
transition: height $transition ease-in-out;
white-space: pre;
&.expanded {
height: 55vh;
}
flex-grow: 1;
}

.error-viewer-buttons {
.error-view-buttons {
display: flex;
flex-direction: row;
gap: 1px;
position: absolute;
bottom: $spacer-2;
right: $grid-gutter-width * 0.5;
top: 0;
right: 0;

.btn-danger {
color: $black;
Expand All @@ -67,13 +98,13 @@
}
}

.error-viewer-copy-button {
.error-view-copy-button {
border-radius: 0;
min-width: 3rem;
}

.error-viewer-expand-button {
border-radius: 0 0 $border-radius 0;
.error-view-expand-button {
border-radius: 0 $border-radius 0 0;
svg {
margin-right: $spacer-1;
}
Expand Down
51 changes: 24 additions & 27 deletions plugins/ui/src/js/src/widget/ErrorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,31 @@ function ErrorView({ message, type = 'Error' }: ErrorViewerProps): JSX.Element {
const [isExpanded, setIsExpanded] = useState(false);

return (
<div className="error-viewer">
<label className="text-danger">
<FontAwesomeIcon icon={vsWarning} />
{type}
</label>
<textarea
readOnly
className={classNames({ expanded: isExpanded })}
value={message}
/>
<div className="error-viewer-buttons">
<CopyButton
kind="danger"
className="error-viewer-copy-button"
tooltip="Copy exception contents"
copy={`${type}: ${message}`}
/>
<Button
kind="danger"
className="error-viewer-expand-button"
onClick={() => {
setIsExpanded(!isExpanded);
}}
icon={isExpanded ? vsDiffRemoved : vsDiffAdded}
>
{isExpanded ? 'Show Less' : 'Show More'}
</Button>
<div className={classNames('error-view', { expanded: isExpanded })}>
<div className="error-view-header">
<label className="text-danger">
<FontAwesomeIcon icon={vsWarning} /> {type}
</label>
<div className="error-view-buttons">
<CopyButton
kind="danger"
className="error-view-copy-button"
tooltip="Copy exception contents"
copy={`${type}: ${message}`.trim()}
/>
<Button
kind="danger"
className="error-view-expand-button"
onClick={() => {
setIsExpanded(!isExpanded);
}}
icon={isExpanded ? vsDiffRemoved : vsDiffAdded}
>
{isExpanded ? 'Show Less' : 'Show More'}
</Button>
</div>
</div>
<textarea readOnly value={message} />
</div>
);
}
Expand Down
9 changes: 0 additions & 9 deletions plugins/ui/src/js/src/widget/JSONRPCUtils.ts

This file was deleted.

27 changes: 27 additions & 0 deletions plugins/ui/src/js/src/widget/WidgetErrorView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { Button } from '@deephaven/components';
import ErrorView from './ErrorView';
import { WidgetError } from './WidgetTypes';

/** Component that takes a WidgetError and displays the contents in an ErrorView, and has a button to reload the widget from a fresh state. */
function WidgetErrorView({
error,
onReload,
}: {
error: WidgetError;
onReload: () => void;
}): JSX.Element {
const displayMessage = `${error.message}\n\n${error.stack}`.trim();
return (
<div className="widget-error-view">
<ErrorView message={displayMessage} type={error.type} />
<div className="widget-error-view-footer">
<Button kind="tertiary" onClick={onReload}>
Reload
</Button>
</div>
</div>
);
}

export default WidgetErrorView;
70 changes: 33 additions & 37 deletions plugins/ui/src/js/src/widget/WidgetHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,10 @@ import {
JSONRPCServer,
JSONRPCServerAndClient,
} from 'json-rpc-2.0';
import {
Content,
Heading,
Icon,
IllustratedMessage,
} from '@deephaven/components';
import { WidgetDescriptor } from '@deephaven/dashboard';
import { vsWarning } from '@deephaven/icons';
import type { dh } from '@deephaven/jsapi-types';
import Log from '@deephaven/log';
import { EMPTY_FUNCTION } from '@deephaven/utils';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
CALLABLE_KEY,
OBJECT_KEY,
Expand All @@ -37,16 +29,14 @@ import {
ReadonlyWidgetData,
WidgetDataUpdate,
WidgetMessageEvent,
WidgetError,
METHOD_DOCUMENT_ERROR,
METHOD_DOCUMENT_UPDATED,
} from './WidgetTypes';
import DocumentHandler from './DocumentHandler';
import { getComponentForElement } from './WidgetUtils';
import {
ErrorNotificationPayload,
METHOD_DOCUMENT_ERROR,
METHOD_DOCUMENT_UPDATED,
} from './JSONRPCUtils';
import ReactPanel from '../layout/ReactPanel';
import ErrorView from './ErrorView';
import WidgetErrorView from './WidgetErrorView';

const log = Log.module('@deephaven/js-plugin-ui/WidgetHandler');

Expand Down Expand Up @@ -76,7 +66,7 @@ function WidgetHandler({
}: WidgetHandlerProps): JSX.Element | null {
const [widget, setWidget] = useState<dh.Widget>();
const [document, setDocument] = useState<ReactNode>();
const [initialData] = useState(initialDataProp);
const [initialData, setInitialData] = useState(initialDataProp);

// When we fetch a widget, the client is then responsible for the exported objects.
// These objects could stay alive even after the widget is closed if we wanted to,
Expand All @@ -86,26 +76,6 @@ function WidgetHandler({
);
const exportedObjectCount = useRef(0);

const setDocumentError = useCallback(
(errorMessage: string, type?: string) => {
// When we get an error for the server, we want to display it to the user.
// Display the error in a panel that the user can see.
setDocument(
<ReactPanel>
<ErrorView message={errorMessage} type={type} />
{/* <IllustratedMessage>
<Icon size="XL">
<FontAwesomeIcon icon={vsWarning} />
</Icon>
<Heading>Error</Heading>
<Content>{errorMessage}</Content>
</IllustratedMessage> */}
</ReactPanel>
);
},
[]
);

// Bi-directional communication as defined in https://www.npmjs.com/package/json-rpc-2.0
const jsonClient = useMemo(
() =>
Expand Down Expand Up @@ -202,6 +172,32 @@ function WidgetHandler({
[]
);

const setDocumentError = useCallback(
(error: WidgetError) => {
// When we get an error for the server, we want to display it to the user.
// Display the error in a panel that the user can see.
setDocument(
<ReactPanel>
<WidgetErrorView
error={error}
onReload={() => {
// Reset the state in the initial data. This will cause the widget to reload.
jsonClient?.request('setState', [{}]).then(
result => {
log.debug('Set state result', result);
},
e => {
log.error('Error setting initial state: ', e);
}
);
}}
/>
</ReactPanel>
);
},
[jsonClient]
);

useEffect(
function initMethods() {
if (jsonClient == null) {
Expand Down Expand Up @@ -232,8 +228,8 @@ function WidgetHandler({

jsonClient.addMethod(METHOD_DOCUMENT_ERROR, (params: [string]) => {
log.error('Document error', params);
const error: ErrorNotificationPayload = JSON.parse(params[0]);
setDocumentError(error.message, error.type);
const error: WidgetError = JSON.parse(params[0]);
setDocumentError(error);
});

return () => {
Expand Down
19 changes: 19 additions & 0 deletions plugins/ui/src/js/src/widget/WidgetTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,22 @@ export type ReadonlyWidgetData = Readonly<WidgetData>;

/** Contains an update for widget data. Only the keys that are updated are passed. */
export type WidgetDataUpdate = Partial<ReadonlyWidgetData>;

/** Widget error details */
export type WidgetError = {
/** Message to display of the error */
message: string;

/** Type of the error */
type?: string;

/** Stack trace of the error */
stack?: string;

/** Specific error code */
code?: number;
};

export const METHOD_DOCUMENT_UPDATED = 'documentUpdated';

export const METHOD_DOCUMENT_ERROR = 'documentError';

0 comments on commit 2669a9e

Please sign in to comment.