diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_error_handler.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_error_handler.tsx new file mode 100644 index 0000000000000..a0ebfdbcbdc86 --- /dev/null +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_error_handler.tsx @@ -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>; + +interface EmbeddableErrorHandlerProps { + children: IReactEmbeddable['catchError']; + embeddable?: IReactEmbeddable; + error: ErrorLike | string; +} + +export function EmbeddableErrorHandler({ + children, + embeddable, + error, +}: EmbeddableErrorHandlerProps) { + const [node, setNode] = useState(); + const ref = useRef(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
{node}
; +} diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_root.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_root.tsx index bfaefe09b5e6b..f94bb99ada83c 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_root.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_root.tsx @@ -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>; @@ -91,7 +92,11 @@ export class EmbeddableRoot extends React.Component {
{this.state.node}
{this.props.loading && } - {this.props.error && {this.props.error}} + {this.props.error && ( + + {({ message }) => {message}} + + )}
); } diff --git a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx index 8dff4ecee8976..1ccaf61064a8a 100644 --- a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx @@ -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'; @@ -33,20 +32,8 @@ export class ErrorEmbeddable extends Embeddable - ); - - return ( -
- -
- ); + const error = typeof this.error === 'string' ? { message: this.error, name: '' } : this.error; + + return ; } } diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 0c1048af9182c..9f88e6b3053b5 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -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'; diff --git a/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss b/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss index 3044a3896fa35..789b9f7227a3c 100644 --- a/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss +++ b/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss @@ -29,13 +29,6 @@ .embPanel__content--fullWidth { width: 100%; } - - .embPanel__content--error { - &:hover { - box-shadow: none; - transform: none; - } - } } // HEADER @@ -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 { diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index f5b072a591225..cf014c2af98b6 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -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'; @@ -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'; @@ -105,7 +111,7 @@ interface State { badges: Array>; notifications: Array>; loading?: boolean; - error?: EmbeddableError; + error?: Error; destroyError?(): void; node?: ReactNode; } @@ -301,11 +307,24 @@ export class EmbeddablePanel extends React.Component { /> )} {this.state.error && ( - + + + + {(error) => ( + + )} + + + )}
{this.state.node} diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel_error.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel_error.tsx index 69af8e7220e62..4aba5ed105154 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel_error.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel_error.tsx @@ -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; @@ -29,27 +28,25 @@ export function EmbeddablePanelError({ error, }: EmbeddablePanelErrorProps) { const [isEditable, setEditable] = useState(false); - const [node, setNode] = useState(); - const ref = useRef(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(() => { @@ -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 ( - - {node} - + + + + } + data-test-subj="embeddableStackError" + iconType="alert" + iconColor="danger" + layout="vertical" + actions={ + isEditable && ( + + {label} + + ) + } + /> ); } diff --git a/src/plugins/visualizations/public/components/visualization_missed_saved_object_error.tsx b/src/plugins/visualizations/public/components/visualization_missed_saved_object_error.tsx index 767a2d38f8581..338a8f6fe7201 100644 --- a/src/plugins/visualizations/public/components/visualization_missed_saved_object_error.tsx +++ b/src/plugins/visualizations/public/components/visualization_missed_saved_object_error.tsx @@ -12,28 +12,25 @@ import React from 'react'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import type { ApplicationStart } from '@kbn/core/public'; import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-plugin/common'; -import type { ViewMode } from '@kbn/embeddable-plugin/common'; import type { RenderMode } from '@kbn/expressions-plugin/common'; interface VisualizationMissedSavedObjectErrorProps { savedObjectMeta: { savedObjectType: typeof DATA_VIEW_SAVED_OBJECT_TYPE | 'search'; - savedObjectId?: string; }; application: ApplicationStart; - viewMode: ViewMode; + message: string; renderMode: RenderMode; } export const VisualizationMissedSavedObjectError = ({ savedObjectMeta, application, - viewMode, + message, renderMode, }: VisualizationMissedSavedObjectErrorProps) => { const { management: isManagementEnabled } = application.capabilities.navLinks; const isIndexPatternManagementEnabled = application.capabilities.management.kibana.indexPatterns; - const isEditVisEnabled = application.capabilities.visualize?.save; return ( ) : null } - body={ - <> -

- {i18n.translate('visualizations.missedDataView.errorMessage', { - defaultMessage: `Could not find the {type}: {id}`, - values: { - id: savedObjectMeta.savedObjectId ?? '-', - type: - savedObjectMeta.savedObjectType === 'search' - ? i18n.translate('visualizations.noSearch.label', { - defaultMessage: 'search', - }) - : i18n.translate('visualizations.noDataView.label', { - defaultMessage: 'data view', - }), - }, - })} -

- {viewMode === 'edit' && renderMode !== 'edit' && isEditVisEnabled ? ( -

- {i18n.translate('visualizations.missedDataView.editInVisualizeEditor', { - defaultMessage: `Edit in Visualize editor to fix the error`, - })} -

- ) : null} - - } + body={

{message}

} /> ); }; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index f2a2a7f8ae000..663d015c429de 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -29,7 +29,6 @@ import { IContainer, ReferenceOrValueEmbeddable, SavedObjectEmbeddableInput, - ViewMode, } from '@kbn/embeddable-plugin/public'; import { ExpressionAstExpression, @@ -401,6 +400,25 @@ export class VisualizeEmbeddable this.abortController.abort(); } this.renderComplete.dispatchError(); + + if (isFallbackDataView(this.vis.data.indexPattern)) { + error = new Error( + i18n.translate('visualizations.missedDataView.errorMessage', { + defaultMessage: `Could not find the {type}: {id}`, + values: { + id: this.vis.data.indexPattern.id ?? '-', + type: this.vis.data.savedSearchId + ? i18n.translate('visualizations.noSearch.label', { + defaultMessage: 'search', + }) + : i18n.translate('visualizations.noDataView.label', { + defaultMessage: 'data view', + }), + }, + }) + ); + } + this.updateOutput({ ...this.getOutput(), rendered: true, @@ -503,7 +521,7 @@ export class VisualizeEmbeddable const { error } = this.getOutput(); if (error) { - render(this.catchError(error), this.domNode); + render(this.renderError(error), this.domNode); } }) ); @@ -511,17 +529,16 @@ export class VisualizeEmbeddable await this.updateHandler(); } - public catchError(error: ErrorLike | string) { + private renderError(error: ErrorLike | string) { if (isFallbackDataView(this.vis.data.indexPattern)) { return ( ); } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index 4dcaa582511c3..93b19953b9b91 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -134,7 +134,7 @@ export class VisualizeEmbeddableFactory public getDisplayName() { return i18n.translate('visualizations.displayName', { - defaultMessage: 'Visualization', + defaultMessage: 'visualization', }); } diff --git a/test/functional/apps/dashboard/group6/dashboard_error_handling.ts b/test/functional/apps/dashboard/group6/dashboard_error_handling.ts index 7e1956b82daf2..b00aec24809cc 100644 --- a/test/functional/apps/dashboard/group6/dashboard_error_handling.ts +++ b/test/functional/apps/dashboard/group6/dashboard_error_handling.ts @@ -51,14 +51,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // wrapping into own describe to make sure new tab is cleaned up even if test failed // see: https://github.com/elastic/kibana/pull/67280#discussion_r430528122 - describe('recreate index pattern link works', () => { - it('recreate index pattern link works', async () => { + describe('when the saved object is missing', () => { + it('shows the missing data view error message', async () => { await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.dashboard.loadSavedDashboard('dashboard with missing index pattern'); await PageObjects.header.waitUntilLoadingHasFinished(); - const errorEmbeddable = await testSubjects.find('visualization-missed-data-view-error'); + const embeddableError = await testSubjects.find('embeddableError'); + const errorMessage = await embeddableError.getVisibleText(); - expect(await errorEmbeddable.isDisplayed()).to.be(true); + expect(errorMessage).to.contain('Could not find the data view'); }); }); }); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 65c91b115ce70..aa5c5bba4741c 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -645,6 +645,25 @@ export class Embeddable } } + private getError(): Error | undefined { + const message = + typeof this.errors?.[0]?.longMessage === 'string' + ? this.errors[0].longMessage + : this.errors?.[0]?.shortMessage; + + if (message != null) { + return new Error(message); + } + + if (!this.expression) { + return new Error( + i18n.translate('xpack.lens.embeddable.failure', { + defaultMessage: "Visualization couldn't be displayed", + }) + ); + } + } + /** * * @param {HTMLElement} domNode @@ -665,7 +684,7 @@ export class Embeddable this.updateOutput({ ...this.getOutput(), loading: true, - error: undefined, + error: this.getError(), }); this.renderComplete.dispatchInProgress(); @@ -697,7 +716,8 @@ export class Embeddable style={input.style} executionContext={this.getExecutionContext()} canEdit={this.getIsEditable() && input.viewMode === 'edit'} - onRuntimeError={() => { + onRuntimeError={(message) => { + this.updateOutput({ error: new Error(message) }); this.logError('runtime'); }} noPadding={this.visDisplayOptions?.noPadding} diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index cef9e7d698faa..7904ba4c38f14 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -79,7 +79,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { getDisplayName() { return i18n.translate('xpack.lens.embeddableDisplayName', { - defaultMessage: 'lens', + defaultMessage: 'Lens', }); } diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx index 3f10fba310b0c..cbb1fedf75497 100644 --- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx @@ -44,7 +44,7 @@ export interface ExpressionWrapperProps { style?: React.CSSProperties; className?: string; canEdit: boolean; - onRuntimeError: () => void; + onRuntimeError: (message?: string) => void; executionContext?: KibanaExecutionContext; lensInspector: LensInspector; noPadding?: boolean; @@ -148,7 +148,9 @@ export function ExpressionWrapper({ syncCursor={syncCursor} executionContext={executionContext} renderError={(errorMessage, error) => { - onRuntimeError(); + const messages = getOriginalRequestErrorMessages(error); + onRuntimeError(messages[0] ?? errorMessage); + return (
@@ -156,7 +158,7 @@ export function ExpressionWrapper({ - {(getOriginalRequestErrorMessages(error) || [errorMessage]).map((message) => ( + {messages.map((message) => ( {message} ))} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 9e334bb8ec7f8..0a57b7c651d19 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -6140,7 +6140,7 @@ "visualizations.deprecatedTag": "Déclassé", "visualizations.disabledLabVisualizationLink": "Lire la documentation", "visualizations.disabledLabVisualizationMessage": "Veuillez activer le mode lab dans les paramètres avancés pour consulter les visualisations lab.", - "visualizations.displayName": "Visualisation", + "visualizations.displayName": "visualisation", "visualizations.editor.createBreadcrumb": "Créer", "visualizations.editor.defaultEditBreadcrumbText": "Modifier la visualisation", "visualizations.embeddable.inspectorTitle": "Inspecteur", @@ -6175,7 +6175,6 @@ "visualizations.listing.table.typeColumnName": "Type", "visualizations.listingPageTitle": "Bibliothèque Visualize", "visualizations.missedDataView.dataViewReconfigure": "Recréez-la dans la page de gestion des vues de données.", - "visualizations.missedDataView.editInVisualizeEditor": "Effectuer des modifications dans l'éditeur Visualize pour corriger l'erreur", "visualizations.newChart.conditionalMessage.advancedSettingsLink": "Paramètres avancés.", "visualizations.newChart.libraryMode.new": "nouveau", "visualizations.newChart.libraryMode.old": "âge", @@ -17450,7 +17449,7 @@ "xpack.lens.embeddable.missingTimeRangeParam.longMessage": "La propriété timeRange est requise pour cette configuration.", "xpack.lens.embeddable.missingTimeRangeParam.shortMessage": "Propriété timeRange manquante", "xpack.lens.embeddable.moreErrors": "Effectuez des modifications dans l'éditeur Lens pour afficher plus d'erreurs", - "xpack.lens.embeddableDisplayName": "lens", + "xpack.lens.embeddableDisplayName": "Lens", "xpack.lens.endValue.nearest": "La plus proche", "xpack.lens.endValue.none": "Masquer", "xpack.lens.endValue.zero": "Zéro", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3ed2387f36680..0b4d191d99bcf 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6169,7 +6169,6 @@ "visualizations.listing.table.typeColumnName": "型", "visualizations.listingPageTitle": "Visualizeライブラリ", "visualizations.missedDataView.dataViewReconfigure": "データビュー管理ページで再作成", - "visualizations.missedDataView.editInVisualizeEditor": "Visualizeエディターで編集し、エラーを修正", "visualizations.newChart.conditionalMessage.advancedSettingsLink": "高度な設定", "visualizations.newChart.libraryMode.new": "新規", "visualizations.newChart.libraryMode.old": "古", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e7194fc92904e..a44380c72b813 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6176,7 +6176,6 @@ "visualizations.listing.table.typeColumnName": "类型", "visualizations.listingPageTitle": "Visualize 库", "visualizations.missedDataView.dataViewReconfigure": "在数据视图管理页面中重新创建", - "visualizations.missedDataView.editInVisualizeEditor": "在 Visualize 编辑器中编辑以修复该错误", "visualizations.newChart.conditionalMessage.advancedSettingsLink": "免费的 API 密钥。", "visualizations.newChart.libraryMode.new": "新", "visualizations.newChart.libraryMode.old": "以前", @@ -17458,7 +17457,7 @@ "xpack.lens.embeddable.missingTimeRangeParam.longMessage": "给定配置需要包含 timeRange 属性", "xpack.lens.embeddable.missingTimeRangeParam.shortMessage": "缺少 timeRange 属性", "xpack.lens.embeddable.moreErrors": "在 Lens 编辑器中编辑以查看更多错误", - "xpack.lens.embeddableDisplayName": "lens", + "xpack.lens.embeddableDisplayName": "Lens", "xpack.lens.endValue.nearest": "最近", "xpack.lens.endValue.none": "隐藏", "xpack.lens.endValue.zero": "零",