[8.7] [Lens] better support for user messages on embeddable (#149458) (#153043)

# Backport

This will backport the following commits from `main` to `8.7`:
- [[Lens] better support for user messages on embeddable
(#149458)](https://github.com/elastic/kibana/pull/149458)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Drew
Tate","email":"drew.tate@elastic.co"},"sourceCommit":{"committedDate":"2023-02-15T14:23:59Z","message":"[Lens]
better support for user messages on embeddable
(#149458)","sha":"24efb8597e8ed598f930373a32238e4b54c7309c","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Visualizations","release_note:skip","Feature:Lens","backport:prev-minor","v8.8.0"],"number":149458,"url":"https://github.com/elastic/kibana/pull/149458","mergeCommit":{"message":"[Lens]
better support for user messages on embeddable
(#149458)","sha":"24efb8597e8ed598f930373a32238e4b54c7309c"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/149458","number":149458,"mergeCommit":{"message":"[Lens]
better support for user messages on embeddable
(#149458)","sha":"24efb8597e8ed598f930373a32238e4b54c7309c"}}]}]
BACKPORT-->

Co-authored-by: Drew Tate <drew.tate@elastic.co>
This commit is contained in:
Kibana Machine 2023-03-09 13:03:13 -05:00 committed by GitHub
parent f484f21039
commit 17e1a1ebb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 211 additions and 168 deletions

View file

@ -77,7 +77,8 @@ Array [
href="fake/url"
style={
Object {
"display": "block",
"textAlign": "center",
"width": "100%",
}
}
>

View file

@ -153,7 +153,7 @@ function getMissingIndexPatternsErrors(
href={core.application.getUrlForApp('management', {
path: '/kibana/indexPatterns/create',
})}
style={{ display: 'block' }}
style={{ width: '100%', textAlign: 'center' }}
data-test-subj="configuration-failure-reconfigure-indexpatterns"
>
{i18n.translate('xpack.lens.editorFrame.dataViewReconfigure', {

View file

@ -74,6 +74,8 @@ import {
MultiClickTriggerEvent,
} from '@kbn/charts-plugin/public';
import { DataViewSpec } from '@kbn/data-views-plugin/common';
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
import { EuiEmptyPrompt } from '@elastic/eui';
import { useEuiFontSize, useEuiTheme } from '@elastic/eui';
import { getExecutionContextEvents, trackUiCounterEvents } from '../lens_ui_telemetry';
import { Document } from '../persistence';
@ -97,6 +99,7 @@ import {
AddUserMessages,
isMessageRemovable,
UserMessagesGetter,
UserMessagesDisplayLocationId,
} from '../types';
import { getEditPath, DOC_TYPE } from '../../common';
@ -195,6 +198,52 @@ export interface ViewUnderlyingDataArgs {
columns: string[];
}
function VisualizationErrorPanel({ errors, canEdit }: { errors: UserMessage[]; canEdit: boolean }) {
const showMore = errors.length > 1;
const canFixInLens = canEdit && errors.some(({ fixableInEditor }) => fixableInEditor);
return (
<div className="lnsEmbeddedError">
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
data-test-subj="embeddable-lens-failure"
body={
<>
{errors.length ? (
<>
<p>{errors[0].longMessage}</p>
{showMore && !canFixInLens ? (
<p>
<FormattedMessage
id="xpack.lens.embeddable.moreErrors"
defaultMessage="Edit in Lens editor to see more errors"
/>
</p>
) : null}
{canFixInLens ? (
<p>
<FormattedMessage
id="xpack.lens.embeddable.fixErrors"
defaultMessage="Edit in Lens editor to fix the error"
/>
</p>
) : null}
</>
) : (
<p>
<FormattedMessage
id="xpack.lens.embeddable.failure"
defaultMessage="Visualization couldn't be displayed"
/>
</p>
)}
</>
}
/>
</div>
);
}
const getExpressionFromDocument = async (
document: Document,
documentToExpression: LensEmbeddableDeps['documentToExpression']
@ -297,6 +346,27 @@ const EmbeddableMessagesPopover = ({ messages }: { messages: UserMessage[] }) =>
);
};
const blockingMessageDisplayLocations: UserMessagesDisplayLocationId[] = [
'visualization',
'visualizationOnEmbeddable',
];
const MessagesBadge = ({ onMount }: { onMount: (el: HTMLDivElement) => void }) => (
<div
css={css({
position: 'absolute',
zIndex: 2,
left: 0,
bottom: 0,
})}
ref={(el) => {
if (el) {
onMount(el);
}
}}
/>
);
export class Embeddable
extends AbstractEmbeddable<LensEmbeddableInput, LensEmbeddableOutput>
implements
@ -312,7 +382,6 @@ export class Embeddable
private savedVis: Document | undefined;
private expression: string | undefined | null;
private domNode: HTMLElement | Element | undefined;
private badgeDomNode: HTMLElement | Element | undefined;
private subscription: Subscription;
private isInitialized = false;
private inputReloadSubscriptions: Subscription[];
@ -495,10 +564,6 @@ export class Embeddable
);
};
private get hasAnyErrors() {
return this.getUserMessages(undefined, { severity: 'error' }).length > 0;
}
private _userMessages: UserMessage[] = [];
// loads all available user messages
@ -575,7 +640,7 @@ export class Embeddable
if (addedMessageIds.length) {
this.additionalUserMessages = newMessageMap;
this.renderBadgeMessages();
this.renderUserMessages();
}
return () => {
@ -829,22 +894,22 @@ export class Embeddable
this.domNode.setAttribute('data-shared-item', '');
const errors = this.getUserMessages(['visualization', 'visualizationOnEmbeddable'], {
const blockingErrors = this.getUserMessages(blockingMessageDisplayLocations, {
severity: 'error',
});
this.updateOutput({
loading: true,
error: errors.length
error: blockingErrors.length
? new Error(
typeof errors[0].longMessage === 'string'
? errors[0].longMessage
: errors[0].shortMessage
typeof blockingErrors[0].longMessage === 'string'
? blockingErrors[0].longMessage
: blockingErrors[0].shortMessage
)
: undefined,
});
if (errors.length) {
if (blockingErrors.length) {
this.renderComplete.dispatchError();
} else {
this.renderComplete.dispatchInProgress();
@ -852,59 +917,94 @@ export class Embeddable
const input = this.getInput();
render(
<KibanaThemeProvider theme$={this.deps.theme.theme$}>
<ExpressionWrapper
ExpressionRenderer={this.expressionRenderer}
expression={this.expression || null}
errors={errors}
lensInspector={this.lensInspector}
searchContext={this.getMergedSearchContext()}
variables={{
embeddableTitle: this.getTitle(),
...(input.palette ? { theme: { palette: input.palette } } : {}),
}}
searchSessionId={this.externalSearchContext.searchSessionId}
handleEvent={this.handleEvent}
onData$={this.updateActiveData}
onRender$={this.onRender}
interactive={!input.disableTriggers}
renderMode={input.renderMode}
syncColors={input.syncColors}
syncTooltips={input.syncTooltips}
syncCursor={input.syncCursor}
hasCompatibleActions={this.hasCompatibleActions}
getCompatibleCellValueActions={this.getCompatibleCellValueActions}
className={input.className}
style={input.style}
executionContext={this.getExecutionContext()}
canEdit={this.getIsEditable() && input.viewMode === 'edit'}
onRuntimeError={(message) => {
this.updateOutput({ error: new Error(message) });
this.logError('runtime');
}}
noPadding={this.visDisplayOptions.noPadding}
/>
<div
css={css({
position: 'absolute',
zIndex: 2,
left: 0,
bottom: 0,
})}
ref={(el) => {
if (el) {
if (this.expression && !blockingErrors.length) {
render(
<>
<KibanaThemeProvider theme$={this.deps.theme.theme$}>
<ExpressionWrapper
ExpressionRenderer={this.expressionRenderer}
expression={this.expression || null}
lensInspector={this.lensInspector}
searchContext={this.getMergedSearchContext()}
variables={{
embeddableTitle: this.getTitle(),
...(input.palette ? { theme: { palette: input.palette } } : {}),
}}
searchSessionId={this.externalSearchContext.searchSessionId}
handleEvent={this.handleEvent}
onData$={this.updateActiveData}
onRender$={this.onRender}
interactive={!input.disableTriggers}
renderMode={input.renderMode}
syncColors={input.syncColors}
syncTooltips={input.syncTooltips}
syncCursor={input.syncCursor}
hasCompatibleActions={this.hasCompatibleActions}
getCompatibleCellValueActions={this.getCompatibleCellValueActions}
className={input.className}
style={input.style}
executionContext={this.getExecutionContext()}
addUserMessages={(messages) => this.addUserMessages(messages)}
onRuntimeError={(message) => {
this.updateOutput({ error: new Error(message) });
this.logError('runtime');
}}
noPadding={this.visDisplayOptions.noPadding}
/>
</KibanaThemeProvider>
<MessagesBadge
onMount={(el) => {
this.badgeDomNode = el;
this.renderBadgeMessages();
}
}}
/>
</KibanaThemeProvider>,
domNode
);
}}
/>
</>,
domNode
);
}
this.renderUserMessages();
}
private renderBadgeMessages() {
private renderUserMessages() {
const errors = this.getUserMessages(['visualization', 'visualizationOnEmbeddable'], {
severity: 'error',
});
if (errors.length && this.domNode) {
render(
<>
<KibanaThemeProvider theme$={this.deps.theme.theme$}>
<I18nProvider>
<VisualizationErrorPanel
errors={errors}
canEdit={this.getIsEditable() && this.input.viewMode === 'edit'}
/>
</I18nProvider>
</KibanaThemeProvider>
<MessagesBadge
onMount={(el) => {
this.badgeDomNode = el;
this.renderBadgeMessages();
}}
/>
</>,
this.domNode
);
}
this.renderBadgeMessages();
}
badgeDomNode?: HTMLDivElement;
/**
* This method is called on every render, and also whenever the badges dom node is created
* That happens after either the expression renderer or the visualization error panel is rendered.
*
* You should not call this method on its own. Use renderUserMessages instead.
*/
private renderBadgeMessages = () => {
const messages = this.getUserMessages('embeddableBadge');
if (messages.length && this.badgeDomNode) {
@ -915,7 +1015,7 @@ export class Embeddable
this.badgeDomNode
);
}
}
};
private readonly hasCompatibleActions = async (
event: ExpressionRendererEvent
@ -1178,7 +1278,10 @@ export class Embeddable
]);
}
if (this.hasAnyErrors) {
const blockingErrors = this.getUserMessages(blockingMessageDisplayLocations, {
severity: 'error',
});
if (blockingErrors.length) {
this.logError('validation');
}

View file

@ -7,8 +7,6 @@
import React from 'react';
import { I18nProvider } from '@kbn/i18n-react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon, EuiEmptyPrompt } from '@elastic/eui';
import {
ExpressionRendererEvent,
ReactExpressionRendererProps,
@ -20,12 +18,11 @@ import { DefaultInspectorAdapters, RenderMode } from '@kbn/expressions-plugin/co
import classNames from 'classnames';
import { getOriginalRequestErrorMessages } from '../editor_frame_service/error_helper';
import { LensInspector } from '../lens_inspector_service';
import { UserMessage } from '../types';
import { AddUserMessages } from '../types';
export interface ExpressionWrapperProps {
ExpressionRenderer: ReactExpressionRendererType;
expression: string | null;
errors: UserMessage[];
variables?: Record<string, unknown>;
interactive?: boolean;
searchContext: ExecutionContextSearch;
@ -44,64 +41,13 @@ export interface ExpressionWrapperProps {
getCompatibleCellValueActions?: ReactExpressionRendererProps['getCompatibleCellValueActions'];
style?: React.CSSProperties;
className?: string;
canEdit: boolean;
addUserMessages: AddUserMessages;
onRuntimeError: (message?: string) => void;
executionContext?: KibanaExecutionContext;
lensInspector: LensInspector;
noPadding?: boolean;
}
interface VisualizationErrorProps {
errors: ExpressionWrapperProps['errors'];
canEdit: boolean;
}
export function VisualizationErrorPanel({ errors, canEdit }: VisualizationErrorProps) {
const showMore = errors.length > 1;
const canFixInLens = canEdit && errors.some(({ fixableInEditor }) => fixableInEditor);
return (
<div className="lnsEmbeddedError">
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
data-test-subj="embeddable-lens-failure"
body={
<>
{errors.length ? (
<>
<p>{errors[0].longMessage}</p>
{showMore && !canFixInLens ? (
<p>
<FormattedMessage
id="xpack.lens.embeddable.moreErrors"
defaultMessage="Edit in Lens editor to see more errors"
/>
</p>
) : null}
{canFixInLens ? (
<p>
<FormattedMessage
id="xpack.lens.embeddable.fixErrors"
defaultMessage="Edit in Lens editor to fix the error"
/>
</p>
) : null}
</>
) : (
<p>
<FormattedMessage
id="xpack.lens.embeddable.failure"
defaultMessage="Visualization couldn't be displayed"
/>
</p>
)}
</>
}
/>
</div>
);
}
export function ExpressionWrapper({
ExpressionRenderer: ExpressionRendererComponent,
expression,
@ -120,60 +66,53 @@ export function ExpressionWrapper({
getCompatibleCellValueActions,
style,
className,
errors,
canEdit,
onRuntimeError,
addUserMessages,
executionContext,
lensInspector,
noPadding,
}: ExpressionWrapperProps) {
if (!expression) return null;
return (
<I18nProvider>
{errors.length || expression === null || expression === '' ? (
<VisualizationErrorPanel errors={errors} canEdit={canEdit} />
) : (
<div className={classNames('lnsExpressionRenderer', className)} style={style}>
<ExpressionRendererComponent
className="lnsExpressionRenderer__component"
padding={noPadding ? undefined : 's'}
variables={variables}
expression={expression}
interactive={interactive}
searchContext={searchContext}
searchSessionId={searchSessionId}
onData$={onData$}
onRender$={onRender$}
inspectorAdapters={lensInspector.adapters}
renderMode={renderMode}
syncColors={syncColors}
syncTooltips={syncTooltips}
syncCursor={syncCursor}
executionContext={executionContext}
renderError={(errorMessage, error) => {
const messages = getOriginalRequestErrorMessages(error);
onRuntimeError(messages[0] ?? errorMessage);
<div className={classNames('lnsExpressionRenderer', className)} style={style}>
<ExpressionRendererComponent
className="lnsExpressionRenderer__component"
padding={noPadding ? undefined : 's'}
variables={variables}
expression={expression}
interactive={interactive}
searchContext={searchContext}
searchSessionId={searchSessionId}
onData$={onData$}
onRender$={onRender$}
inspectorAdapters={lensInspector.adapters}
renderMode={renderMode}
syncColors={syncColors}
syncTooltips={syncTooltips}
syncCursor={syncCursor}
executionContext={executionContext}
renderError={(errorMessage, error) => {
const messages = getOriginalRequestErrorMessages(error);
addUserMessages(
messages.map((message) => ({
uniqueId: message,
severity: 'error',
displayLocations: [{ id: 'visualizationOnEmbeddable' }],
longMessage: message,
shortMessage: message,
fixableInEditor: false,
}))
);
onRuntimeError(messages[0] ?? errorMessage);
return (
<div data-test-subj="expression-renderer-error">
<EuiFlexGroup direction="column" alignItems="center" justifyContent="center">
<EuiFlexItem>
<EuiIcon type="alert" color="danger" />
</EuiFlexItem>
<EuiFlexItem>
{messages.map((message) => (
<EuiText size="s">{message}</EuiText>
))}
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
}}
onEvent={handleEvent}
hasCompatibleActions={hasCompatibleActions}
getCompatibleCellValueActions={getCompatibleCellValueActions}
/>
</div>
)}
return <></>; // the embeddable will take care of displaying the messages
}}
onEvent={handleEvent}
hasCompatibleActions={hasCompatibleActions}
getCompatibleCellValueActions={getCompatibleCellValueActions}
/>
</div>
</I18nProvider>
);
}

View file

@ -289,4 +289,4 @@
"type": "dashboard",
"updated_at": "2023-02-07T14:59:20.822Z",
"version": "WzM5MCwxXQ=="
}
}