mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Infra] Display friendlier Lens generated error messages (#189099)
closes [#189064](https://github.com/elastic/kibana/issues/189064) ## Summary This PR changes Lens embeddable to support creating custom error messages by using the new `customBadgeMessages` prop. <img width="500px" alt="image" src="https://github.com/user-attachments/assets/d48d86fa-b4a7-4dd5-ad84-9fe4bf144672"> The custom error message will only be displayed in the context of the hosts view and asset details, which is where this new prop is being used. Everywhere else will display the default error handling provided by Lens <img width="500px" alt="image" src="https://github.com/user-attachments/assets/38ecaee9-5f25-4d34-85e4-f176095982c5"> ### How to test - Start a local kibana and es instances - Run `node scripts/synthtrace infra_hosts_with_apm_hosts --live` - Change the the index patter in Settings to `metrics-apm` - Navigate to Infrastructure > Hosts View and open the asset details flyout - The missing field message should be displayed as the screenshot above - Open a chart in lens - The default Lens message will be shown --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
829d22cd70
commit
c07595fe00
9 changed files with 233 additions and 15 deletions
|
@ -107,7 +107,7 @@ export const MessageList = ({
|
|||
closePopover={closePopover}
|
||||
>
|
||||
<ul className="lnsWorkspaceWarningList">
|
||||
{messages.map((message, index) => (
|
||||
{messages.map(({ hidePopoverIcon = false, ...message }, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="lnsWorkspaceWarningList__item"
|
||||
|
@ -121,13 +121,15 @@ export const MessageList = ({
|
|||
responsive={false}
|
||||
className="lnsWorkspaceWarningList__textItem"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{message.severity === 'error' ? (
|
||||
<EuiIcon type="error" color="danger" />
|
||||
) : (
|
||||
<EuiIcon type="alert" color="warning" />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{!hidePopoverIcon && (
|
||||
<EuiFlexItem grow={false}>
|
||||
{message.severity === 'error' ? (
|
||||
<EuiIcon type="error" color="danger" />
|
||||
) : (
|
||||
<EuiIcon type="alert" color="warning" />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={1} className="lnsWorkspaceWarningList__description">
|
||||
<EuiText size="s">{message.longMessage}</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -32,6 +32,7 @@ import { act } from 'react-dom/test-utils';
|
|||
import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks';
|
||||
import { Visualization } from '../types';
|
||||
import { createMockDatasource, createMockVisualization } from '../mocks';
|
||||
import { FIELD_NOT_FOUND, FIELD_WRONG_TYPE } from '../user_messages_ids';
|
||||
|
||||
jest.mock('@kbn/inspector-plugin/public', () => ({
|
||||
isAvailable: false,
|
||||
|
@ -272,6 +273,90 @@ describe('embeddable', () => {
|
|||
expect(expressionRenderer).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should override embeddableBadge message', async () => {
|
||||
const getBadgeMessage = jest.fn(
|
||||
(): ReturnType<NonNullable<LensEmbeddableInput['onBeforeBadgesRender']>> => [
|
||||
{
|
||||
uniqueId: FIELD_NOT_FOUND,
|
||||
severity: 'warning',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [
|
||||
{ id: 'embeddableBadge' },
|
||||
{ id: 'dimensionButton', dimensionId: '1' },
|
||||
],
|
||||
longMessage: 'custom',
|
||||
shortMessage: '',
|
||||
hidePopoverIcon: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
const embeddable = new Embeddable(
|
||||
getEmbeddableProps({
|
||||
datasourceMap: {
|
||||
...defaultDatasourceMap,
|
||||
[defaultDatasourceId]: {
|
||||
...defaultDatasourceMap[defaultDatasourceId],
|
||||
getUserMessages: jest.fn(() => [
|
||||
{
|
||||
uniqueId: FIELD_NOT_FOUND,
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [
|
||||
{ id: 'embeddableBadge' },
|
||||
{ id: 'dimensionButton', dimensionId: '1' },
|
||||
],
|
||||
longMessage: 'original',
|
||||
shortMessage: '',
|
||||
},
|
||||
{
|
||||
uniqueId: FIELD_WRONG_TYPE,
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'visualization' }],
|
||||
longMessage: 'original',
|
||||
shortMessage: '',
|
||||
},
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
onBeforeBadgesRender: getBadgeMessage as LensEmbeddableInput['onBeforeBadgesRender'],
|
||||
} as LensEmbeddableInput
|
||||
);
|
||||
|
||||
const getUserMessagesSpy = jest.spyOn(embeddable, 'getUserMessages');
|
||||
await embeddable.initializeSavedVis({} as LensEmbeddableInput);
|
||||
|
||||
embeddable.render(mountpoint);
|
||||
|
||||
expect(getUserMessagesSpy.mock.results.flatMap((r) => r.value)).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
uniqueId: FIELD_WRONG_TYPE,
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'visualization' }],
|
||||
longMessage: 'original',
|
||||
shortMessage: '',
|
||||
},
|
||||
{
|
||||
uniqueId: FIELD_NOT_FOUND,
|
||||
severity: 'warning',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [
|
||||
{ id: 'embeddableBadge' },
|
||||
{ id: 'dimensionButton', dimensionId: '1' },
|
||||
],
|
||||
longMessage: 'custom',
|
||||
shortMessage: '',
|
||||
hidePopoverIcon: true,
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should not render the vis if loaded saved object conflicts', async () => {
|
||||
attributeService.unwrapAttributes = jest.fn(
|
||||
(_input: LensByValueInput | LensByReferenceInput) => {
|
||||
|
|
|
@ -185,6 +185,7 @@ interface LensBaseEmbeddableInput extends EmbeddableInput {
|
|||
data: Simplify<LensTableRowContextMenuEvent['data'] & PreventableEvent>
|
||||
) => void;
|
||||
abortController?: AbortController;
|
||||
onBeforeBadgesRender?: (userMessages: UserMessage[]) => UserMessage[];
|
||||
}
|
||||
|
||||
export type LensByValueInput = {
|
||||
|
@ -610,6 +611,22 @@ export class Embeddable
|
|||
|
||||
private fullAttributes: LensSavedObjectAttributes | undefined;
|
||||
|
||||
private handleExternalUserMessage = (messages: UserMessage[]) => {
|
||||
if (this.input.onBeforeBadgesRender) {
|
||||
// we need something else to better identify those errors
|
||||
const [messagesToHandle, originalMessages] = partition(messages, (message) =>
|
||||
message.displayLocations.some((location) => location.id === 'embeddableBadge')
|
||||
);
|
||||
|
||||
if (messagesToHandle.length > 0) {
|
||||
const customBadgeMessages = this.input.onBeforeBadgesRender(messagesToHandle);
|
||||
return [...originalMessages, ...customBadgeMessages];
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
};
|
||||
|
||||
public getUserMessages: UserMessagesGetter = (locationId, filters) => {
|
||||
const userMessages: UserMessage[] = [];
|
||||
userMessages.push(
|
||||
|
@ -637,8 +654,9 @@ export class Embeddable
|
|||
);
|
||||
|
||||
if (!this.savedVis) {
|
||||
return userMessages;
|
||||
return this.handleExternalUserMessage(userMessages);
|
||||
}
|
||||
|
||||
const mergedSearchContext = this.getMergedSearchContext();
|
||||
|
||||
const framePublicAPI: FramePublicAPI = {
|
||||
|
@ -683,10 +701,12 @@ export class Embeddable
|
|||
}) ?? [])
|
||||
);
|
||||
|
||||
return filterAndSortUserMessages(
|
||||
[...userMessages, ...Object.values(this.additionalUserMessages)],
|
||||
locationId,
|
||||
filters ?? {}
|
||||
return this.handleExternalUserMessage(
|
||||
filterAndSortUserMessages(
|
||||
[...userMessages, ...Object.values(this.additionalUserMessages)],
|
||||
locationId,
|
||||
filters ?? {}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ export type {
|
|||
Visualization,
|
||||
VisualizationSuggestion,
|
||||
Suggestion,
|
||||
UserMessage,
|
||||
} from './types';
|
||||
export type {
|
||||
LegacyMetricState as MetricState,
|
||||
|
|
|
@ -300,6 +300,7 @@ export type UserMessagesDisplayLocationId = UserMessageDisplayLocation['id'];
|
|||
export interface UserMessage {
|
||||
uniqueId: string;
|
||||
severity: 'error' | 'warning' | 'info';
|
||||
hidePopoverIcon?: boolean;
|
||||
shortMessage: string;
|
||||
longMessage: string | React.ReactNode | ((closePopover: () => void) => React.ReactNode);
|
||||
fixableInEditor: boolean;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
export const HOST_METRICS_DOC_HREF = 'https://ela.st/docs-infra-host-metrics';
|
||||
export const HOST_METRICS_DOTTED_LINES_DOC_HREF = 'https://ela.st/docs-infra-why-dotted';
|
||||
export const CONTAINER_METRICS_DOC_HREF = 'https://ela.st/docs-infra-docker-container-metrics';
|
||||
export const HOST_MISSING_FIELDS = 'https://ela.st/hosts-missing-fields';
|
||||
|
||||
export const KPI_CHART_HEIGHT = 150;
|
||||
export const METRIC_CHART_HEIGHT = 300;
|
||||
|
|
|
@ -4,15 +4,25 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiPanel, EuiToolTip, type EuiPanelProps } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
EuiPanel,
|
||||
EuiToolTip,
|
||||
EuiText,
|
||||
EuiHorizontalRule,
|
||||
EuiLink,
|
||||
type EuiPanelProps,
|
||||
} from '@elastic/eui';
|
||||
import { Action } from '@kbn/ui-actions-plugin/public';
|
||||
import { UserMessage } from '@kbn/lens-plugin/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { css } from '@emotion/react';
|
||||
import { useLensAttributes, type UseLensAttributesParams } from '../../hooks/use_lens_attributes';
|
||||
import type { BaseChartProps } from './types';
|
||||
import type { TooltipContentProps } from './metric_explanation/tooltip_content';
|
||||
import { LensWrapper } from './lens_wrapper';
|
||||
import { ChartLoadError } from './chart_load_error';
|
||||
import { HOST_MISSING_FIELDS } from '../../common/visualizations/constants';
|
||||
|
||||
const MIN_HEIGHT = 300;
|
||||
|
||||
|
@ -54,6 +64,54 @@ export const LensChart = React.memo(
|
|||
searchSessionId,
|
||||
});
|
||||
|
||||
const handleBeforeBadgesRender = useCallback((messages: UserMessage[]) => {
|
||||
const missingFieldsMessage = messages.find(
|
||||
(m) => m.uniqueId === 'field_not_found' && m.severity === 'error'
|
||||
);
|
||||
return missingFieldsMessage
|
||||
? [
|
||||
{
|
||||
...missingFieldsMessage,
|
||||
severity: 'warning' as const,
|
||||
hidePopoverIcon: true,
|
||||
longMessage: (
|
||||
<>
|
||||
<EuiText size="s">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.lens.customErrorHandler.title"
|
||||
defaultMessage="No results found"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiText size="xs" data-test-subj="infraLensCustomErrorHanlderText">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.lens.customErrorHandler.description"
|
||||
defaultMessage="To display this chart, please ensure you are collecting the following fields:"
|
||||
/>
|
||||
</p>
|
||||
<p>{missingFieldsMessage && missingFieldsMessage.longMessage}</p>
|
||||
</EuiText>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiLink
|
||||
data-test-subj="infraLensCustomErrorHanlderLink"
|
||||
href={HOST_MISSING_FIELDS}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.customErrorHandler.learnMoreLink"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
: messages;
|
||||
}, []);
|
||||
|
||||
const lens = (
|
||||
<LensWrapper
|
||||
id={id}
|
||||
|
@ -70,6 +128,7 @@ export const LensChart = React.memo(
|
|||
onBrushEnd={onBrushEnd}
|
||||
searchSessionId={searchSessionId}
|
||||
onFilter={onFilter}
|
||||
onBeforeBadgesRender={handleBeforeBadgesRender}
|
||||
/>
|
||||
);
|
||||
const content = !toolTip ? (
|
||||
|
|
|
@ -47,6 +47,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
const kibanaServer = getService('kibanaServer');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const infraSynthtraceKibanaClient = getService('infraSynthtraceKibanaClient');
|
||||
const infraSourceConfigurationForm = getService('infraSourceConfigurationForm');
|
||||
const esClient = getService('es');
|
||||
const retry = getService('retry');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
@ -102,6 +103,42 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
};
|
||||
|
||||
describe('Node Details', () => {
|
||||
describe('#Missing fields', function () {
|
||||
before(async () => {
|
||||
await pageObjects.common.navigateToUrlWithBrowserHistory('infraOps', '/settings');
|
||||
|
||||
const metricIndicesInput = await infraSourceConfigurationForm.getMetricIndicesInput();
|
||||
await metricIndicesInput.clearValueWithKeyboard({ charByChar: true });
|
||||
await metricIndicesInput.type('metrics-apm*');
|
||||
|
||||
await infraSourceConfigurationForm.saveInfraSettings();
|
||||
await pageObjects.infraHome.waitForLoading();
|
||||
await pageObjects.infraHome.getInfraMissingMetricsIndicesCallout();
|
||||
|
||||
await navigateToNodeDetails('Jennys-MBP.fritz.box', 'Jennys-MBP.fritz.box', 'host');
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await pageObjects.common.navigateToUrlWithBrowserHistory('infraOps', '/settings');
|
||||
|
||||
const metricIndicesInput = await infraSourceConfigurationForm.getMetricIndicesInput();
|
||||
await metricIndicesInput.clearValueWithKeyboard({ charByChar: true });
|
||||
await metricIndicesInput.type('metrics-*,metricbeat-*');
|
||||
|
||||
await infraSourceConfigurationForm.saveInfraSettings();
|
||||
|
||||
await pageObjects.infraHome.waitForLoading();
|
||||
await pageObjects.infraHome.getInfraMissingRemoteClusterIndicesCallout();
|
||||
});
|
||||
|
||||
describe('KPIs', () => {
|
||||
it('should render custom badge message', async () => {
|
||||
await pageObjects.assetDetails.getAssetDetailsKPIMissingFieldMessageExists('cpuUsage');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#With Asset Details', () => {
|
||||
before(async () => {
|
||||
await Promise.all([
|
||||
|
|
|
@ -40,6 +40,18 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) {
|
|||
return div.getAttribute('title');
|
||||
},
|
||||
|
||||
async getAssetDetailsKPIMissingFieldMessageExists(type: string) {
|
||||
const element = await testSubjects.find(`infraAssetDetailsKPI${type}`);
|
||||
const badge = await element.findByTestSubject('lens-message-list-trigger');
|
||||
|
||||
await badge.click();
|
||||
|
||||
await testSubjects.existOrFail('lens-message-list-warning');
|
||||
await testSubjects.existOrFail('infraLensCustomErrorHanlderText');
|
||||
|
||||
await badge.click();
|
||||
},
|
||||
|
||||
async overviewAlertsTitleExists() {
|
||||
return testSubjects.existOrFail('infraAssetDetailsAlertsTitle');
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue