mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# 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:
parent
f484f21039
commit
17e1a1ebb9
5 changed files with 211 additions and 168 deletions
|
@ -77,7 +77,8 @@ Array [
|
|||
href="fake/url"
|
||||
style={
|
||||
Object {
|
||||
"display": "block",
|
||||
"textAlign": "center",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -289,4 +289,4 @@
|
|||
"type": "dashboard",
|
||||
"updated_at": "2023-02-07T14:59:20.822Z",
|
||||
"version": "WzM5MCwxXQ=="
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue