Skip to content

Commit

Permalink
[Embeddable] Add unified error UI (#143367)
Browse files Browse the repository at this point in the history
* Refactor embeddable error handler
* Remove embeddable error handler from the visualization embeddable
* Update Lens embeddable to handle errors correctly
  • Loading branch information
dokmic authored Nov 4, 2022
1 parent 4b61704 commit 0a72c67
Show file tree
Hide file tree
Showing 17 changed files with 196 additions and 141 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { isFunction } from 'lodash';
import React, { ReactNode, useEffect, useRef, useState } from 'react';
import { isPromise } from '@kbn/std';
import type { MaybePromise } from '@kbn/utility-types';
import type { ErrorLike } from '@kbn/expressions-plugin/common';
import type { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';

type IReactEmbeddable = IEmbeddable<EmbeddableInput, EmbeddableOutput, MaybePromise<ReactNode>>;

interface EmbeddableErrorHandlerProps {
children: IReactEmbeddable['catchError'];
embeddable?: IReactEmbeddable;
error: ErrorLike | string;
}

export function EmbeddableErrorHandler({
children,
embeddable,
error,
}: EmbeddableErrorHandlerProps) {
const [node, setNode] = useState<ReactNode>();
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!ref.current) {
return;
}

const handler = embeddable?.catchError?.bind(embeddable) ?? children;
if (!handler) {
return;
}

const renderedNode = handler(
typeof error === 'string' ? { message: error, name: '' } : error,
ref.current
);
if (isFunction(renderedNode)) {
return renderedNode;
}
if (isPromise(renderedNode)) {
renderedNode.then(setNode);
} else {
setNode(renderedNode);
}
}, [children, embeddable, error]);

return <div ref={ref}>{node}</div>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { EuiText } from '@elastic/eui';
import { isPromise } from '@kbn/std';
import { MaybePromise } from '@kbn/utility-types';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { EmbeddableErrorHandler } from './embeddable_error_handler';

interface Props {
embeddable?: IEmbeddable<EmbeddableInput, EmbeddableOutput, MaybePromise<ReactNode>>;
Expand Down Expand Up @@ -91,7 +92,11 @@ export class EmbeddableRoot extends React.Component<Props, State> {
<React.Fragment>
<div ref={this.root}>{this.state.node}</div>
{this.props.loading && <EuiLoadingSpinner data-test-subj="embedSpinner" />}
{this.props.error && <EuiText data-test-subj="embedError">{this.props.error}</EuiText>}
{this.props.error && (
<EmbeddableErrorHandler embeddable={this.props.embeddable} error={this.props.error}>
{({ message }) => <EuiText data-test-subj="embedError">{message}</EuiText>}
</EmbeddableErrorHandler>
)}
</React.Fragment>
);
}
Expand Down
21 changes: 4 additions & 17 deletions src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
* Side Public License, v 1.
*/

import { EuiEmptyPrompt } from '@elastic/eui';
import React, { ReactNode } from 'react';
import { Markdown } from '@kbn/kibana-react-plugin/public';
import { EmbeddablePanelError } from '../panel/embeddable_panel_error';
import { Embeddable } from './embeddable';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { IContainer } from '../containers';
Expand All @@ -33,20 +32,8 @@ export class ErrorEmbeddable extends Embeddable<EmbeddableInput, EmbeddableOutpu
public reload() {}

public render() {
const title = typeof this.error === 'string' ? this.error : this.error.message;
const body = (
<Markdown markdown={title} openLinksInNewTab={true} data-test-subj="errorMessageMarkdown" />
);

return (
<div className="embPanel__content" data-test-subj="embeddableStackError">
<EuiEmptyPrompt
className="embPanel__error"
iconType="alert"
iconColor="danger"
body={body}
/>
</div>
);
const error = typeof this.error === 'string' ? { message: this.error, name: '' } : this.error;

return <EmbeddablePanelError embeddable={this} error={error} />;
}
}
1 change: 1 addition & 0 deletions src/plugins/embeddable/public/lib/embeddables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
export type { EmbeddableOutput, EmbeddableInput, IEmbeddable } from './i_embeddable';
export { isEmbeddable } from './is_embeddable';
export { Embeddable } from './embeddable';
export { EmbeddableErrorHandler } from './embeddable_error_handler';
export * from './embeddable_factory';
export * from './embeddable_factory_definition';
export * from './default_embeddable_factory_provider';
Expand Down
18 changes: 6 additions & 12 deletions src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,6 @@
.embPanel__content--fullWidth {
width: 100%;
}

.embPanel__content--error {
&:hover {
box-shadow: none;
transform: none;
}
}
}

// HEADER
Expand Down Expand Up @@ -165,11 +158,12 @@
}

.embPanel__error {
text-align: center;
justify-content: center;
flex-direction: column;
overflow: auto;
padding: $euiSizeS;
padding: $euiSizeL;

& > * {
max-height: 100%;
overflow: auto;
}
}

.embPanel__label {
Expand Down
41 changes: 30 additions & 11 deletions src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
* Side Public License, v 1.
*/

import { EuiContextMenuPanelDescriptor, EuiPanel, htmlIdGenerator } from '@elastic/eui';
import {
EuiContextMenuPanelDescriptor,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
htmlIdGenerator,
} from '@elastic/eui';
import classNames from 'classnames';
import React, { ReactNode } from 'react';
import { Subscription } from 'rxjs';
Expand All @@ -27,11 +33,11 @@ import {
contextMenuTrigger,
} from '../triggers';
import {
IEmbeddable,
EmbeddableOutput,
EmbeddableError,
EmbeddableErrorHandler,
EmbeddableInput,
} from '../embeddables/i_embeddable';
EmbeddableOutput,
IEmbeddable,
} from '../embeddables';
import { ViewMode } from '../types';

import { EmbeddablePanelError } from './embeddable_panel_error';
Expand Down Expand Up @@ -105,7 +111,7 @@ interface State {
badges: Array<Action<EmbeddableContext>>;
notifications: Array<Action<EmbeddableContext>>;
loading?: boolean;
error?: EmbeddableError;
error?: Error;
destroyError?(): void;
node?: ReactNode;
}
Expand Down Expand Up @@ -301,11 +307,24 @@ export class EmbeddablePanel extends React.Component<Props, State> {
/>
)}
{this.state.error && (
<EmbeddablePanelError
editPanelAction={this.state.universalActions.editPanel}
embeddable={this.props.embeddable}
error={this.state.error}
/>
<EuiFlexGroup
alignItems="center"
className="eui-fullHeight embPanel__error"
data-test-subj="embeddableError"
justifyContent="center"
>
<EuiFlexItem>
<EmbeddableErrorHandler embeddable={this.props.embeddable} error={this.state.error}>
{(error) => (
<EmbeddablePanelError
editPanelAction={this.state.universalActions.editPanel}
embeddable={this.props.embeddable}
error={error}
/>
)}
</EmbeddableErrorHandler>
</EuiFlexItem>
</EuiFlexGroup>
)}
<div className="embPanel__content" ref={this.embeddableRoot} {...contentAttrs}>
{this.state.node}
Expand Down
76 changes: 30 additions & 46 deletions src/plugins/embeddable/public/lib/panel/embeddable_panel_error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@
* Side Public License, v 1.
*/

import { isFunction } from 'lodash';
import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { EuiPanel } from '@elastic/eui';
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
import { EuiButtonEmpty, EuiEmptyPrompt, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isPromise } from '@kbn/std';
import { Markdown } from '@kbn/kibana-react-plugin/public';
import type { MaybePromise } from '@kbn/utility-types';
import { ErrorLike } from '@kbn/expressions-plugin/common';
import { distinctUntilChanged, merge, of, switchMap } from 'rxjs';
import { EditPanelAction } from '../actions';
import { EmbeddableInput, EmbeddableOutput, ErrorEmbeddable, IEmbeddable } from '../embeddables';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../embeddables';

interface EmbeddablePanelErrorProps {
editPanelAction?: EditPanelAction;
Expand All @@ -29,27 +28,25 @@ export function EmbeddablePanelError({
error,
}: EmbeddablePanelErrorProps) {
const [isEditable, setEditable] = useState(false);
const [node, setNode] = useState<ReactNode>();
const ref = useRef<HTMLDivElement>(null);
const handleErrorClick = useMemo(
() => (isEditable ? () => editPanelAction?.execute({ embeddable }) : undefined),
[editPanelAction, embeddable, isEditable]
);

const title = embeddable.getTitle();
const actionDisplayName = useMemo(
const label = useMemo(
() => editPanelAction?.getDisplayName({ embeddable }),
[editPanelAction, embeddable]
);
const title = useMemo(() => embeddable.getTitle(), [embeddable]);
const ariaLabel = useMemo(
() =>
!title
? actionDisplayName
? label
: i18n.translate('embeddableApi.panel.editPanel.displayName', {
defaultMessage: 'Edit {value}',
values: { value: title },
}),
[title, actionDisplayName]
[label, title]
);

useEffect(() => {
Expand All @@ -62,42 +59,29 @@ export function EmbeddablePanelError({

return () => subscription.unsubscribe();
}, [editPanelAction, embeddable]);
useEffect(() => {
if (!ref.current) {
return;
}

if (!embeddable.catchError) {
const errorEmbeddable = new ErrorEmbeddable(error, { id: embeddable.id });
setNode(errorEmbeddable.render());

return () => errorEmbeddable.destroy();
}

const renderedNode = embeddable.catchError(error, ref.current);
if (isFunction(renderedNode)) {
return renderedNode;
}
if (isPromise(renderedNode)) {
renderedNode.then(setNode);
} else {
setNode(renderedNode);
}
}, [embeddable, error]);

return (
<EuiPanel
element="div"
className="embPanel__content embPanel__content--error"
color="transparent"
paddingSize="none"
data-test-subj="embeddableError"
panelRef={ref}
role={isEditable ? 'button' : undefined}
aria-label={isEditable ? ariaLabel : undefined}
onClick={handleErrorClick}
>
{node}
</EuiPanel>
<EuiEmptyPrompt
body={
<EuiText size="s">
<Markdown
markdown={error.message}
openLinksInNewTab={true}
data-test-subj="errorMessageMarkdown"
/>
</EuiText>
}
data-test-subj="embeddableStackError"
iconType="alert"
iconColor="danger"
layout="vertical"
actions={
isEditable && (
<EuiButtonEmpty aria-label={ariaLabel} onClick={handleErrorClick} size="s">
{label}
</EuiButtonEmpty>
)
}
/>
);
}
Loading

0 comments on commit 0a72c67

Please sign in to comment.