Skip to content

Commit

Permalink
Change up the look of the error messages
Browse files Browse the repository at this point in the history
- Doesn't show the full message until you click the Info button
- Reload button only visible if added as an action
  • Loading branch information
mofojed committed Jun 14, 2024
1 parent dbdbfc8 commit ea0363f
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 37 deletions.
9 changes: 9 additions & 0 deletions plugins/ui/src/js/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,12 @@
max-width: 100%;
}
}

.ui-text-wrap-balance {
text-wrap: balance;
}

.ui-monospace-text {
font-family: $font-family-monospace;
white-space: pre;
}
79 changes: 59 additions & 20 deletions plugins/ui/src/js/src/widget/WidgetErrorView.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,69 @@
import React from 'react';
import { Button, ErrorView } from '@deephaven/components';
import { vsRefresh } from '@deephaven/icons';
import { WidgetError } from './WidgetTypes';
import {
Button,
Content,
ContextualHelp,
CopyButton,
Flex,
Heading,
Icon,
IllustratedMessage,
Text,
} from '@deephaven/components';
import { vsWarning } from '@deephaven/icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
getErrorAction,
getErrorMessage,
getErrorName,
getErrorShortMessage,
getErrorStack,
} from './WidgetUtils';

/** Component that takes a WidgetError and displays the contents in an ErrorView, and has a button to reload the widget from a fresh state. */
/** Component that display an error message. Will automatically show a button for more info and an action button if the error has an Action defined */
function WidgetErrorView({
error,
onReload: onReset,
}: {
error: WidgetError;
onReload: () => void;
error: NonNullable<unknown>;
}): JSX.Element {
const displayMessage = `${error.message.trim()}\n\n${
error.stack ?? ''
}`.trim();
const name = getErrorName(error);
const shortMessage = getErrorShortMessage(error);
const message = getErrorMessage(error);
const stack = getErrorStack(error);
const action = getErrorAction(error);

return (
<div className="ui-widget-error-view">
<div className="widget-error-view-content">
<ErrorView message={displayMessage} type={error.type} isExpanded />
</div>
<div className="widget-error-view-footer">
<Button kind="tertiary" icon={vsRefresh} onClick={onReset}>
Reload
</Button>
</div>
</div>
<IllustratedMessage>
<Icon size="XXL" marginBottom="size-100">
<FontAwesomeIcon icon={vsWarning} />
</Icon>
<Heading UNSAFE_className="ui-text-wrap-balance">{name}</Heading>
<Content>
<Flex direction="column" gap="size-150">
<Text UNSAFE_className="ui-text-wrap-balance">
{shortMessage}
<ContextualHelp variant="info">
<Heading>
{name}{' '}
<CopyButton
copy={() => `${name}\n\n${message}\n\n${stack}`.trim()}
/>
</Heading>
<Content>
<Text UNSAFE_className="ui-monospace-text">
{`${message}\n\n${stack}`.trim()}
</Text>
</Content>
</ContextualHelp>
</Text>
{action != null && (
<Button kind="tertiary" onClick={action.action}>
{action.title}
</Button>
)}
</Flex>
</Content>
</IllustratedMessage>
);
}

Expand Down
27 changes: 11 additions & 16 deletions plugins/ui/src/js/src/widget/WidgetHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import {
WidgetError,
METHOD_DOCUMENT_ERROR,
METHOD_DOCUMENT_UPDATED,
isWidgetError,
} from './WidgetTypes';
import DocumentHandler from './DocumentHandler';
import { getComponentForElement } from './WidgetUtils';
Expand Down Expand Up @@ -64,25 +63,17 @@ function WidgetHandler({
}: WidgetHandlerProps): JSX.Element | null {
const { widget, error: widgetError } = useWidget(widgetDescriptor);

log.debug2('WidgetHandler', widgetDescriptor, widget, widgetError);
const [document, setDocument] = useState<ReactNode>();

// We want to update the initial data if the widget changes, as we'll need to re-fetch the widget and want to start with a fresh state.
// eslint-disable-next-line react-hooks/exhaustive-deps
const initialData = useMemo(() => initialDataProp, [widget]);
const [internalError, setInternalError] = useState<WidgetError>();

const error = useMemo(() => {
if (internalError != null) {
return internalError;
}

if (isWidgetError(widgetError)) {
return widgetError;
}

return undefined;
}, [internalError, widgetError]);
const error = useMemo(
() => internalError ?? widgetError ?? undefined,
[internalError, widgetError]
);

// 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 Down Expand Up @@ -239,14 +230,18 @@ function WidgetHandler({
jsonClient.addMethod(METHOD_DOCUMENT_ERROR, (params: [string]) => {
log.error('Document error', params);
const newError: WidgetError = JSON.parse(params[0]);
newError.action = {
title: 'Reload',
action: () => sendSetState(),
};
setInternalError(newError);
});

return () => {
jsonClient.rejectAllPendingRequests('Widget was changed');
};
},
[jsonClient, onDataChange, parseDocument]
[jsonClient, onDataChange, parseDocument, sendSetState]
);

/**
Expand Down Expand Up @@ -318,10 +313,10 @@ function WidgetHandler({

const errorView = useMemo(() => {
if (error != null) {
return <WidgetErrorView error={error} onReload={() => sendSetState()} />;
return <WidgetErrorView error={error} />;
}
return null;
}, [error, sendSetState]);
}, [error]);

const contentOverlay = useMemo(() => {
// We only show it as an overlay if there's already a document to show
Expand Down
17 changes: 17 additions & 0 deletions plugins/ui/src/js/src/widget/WidgetTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ export function isWidgetError(value: unknown): value is WidgetError {
return typeof value === 'object' && value !== null && 'message' in value;
}

export type WidgetAction = {
title: string;
action: () => void;
};

export function isWidgetAction(value: unknown): value is WidgetAction {
return (
typeof value === 'object' &&
value !== null &&
'title' in value &&
'action' in value
);
}

/** Widget error details */
export type WidgetError = {
/** Message to display of the error */
Expand All @@ -42,6 +56,9 @@ export type WidgetError = {

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

/** An action to take to recover from the error */
action?: WidgetAction;
};

/** Message containing a new document update */
Expand Down
85 changes: 84 additions & 1 deletion plugins/ui/src/js/src/widget/WidgetUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
Section,
} from '@deephaven/components';
import { ValueOf } from '@deephaven/utils';
import { ReadonlyWidgetData } from './WidgetTypes';
import {
ReadonlyWidgetData,
WidgetAction,
isWidgetAction,
} from './WidgetTypes';
import {
ElementNode,
ELEMENT_KEY,
Expand Down Expand Up @@ -115,3 +119,82 @@ export function getPreservedData(
Object.entries(oldData).filter(([key]) => PRESERVED_DATA_KEYS_SET.has(key))
);
}

/**
* Get the name of an error type
* @param error Name of an error
* @returns The name of the error
*/
export function getErrorName(error: unknown): string {
if (
typeof error === 'object' &&
error != null &&
'name' in error &&
typeof error.name === 'string'
) {
return error.constructor.name;
}
return 'Unknown error';
}

/**
* Get the message of an error
* @param error Error object
* @returns The error message
*/
export function getErrorMessage(error: unknown): string {
if (
typeof error === 'object' &&
error != null &&
'message' in error &&
typeof error.message === 'string'
) {
return error.message.trim();
}
return 'Unknown error';
}

/**
* Get the short message of an error. Just the first line of the error message.
* @param error Error object
* @returns The error short message
*/
export function getErrorShortMessage(error: unknown): string {
const message = getErrorMessage(error);
const lines = message.split('\n');
return lines[0].trim();
}

/**
* Get the stack trace of an error
* @param error Error object
* @returns The error stack trace
*/
export function getErrorStack(error: unknown): string {
if (
typeof error === 'object' &&
error != null &&
'stack' in error &&
typeof error.stack === 'string'
) {
return error.stack;
}
return '';
}

/**
* Get the action from an error object if it exists
* @param error Error object
* @returns The action from the error, if it exists
*/
export function getErrorAction(error: unknown): WidgetAction | null {
if (
typeof error === 'object' &&
error != null &&
'action' in error &&
isWidgetAction(error.action)
) {
return error.action;
}
return null;
}

0 comments on commit ea0363f

Please sign in to comment.