>
+ >) = newCapabilities;
+ return core;
+ }
+
+ const irrelevantProps = {
+ dataViews: {} as DataViewsState,
+ visualizationMap: { foo: {} as Visualization },
+ visualization: { activeId: 'foo', state: {} },
+ };
+
+ it('generates error if missing an index pattern', () => {
+ expect(
+ getApplicationUserMessages({
+ visualizationType: '123',
+ activeDatasource: {
+ checkIntegrity: jest.fn(() => ['missing_pattern']),
+ } as unknown as Datasource,
+ activeDatasourceState: { state: {} },
+ core: createCoreStartWithPermissions(),
+ ...irrelevantProps,
+ })
+ ).toMatchSnapshot();
+ });
+
+ it('doesnt show a recreate link if user has no access', () => {
+ expect(
+ mountWithIntl(
+
+ {
+ getApplicationUserMessages({
+ visualizationType: '123',
+ activeDatasource: {
+ checkIntegrity: jest.fn(() => ['missing_pattern']),
+ } as unknown as Datasource,
+ activeDatasourceState: { state: {} },
+ // user can go to management, but indexPatterns management is not accessible
+ core: createCoreStartWithPermissions({
+ navLinks: { management: true },
+ management: { kibana: { indexPatterns: false } },
+ }),
+ ...irrelevantProps,
+ })[0].longMessage
+ }
+
+ ).exists(RedirectAppLinks)
+ ).toBeFalsy();
+
+ expect(
+ shallow(
+
+ {
+ getApplicationUserMessages({
+ visualizationType: '123',
+ activeDatasource: {
+ checkIntegrity: jest.fn(() => ['missing_pattern']),
+ } as unknown as Datasource,
+ activeDatasourceState: { state: {} },
+ // user can't go to management at all
+ core: createCoreStartWithPermissions({
+ navLinks: { management: false },
+ management: { kibana: { indexPatterns: true } },
+ }),
+ ...irrelevantProps,
+ })[0].longMessage
+ }
+
+ ).exists(RedirectAppLinks)
+ ).toBeFalsy();
+ });
+ });
+});
+
+describe('filtering user messages', () => {
+ const dimensionId1 = 'foo';
+ const dimensionId2 = 'baz';
+
+ const userMessages: UserMessage[] = [
+ {
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'dimensionTrigger', dimensionId: dimensionId1 }],
+ shortMessage: 'Warning on dimension 1!',
+ longMessage: '',
+ },
+ {
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'dimensionTrigger', dimensionId: dimensionId2 }],
+ shortMessage: 'Warning on dimension 2!',
+ longMessage: '',
+ },
+ {
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'banner' }],
+ shortMessage: 'Deprecation notice!',
+ longMessage: '',
+ },
+ {
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: 'Visualization error!',
+ longMessage: '',
+ },
+ {
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualizationInEditor' }],
+ shortMessage: 'Visualization editor error!',
+ longMessage: '',
+ },
+ {
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualizationOnEmbeddable' }],
+ shortMessage: 'Visualization embeddable warning!',
+ longMessage: '',
+ },
+ ];
+
+ it('filters by location', () => {
+ expect(filterUserMessages(userMessages, 'banner', {})).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "displayLocations": Array [
+ Object {
+ "id": "banner",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "",
+ "severity": "warning",
+ "shortMessage": "Deprecation notice!",
+ },
+ ]
+ `);
+ expect(
+ filterUserMessages(userMessages, 'dimensionTrigger', {
+ dimensionId: dimensionId1,
+ })
+ ).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "displayLocations": Array [
+ Object {
+ "dimensionId": "foo",
+ "id": "dimensionTrigger",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "",
+ "severity": "error",
+ "shortMessage": "Warning on dimension 1!",
+ },
+ ]
+ `);
+ expect(
+ filterUserMessages(userMessages, 'dimensionTrigger', {
+ dimensionId: dimensionId2,
+ })
+ ).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "displayLocations": Array [
+ Object {
+ "dimensionId": "baz",
+ "id": "dimensionTrigger",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "",
+ "severity": "warning",
+ "shortMessage": "Warning on dimension 2!",
+ },
+ ]
+ `);
+ expect(filterUserMessages(userMessages, ['visualization', 'visualizationInEditor'], {}))
+ .toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "displayLocations": Array [
+ Object {
+ "id": "visualization",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "",
+ "severity": "error",
+ "shortMessage": "Visualization error!",
+ },
+ Object {
+ "displayLocations": Array [
+ Object {
+ "id": "visualizationInEditor",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "",
+ "severity": "error",
+ "shortMessage": "Visualization editor error!",
+ },
+ ]
+ `);
+ });
+
+ it('filters by severity', () => {
+ const warnings = filterUserMessages(userMessages, undefined, { severity: 'warning' });
+ const errors = filterUserMessages(userMessages, undefined, { severity: 'error' });
+
+ expect(warnings.length + errors.length).toBe(userMessages.length);
+ expect(warnings.every((message) => message.severity === 'warning'));
+ expect(errors.every((message) => message.severity === 'error'));
+ });
+
+ it('filters by both', () => {
+ expect(
+ filterUserMessages(userMessages, ['visualization', 'visualizationOnEmbeddable'], {
+ severity: 'warning',
+ })
+ ).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "displayLocations": Array [
+ Object {
+ "id": "visualizationOnEmbeddable",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "",
+ "severity": "warning",
+ "shortMessage": "Visualization embeddable warning!",
+ },
+ ]
+ `);
+ });
+});
diff --git a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx
new file mode 100644
index 0000000000000..32faa1742bf0f
--- /dev/null
+++ b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx
@@ -0,0 +1,219 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
+import { FormattedMessage } from '@kbn/i18n-react';
+import type { CoreStart } from '@kbn/core/public';
+import type { DataViewsState, VisualizationState } from '../state_management';
+import type {
+ Datasource,
+ UserMessage,
+ UserMessageFilters,
+ UserMessagesDisplayLocationId,
+ VisualizationMap,
+} from '../types';
+import { getMissingIndexPattern } from '../editor_frame_service/editor_frame/state_helpers';
+
+/**
+ * Provides a place to register general user messages that don't belong in the datasource or visualization objects
+ */
+export const getApplicationUserMessages = ({
+ visualizationType,
+ visualization,
+ visualizationMap,
+ activeDatasource,
+ activeDatasourceState,
+ dataViews,
+ core,
+}: {
+ visualizationType: string | null | undefined;
+ visualization: VisualizationState;
+ visualizationMap: VisualizationMap;
+ activeDatasource: Datasource | null;
+ activeDatasourceState: { state: unknown } | null;
+ dataViews: DataViewsState;
+ core: CoreStart;
+}): UserMessage[] => {
+ const messages: UserMessage[] = [];
+
+ if (!visualizationType) {
+ messages.push(getMissingVisTypeError());
+ }
+
+ if (visualization.activeId && !visualizationMap[visualization.activeId]) {
+ messages.push(getUnknownVisualizationTypeError(visualization.activeId));
+ }
+
+ if (!activeDatasource) {
+ messages.push(getUnknownDatasourceTypeError());
+ }
+
+ const missingIndexPatterns = getMissingIndexPattern(
+ activeDatasource,
+ activeDatasourceState,
+ dataViews.indexPatterns
+ );
+
+ if (missingIndexPatterns.length) {
+ messages.push(...getMissingIndexPatternsErrors(core, missingIndexPatterns));
+ }
+
+ return messages;
+};
+
+function getMissingVisTypeError(): UserMessage {
+ return {
+ severity: 'warning',
+ displayLocations: [{ id: 'visualization' }],
+ fixableInEditor: true,
+ shortMessage: '',
+ longMessage: i18n.translate('xpack.lens.editorFrame.expressionMissingVisualizationType', {
+ defaultMessage: 'Visualization type not found.',
+ }),
+ };
+}
+
+function getUnknownVisualizationTypeError(visType: string): UserMessage {
+ return {
+ severity: 'error',
+ fixableInEditor: false,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: i18n.translate('xpack.lens.unknownVisType.shortMessage', {
+ defaultMessage: `Unknown visualization type`,
+ }),
+ longMessage: i18n.translate('xpack.lens.unknownVisType.longMessage', {
+ defaultMessage: `The visualization type {visType} could not be resolved.`,
+ values: {
+ visType,
+ },
+ }),
+ };
+}
+
+function getUnknownDatasourceTypeError(): UserMessage {
+ return {
+ severity: 'error',
+ fixableInEditor: false,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: i18n.translate('xpack.lens.unknownDatasourceType.shortMessage', {
+ defaultMessage: `Unknown datasource type`,
+ }),
+ longMessage: i18n.translate('xpack.lens.editorFrame.expressionMissingDatasource', {
+ defaultMessage: 'Could not find datasource for the visualization',
+ }),
+ };
+}
+
+function getMissingIndexPatternsErrors(
+ core: CoreStart,
+ missingIndexPatterns: string[]
+): UserMessage[] {
+ // Check for access to both Management app && specific indexPattern section
+ const { management: isManagementEnabled } = core.application.capabilities.navLinks;
+ const isIndexPatternManagementEnabled =
+ core.application.capabilities.management.kibana.indexPatterns;
+ const canFix = isManagementEnabled && isIndexPatternManagementEnabled;
+ return [
+ {
+ severity: 'error',
+ fixableInEditor: canFix,
+ displayLocations: [{ id: 'visualizationInEditor' }],
+ shortMessage: '',
+ longMessage: (
+ <>
+
+
+
+
+
+ {canFix && (
+
+
+ {i18n.translate('xpack.lens.editorFrame.dataViewReconfigure', {
+ defaultMessage: `Recreate it in the data view management page.`,
+ })}
+
+
+ )}
+
+ >
+ ),
+ },
+ {
+ severity: 'error',
+ fixableInEditor: canFix,
+ displayLocations: [{ id: 'visualizationOnEmbeddable' }],
+ shortMessage: '',
+ longMessage: i18n.translate('xpack.lens.editorFrame.expressionMissingDataView', {
+ defaultMessage:
+ 'Could not find the {count, plural, one {data view} other {data views}}: {ids}',
+ values: { count: missingIndexPatterns.length, ids: missingIndexPatterns.join(', ') },
+ }),
+ },
+ ];
+}
+
+export const filterUserMessages = (
+ userMessages: UserMessage[],
+ locationId: UserMessagesDisplayLocationId | UserMessagesDisplayLocationId[] | undefined,
+ { dimensionId, severity }: UserMessageFilters
+) => {
+ const locationIds = Array.isArray(locationId)
+ ? locationId
+ : typeof locationId === 'string'
+ ? [locationId]
+ : [];
+
+ return userMessages.filter((message) => {
+ if (locationIds.length) {
+ const hasMatch = message.displayLocations.some((location) => {
+ if (!locationIds.includes(location.id)) {
+ return false;
+ }
+
+ if (location.id === 'dimensionTrigger' && location.dimensionId !== dimensionId) {
+ return false;
+ }
+
+ return true;
+ });
+
+ if (!hasMatch) {
+ return false;
+ }
+ }
+
+ if (severity && message.severity !== severity) {
+ return false;
+ }
+
+ return true;
+ });
+};
diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx
index 4a498cbb23266..14eaaab75ba73 100644
--- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx
@@ -281,6 +281,7 @@ export const LensTopNavMenu = ({
indexPatternService,
currentDoc,
onTextBasedSavedAndExit,
+ getUserMessages,
shortUrlService,
isCurrentStateDirty,
}: LensTopNavMenuProps) => {
@@ -1032,21 +1033,9 @@ export const LensTopNavMenu = ({
textBasedLanguages: supportedTextBasedLanguages as DataViewPickerProps['textBasedLanguages'],
};
- // text based languages errors should also appear to the unified search bar
- const textBasedLanguageModeErrors: Error[] = [];
- if (activeDatasourceId && allLoaded) {
- if (
- datasourceMap[activeDatasourceId] &&
- datasourceMap[activeDatasourceId].getUnifiedSearchErrors
- ) {
- const errors = datasourceMap[activeDatasourceId].getUnifiedSearchErrors?.(
- datasourceStates[activeDatasourceId].state
- );
- if (errors) {
- textBasedLanguageModeErrors.push(...errors);
- }
- }
- }
+ const textBasedLanguageModeErrors = getUserMessages('textBasedLanguagesQueryInput', {
+ severity: 'error',
+ }).map(({ shortMessage }) => new Error(shortMessage));
return (
);
diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts
index 1411598c4033e..314bc3a2e52f5 100644
--- a/x-pack/plugins/lens/public/app_plugin/types.ts
+++ b/x-pack/plugins/lens/public/app_plugin/types.ts
@@ -13,6 +13,7 @@ import type {
ApplicationStart,
AppMountParameters,
ChromeStart,
+ CoreStart,
CoreTheme,
ExecutionContextStart,
HttpStart,
@@ -50,6 +51,7 @@ import type {
VisualizeEditorContext,
LensTopNavMenuEntryGenerator,
VisualizationMap,
+ UserMessagesGetter,
} from '../types';
import type { LensAttributeService } from '../lens_attribute_service';
import type { LensEmbeddableInput } from '../embeddable/embeddable';
@@ -82,6 +84,7 @@ export interface LensAppProps {
contextOriginatingApp?: string;
topNavMenuEntryGenerators: LensTopNavMenuEntryGenerator[];
theme$: Observable;
+ coreStart: CoreStart;
}
export type RunSave = (
@@ -121,6 +124,7 @@ export interface LensTopNavMenuProps {
theme$: Observable;
indexPatternService: IndexPatternServiceAPI;
onTextBasedSavedAndExit: ({ onSave }: { onSave: () => void }) => Promise;
+ getUserMessages: UserMessagesGetter;
shortUrlService: (params: LensAppLocatorParams) => Promise;
isCurrentStateDirty: boolean;
}
diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.tsx
index f4bb61b9758b6..533abb4bc4def 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { memo, useMemo } from 'react';
+import React, { memo } from 'react';
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public';
import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
@@ -15,12 +15,10 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DatasourceDimensionTriggerProps, DatasourceDimensionEditorProps } from '../../../types';
import { GenericIndexPatternColumn } from '../form_based';
-import { isColumnInvalid } from '../utils';
import { FormBasedPrivateState } from '../types';
import { DimensionEditor } from './dimension_editor';
import { DateRange } from '../../../../common';
import { getOperationSupportMatrix } from './operation_support';
-import { DimensionTrigger } from '../../../shared_components/dimension_trigger';
export type FormBasedDimensionTriggerProps =
DatasourceDimensionTriggerProps & {
@@ -42,43 +40,6 @@ export type FormBasedDimensionEditorProps =
dateRange: DateRange;
};
-function wrapOnDot(str?: string) {
- // u200B is a non-width white-space character, which allows
- // the browser to efficiently word-wrap right after the dot
- // without us having to draw a lot of extra DOM elements, etc
- return str ? str.replace(/\./g, '.\u200B') : '';
-}
-
-export const FormBasedDimensionTriggerComponent = function FormBasedDimensionTrigger(
- props: FormBasedDimensionTriggerProps
-) {
- const { columnId, uniqueLabel, invalid, invalidMessage, hideTooltip, layerId, dateRange } = props;
- const layer = props.state.layers[layerId];
- const currentIndexPattern = props.indexPatterns[layer.indexPatternId];
-
- const currentColumnHasErrors = useMemo(
- () => invalid || isColumnInvalid(layer, columnId, currentIndexPattern, dateRange),
- [layer, columnId, currentIndexPattern, invalid, dateRange]
- );
-
- const selectedColumn: GenericIndexPatternColumn | null = layer.columns[props.columnId] ?? null;
-
- if (!selectedColumn) {
- return null;
- }
- const formattedLabel = wrapOnDot(uniqueLabel);
-
- return (
-
- );
-};
-
export const FormBasedDimensionEditorComponent = function FormBasedDimensionPanel(
props: FormBasedDimensionEditorProps
) {
@@ -102,5 +63,4 @@ export const FormBasedDimensionEditorComponent = function FormBasedDimensionPane
);
};
-export const FormBasedDimensionTrigger = memo(FormBasedDimensionTriggerComponent);
export const FormBasedDimensionEditor = memo(FormBasedDimensionEditorComponent);
diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts
index 549dff69eabb1..5c5fe11ebfc94 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts
+++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { ReactElement } from 'react';
+import { ReactElement } from 'react';
import { SavedObjectReference } from '@kbn/core/public';
import { isFragment } from 'react-is';
import { coreMock } from '@kbn/core/public/mocks';
@@ -21,7 +21,14 @@ import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks';
import { TinymathAST } from '@kbn/tinymath';
import { getFormBasedDatasource, GenericIndexPatternColumn } from './form_based';
-import { DatasourcePublicAPI, Datasource, FramePublicAPI, OperationDescriptor } from '../../types';
+import {
+ DatasourcePublicAPI,
+ Datasource,
+ FramePublicAPI,
+ OperationDescriptor,
+ FrameDatasourceAPI,
+ UserMessage,
+} from '../../types';
import { getFieldByNameFactory } from './pure_helpers';
import {
operationDefinitionMap,
@@ -43,6 +50,7 @@ import { createMockedFullReference } from './operations/mocks';
import { cloneDeep } from 'lodash';
import { DatatableColumn } from '@kbn/expressions-plugin/common';
import { createMockFramePublicAPI } from '../../mocks';
+import { filterUserMessages } from '../../app_plugin/get_application_user_messages';
jest.mock('./loader');
jest.mock('../../id_generator');
@@ -3004,237 +3012,298 @@ describe('IndexPattern Data Source', () => {
});
});
- describe('#getErrorMessages', () => {
- it('should use the results of getErrorMessages directly when single layer', () => {
- (getErrorMessages as jest.Mock).mockClear();
- (getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']);
- const state: FormBasedPrivateState = {
- layers: {
- first: {
- indexPatternId: '1',
- columnOrder: [],
- columns: {},
+ describe('#getUserMessages', () => {
+ describe('error messages', () => {
+ it('should generate error messages for a single layer', () => {
+ (getErrorMessages as jest.Mock).mockClear();
+ (getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']);
+ const state: FormBasedPrivateState = {
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: [],
+ columns: {},
+ },
},
- },
- currentIndexPatternId: '1',
- };
- expect(FormBasedDatasource.getErrorMessages(state, indexPatterns)).toEqual([
- { longMessage: 'error 1', shortMessage: '' },
- { longMessage: 'error 2', shortMessage: '' },
- ]);
- expect(getErrorMessages).toHaveBeenCalledTimes(1);
- });
+ currentIndexPatternId: '1',
+ };
+ expect(
+ FormBasedDatasource.getUserMessages(state, {
+ frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
+ setState: () => {},
+ })
+ ).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "displayLocations": Array [
+ Object {
+ "id": "visualization",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "error 1",
+ "severity": "error",
+ "shortMessage": "",
+ },
+ Object {
+ "displayLocations": Array [
+ Object {
+ "id": "visualization",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "error 2",
+ "severity": "error",
+ "shortMessage": "",
+ },
+ ]
+ `);
+ expect(getErrorMessages).toHaveBeenCalledTimes(1);
+ });
- it('should prepend each error with its layer number on multi-layer chart', () => {
- (getErrorMessages as jest.Mock).mockClear();
- (getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']);
- const state: FormBasedPrivateState = {
- layers: {
- first: {
- indexPatternId: '1',
- columnOrder: [],
- columns: {},
- },
- second: {
- indexPatternId: '1',
- columnOrder: [],
- columns: {},
+ it('should prepend each error with its layer number on multi-layer chart', () => {
+ (getErrorMessages as jest.Mock).mockClear();
+ (getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']);
+ const state: FormBasedPrivateState = {
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: [],
+ columns: {},
+ },
+ second: {
+ indexPatternId: '1',
+ columnOrder: [],
+ columns: {},
+ },
},
- },
- currentIndexPatternId: '1',
- };
- expect(FormBasedDatasource.getErrorMessages(state, indexPatterns)).toEqual([
- { longMessage: 'Layer 1 error: error 1', shortMessage: '' },
- { longMessage: 'Layer 1 error: error 2', shortMessage: '' },
- ]);
- expect(getErrorMessages).toHaveBeenCalledTimes(2);
+ currentIndexPatternId: '1',
+ };
+ expect(
+ FormBasedDatasource.getUserMessages(state, {
+ frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
+ setState: () => {},
+ })
+ ).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "displayLocations": Array [
+ Object {
+ "id": "visualization",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage":
+ error 1
+ ,
+ }
+ }
+ />,
+ "severity": "error",
+ "shortMessage": "Layer 1 error: ",
+ },
+ Object {
+ "displayLocations": Array [
+ Object {
+ "id": "visualization",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage":
+ error 2
+ ,
+ }
+ }
+ />,
+ "severity": "error",
+ "shortMessage": "Layer 1 error: ",
+ },
+ ]
+ `);
+ expect(getErrorMessages).toHaveBeenCalledTimes(2);
+ });
});
- });
- describe('#getWarningMessages', () => {
- let state: FormBasedPrivateState;
- let framePublicAPI: FramePublicAPI;
+ describe('warning messages', () => {
+ let state: FormBasedPrivateState;
+ let framePublicAPI: FramePublicAPI;
- beforeEach(() => {
- const termsColumn: TermsIndexPatternColumn = {
- operationType: 'terms',
- dataType: 'number',
- isBucketed: true,
- label: '123211',
- sourceField: 'foo',
- params: {
- size: 10,
- orderBy: {
- type: 'alphabetical',
- },
- orderDirection: 'asc',
- },
- };
+ beforeEach(() => {
+ (getErrorMessages as jest.Mock).mockReturnValueOnce([]);
- state = {
- layers: {
- first: {
- indexPatternId: '1',
- columnOrder: ['col1', 'col2', 'col3', 'col4', 'col5', 'col6'],
- columns: {
- col1: {
- operationType: 'date_histogram',
- params: {
- interval: '12h',
+ const termsColumn: TermsIndexPatternColumn = {
+ operationType: 'terms',
+ dataType: 'number',
+ isBucketed: true,
+ label: '123211',
+ sourceField: 'foo',
+ params: {
+ size: 10,
+ orderBy: {
+ type: 'alphabetical',
+ },
+ orderDirection: 'asc',
+ },
+ };
+
+ state = {
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: ['col1', 'col2', 'col3', 'col4', 'col5', 'col6'],
+ columns: {
+ col1: {
+ operationType: 'date_histogram',
+ params: {
+ interval: '12h',
+ },
+ label: '',
+ dataType: 'date',
+ isBucketed: true,
+ sourceField: 'timestamp',
+ } as DateHistogramIndexPatternColumn,
+ col2: {
+ operationType: 'count',
+ label: '',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'records',
},
- label: '',
- dataType: 'date',
- isBucketed: true,
- sourceField: 'timestamp',
- } as DateHistogramIndexPatternColumn,
- col2: {
- operationType: 'count',
- label: '',
- dataType: 'number',
- isBucketed: false,
- sourceField: 'records',
- },
- col3: {
- operationType: 'count',
- timeShift: '1h',
- label: '',
- dataType: 'number',
- isBucketed: false,
- sourceField: 'records',
- },
- col4: {
- operationType: 'count',
- timeShift: '13h',
- label: '',
- dataType: 'number',
- isBucketed: false,
- sourceField: 'records',
- },
- col5: {
- operationType: 'count',
- timeShift: '1w',
- label: '',
- dataType: 'number',
- isBucketed: false,
- sourceField: 'records',
- },
- col6: {
- operationType: 'count',
- timeShift: 'previous',
- label: '',
- dataType: 'number',
- isBucketed: false,
- sourceField: 'records',
+ col3: {
+ operationType: 'count',
+ timeShift: '1h',
+ label: '',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'records',
+ },
+ col4: {
+ operationType: 'count',
+ timeShift: '13h',
+ label: '',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'records',
+ },
+ col5: {
+ operationType: 'count',
+ timeShift: '1w',
+ label: '',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'records',
+ },
+ col6: {
+ operationType: 'count',
+ timeShift: 'previous',
+ label: '',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'records',
+ },
+ termsCol: termsColumn,
},
- termsCol: termsColumn,
},
},
- },
- currentIndexPatternId: '1',
- };
+ currentIndexPatternId: '1',
+ };
- framePublicAPI = {
- activeData: {
- first: {
- type: 'datatable',
- rows: [],
- columns: [
- {
- id: 'col1',
- name: 'col1',
- meta: {
- type: 'date',
- source: 'esaggs',
- sourceParams: {
- type: 'date_histogram',
- params: {
- used_interval: '12h',
+ framePublicAPI = {
+ activeData: {
+ first: {
+ type: 'datatable',
+ rows: [],
+ columns: [
+ {
+ id: 'col1',
+ name: 'col1',
+ meta: {
+ type: 'date',
+ source: 'esaggs',
+ sourceParams: {
+ type: 'date_histogram',
+ params: {
+ used_interval: '12h',
+ },
},
},
},
- },
- {
- id: 'termsCol',
- name: 'termsCol',
- meta: {
- type: 'string',
- source: 'esaggs',
- sourceParams: {
- type: 'terms',
+ {
+ id: 'termsCol',
+ name: 'termsCol',
+ meta: {
+ type: 'string',
+ source: 'esaggs',
+ sourceParams: {
+ type: 'terms',
+ },
},
- },
- } as DatatableColumn,
- ],
+ } as DatatableColumn,
+ ],
+ },
},
- },
- dataViews: {
- ...createMockFramePublicAPI().dataViews,
- indexPatterns: expectedIndexPatterns,
- indexPatternRefs: Object.values(expectedIndexPatterns).map(({ id, title }) => ({
- id,
- title,
- })),
- },
- } as unknown as FramePublicAPI;
- });
-
- const extractTranslationIdsFromWarnings = (warnings: React.ReactNode[] | undefined) =>
- warnings?.map((item) =>
- isFragment(item)
- ? (item as ReactElement).props.children[0].props.id
- : (item as ReactElement).props.id
- );
-
- it('should return mismatched time shifts', () => {
- const warnings = FormBasedDatasource.getWarningMessages!(state, framePublicAPI, {}, () => {});
-
- expect(extractTranslationIdsFromWarnings(warnings)).toMatchInlineSnapshot(`
- Array [
- "xpack.lens.indexPattern.timeShiftSmallWarning",
- "xpack.lens.indexPattern.timeShiftMultipleWarning",
- ]
- `);
- });
+ dataViews: {
+ ...createMockFramePublicAPI().dataViews,
+ indexPatterns: expectedIndexPatterns,
+ indexPatternRefs: Object.values(expectedIndexPatterns).map(({ id, title }) => ({
+ id,
+ title,
+ })),
+ },
+ } as unknown as FramePublicAPI;
+ });
- it('should show different types of warning messages', () => {
- framePublicAPI.activeData!.first.columns[1].meta.sourceParams!.hasPrecisionError = true;
+ const extractTranslationIdsFromWarnings = (warnings: UserMessage[]) => {
+ const onlyWarnings = filterUserMessages(warnings, undefined, { severity: 'warning' });
+ return onlyWarnings.map(({ longMessage }) =>
+ isFragment(longMessage)
+ ? (longMessage as ReactElement).props.children[0].props.id
+ : (longMessage as ReactElement).props.id
+ );
+ };
- const warnings = FormBasedDatasource.getWarningMessages!(state, framePublicAPI, {}, () => {});
+ it('should return mismatched time shifts', () => {
+ const warnings = FormBasedDatasource.getUserMessages!(state, {
+ frame: framePublicAPI as FrameDatasourceAPI,
+ setState: () => {},
+ });
- expect(extractTranslationIdsFromWarnings(warnings)).toMatchInlineSnapshot(`
- Array [
- "xpack.lens.indexPattern.timeShiftSmallWarning",
- "xpack.lens.indexPattern.timeShiftMultipleWarning",
- "xpack.lens.indexPattern.precisionErrorWarning.accuracyDisabled",
- ]
- `);
- });
+ expect(extractTranslationIdsFromWarnings(warnings)).toMatchInlineSnapshot(`
+ Array [
+ "xpack.lens.indexPattern.timeShiftSmallWarning",
+ "xpack.lens.indexPattern.timeShiftMultipleWarning",
+ ]
+ `);
+ });
- it('should prepend each error with its layer number on multi-layer chart', () => {
- (getErrorMessages as jest.Mock).mockClear();
- (getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']);
+ it('should show different types of warning messages', () => {
+ framePublicAPI.activeData!.first.columns[1].meta.sourceParams!.hasPrecisionError = true;
- state = {
- layers: {
- first: {
- indexPatternId: '1',
- columnOrder: [],
- columns: {},
- },
- second: {
- indexPatternId: '1',
- columnOrder: [],
- columns: {},
- },
- },
- currentIndexPatternId: '1',
- };
+ const warnings = FormBasedDatasource.getUserMessages!(state, {
+ frame: framePublicAPI as FrameDatasourceAPI,
+ setState: () => {},
+ });
- expect(FormBasedDatasource.getErrorMessages(state, indexPatterns)).toEqual([
- { longMessage: 'Layer 1 error: error 1', shortMessage: '' },
- { longMessage: 'Layer 1 error: error 2', shortMessage: '' },
- ]);
- expect(getErrorMessages).toHaveBeenCalledTimes(2);
+ expect(extractTranslationIdsFromWarnings(warnings)).toMatchInlineSnapshot(`
+ Array [
+ "xpack.lens.indexPattern.timeShiftSmallWarning",
+ "xpack.lens.indexPattern.timeShiftMultipleWarning",
+ "xpack.lens.indexPattern.precisionErrorWarning.accuracyDisabled",
+ ]
+ `);
+ });
});
});
diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx
index e3f6df8e0887f..1ce6832f78109 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx
@@ -23,7 +23,7 @@ import { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
-import { EuiCallOut, EuiLink } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type {
DatasourceDimensionEditorProps,
@@ -38,6 +38,9 @@ import type {
IndexPatternRef,
DatasourceLayerSettingsProps,
DataSourceInfo,
+ UserMessage,
+ FrameDatasourceAPI,
+ StateSetter,
} from '../../types';
import {
changeIndexPattern,
@@ -50,12 +53,7 @@ import {
triggerActionOnIndexPatternChange,
} from './loader';
import { toExpression } from './to_expression';
-import {
- FormBasedDimensionTrigger,
- FormBasedDimensionEditor,
- getDropProps,
- onDrop,
-} from './dimension_panel';
+import { FormBasedDimensionEditor, getDropProps, onDrop } from './dimension_panel';
import { FormBasedDataPanel } from './datapanel';
import {
getDatasourceSuggestionsForField,
@@ -68,6 +66,7 @@ import {
getFiltersInLayer,
getShardFailuresWarningMessages,
getVisualDefaultsForLayer,
+ getDeprecatedSamplingWarningMessage,
isColumnInvalid,
cloneLayer,
} from './utils';
@@ -101,9 +100,17 @@ import { DOCUMENT_FIELD_NAME } from '../../../common/constants';
import { isColumnOfType } from './operations/definitions/helpers';
import { LayerSettingsPanel } from './layer_settings';
import { FormBasedLayer } from '../..';
+import { DimensionTrigger } from '../../shared_components/dimension_trigger';
export type { OperationType, GenericIndexPatternColumn } from './operations';
export { deleteColumn } from './operations';
+function wrapOnDot(str?: string) {
+ // u200B is a non-width white-space character, which allows
+ // the browser to efficiently word-wrap right after the dot
+ // without us having to draw a lot of extra DOM elements, etc
+ return str ? str.replace(/\./g, '.\u200B') : '';
+}
+
export function columnToOperation(
column: GenericIndexPatternColumn,
uniqueLabel?: string,
@@ -526,6 +533,8 @@ export function getFormBasedDatasource({
props: DatasourceDimensionTriggerProps
) => {
const columnLabelMap = formBasedDatasource.uniqueLabels(props.state);
+ const uniqueLabel = columnLabelMap[props.columnId];
+ const formattedLabel = wrapOnDot(uniqueLabel);
render(
@@ -542,7 +551,13 @@ export function getFormBasedDatasource({
unifiedSearch,
}}
>
-
+
,
@@ -815,119 +830,58 @@ export function getFormBasedDatasource({
getDatasourceSuggestionsForVisualizeField,
getDatasourceSuggestionsForVisualizeCharts,
- getErrorMessages(state, indexPatterns) {
+ getUserMessages(state, { frame: frameDatasourceAPI, setState }) {
if (!state) {
- return;
+ return [];
}
- // Forward the indexpattern as well, as it is required by some operationType checks
- const layerErrors = Object.entries(state.layers)
- .filter(([_, layer]) => !!indexPatterns[layer.indexPatternId])
- .map(([layerId, layer]) =>
- (
- getErrorMessages(
- layer,
- indexPatterns[layer.indexPatternId],
- state,
- layerId,
- core,
- data
- ) ?? []
- ).map((message) => ({
- shortMessage: '', // Not displayed currently
- longMessage: typeof message === 'string' ? message : message.message,
- fixAction: typeof message === 'object' ? message.fixAction : undefined,
- }))
- );
+ const layerErrorMessages = getLayerErrorMessages(
+ state,
+ frameDatasourceAPI,
+ setState,
+ core,
+ data
+ );
- // Single layer case, no need to explain more
- if (layerErrors.length <= 1) {
- return layerErrors[0]?.length ? layerErrors[0] : undefined;
- }
+ const dimensionErrorMessages = getDimensionErrorMessages(state, (layerId, columnId) =>
+ this.isValidColumn(state, frameDatasourceAPI.dataViews.indexPatterns, layerId, columnId)
+ );
- // For multiple layers we will prepend each error with the layer number
- const messages = layerErrors.flatMap((errors, index) => {
- return errors.map((error) => {
- const { shortMessage, longMessage } = error;
- return {
- shortMessage: shortMessage
- ? i18n.translate('xpack.lens.indexPattern.layerErrorWrapper', {
- defaultMessage: 'Layer {position} error: {wrappedMessage}',
- values: {
- position: index + 1,
- wrappedMessage: shortMessage,
- },
- })
- : '',
- longMessage: longMessage
- ? i18n.translate('xpack.lens.indexPattern.layerErrorWrapper', {
- defaultMessage: 'Layer {position} error: {wrappedMessage}',
- values: {
- position: index + 1,
- wrappedMessage: longMessage,
- },
- })
- : '',
+ const warningMessages = [
+ ...[
+ ...(getStateTimeShiftWarningMessages(
+ data.datatableUtilities,
+ state,
+ frameDatasourceAPI
+ ) || []),
+ ...getPrecisionErrorWarningMessages(
+ data.datatableUtilities,
+ state,
+ frameDatasourceAPI,
+ core.docLinks,
+ setState
+ ),
+ ].map((longMessage) => {
+ const message: UserMessage = {
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'toolbar' }],
+ shortMessage: '',
+ longMessage,
};
- });
- });
- return messages.length ? messages : undefined;
- },
- getWarningMessages: (state, frame, adapters, setState) => {
- return [
- ...(getStateTimeShiftWarningMessages(data.datatableUtilities, state, frame) || []),
- ...getPrecisionErrorWarningMessages(
- data.datatableUtilities,
- state,
- frame,
- core.docLinks,
- setState
- ),
+
+ return message;
+ }),
+ ...getDeprecatedSamplingWarningMessage(core),
];
+
+ return [...layerErrorMessages, ...dimensionErrorMessages, ...warningMessages];
},
+
getSearchWarningMessages: (state, warning, request, response) => {
return [...getShardFailuresWarningMessages(state, warning, request, response, core.theme)];
},
- getDeprecationMessages: () => {
- const deprecatedMessages: React.ReactNode[] = [];
- const useFieldExistenceSamplingKey = 'lens:useFieldExistenceSampling';
- const isUsingSampling = core.uiSettings.get(useFieldExistenceSamplingKey);
-
- if (isUsingSampling) {
- deprecatedMessages.push(
- {
- core.application.navigateToApp('management', {
- path: `/kibana/settings?query=${useFieldExistenceSamplingKey}`,
- });
- }}
- >
-
-
- ),
- }}
- />
- }
- />
- );
- }
- return deprecatedMessages;
- },
checkIntegrity: (state, indexPatterns) => {
const ids = Object.values(state.layers || {}).map(({ indexPatternId }) => indexPatternId);
return ids.filter((id) => !indexPatterns[id]);
@@ -1024,3 +978,125 @@ function blankLayer(indexPatternId: string, linkToLayers?: string[]): FormBasedL
sampling: 1,
};
}
+
+function getLayerErrorMessages(
+ state: FormBasedPrivateState,
+ frameDatasourceAPI: FrameDatasourceAPI,
+ setState: StateSetter,
+ core: CoreStart,
+ data: DataPublicPluginStart
+) {
+ const indexPatterns = frameDatasourceAPI.dataViews.indexPatterns;
+
+ const layerErrors: UserMessage[][] = Object.entries(state.layers)
+ .filter(([_, layer]) => !!indexPatterns[layer.indexPatternId])
+ .map(([layerId, layer]) =>
+ (
+ getErrorMessages(layer, indexPatterns[layer.indexPatternId], state, layerId, core, data) ??
+ []
+ ).map((error) => {
+ const message: UserMessage = {
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: '',
+ longMessage:
+ typeof error === 'string' ? (
+ error
+ ) : (
+ <>
+ {error.message}
+ {error.fixAction && (
+ {
+ const newState = await error.fixAction?.newState(frameDatasourceAPI);
+ if (newState) {
+ setState(newState);
+ }
+ }}
+ >
+ {error.fixAction?.label}
+
+ )}
+ >
+ ),
+ };
+
+ return message;
+ })
+ );
+
+ let errorMessages: UserMessage[];
+ if (layerErrors.length <= 1) {
+ // Single layer case, no need to explain more
+ errorMessages = layerErrors[0]?.length ? layerErrors[0] : [];
+ } else {
+ // For multiple layers we will prepend each error with the layer number
+ errorMessages = layerErrors.flatMap((errors, index) => {
+ return errors.map((error) => {
+ const message: UserMessage = {
+ ...error,
+ shortMessage: i18n.translate('xpack.lens.indexPattern.layerErrorWrapper', {
+ defaultMessage: 'Layer {position} error: {wrappedMessage}',
+ values: {
+ position: index + 1,
+ wrappedMessage: error.shortMessage,
+ },
+ }),
+ longMessage: (
+ {error.longMessage}>,
+ }}
+ />
+ ),
+ };
+
+ return message;
+ });
+ });
+ }
+
+ return errorMessages;
+}
+
+function getDimensionErrorMessages(
+ state: FormBasedPrivateState,
+ isValidColumn: (layerId: string, columnId: string) => boolean
+) {
+ // generate messages for invalid columns
+ const columnErrorMessages: UserMessage[] = Object.keys(state.layers)
+ .map((layerId) => {
+ const messages: UserMessage[] = [];
+ for (const columnId of Object.keys(state.layers[layerId].columns)) {
+ if (!isValidColumn(layerId, columnId)) {
+ messages.push({
+ severity: 'error',
+ displayLocations: [{ id: 'dimensionTrigger', dimensionId: columnId }],
+ fixableInEditor: true,
+ shortMessage: '',
+ longMessage: (
+
+ {i18n.translate('xpack.lens.configure.invalidConfigTooltip', {
+ defaultMessage: 'Invalid configuration.',
+ })}
+
+ {i18n.translate('xpack.lens.configure.invalidConfigTooltipClick', {
+ defaultMessage: 'Click for more details.',
+ })}
+
+ ),
+ });
+ }
+ }
+
+ return messages;
+ })
+ .flat();
+
+ return columnErrorMessages;
+}
diff --git a/x-pack/plugins/lens/public/datasources/form_based/utils.tsx b/x-pack/plugins/lens/public/datasources/form_based/utils.tsx
index dee8dd71592ce..e8d98516ec8a1 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/utils.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/utils.tsx
@@ -8,10 +8,10 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
-import type { DocLinksStart, ThemeServiceStart } from '@kbn/core/public';
+import type { CoreStart, DocLinksStart, ThemeServiceStart } from '@kbn/core/public';
import type { DatatableUtilitiesService } from '@kbn/data-plugin/common';
import { TimeRange } from '@kbn/es-query';
-import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
+import { EuiCallOut, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { groupBy, escape, uniq } from 'lodash';
@@ -26,7 +26,7 @@ import {
import { estypes } from '@elastic/elasticsearch';
import type { DateRange } from '../../../common/types';
-import type { FramePublicAPI, IndexPattern, StateSetter } from '../../types';
+import type { FramePublicAPI, IndexPattern, StateSetter, UserMessage } from '../../types';
import { renewIDs } from '../../utils';
import type { FormBasedLayer, FormBasedPersistedState, FormBasedPrivateState } from './types';
import type { ReferenceBasedIndexPatternColumn } from './operations/definitions/column_types';
@@ -186,7 +186,7 @@ export function getShardFailuresWarningMessages(
request: SearchRequest,
response: estypes.SearchResponse,
theme: ThemeServiceStart
-): Array {
+): UserMessage[] {
if (state) {
if (warning.type === 'shard_failure') {
switch (warning.reason.type) {
@@ -205,38 +205,55 @@ export function getShardFailuresWarningMessages(
].includes(col.operationType)
)
.map((col) => col.label)
- ).map((label) =>
- i18n.translate('xpack.lens.indexPattern.tsdbRollupWarning', {
- defaultMessage:
- '{label} uses a function that is unsupported by rolled up data. Select a different function or change the time range.',
- values: {
- label,
- },
- })
+ ).map(
+ (label) =>
+ ({
+ uniqueId: `unsupported_aggregation_on_downsampled_index--${label}`,
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'toolbar' }, { id: 'embeddableBadge' }],
+ shortMessage: '',
+ longMessage: i18n.translate('xpack.lens.indexPattern.tsdbRollupWarning', {
+ defaultMessage:
+ '{label} uses a function that is unsupported by rolled up data. Select a different function or change the time range.',
+ values: {
+ label,
+ },
+ }),
+ } as UserMessage)
)
);
default:
return [
- <>
-
- {warning.message}
- {warning.text}
-
-
- {warning.text ? (
- ({
- request: request as ShardFailureRequest,
- response,
- })}
- color="primary"
- isButtonEmpty={true}
- />
- ) : null}
- >,
+ {
+ uniqueId: `shard_failure`,
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'toolbar' }, { id: 'embeddableBadge' }],
+ shortMessage: '',
+ longMessage: (
+ <>
+
+ {warning.message}
+ {warning.text}
+
+
+ {warning.text ? (
+ ({
+ request: request as ShardFailureRequest,
+ response,
+ })}
+ color="primary"
+ isButtonEmpty={true}
+ />
+ ) : null}
+ >
+ ),
+ } as UserMessage,
];
}
}
@@ -376,6 +393,52 @@ export function getPrecisionErrorWarningMessages(
return warningMessages;
}
+export function getDeprecatedSamplingWarningMessage(core: CoreStart): UserMessage[] {
+ const useFieldExistenceSamplingKey = 'lens:useFieldExistenceSampling';
+ const isUsingSampling = core.uiSettings.get(useFieldExistenceSamplingKey);
+
+ return isUsingSampling
+ ? [
+ {
+ severity: 'warning',
+ fixableInEditor: false,
+ displayLocations: [{ id: 'banner' }],
+ shortMessage: '',
+ longMessage: (
+ {
+ core.application.navigateToApp('management', {
+ path: `/kibana/settings?query=${useFieldExistenceSamplingKey}`,
+ });
+ }}
+ >
+
+
+ ),
+ }}
+ />
+ }
+ />
+ ),
+ },
+ ]
+ : [];
+}
+
export function getVisualDefaultsForLayer(layer: FormBasedLayer) {
return Object.keys(layer.columns).reduce>>(
(memo, columnId) => {
diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts
index 7c79fc16d2b6b..60edc6e613451 100644
--- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts
+++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts
@@ -13,7 +13,7 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { getTextBasedDatasource } from './text_based_languages';
import { generateId } from '../../id_generator';
-import { DatasourcePublicAPI, Datasource } from '../../types';
+import { DatasourcePublicAPI, Datasource, FrameDatasourceAPI } from '../../types';
jest.mock('../../id_generator');
@@ -496,8 +496,8 @@ describe('Textbased Data Source', () => {
});
});
- describe('#getErrorMessages', () => {
- it('should use the results of getErrorMessages directly when single layer', () => {
+ describe('#getUserMessages', () => {
+ it('should use the results of getUserMessages directly when single layer', () => {
const state = {
layers: {
a: {
@@ -539,10 +539,43 @@ describe('Textbased Data Source', () => {
},
},
} as unknown as TextBasedPrivateState;
- expect(TextBasedDatasource.getErrorMessages(state, indexPatterns)).toEqual([
- { longMessage: 'error 1', shortMessage: 'error 1' },
- { longMessage: 'error 2', shortMessage: 'error 2' },
- ]);
+ expect(
+ TextBasedDatasource.getUserMessages(state, {
+ frame: { dataViews: indexPatterns } as unknown as FrameDatasourceAPI,
+ setState: () => {},
+ })
+ ).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "displayLocations": Array [
+ Object {
+ "id": "visualization",
+ },
+ Object {
+ "id": "textBasedLanguagesQueryInput",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "error 1",
+ "severity": "error",
+ "shortMessage": "error 1",
+ },
+ Object {
+ "displayLocations": Array [
+ Object {
+ "id": "visualization",
+ },
+ Object {
+ "id": "textBasedLanguagesQueryInput",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "error 2",
+ "severity": "error",
+ "shortMessage": "error 2",
+ },
+ ]
+ `);
});
});
diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx
index f422aa4a0ea64..9912fa866245f 100644
--- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx
+++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx
@@ -28,6 +28,7 @@ import {
TableChangeType,
DatasourceDimensionTriggerProps,
DataSourceInfo,
+ UserMessage,
} from '../../types';
import { generateId } from '../../id_generator';
import { toExpression } from './to_expression';
@@ -164,7 +165,7 @@ export function getTextBasedDatasource({
checkIntegrity: () => {
return [];
},
- getErrorMessages: (state) => {
+ getUserMessages: (state) => {
const errors: Error[] = [];
Object.values(state.layers).forEach((layer) => {
@@ -173,22 +174,16 @@ export function getTextBasedDatasource({
}
});
return errors.map((err) => {
- return {
+ const message: UserMessage = {
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }, { id: 'textBasedLanguagesQueryInput' }],
shortMessage: err.message,
longMessage: err.message,
};
+ return message;
});
},
- getUnifiedSearchErrors: (state) => {
- const errors: Error[] = [];
-
- Object.values(state.layers).forEach((layer) => {
- if (layer.errors && layer.errors.length > 0) {
- errors.push(...layer.errors);
- }
- });
- return errors;
- },
initialize(
state?: TextBasedPersistedState,
savedObjectReferences?,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/dimension_button.tsx
index 508148be8b2a9..c4b4f8c37dc8a 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/dimension_button.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/dimension_button.tsx
@@ -43,7 +43,7 @@ export function DimensionButton({
onClick={() => onClick(accessorConfig.columnId)}
aria-label={triggerLinkA11yText(label)}
title={triggerLinkA11yText(label)}
- color={invalid || group.invalid ? 'danger' : undefined}
+ color={invalid ? 'danger' : undefined}
>
{children}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx
index af63826c1e71d..7f509c9f921b4 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx
@@ -135,6 +135,7 @@ describe('ConfigPanel', () => {
isFullscreen: false,
toggleFullscreen: jest.fn(),
uiActions,
+ getUserMessages: () => [],
};
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx
index f1d5ea36bd199..e2bb44a61e68e 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx
@@ -113,6 +113,7 @@ describe('LayerPanel', () => {
onEmptyDimensionAdd: jest.fn(),
onChangeIndexPattern: jest.fn(),
indexPatternService: createIndexPatternServiceMock(),
+ getUserMessages: () => [],
};
}
@@ -1363,5 +1364,7 @@ describe('LayerPanel', () => {
expect(mockDatasource.renderDimensionTrigger).not.toHaveBeenCalled();
expect(mockVisualization.renderDimensionTrigger).toHaveBeenCalled();
});
+
+ // TODO - test user message display
});
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
index 03aed23f95237..fa55e9f5dfb21 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
@@ -32,6 +32,7 @@ import {
isOperation,
LayerAction,
VisualizationDimensionGroupConfig,
+ UserMessagesGetter,
} from '../../../types';
import { DragDropIdentifier, ReorderProvider } from '../../../drag_drop';
import { LayerSettings } from './layer_settings';
@@ -91,6 +92,7 @@ export function LayerPanel(
visualizationId?: string;
}) => void;
indexPatternService: IndexPatternServiceAPI;
+ getUserMessages: UserMessagesGetter;
}
) {
const [activeDimension, setActiveDimension] = useState(
@@ -509,6 +511,18 @@ export function LayerPanel(
{group.accessors.map((accessorConfig, accessorIndex) => {
const { columnId } = accessorConfig;
+
+ const messages = props.getUserMessages('dimensionTrigger', {
+ // TODO - support warnings
+ severity: 'error',
+ dimensionId: columnId,
+ });
+
+ const hasMessages = Boolean(messages.length);
+ const messageToDisplay = hasMessages
+ ? messages[0].shortMessage || messages[0].longMessage
+ : undefined;
+
return (
@@ -591,13 +605,8 @@ export function LayerPanel(
columnId,
label: columnLabelMap?.[columnId] ?? '',
hideTooltip,
- ...(activeVisualization?.validateColumn?.(
- visualizationState,
- { dataViews },
- layerId,
- columnId,
- group
- ) || { invalid: false }),
+ invalid: hasMessages,
+ invalidMessage: messageToDisplay,
})}
>
)}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts
index c4a2d77c30bab..ca47938710b11 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts
@@ -15,6 +15,7 @@ import {
VisualizationDimensionGroupConfig,
DatasourceMap,
VisualizationMap,
+ UserMessagesGetter,
} from '../../../types';
export interface ConfigPanelWrapperProps {
framePublicAPI: FramePublicAPI;
@@ -23,6 +24,7 @@ export interface ConfigPanelWrapperProps {
core: DatasourceDimensionEditorProps['core'];
indexPatternService: IndexPatternServiceAPI;
uiActions: UiActionsStart;
+ getUserMessages: UserMessagesGetter;
}
export interface LayerPanelProps {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
index 2530b8e215ddb..e414525f18dd9 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
@@ -99,6 +99,8 @@ function getDefaultProps() {
lensInspector: getLensInspectorService(inspectorPluginMock.createStartContract()),
showNoDataPopover: jest.fn(),
indexPatternService: createIndexPatternServiceMock(),
+ getUserMessages: () => [],
+ addUserMessages: () => () => {},
};
return defaultProps;
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
index 4100e25da7667..a593165a224f8 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
@@ -5,11 +5,18 @@
* 2.0.
*/
-import React, { useCallback, useRef, useMemo } from 'react';
+import React, { useCallback, useRef } from 'react';
import { CoreStart } from '@kbn/core/public';
import { ReactExpressionRendererType } from '@kbn/expressions-plugin/public';
import { trackUiCounterEvents } from '../../lens_ui_telemetry';
-import { DatasourceMap, FramePublicAPI, VisualizationMap, Suggestion } from '../../types';
+import {
+ DatasourceMap,
+ FramePublicAPI,
+ VisualizationMap,
+ Suggestion,
+ UserMessagesGetter,
+ AddUserMessages,
+} from '../../types';
import { DataPanelWrapper } from './data_panel_wrapper';
import { BannerWrapper } from './banner_wrapper';
import { ConfigPanelWrapper } from './config_panel';
@@ -41,6 +48,8 @@ export interface EditorFrameProps {
showNoDataPopover: () => void;
lensInspector: LensInspector;
indexPatternService: IndexPatternServiceAPI;
+ getUserMessages: UserMessagesGetter;
+ addUserMessages: AddUserMessages;
}
export function EditorFrame(props: EditorFrameProps) {
@@ -96,21 +105,15 @@ export function EditorFrame(props: EditorFrameProps) {
showMemoizedErrorNotification(error);
}, []);
- const bannerMessages: React.ReactNode[] | undefined = useMemo(() => {
- if (activeDatasourceId) {
- return datasourceMap[activeDatasourceId].getDeprecationMessages?.(
- datasourceStates[activeDatasourceId].state
- );
- }
- }, [activeDatasourceId, datasourceMap, datasourceStates]);
+ const bannerMessages = props.getUserMessages('banner', { severity: 'warning' });
return (
-
+ longMessage)} />
) : undefined
}
@@ -139,6 +142,7 @@ export function EditorFrame(props: EditorFrameProps) {
framePublicAPI={framePublicAPI}
uiActions={props.plugins.uiActions}
indexPatternService={props.indexPatternService}
+ getUserMessages={props.getUserMessages}
/>
)
@@ -156,6 +160,8 @@ export function EditorFrame(props: EditorFrameProps) {
visualizationMap={visualizationMap}
framePublicAPI={framePublicAPI}
getSuggestionForField={getSuggestionForField.current}
+ getUserMessages={props.getUserMessages}
+ addUserMessages={props.addUserMessages}
/>
)
@@ -169,6 +175,7 @@ export function EditorFrame(props: EditorFrameProps) {
datasourceMap={datasourceMap}
visualizationMap={visualizationMap}
frame={framePublicAPI}
+ getUserMessages={props.getUserMessages}
/>
)
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
index 9f53e4822e239..4cda8d1c7c0bd 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
@@ -18,26 +18,16 @@ import {
Datasource,
DatasourceLayers,
DatasourceMap,
- FramePublicAPI,
IndexPattern,
IndexPatternMap,
IndexPatternRef,
InitializationOptions,
- Visualization,
VisualizationMap,
VisualizeEditorContext,
} from '../../types';
import { buildExpression } from './expression_helpers';
-import { showMemoizedErrorNotification } from '../../lens_ui_errors';
import { Document } from '../../persistence/saved_object_store';
import { getActiveDatasourceIdFromDoc, sortDataViewRefs } from '../../utils';
-import type { ErrorMessage } from '../types';
-import {
- getMissingCurrentDatasource,
- getMissingIndexPatterns,
- getMissingVisualizationTypeError,
- getUnknownVisualizationTypeError,
-} from '../error_helper';
import type { DatasourceStates, DataViewsState, VisualizationState } from '../../state_management';
import { readFromStorage } from '../../settings_storage';
import { loadIndexPatternRefs, loadIndexPatterns } from '../../data_views_service/loader';
@@ -325,7 +315,11 @@ export async function persistedStateToExpression(
dataViews: DataViewsContract;
timefilter: TimefilterContract;
}
-): Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }> {
+): Promise<{
+ ast: Ast | null;
+ indexPatterns: IndexPatternMap;
+ indexPatternRefs: IndexPatternRef[];
+}> {
const {
state: {
visualization: persistedVisualizationState,
@@ -339,16 +333,7 @@ export async function persistedStateToExpression(
description,
} = doc;
if (!visualizationType) {
- return {
- ast: null,
- errors: [{ shortMessage: '', longMessage: getMissingVisualizationTypeError() }],
- };
- }
- if (!visualizations[visualizationType]) {
- return {
- ast: null,
- errors: [getUnknownVisualizationTypeError(visualizationType)],
- };
+ return { ast: null, indexPatterns: {}, indexPatternRefs: [] };
}
const visualization = visualizations[visualizationType!];
const visualizationState = initializeVisualization({
@@ -391,30 +376,11 @@ export async function persistedStateToExpression(
if (datasourceId == null) {
return {
ast: null,
- errors: [{ shortMessage: '', longMessage: getMissingCurrentDatasource() }],
- };
- }
-
- const indexPatternValidation = validateRequiredIndexPatterns(
- datasourceMap[datasourceId],
- datasourceStates[datasourceId],
- indexPatterns
- );
-
- if (indexPatternValidation) {
- return {
- ast: null,
- errors: indexPatternValidation,
+ indexPatterns,
+ indexPatternRefs,
};
}
- const validationResult = validateDatasourceAndVisualization(
- datasourceMap[datasourceId],
- datasourceStates[datasourceId].state,
- visualization,
- visualizationState,
- { datasourceLayers, dataViews: { indexPatterns } as DataViewsState }
- );
const currentTimeRange = services.timefilter.getAbsoluteTime();
return {
@@ -429,7 +395,8 @@ export async function persistedStateToExpression(
indexPatterns,
dateRange: { fromDate: currentTimeRange.from, toDate: currentTimeRange.to },
}),
- errors: validationResult,
+ indexPatterns,
+ indexPatternRefs,
};
}
@@ -438,7 +405,7 @@ export function getMissingIndexPattern(
currentDatasourceState: { state: unknown } | null,
indexPatterns: IndexPatternMap
) {
- if (currentDatasourceState == null || currentDatasource == null) {
+ if (currentDatasourceState?.state == null || currentDatasource == null) {
return [];
}
const missingIds = currentDatasource.checkIntegrity(currentDatasourceState.state, indexPatterns);
@@ -447,55 +414,3 @@ export function getMissingIndexPattern(
}
return missingIds;
}
-
-const validateRequiredIndexPatterns = (
- currentDatasource: Datasource,
- currentDatasourceState: { state: unknown } | null,
- indexPatterns: IndexPatternMap
-): ErrorMessage[] | undefined => {
- const missingIds = getMissingIndexPattern(
- currentDatasource,
- currentDatasourceState,
- indexPatterns
- );
-
- if (!missingIds.length) {
- return;
- }
-
- return [{ shortMessage: '', longMessage: getMissingIndexPatterns(missingIds), type: 'fixable' }];
-};
-
-export const validateDatasourceAndVisualization = (
- currentDataSource: Datasource | null,
- currentDatasourceState: unknown | null,
- currentVisualization: Visualization | null,
- currentVisualizationState: unknown | undefined,
- frame: Pick
-): ErrorMessage[] | undefined => {
- try {
- const datasourceValidationErrors = currentDatasourceState
- ? currentDataSource?.getErrorMessages(currentDatasourceState, frame.dataViews.indexPatterns)
- : undefined;
-
- const visualizationValidationErrors = currentVisualizationState
- ? currentVisualization?.getErrorMessages(currentVisualizationState, frame)
- : undefined;
-
- if (datasourceValidationErrors?.length || visualizationValidationErrors?.length) {
- return [...(datasourceValidationErrors || []), ...(visualizationValidationErrors || [])];
- }
- } catch (e) {
- showMemoizedErrorNotification(e);
- if (e.message) {
- return [
- {
- shortMessage: e.message,
- longMessage: e.message,
- type: 'critical',
- },
- ];
- }
- }
- return undefined;
-};
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx
index 7f0034ad73366..84a6b9d6134cc 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx
@@ -104,6 +104,7 @@ describe('suggestion_panel', () => {
},
ExpressionRenderer: expressionRendererMock,
frame: createMockFramePublicAPI(),
+ getUserMessages: () => [],
};
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx
index 0e2cf398ab728..6190c1c9d6615 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx
@@ -38,15 +38,13 @@ import {
DatasourceMap,
VisualizationMap,
DatasourceLayers,
+ UserMessagesGetter,
+ FrameDatasourceAPI,
} from '../../types';
import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
import { getDatasourceExpressionsByLayers } from './expression_helpers';
import { showMemoizedErrorNotification } from '../../lens_ui_errors/memoized_error_notification';
-import {
- getMissingIndexPattern,
- validateDatasourceAndVisualization,
- getDatasourceLayers,
-} from './state_helpers';
+import { getMissingIndexPattern } from './state_helpers';
import {
rollbackSuggestion,
selectExecutionContextSearch,
@@ -63,16 +61,45 @@ import {
selectChangesApplied,
applyChanges,
selectStagedActiveData,
+ selectFrameDatasourceAPI,
} from '../../state_management';
+import { filterUserMessages } from '../../app_plugin/get_application_user_messages';
const MAX_SUGGESTIONS_DISPLAYED = 5;
const LOCAL_STORAGE_SUGGESTIONS_PANEL = 'LENS_SUGGESTIONS_PANEL_HIDDEN';
+const configurationsValid = (
+ currentDataSource: Datasource | null,
+ currentDatasourceState: unknown,
+ currentVisualization: Visualization,
+ currentVisualizationState: unknown,
+ frame: FrameDatasourceAPI
+): boolean => {
+ try {
+ return (
+ filterUserMessages(
+ [
+ ...(currentDataSource?.getUserMessages?.(currentDatasourceState, {
+ frame,
+ setState: () => {},
+ }) ?? []),
+ ...(currentVisualization?.getUserMessages?.(currentVisualizationState, { frame }) ?? []),
+ ],
+ undefined,
+ { severity: 'error' }
+ ).length === 0
+ );
+ } catch (e) {
+ return false;
+ }
+};
+
export interface SuggestionPanelProps {
datasourceMap: DatasourceMap;
visualizationMap: VisualizationMap;
ExpressionRenderer: ReactExpressionRendererType;
frame: FramePublicAPI;
+ getUserMessages: UserMessagesGetter;
}
const PreviewRenderer = ({
@@ -189,6 +216,7 @@ export function SuggestionPanel({
visualizationMap,
frame,
ExpressionRenderer: ExpressionRendererComponent,
+ getUserMessages,
}: SuggestionPanelProps) {
const dispatchLens = useLensDispatch();
const activeDatasourceId = useLensSelector(selectActiveDatasourceId);
@@ -197,6 +225,10 @@ export function SuggestionPanel({
const existsStagedPreview = useLensSelector((state) => Boolean(state.lens.stagedPreview));
const currentVisualization = useLensSelector(selectCurrentVisualization);
const currentDatasourceStates = useLensSelector(selectCurrentDatasourceStates);
+
+ const frameDatasourceAPI = useLensSelector((state) =>
+ selectFrameDatasourceAPI(state, datasourceMap)
+ );
const changesApplied = useLensSelector(selectChangesApplied);
// get user's selection from localStorage, this key defines if the suggestions panel will be hidden or not
const [hideSuggestions, setHideSuggestions] = useLocalStorage(
@@ -237,28 +269,13 @@ export function SuggestionPanel({
}) => {
return (
!hide &&
- validateDatasourceAndVisualization(
+ configurationsValid(
suggestionDatasourceId ? datasourceMap[suggestionDatasourceId] : null,
suggestionDatasourceState,
visualizationMap[visualizationId],
suggestionVisualizationState,
- {
- ...frame,
- dataViews: frame.dataViews,
- datasourceLayers: getDatasourceLayers(
- suggestionDatasourceId
- ? {
- [suggestionDatasourceId]: {
- isLoading: true,
- state: suggestionDatasourceState,
- },
- }
- : {},
- datasourceMap,
- frame.dataViews.indexPatterns
- ),
- }
- ) == null
+ frameDatasourceAPI
+ )
);
}
)
@@ -274,16 +291,11 @@ export function SuggestionPanel({
),
}));
- const validationErrors = validateDatasourceAndVisualization(
- activeDatasourceId ? datasourceMap[activeDatasourceId] : null,
- activeDatasourceId && currentDatasourceStates[activeDatasourceId]?.state,
- currentVisualization.activeId ? visualizationMap[currentVisualization.activeId] : null,
- currentVisualization.state,
- frame
- );
+ const hasErrors =
+ getUserMessages(['visualization', 'visualizationInEditor'], { severity: 'error' }).length > 0;
const newStateExpression =
- currentVisualization.state && currentVisualization.activeId && !validationErrors
+ currentVisualization.state && currentVisualization.activeId && !hasErrors
? preparePreviewExpression(
{ visualizationState: currentVisualization.state },
visualizationMap[currentVisualization.activeId],
@@ -296,7 +308,7 @@ export function SuggestionPanel({
return {
suggestions: newSuggestions,
currentStateExpression: newStateExpression,
- currentStateError: validationErrors,
+ currentStateError: hasErrors,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
@@ -387,7 +399,7 @@ export function SuggestionPanel({
{currentVisualization.activeId && !hideSuggestions && (
+
+
+
+
+ Error: Unable to parse expression: Expected "/*", "//", [ ,\\t,\\r,\\n], or function but "|" found.
+
+ ,
+ "severity": "error",
+ "shortMessage": "An unexpected error occurred while preparing the chart",
+ "uniqueId": "expression_build_error",
+ },
+ ],
+]
+`;
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx
index 257d6d78cff99..b876645319d3a 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { ReactExpressionRendererProps } from '@kbn/expressions-plugin/public';
-import { FramePublicAPI, Visualization } from '../../../types';
+import { FramePublicAPI, UserMessage, Visualization } from '../../../types';
import {
createMockVisualization,
createMockDatasource,
@@ -61,6 +61,7 @@ function createCoreStartWithPermissions(newCapabilities = defaultPermissions) {
const defaultProps = {
datasourceMap: {},
+ visualizationMap: {},
framePublicAPI: createMockFramePublicAPI(),
ExpressionRenderer: createExpressionRendererMock(),
core: createCoreStartWithPermissions(),
@@ -71,6 +72,8 @@ const defaultProps = {
getSuggestionForField: () => undefined,
lensInspector: getLensInspectorService(inspectorPluginMock.createStartContract()),
toggleFullscreen: jest.fn(),
+ getUserMessages: () => [],
+ addUserMessages: () => () => {},
};
const toExpr = (
@@ -309,15 +312,6 @@ describe('workspace_panel', () => {
});
it('should base saveability on working changes when auto-apply disabled', async () => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- mockVisualization.getErrorMessages.mockImplementation((currentVisualizationState: any) => {
- if (currentVisualizationState.hasProblem) {
- return [{ shortMessage: 'An error occurred', longMessage: 'An long description here' }];
- } else {
- return [];
- }
- });
-
const framePublicAPI = createMockFramePublicAPI();
framePublicAPI.datasourceLayers = {
first: mockDatasource.publicAPIMock,
@@ -325,9 +319,12 @@ describe('workspace_panel', () => {
mockDatasource.toExpression.mockReturnValue('datasource');
mockDatasource.getLayers.mockReturnValue(['first']);
+ let userMessages: UserMessage[] = [];
+
const mounted = await mountWithProvider(
userMessages}
datasourceMap={{
testDatasource: mockDatasource,
}}
@@ -355,11 +352,24 @@ describe('workspace_panel', () => {
`);
expect(isSaveable()).toBe(true);
+ // note that populating the user messages and then dispatching a state update is true to
+ // how Lens interacts with the workspace panel from its perspective. all the panel does is call
+ // that getUserMessages function any time it gets re-rendered (aka state update)
+ userMessages = [
+ {
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: 'hey there',
+ longMessage: "i'm another error",
+ },
+ ] as UserMessage[];
+
act(() => {
mounted.lensStore.dispatch(
updateVisualizationState({
visualizationId: 'testVis',
- newState: { activeId: 'testVis', hasProblem: true },
+ newState: {},
})
);
});
@@ -564,7 +574,6 @@ describe('workspace_panel', () => {
type: 'lens/onActiveDataChange',
payload: {
activeData: tablesData,
- requestWarnings: [],
},
});
});
@@ -662,203 +671,25 @@ describe('workspace_panel', () => {
expect(expressionRendererMock).toHaveBeenCalledTimes(3);
});
- it('should show an error message if there are missing indexpatterns in the visualization', async () => {
- mockDatasource.getLayers.mockReturnValue(['first']);
- mockDatasource.checkIntegrity.mockReturnValue(['a']);
- const framePublicAPI = createMockFramePublicAPI();
- framePublicAPI.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- };
- const mounted = await mountWithProvider(
- 'testVis' },
- }}
- />,
-
- {
- preloadedState: {
- datasourceStates: {
- testDatasource: {
- // define a layer with an indexpattern not available
- state: { layers: { indexPatternId: 'a' }, indexPatterns: {} },
- isLoading: false,
- },
- },
- },
- }
- );
- instance = mounted.instance;
-
- expect(instance.find('[data-test-subj="missing-refs-failure"]').exists()).toBeTruthy();
- expect(instance.find(expressionRendererMock)).toHaveLength(0);
- });
-
- it('should not show the management action in case of missing indexpattern and no navigation permissions', async () => {
- mockDatasource.getLayers.mockReturnValue(['first']);
- const framePublicAPI = createMockFramePublicAPI();
- framePublicAPI.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- };
-
- const mounted = await mountWithProvider(
- 'testVis' },
- }}
- // Use cannot navigate to the management page
- core={createCoreStartWithPermissions({
- navLinks: { management: false },
- management: { kibana: { indexPatterns: true } },
- })}
- />,
-
+ it('should show configuration error messages if present', async () => {
+ const messages: UserMessage[] = [
{
- preloadedState: {
- datasourceStates: {
- testDatasource: {
- // define a layer with an indexpattern not available
- state: { layers: { indexPatternId: 'a' }, indexPatterns: {} },
- isLoading: false,
- },
- },
- },
- }
- );
- instance = mounted.instance;
-
- expect(
- instance.find('[data-test-subj="configuration-failure-reconfigure-indexpatterns"]').exists()
- ).toBeFalsy();
- });
-
- it('should not show the management action in case of missing indexpattern and no indexPattern specific permissions', async () => {
- mockDatasource.getLayers.mockReturnValue(['first']);
- const framePublicAPI = createMockFramePublicAPI();
- framePublicAPI.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- };
-
- const mounted = await mountWithProvider(
- 'testVis' },
- }}
- // user can go to management, but indexPatterns management is not accessible
- core={createCoreStartWithPermissions({
- navLinks: { management: true },
- management: { kibana: { indexPatterns: false } },
- })}
- />,
-
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: 'hey there',
+ longMessage: "i'm an error",
+ },
{
- preloadedState: {
- datasourceStates: {
- testDatasource: {
- // define a layer with an indexpattern not available
- state: { layers: { indexPatternId: 'a' }, indexPatterns: {} },
- isLoading: false,
- },
- },
- },
- }
- );
- instance = mounted.instance;
-
- expect(
- instance.find('[data-test-subj="configuration-failure-reconfigure-indexpatterns"]').exists()
- ).toBeFalsy();
- });
-
- it('should show an error message if validation on datasource does not pass', async () => {
- mockDatasource.getErrorMessages.mockReturnValue([
- { shortMessage: 'An error occurred', longMessage: 'An long description here' },
- ]);
- mockDatasource.getLayers.mockReturnValue(['first']);
- const framePublicAPI = createMockFramePublicAPI();
- framePublicAPI.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- };
-
- const mounted = await mountWithProvider(
- 'testVis' },
- }}
- />
- );
- instance = mounted.instance;
- act(() => {
- instance.update();
- });
-
- expect(instance.find('[data-test-subj="configuration-failure"]').exists()).toBeTruthy();
- expect(instance.find(expressionRendererMock)).toHaveLength(0);
- });
-
- it('should show an error message if validation on visualization does not pass', async () => {
- mockDatasource.getErrorMessages.mockReturnValue(undefined);
- mockDatasource.getLayers.mockReturnValue(['first']);
- mockVisualization.getErrorMessages.mockReturnValue([
- { shortMessage: 'Some error happened', longMessage: 'Some long description happened' },
- ]);
- mockVisualization.toExpression.mockReturnValue('testVis');
- const framePublicAPI = createMockFramePublicAPI();
- framePublicAPI.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- };
-
- const mounted = await mountWithProvider(
-
- );
- instance = mounted.instance;
-
- expect(instance.find('[data-test-subj="configuration-failure"]').exists()).toBeTruthy();
- expect(instance.find(expressionRendererMock)).toHaveLength(0);
- });
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: 'hey there',
+ longMessage: "i'm another error",
+ },
+ ];
- it('should show an error message if validation on both datasource and visualization do not pass', async () => {
- mockDatasource.getErrorMessages.mockReturnValue([
- { shortMessage: 'An error occurred', longMessage: 'An long description here' },
- ]);
- mockDatasource.getLayers.mockReturnValue(['first']);
- mockVisualization.getErrorMessages.mockReturnValue([
- { shortMessage: 'Some error happened', longMessage: 'Some long description happened' },
- ]);
- mockVisualization.toExpression.mockReturnValue('testVis');
- const framePublicAPI = createMockFramePublicAPI();
- framePublicAPI.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- };
+ const getUserMessages = jest.fn(() => messages);
const mounted = await mountWithProvider(
{
datasourceMap={{
testDatasource: mockDatasource,
}}
- framePublicAPI={framePublicAPI}
- visualizationMap={{
- testVis: mockVisualization,
- }}
+ getUserMessages={getUserMessages}
/>
);
instance = mounted.instance;
// EuiFlexItem duplicates internally the attribute, so we need to filter only the most inner one here
- expect(
- instance.find('[data-test-subj="configuration-failure-more-errors"]').last().text()
- ).toEqual(' +1 error');
+ expect(instance.find('[data-test-subj="workspace-more-errors-button"]').last().text()).toEqual(
+ ' +1 error'
+ );
expect(instance.find(expressionRendererMock)).toHaveLength(0);
+ expect(getUserMessages).toHaveBeenCalledWith(['visualization', 'visualizationInEditor'], {
+ severity: 'error',
+ });
});
- it('should NOT display errors for unapplied changes', async () => {
+ it('should NOT display config errors for unapplied changes', async () => {
// this test is important since we don't want the workspace panel to
// display errors if the user has disabled auto-apply, messed something up,
// but not yet applied their changes
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- mockDatasource.getErrorMessages.mockImplementation((currentDatasourceState: any) => {
- if (currentDatasourceState.hasProblem) {
- return [{ shortMessage: 'An error occurred', longMessage: 'An long description here' }];
- } else {
- return [];
- }
- });
- mockDatasource.getLayers.mockReturnValue(['first']);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- mockVisualization.getErrorMessages.mockImplementation((currentVisualizationState: any) => {
- if (currentVisualizationState.hasProblem) {
- return [{ shortMessage: 'An error occurred', longMessage: 'An long description here' }];
- } else {
- return [];
- }
- });
- mockVisualization.toExpression.mockReturnValue('testVis');
- const framePublicAPI = createMockFramePublicAPI();
- framePublicAPI.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- };
+ let userMessages = [] as UserMessage[];
const mounted = await mountWithProvider(
userMessages}
datasourceMap={{
testDatasource: mockDatasource,
}}
- framePublicAPI={framePublicAPI}
visualizationMap={{
testVis: mockVisualization,
}}
@@ -925,9 +735,7 @@ describe('workspace_panel', () => {
instance = mounted.instance;
const lensStore = mounted.lensStore;
- const showingErrors = () =>
- instance.exists('[data-test-subj="configuration-failure-error"]') ||
- instance.exists('[data-test-subj="configuration-failure-more-errors"]');
+ const showingErrors = () => instance.exists('[data-test-subj="workspace-error-message"]');
expect(showingErrors()).toBeFalsy();
@@ -939,27 +747,24 @@ describe('workspace_panel', () => {
expect(showingErrors()).toBeFalsy();
// introduce some issues
+ userMessages = [
+ {
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: 'hey there',
+ longMessage: "i'm another error",
+ },
+ ] as UserMessage[];
+
act(() => {
lensStore.dispatch(
updateDatasourceState({
datasourceId: 'testDatasource',
- updater: { hasProblem: true },
+ updater: {},
})
);
});
- instance.update();
-
- expect(showingErrors()).toBeFalsy();
-
- act(() => {
- lensStore.dispatch(
- updateVisualizationState({
- visualizationId: 'testVis',
- newState: { activeId: 'testVis', hasProblem: true },
- })
- );
- });
- instance.update();
expect(showingErrors()).toBeFalsy();
@@ -970,6 +775,7 @@ describe('workspace_panel', () => {
expect(showingErrors()).toBeTruthy();
});
+ // TODO - test refresh after expression failure error
it('should show an error message if the expression fails to parse', async () => {
mockDatasource.toExpression.mockReturnValue('|||');
mockDatasource.getLayers.mockReturnValue(['first']);
@@ -978,6 +784,10 @@ describe('workspace_panel', () => {
first: mockDatasource.publicAPIMock,
};
+ const mockRemoveUserMessages = jest.fn();
+ const mockAddUserMessages = jest.fn(() => mockRemoveUserMessages);
+ const mockGetUserMessages = jest.fn(() => []);
+
const mounted = await mountWithProvider(
{
visualizationMap={{
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
}}
+ addUserMessages={mockAddUserMessages}
+ getUserMessages={mockGetUserMessages}
/>
);
instance = mounted.instance;
- expect(instance.find('[data-test-subj="expression-failure"]').exists()).toBeTruthy();
+ expect(mockAddUserMessages.mock.lastCall).toMatchSnapshot();
expect(instance.find(expressionRendererMock)).toHaveLength(0);
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
index e5b4495b2a7a5..165dba174b0ac 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
@@ -18,13 +18,11 @@ import {
EuiText,
EuiButtonEmpty,
EuiLink,
- EuiButton,
- EuiSpacer,
EuiTextColor,
+ EuiSpacer,
} from '@elastic/eui';
import type { CoreStart } from '@kbn/core/public';
import type { DataPublicPluginStart, ExecutionContextSearch } from '@kbn/data-plugin/public';
-import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import type {
ExpressionRendererEvent,
ExpressionRenderError,
@@ -44,9 +42,12 @@ import {
isLensEditEvent,
VisualizationMap,
DatasourceMap,
- DatasourceFixAction,
Suggestion,
DatasourceLayers,
+ UserMessage,
+ UserMessagesGetter,
+ AddUserMessages,
+ isMessageRemovable,
} from '../../../types';
import { DragDrop, DragContext, DragDropIdentifier } from '../../../drag_drop';
import { switchToSuggestion } from '../suggestion_helpers';
@@ -54,16 +55,11 @@ import { buildExpression } from '../expression_helpers';
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
import applyChangesIllustrationDark from '../../../assets/render_dark@2x.png';
import applyChangesIllustrationLight from '../../../assets/render_light@2x.png';
-import {
- getOriginalRequestErrorMessages,
- getUnknownVisualizationTypeError,
-} from '../../error_helper';
-import { getMissingIndexPattern, validateDatasourceAndVisualization } from '../state_helpers';
+import { getOriginalRequestErrorMessages } from '../../error_helper';
import {
onActiveDataChange,
useLensDispatch,
editVisualizationAction,
- updateDatasourceState,
setSaveable,
useLensSelector,
selectExecutionContext,
@@ -94,14 +90,11 @@ export interface WorkspacePanelProps {
plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart };
getSuggestionForField: (field: DragDropIdentifier) => Suggestion | undefined;
lensInspector: LensInspector;
+ getUserMessages: UserMessagesGetter;
+ addUserMessages: AddUserMessages;
}
interface WorkspaceState {
- expressionBuildError?: Array<{
- shortMessage: string;
- longMessage: React.ReactNode;
- fixAction?: DatasourceFixAction;
- }>;
expandError: boolean;
expressionToRender: string | null | undefined;
}
@@ -125,7 +118,8 @@ const executionContext: KibanaExecutionContext = {
},
};
-// Exported for testing purposes only.
+const EXPRESSION_BUILD_ERROR_ID = 'expression_build_error';
+
export const WorkspacePanel = React.memo(function WorkspacePanel(props: WorkspacePanelProps) {
const { getSuggestionForField, ...restProps } = props;
@@ -151,6 +145,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
ExpressionRenderer: ExpressionRendererComponent,
suggestionForDraggedField,
lensInspector,
+ getUserMessages,
+ addUserMessages,
}: Omit & {
suggestionForDraggedField: Suggestion | undefined;
}) {
@@ -166,12 +162,10 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
const searchSessionId = useLensSelector(selectSearchSessionId);
const [localState, setLocalState] = useState({
- expressionBuildError: undefined,
expandError: false,
expressionToRender: undefined,
});
- // const expressionToRender = useRef();
const initialRenderComplete = useRef();
const renderDeps = useRef<{
@@ -228,14 +222,17 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
}
}, []);
+ const removeSearchWarningMessagesRef = useRef<() => void>();
+ const removeExpressionBuildErrorsRef = useRef<() => void>();
+
const onData$ = useCallback(
- (data: unknown, adapters?: Partial) => {
+ (_data: unknown, adapters?: Partial) => {
if (renderDeps.current) {
const [defaultLayerId] = Object.keys(renderDeps.current.datasourceLayers);
const datasource = Object.values(renderDeps.current.datasourceMap)[0];
const datasourceState = Object.values(renderDeps.current.datasourceStates)[0].state;
- let requestWarnings: Array = [];
+ let requestWarnings: UserMessage[] = [];
if (adapters?.requests) {
requestWarnings = getSearchWarningMessages(
@@ -248,24 +245,31 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
);
}
- dispatchLens(
- onActiveDataChange({
- activeData:
- adapters && adapters.tables
- ? Object.entries(adapters.tables?.tables).reduce>(
- (acc, [key, value], index, tables) => ({
- ...acc,
- [tables.length === 1 ? defaultLayerId : key]: value,
- }),
- {}
- )
- : undefined,
- requestWarnings,
- })
- );
+ if (requestWarnings.length) {
+ removeSearchWarningMessagesRef.current = addUserMessages(
+ requestWarnings.filter(isMessageRemovable)
+ );
+ } else if (removeSearchWarningMessagesRef.current) {
+ removeSearchWarningMessagesRef.current();
+ removeSearchWarningMessagesRef.current = undefined;
+ }
+
+ if (adapters && adapters.tables) {
+ dispatchLens(
+ onActiveDataChange({
+ activeData: Object.entries(adapters.tables?.tables).reduce>(
+ (acc, [key, value], _index, tables) => ({
+ ...acc,
+ [tables.length === 1 ? defaultLayerId : key]: value,
+ }),
+ {}
+ ),
+ })
+ );
+ }
}
},
- [dispatchLens, plugins.data.search]
+ [addUserMessages, dispatchLens, plugins.data.search]
);
const shouldApplyExpression = autoApplyEnabled || !initialRenderComplete.current || triggerApply;
@@ -273,56 +277,15 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
? visualizationMap[visualization.activeId]
: null;
- const missingIndexPatterns = getMissingIndexPattern(
- activeDatasourceId ? datasourceMap[activeDatasourceId] : null,
- activeDatasourceId ? datasourceStates[activeDatasourceId] : null,
- dataViews.indexPatterns
- );
-
- const missingRefsErrors = missingIndexPatterns.length
- ? [
- {
- shortMessage: '',
- longMessage: i18n.translate('xpack.lens.indexPattern.missingDataView', {
- defaultMessage:
- 'The {count, plural, one {data view} other {data views}} ({count, plural, one {id} other {ids}}: {indexpatterns}) cannot be found',
- values: {
- count: missingIndexPatterns.length,
- indexpatterns: missingIndexPatterns.join(', '),
- },
- }),
- },
- ]
- : [];
-
- const unknownVisError = visualization.activeId && !activeVisualization;
-
- // Note: mind to all these eslint disable lines: the frameAPI will change too frequently
- // and to prevent race conditions it is ok to leave them there.
-
- const configurationValidationError = useMemo(
- () =>
- validateDatasourceAndVisualization(
- activeDatasourceId ? datasourceMap[activeDatasourceId] : null,
- activeDatasourceId && datasourceStates[activeDatasourceId]?.state,
- activeVisualization,
- visualization.state,
- framePublicAPI
- ),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [
- activeVisualization,
- visualization.state,
- activeDatasourceId,
- datasourceMap,
- datasourceStates,
- framePublicAPI.dateRange,
- ]
- );
+ const workspaceErrors = getUserMessages(['visualization', 'visualizationInEditor'], {
+ severity: 'error',
+ });
// if the expression is undefined, it means we hit an error that should be displayed to the user
const unappliedExpression = useMemo(() => {
- if (!configurationValidationError?.length && !missingRefsErrors.length && !unknownVisError) {
+ // shouldn't build expression if there is any type of error other than an expression build error
+ // (in which case we try again every time because the config might have changed)
+ if (workspaceErrors.every((error) => error.uniqueId === EXPRESSION_BUILD_ERROR_ID)) {
try {
const ast = buildExpression({
visualization: activeVisualization,
@@ -344,39 +307,42 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
return null;
}
} catch (e) {
- const buildMessages = activeVisualization?.getErrorMessages(visualization.state);
- const defaultMessage = {
- shortMessage: i18n.translate('xpack.lens.editorFrame.buildExpressionError', {
- defaultMessage: 'An unexpected error occurred while preparing the chart',
- }),
- longMessage: e.toString(),
- };
- // Most likely an error in the expression provided by a datasource or visualization
- setLocalState((s) => ({
- ...s,
- expressionBuildError: buildMessages ?? [defaultMessage],
- }));
+ removeExpressionBuildErrorsRef.current = addUserMessages([
+ {
+ uniqueId: EXPRESSION_BUILD_ERROR_ID,
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: i18n.translate('xpack.lens.editorFrame.buildExpressionError', {
+ defaultMessage: 'An unexpected error occurred while preparing the chart',
+ }),
+ longMessage: (
+ <>
+
+
+
+
+ {e.toString()}
+ >
+ ),
+ },
+ ]);
}
}
- if (unknownVisError) {
- setLocalState((s) => ({
- ...s,
- expressionBuildError: [getUnknownVisualizationTypeError(visualization.activeId!)],
- }));
- }
}, [
- configurationValidationError?.length,
- missingRefsErrors.length,
- unknownVisError,
+ workspaceErrors,
activeVisualization,
visualization.state,
- visualization.activeId,
datasourceMap,
datasourceStates,
datasourceLayers,
dataViews.indexPatterns,
- searchSessionId,
framePublicAPI.dateRange,
+ searchSessionId,
+ addUserMessages,
]);
useEffect(() => {
@@ -399,6 +365,15 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
}, [unappliedExpression, shouldApplyExpression]);
const expressionExists = Boolean(localState.expressionToRender);
+
+ useEffect(() => {
+ // reset expression error if component attempts to run it again
+ if (expressionExists && removeExpressionBuildErrorsRef.current) {
+ removeExpressionBuildErrorsRef.current();
+ removeExpressionBuildErrorsRef.current = undefined;
+ }
+ }, [expressionExists]);
+
useEffect(() => {
// null signals an empty workspace which should count as an initial render
if (
@@ -464,16 +439,6 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
[plugins.uiActions]
);
- useEffect(() => {
- // reset expression error if component attempts to run it again
- if (expressionExists && localState.expressionBuildError) {
- setLocalState((s) => ({
- ...s,
- expressionBuildError: undefined,
- }));
- }
- }, [expressionExists, localState.expressionBuildError]);
-
const onDrop = useCallback(() => {
if (suggestionForDraggedField) {
trackUiCounterEvents('drop_onto_workspace');
@@ -588,7 +553,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
onEvent={onEvent}
hasCompatibleActions={hasCompatibleActions}
setLocalState={setLocalState}
- localState={{ ...localState, configurationValidationError, missingRefsErrors }}
+ localState={{ ...localState }}
+ errors={workspaceErrors}
ExpressionRendererComponent={ExpressionRendererComponent}
core={core}
activeDatasourceId={activeDatasourceId}
@@ -650,6 +616,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
visualizationMap={visualizationMap}
isFullscreen={isFullscreen}
lensInspector={lensInspector}
+ getUserMessages={getUserMessages}
>
{renderWorkspace()}
@@ -664,6 +631,7 @@ export const VisualizationWrapper = ({
hasCompatibleActions,
setLocalState,
localState,
+ errors,
ExpressionRendererComponent,
core,
activeDatasourceId,
@@ -676,15 +644,8 @@ export const VisualizationWrapper = ({
onEvent: (event: ExpressionRendererEvent) => void;
hasCompatibleActions: (event: ExpressionRendererEvent) => Promise;
setLocalState: (dispatch: (prevState: WorkspaceState) => WorkspaceState) => void;
- localState: WorkspaceState & {
- configurationValidationError?: Array<{
- shortMessage: string;
- longMessage: React.ReactNode;
- fixAction?: DatasourceFixAction;
- }>;
- missingRefsErrors?: Array<{ shortMessage: string; longMessage: React.ReactNode }>;
- unknownVisError?: Array<{ shortMessage: string; longMessage: React.ReactNode }>;
- };
+ localState: WorkspaceState;
+ errors: UserMessage[];
ExpressionRendererComponent: ReactExpressionRendererType;
core: CoreStart;
activeDatasourceId: string | null;
@@ -706,171 +667,46 @@ export const VisualizationWrapper = ({
);
const searchSessionId = useLensSelector(selectSearchSessionId);
- const dispatchLens = useLensDispatch();
-
- function renderFixAction(
- validationError:
- | {
- shortMessage: string;
- longMessage: React.ReactNode;
- fixAction?: DatasourceFixAction;
- }
- | undefined
- ) {
- return (
- validationError &&
- validationError.fixAction &&
- activeDatasourceId && (
- <>
- {
- const newState = await validationError.fixAction?.newState({
- ...framePublicAPI,
- ...context,
- });
- dispatchLens(
- updateDatasourceState({
- updater: newState,
- datasourceId: activeDatasourceId,
- })
- );
- }}
- >
- {validationError.fixAction.label}
-
-
- >
- )
- );
- }
-
- if (localState.configurationValidationError?.length) {
- let showExtraErrors = null;
- let showExtraErrorsAction = null;
-
- if (localState.configurationValidationError.length > 1) {
- if (localState.expandError) {
- showExtraErrors = localState.configurationValidationError
- .slice(1)
- .map((validationError) => (
- <>
-
- {validationError.longMessage}
-
- {renderFixAction(validationError)}
- >
- ));
- } else {
- showExtraErrorsAction = (
- {
- setLocalState((prevState: WorkspaceState) => ({
- ...prevState,
- expandError: !prevState.expandError,
- }));
- }}
- data-test-subj="configuration-failure-more-errors"
- >
- {i18n.translate('xpack.lens.editorFrame.configurationFailureMoreErrors', {
- defaultMessage: ` +{errors} {errors, plural, one {error} other {errors}}`,
- values: { errors: localState.configurationValidationError.length - 1 },
- })}
-
- );
- }
- }
-
- return (
-
-
-
-
- {localState.configurationValidationError[0].longMessage}
-
- {renderFixAction(localState.configurationValidationError?.[0])}
-
- {showExtraErrors}
- >
- }
- iconColor="danger"
- iconType="alert"
- />
-
-
- );
- }
-
- if (localState.missingRefsErrors?.length) {
- // Check for access to both Management app && specific indexPattern section
- const { management: isManagementEnabled } = core.application.capabilities.navLinks;
- const isIndexPatternManagementEnabled =
- core.application.capabilities.management.kibana.indexPatterns;
- return (
-
-
-
-
- {i18n.translate('xpack.lens.editorFrame.dataViewReconfigure', {
- defaultMessage: `Recreate it in the data view management page`,
- })}
-
-
- ) : null
- }
- body={
- <>
-
-
-
-
- {localState.missingRefsErrors[0].longMessage}
-
- >
- }
- iconColor="danger"
- iconType="alert"
- />
-
-
- );
- }
+ if (errors?.length) {
+ const showExtraErrorsAction =
+ !localState.expandError && errors.length > 1 ? (
+ {
+ setLocalState((prevState: WorkspaceState) => ({
+ ...prevState,
+ expandError: !prevState.expandError,
+ }));
+ }}
+ data-test-subj="workspace-more-errors-button"
+ >
+ {i18n.translate('xpack.lens.editorFrame.configurationFailureMoreErrors', {
+ defaultMessage: ` +{errors} {errors, plural, one {error} other {errors}}`,
+ values: { errors: errors.length - 1 },
+ })}
+
+ ) : null;
+
+ const [firstMessage, ...rest] = errors;
- if (localState.expressionBuildError?.length) {
- const firstError = localState.expressionBuildError[0];
return (
-
-
-
-
- {firstError.longMessage}
+ {firstMessage.longMessage}
+ {localState.expandError && (
+ <>
+
+ {rest.map((message) => (
+
+ {message.longMessage}
+
+
+ ))}
+ >
+ )}
>
}
iconColor="danger"
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx
index 844f3e6cb845e..42735bde405c4 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx
@@ -41,6 +41,7 @@ describe('workspace_panel_wrapper', () => {
datasourceStates={{}}
isFullscreen={false}
lensInspector={{} as unknown as LensInspector}
+ getUserMessages={() => []}
>
@@ -63,6 +64,7 @@ describe('workspace_panel_wrapper', () => {
datasourceStates={{}}
isFullscreen={false}
lensInspector={{} as unknown as LensInspector}
+ getUserMessages={() => []}
/>
);
@@ -118,6 +120,7 @@ describe('workspace_panel_wrapper', () => {
datasourceStates={{}}
isFullscreen={false}
lensInspector={{} as unknown as LensInspector}
+ getUserMessages={() => []}
>
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx
index c5adf2089ecb9..9013621c280bf 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx
@@ -11,7 +11,12 @@ import React, { useCallback } from 'react';
import { EuiPageTemplate, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import classNames from 'classnames';
import { FormattedMessage } from '@kbn/i18n-react';
-import { DatasourceMap, FramePublicAPI, VisualizationMap } from '../../../types';
+import {
+ DatasourceMap,
+ FramePublicAPI,
+ UserMessagesGetter,
+ VisualizationMap,
+} from '../../../types';
import { DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from '../../../utils';
import { NativeRenderer } from '../../../native_renderer';
import { ChartSwitch } from './chart_switch';
@@ -21,7 +26,6 @@ import {
updateVisualizationState,
DatasourceStates,
VisualizationState,
- updateDatasourceState,
useLensSelector,
selectChangesApplied,
applyChanges,
@@ -43,6 +47,7 @@ export interface WorkspacePanelWrapperProps {
datasourceStates: DatasourceStates;
isFullscreen: boolean;
lensInspector: LensInspector;
+ getUserMessages: UserMessagesGetter;
}
export function WorkspacePanelWrapper({
@@ -52,9 +57,8 @@ export function WorkspacePanelWrapper({
visualizationId,
visualizationMap,
datasourceMap,
- datasourceStates,
isFullscreen,
- lensInspector,
+ getUserMessages,
}: WorkspacePanelWrapperProps) {
const dispatchLens = useLensDispatch();
@@ -77,37 +81,13 @@ export function WorkspacePanelWrapper({
},
[dispatchLens, activeVisualization]
);
- const setDatasourceState = useCallback(
- (updater: unknown, datasourceId: string) => {
- dispatchLens(
- updateDatasourceState({
- updater,
- datasourceId,
- })
- );
- },
- [dispatchLens]
- );
const warningMessages: React.ReactNode[] = [];
- if (activeVisualization?.getWarningMessages) {
- warningMessages.push(
- ...(activeVisualization.getWarningMessages(visualizationState, framePublicAPI) || [])
- );
- }
- Object.entries(datasourceStates).forEach(([datasourceId, datasourceState]) => {
- const datasource = datasourceMap[datasourceId];
- if (!datasourceState.isLoading && datasource.getWarningMessages) {
- warningMessages.push(
- ...(datasource.getWarningMessages(
- datasourceState.state,
- framePublicAPI,
- lensInspector.adapters,
- (updater) => setDatasourceState(updater, datasourceId)
- ) || [])
- );
- }
- });
+
+ warningMessages.push(
+ ...getUserMessages('toolbar', { severity: 'warning' }).map(({ longMessage }) => longMessage)
+ );
+
if (requestWarnings) {
warningMessages.push(...requestWarnings);
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts b/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts
index 91c1237027771..6086c65c25405 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts
@@ -149,35 +149,6 @@ export function getOriginalRequestErrorMessages(error?: ExpressionRenderError |
return errorMessages;
}
-export function getMissingVisualizationTypeError() {
- return i18n.translate('xpack.lens.editorFrame.expressionMissingVisualizationType', {
- defaultMessage: 'Visualization type not found.',
- });
-}
-
-export function getMissingCurrentDatasource() {
- return i18n.translate('xpack.lens.editorFrame.expressionMissingDatasource', {
- defaultMessage: 'Could not find datasource for the visualization',
- });
-}
-
-export function getMissingIndexPatterns(indexPatternIds: string[]) {
- return i18n.translate('xpack.lens.editorFrame.expressionMissingDataView', {
- defaultMessage: 'Could not find the {count, plural, one {data view} other {data views}}: {ids}',
- values: { count: indexPatternIds.length, ids: indexPatternIds.join(', ') },
- });
-}
-
-export function getUnknownVisualizationTypeError(visType: string) {
- return {
- shortMessage: i18n.translate('xpack.lens.unknownVisType.shortMessage', {
- defaultMessage: `Unknown visualization type`,
- }),
- longMessage: i18n.translate('xpack.lens.unknownVisType.longMessage', {
- defaultMessage: `The visualization type {visType} could not be resolved.`,
- values: {
- visType,
- },
- }),
- };
-}
+// NOTE - if you are adding a new error message, add it as a UserMessage in get_application_error_messages
+// or the getUserMessages method of a particular datasource or visualization class! Alternatively, use the
+// addUserMessage function passed down by the application component.
diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx
index d375b75866cbb..80a30ab9325e0 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx
@@ -118,7 +118,13 @@ export class EditorFrameService {
const { EditorFrame } = await import('../async_services');
return {
- EditorFrameContainer: ({ showNoDataPopover, lensInspector, indexPatternService }) => {
+ EditorFrameContainer: ({
+ showNoDataPopover,
+ lensInspector,
+ indexPatternService,
+ getUserMessages,
+ addUserMessages,
+ }) => {
return (
;
-
-export interface ErrorMessage {
- shortMessage: string;
- longMessage: React.ReactNode;
- type?: 'fixable' | 'critical';
-}
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx
index 5a2f3d002ad8e..db00db6a87f7f 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx
@@ -23,7 +23,7 @@ import { Document } from '../persistence';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public/embeddable';
import { coreMock, httpServiceMock, themeServiceMock } from '@kbn/core/public/mocks';
-import { IBasePath, IUiSettingsClient } from '@kbn/core/public';
+import { CoreStart, IBasePath, IUiSettingsClient } from '@kbn/core/public';
import { AttributeService, ViewMode } from '@kbn/embeddable-plugin/public';
import { LensAttributeService } from '../lens_attribute_service';
import { OnSaveProps } from '@kbn/saved-objects-plugin/public/save_modal';
@@ -143,6 +143,7 @@ describe('embeddable', () => {
attributeService,
data: dataMock,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
dataViews: {} as DataViewsContract,
capabilities: {
@@ -166,7 +167,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
},
@@ -194,6 +196,7 @@ describe('embeddable', () => {
attributeService,
data: dataMock,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
dataViews: {} as DataViewsContract,
capabilities: {
@@ -217,7 +220,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
},
@@ -253,6 +257,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
dataViews: {} as DataViewsContract,
inspector: inspectorPluginMock.createStartContract(),
@@ -276,7 +281,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{
@@ -305,6 +311,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -328,11 +335,23 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: [{ shortMessage: '', longMessage: 'my validation error' }],
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{} as LensEmbeddableInput
);
+
+ jest.spyOn(embeddable, 'getUserMessages').mockReturnValue([
+ {
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
+ longMessage: 'lol',
+ shortMessage: 'lol',
+ },
+ ]);
+
await embeddable.initializeSavedVis({} as LensEmbeddableInput);
embeddable.render(mountpoint);
@@ -368,6 +387,7 @@ describe('embeddable', () => {
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
inspector: inspectorPluginMock.createStartContract(),
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
dataViews: {} as DataViewsContract,
spaces: spacesPluginStart,
@@ -391,7 +411,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{} as LensEmbeddableInput
@@ -418,6 +439,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {
@@ -443,7 +465,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{} as LensEmbeddableInput
@@ -469,6 +492,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {
@@ -494,7 +518,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{} as LensEmbeddableInput
@@ -518,6 +543,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -541,7 +567,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{ id: '123' } as LensEmbeddableInput
@@ -571,6 +598,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -594,7 +622,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{ id: '123' } as LensEmbeddableInput
@@ -628,6 +657,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -651,7 +681,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{ id: '123' } as LensEmbeddableInput
@@ -683,6 +714,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -706,7 +738,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{ id: '123' } as LensEmbeddableInput
@@ -745,6 +778,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -768,7 +802,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
input
@@ -808,6 +843,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -831,7 +867,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
input
@@ -874,6 +911,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: { get: jest.fn() } as unknown as DataViewsContract,
@@ -897,7 +935,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
input
@@ -925,6 +964,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -948,7 +988,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{ id: '123' } as LensEmbeddableInput
@@ -978,6 +1019,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -1001,7 +1043,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{ id: '123' } as LensEmbeddableInput
@@ -1028,6 +1071,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -1051,7 +1095,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{ id: '123', timeRange, query, filters } as LensEmbeddableInput
@@ -1094,6 +1139,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -1117,7 +1163,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{ id: '123', onLoad } as unknown as LensEmbeddableInput
@@ -1178,6 +1225,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -1201,7 +1249,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{ id: '123', onFilter } as unknown as LensEmbeddableInput
@@ -1237,6 +1286,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -1260,7 +1310,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{ id: '123', onBrushEnd } as unknown as LensEmbeddableInput
@@ -1293,6 +1344,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -1316,7 +1368,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
},
{ id: '123', onTableRowClick } as unknown as LensEmbeddableInput
@@ -1347,7 +1400,8 @@ describe('embeddable', () => {
},
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
};
});
@@ -1370,6 +1424,7 @@ describe('embeddable', () => {
data: dataMock,
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
inspector: inspectorPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
@@ -1385,6 +1440,7 @@ describe('embeddable', () => {
visualizationMap: {
[visDocument.visualizationType as string]: {
onEditAction: onEditActionMock,
+ initialize: () => {},
} as unknown as Visualization,
},
datasourceMap: {},
@@ -1438,6 +1494,7 @@ describe('embeddable', () => {
attributeService: attributeServiceMockFromSavedVis(visDocument),
data: dataMock,
expressionRenderer,
+ coreStart: {} as CoreStart,
basePath,
dataViews: {} as DataViewsContract,
capabilities: {
@@ -1452,6 +1509,7 @@ describe('embeddable', () => {
visualizationMap: {
[visDocument.visualizationType as string]: {
getDisplayOptions: displayOptions ? () => displayOptions : undefined,
+ initialize: () => {},
} as unknown as Visualization,
},
datasourceMap: {},
@@ -1465,7 +1523,8 @@ describe('embeddable', () => {
{ type: 'function', function: 'expression', arguments: {} },
],
},
- errors: undefined,
+ indexPatterns: {},
+ indexPatternRefs: [],
}),
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
},
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
index af1feb2223e2b..4bc431b2e50da 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
@@ -60,6 +60,7 @@ import type { Action, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public';
import type {
Capabilities,
+ CoreStart,
IBasePath,
IUiSettingsClient,
KibanaExecutionContext,
@@ -67,7 +68,7 @@ import type {
} from '@kbn/core/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import { BrushTriggerEvent, ClickTriggerEvent, Warnings } from '@kbn/charts-plugin/public';
-import { DataViewPersistableStateService, DataViewSpec } from '@kbn/data-views-plugin/common';
+import { DataViewSpec } from '@kbn/data-views-plugin/common';
import { getExecutionContextEvents, trackUiCounterEvents } from '../lens_ui_telemetry';
import { Document } from '../persistence';
import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper';
@@ -83,11 +84,17 @@ import {
Datasource,
IndexPatternMap,
GetCompatibleCellValueActions,
+ UserMessage,
+ IndexPatternRef,
+ FrameDatasourceAPI,
+ AddUserMessages,
+ isMessageRemovable,
+ UserMessagesGetter,
} from '../types';
import { getEditPath, DOC_TYPE } from '../../common';
import { LensAttributeService } from '../lens_attribute_service';
-import type { ErrorMessage, TableInspectorAdapter } from '../editor_frame_service/types';
+import type { TableInspectorAdapter } from '../editor_frame_service/types';
import { getLensInspectorService, LensInspector } from '../lens_inspector_service';
import { SharingSavedObjectProps, VisualizationDisplayOptions } from '../types';
import {
@@ -98,7 +105,10 @@ import {
inferTimeField,
} from '../utils';
import { getLayerMetaInfo, combineQueryAndFilters } from '../app_plugin/show_underlying_data';
-import { convertDataViewIntoLensIndexPattern } from '../data_views_service/loader';
+import {
+ filterUserMessages,
+ getApplicationUserMessages,
+} from '../app_plugin/get_application_user_messages';
export type LensSavedObjectAttributes = Omit;
@@ -141,9 +151,11 @@ export interface LensEmbeddableOutput extends EmbeddableOutput {
export interface LensEmbeddableDeps {
attributeService: LensAttributeService;
data: DataPublicPluginStart;
- documentToExpression: (
- doc: Document
- ) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>;
+ documentToExpression: (doc: Document) => Promise<{
+ ast: Ast | null;
+ indexPatterns: IndexPatternMap;
+ indexPatternRefs: IndexPatternRef[];
+ }>;
injectFilterReferences: FilterManager['inject'];
visualizationMap: VisualizationMap;
datasourceMap: DatasourceMap;
@@ -160,6 +172,7 @@ export interface LensEmbeddableDeps {
navLinks: Capabilities['navLinks'];
discover: Capabilities['discover'];
};
+ coreStart: CoreStart;
usageCollection?: UsageCollectionSetup;
spaces?: SpacesPluginStart;
theme: ThemeServiceStart;
@@ -178,11 +191,8 @@ const getExpressionFromDocument = async (
document: Document,
documentToExpression: LensEmbeddableDeps['documentToExpression']
) => {
- const { ast, errors } = await documentToExpression(document);
- return {
- expression: ast ? toExpression(ast) : null,
- errors,
- };
+ const { ast, indexPatterns, indexPatternRefs } = await documentToExpression(document);
+ return { ast: ast ? toExpression(ast) : null, indexPatterns, indexPatternRefs };
};
function getViewUnderlyingDataArgs({
@@ -276,7 +286,6 @@ export class Embeddable
private warningDomNode: HTMLElement | Element | undefined;
private subscription: Subscription;
private isInitialized = false;
- private errors: ErrorMessage[] | undefined;
private inputReloadSubscriptions: Subscription[];
private isDestroyed?: boolean;
private embeddableTitle?: string;
@@ -296,15 +305,9 @@ export class Embeddable
searchSessionId?: string;
} = {};
- private activeDataInfo: {
- activeData?: TableInspectorAdapter;
- activeDatasource?: Datasource;
- activeDatasourceState?: unknown;
- activeVisualization?: Visualization;
- activeVisualizationState?: unknown;
- } = {};
+ private activeData?: TableInspectorAdapter;
- private indexPatterns: DataView[] = [];
+ private dataViews: DataView[] = [];
private viewUnderlyingDataArgs?: ViewUnderlyingDataArgs;
@@ -326,6 +329,7 @@ export class Embeddable
let containerStateChangedCalledAlready = false;
this.initializeSavedVis(initialInput)
.then(() => {
+ this.loadUserMessages();
if (!containerStateChangedCalledAlready) {
this.onContainerStateChanged(initialInput);
} else {
@@ -417,6 +421,152 @@ export class Embeddable
);
}
+ private get activeDatasourceId() {
+ return getActiveDatasourceIdFromDoc(this.savedVis);
+ }
+
+ private get activeDatasource() {
+ if (!this.activeDatasourceId) return;
+ return this.deps.datasourceMap[this.activeDatasourceId];
+ }
+
+ private get activeVisualizationId() {
+ return getActiveVisualizationIdFromDoc(this.savedVis);
+ }
+
+ private get activeVisualization() {
+ if (!this.activeVisualizationId) return;
+ return this.deps.visualizationMap[this.activeVisualizationId];
+ }
+
+ private get activeVisualizationState() {
+ if (!this.activeVisualization) return;
+ return this.activeVisualization.initialize(() => '', this.savedVis?.state.visualization);
+ }
+
+ private indexPatterns: IndexPatternMap = {};
+
+ private indexPatternRefs: IndexPatternRef[] = [];
+
+ private get activeDatasourceState(): undefined | unknown {
+ if (!this.activeDatasourceId || !this.activeDatasource) return;
+
+ const docDatasourceState = this.savedVis?.state.datasourceStates[this.activeDatasourceId];
+
+ return this.activeDatasource.initialize(
+ docDatasourceState,
+ [...(this.savedVis?.references || []), ...(this.savedVis?.state.internalReferences || [])],
+ undefined,
+ undefined,
+ this.indexPatterns
+ );
+ }
+
+ public getUserMessages: UserMessagesGetter = (locationId, filters) => {
+ return filterUserMessages(
+ [...this._userMessages, ...Object.values(this.additionalUserMessages)],
+ locationId,
+ filters
+ );
+ };
+
+ private get hasAnyErrors() {
+ return this.getUserMessages(undefined, { severity: 'error' }).length > 0;
+ }
+
+ private _userMessages: UserMessage[] = [];
+
+ // loads all available user messages
+ private loadUserMessages() {
+ const userMessages: UserMessage[] = [];
+
+ if (this.activeVisualizationState && this.activeDatasource) {
+ userMessages.push(
+ ...getApplicationUserMessages({
+ visualizationType: this.savedVis?.visualizationType,
+ visualization: {
+ state: this.activeVisualizationState,
+ activeId: this.activeVisualizationId,
+ },
+ visualizationMap: this.deps.visualizationMap,
+ activeDatasource: this.activeDatasource,
+ activeDatasourceState: { state: this.activeDatasourceState },
+ dataViews: {
+ indexPatterns: this.indexPatterns,
+ indexPatternRefs: this.indexPatternRefs, // TODO - are these actually used?
+ },
+ core: this.deps.coreStart,
+ })
+ );
+ }
+
+ const mergedSearchContext = this.getMergedSearchContext();
+
+ if (!this.savedVis) {
+ return userMessages;
+ }
+
+ const frameDatasourceAPI: FrameDatasourceAPI = {
+ dataViews: {
+ indexPatterns: this.indexPatterns,
+ indexPatternRefs: this.indexPatternRefs,
+ },
+ datasourceLayers: {}, // TODO
+ query: this.savedVis.state.query,
+ filters: mergedSearchContext.filters ?? [],
+ dateRange: {
+ fromDate: mergedSearchContext.timeRange?.from ?? '',
+ toDate: mergedSearchContext.timeRange?.to ?? '',
+ },
+ activeData: this.activeData,
+ };
+
+ userMessages.push(
+ ...(this.activeDatasource?.getUserMessages(this.activeDatasourceState, {
+ setState: () => {},
+ frame: frameDatasourceAPI,
+ }) ?? []),
+ ...(this.activeVisualization?.getUserMessages?.(this.activeVisualizationState, {
+ frame: frameDatasourceAPI,
+ }) ?? [])
+ );
+
+ this._userMessages = userMessages;
+ }
+
+ private additionalUserMessages: Record = {};
+
+ // used to add warnings and errors from elsewhere in the embeddable
+ private addUserMessages: AddUserMessages = (messages) => {
+ const newMessageMap = {
+ ...this.additionalUserMessages,
+ };
+
+ const addedMessageIds: string[] = [];
+ messages.forEach((message) => {
+ if (!newMessageMap[message.uniqueId]) {
+ addedMessageIds.push(message.uniqueId);
+ newMessageMap[message.uniqueId] = message;
+ }
+ });
+
+ if (addedMessageIds.length) {
+ this.additionalUserMessages = newMessageMap;
+ }
+
+ this.reload();
+
+ return () => {
+ const withMessagesRemoved = {
+ ...this.additionalUserMessages,
+ };
+
+ messages.map(({ uniqueId }) => uniqueId).forEach((id) => delete withMessagesRemoved[id]);
+
+ this.additionalUserMessages = withMessagesRemoved;
+ };
+ };
+
public reportsEmbeddableLoad() {
return true;
}
@@ -433,54 +583,6 @@ export class Embeddable
return this.lensInspector.adapters;
}
- private maybeAddConflictError(
- errors?: ErrorMessage[],
- sharingSavedObjectProps?: SharingSavedObjectProps
- ) {
- const ret = [...(errors || [])];
-
- if (sharingSavedObjectProps?.outcome === 'conflict' && !!this.deps.spaces) {
- ret.push({
- shortMessage: i18n.translate('xpack.lens.embeddable.legacyURLConflict.shortMessage', {
- defaultMessage: `You've encountered a URL conflict`,
- }),
- longMessage: (
-
- ),
- });
- }
-
- return ret?.length ? ret : undefined;
- }
-
- private maybeAddTimeRangeError(
- errors: ErrorMessage[] | undefined,
- input: LensEmbeddableInput,
- indexPatterns: DataView[]
- ) {
- // if at least one indexPattern is time based, then the Lens embeddable requires the timeRange prop
- if (
- input.timeRange == null &&
- indexPatterns.some((indexPattern) => indexPattern.isTimeBased())
- ) {
- return [
- ...(errors || []),
- {
- shortMessage: i18n.translate('xpack.lens.embeddable.missingTimeRangeParam.shortMessage', {
- defaultMessage: `Missing timeRange property`,
- }),
- longMessage: i18n.translate('xpack.lens.embeddable.missingTimeRangeParam.longMessage', {
- defaultMessage: `The timeRange property is required for the given configuration`,
- }),
- },
- ];
- }
- return errors;
- }
-
async initializeSavedVis(input: LensEmbeddableInput) {
const unwrapResult: LensUnwrapResult | false = await this.deps.attributeService
.unwrapAttributes(input)
@@ -500,15 +602,40 @@ export class Embeddable
savedObjectId: (input as LensByReferenceInput)?.savedObjectId,
};
- const { expression, errors } = await getExpressionFromDocument(
+ const { ast, indexPatterns, indexPatternRefs } = await getExpressionFromDocument(
this.savedVis,
this.deps.documentToExpression
);
- this.expression = expression;
- this.errors = this.maybeAddConflictError(errors, metaInfo?.sharingSavedObjectProps);
+
+ this.expression = ast;
+ this.indexPatterns = indexPatterns;
+ this.indexPatternRefs = indexPatternRefs;
+
+ if (metaInfo?.sharingSavedObjectProps?.outcome === 'conflict' && !!this.deps.spaces) {
+ this.addUserMessages([
+ {
+ uniqueId: 'url-conflict',
+ severity: 'error',
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: i18n.translate('xpack.lens.embeddable.legacyURLConflict.shortMessage', {
+ defaultMessage: `You've encountered a URL conflict`,
+ }),
+ longMessage: (
+
+ ),
+ fixableInEditor: false,
+ },
+ ]);
+ }
await this.initializeOutput();
+ // deferred loading of this embeddable is complete
+ this.setInitializationFinished();
+
this.isInitialized = true;
}
@@ -551,37 +678,27 @@ export class Embeddable
return isDirty;
}
- private handleWarnings(adapters?: Partial) {
- const activeDatasourceId = getActiveDatasourceIdFromDoc(this.savedVis);
-
- if (!activeDatasourceId || !adapters?.requests) {
- return;
+ private getSearchWarningMessages(adapters?: Partial): UserMessage[] {
+ if (!this.activeDatasource || !this.activeDatasourceId || !adapters?.requests) {
+ return [];
}
- const activeDatasource = this.deps.datasourceMap[activeDatasourceId];
- const docDatasourceState = this.savedVis?.state.datasourceStates[activeDatasourceId];
+ const docDatasourceState = this.savedVis?.state.datasourceStates[this.activeDatasourceId];
const requestWarnings = getSearchWarningMessages(
adapters.requests,
- activeDatasource,
+ this.activeDatasource,
docDatasourceState,
{
searchService: this.deps.data.search,
}
);
- if (requestWarnings.length && this.warningDomNode) {
- render(
-
-
- ,
- this.warningDomNode
- );
- }
+ return requestWarnings;
}
+ private removeActiveDataWarningMessages: () => void = () => {};
private updateActiveData: ExpressionWrapperProps['onData$'] = (data, adapters) => {
- this.activeDataInfo.activeData = adapters?.tables?.tables;
if (this.input.onLoad) {
// once onData$ is get's called from expression renderer, loading becomes false
this.input.onLoad(false, adapters);
@@ -589,12 +706,23 @@ export class Embeddable
const { type, error } = data as { type: string; error: ErrorLike };
this.updateOutput({
- ...this.getOutput(),
loading: false,
error: type === 'error' ? error : undefined,
});
- this.handleWarnings(adapters);
+ const newActiveData = adapters?.tables?.tables;
+
+ if (!fastIsEqual(this.activeData, newActiveData)) {
+ // we check equality because this.addUserMessage triggers a render, so we get an infinite loop
+ // if we just execute without checking if the data has changed
+ this.removeActiveDataWarningMessages();
+ const searchWarningMessages = this.getSearchWarningMessages(adapters);
+ this.removeActiveDataWarningMessages = this.addUserMessages(
+ searchWarningMessages.filter(isMessageRemovable)
+ );
+ }
+
+ this.activeData = newActiveData;
};
private onRender: ExpressionWrapperProps['onRender$'] = () => {
@@ -662,25 +790,6 @@ 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
@@ -698,15 +807,22 @@ export class Embeddable
this.domNode.setAttribute('data-shared-item', '');
- const error = this.getError();
+ const errors = this.getUserMessages(['visualization', 'visualizationOnEmbeddable'], {
+ severity: 'error',
+ });
this.updateOutput({
- ...this.getOutput(),
loading: true,
- error,
+ error: errors.length
+ ? new Error(
+ typeof errors[0].longMessage === 'string'
+ ? errors[0].longMessage
+ : errors[0].shortMessage
+ )
+ : undefined,
});
- if (error) {
+ if (errors.length) {
this.renderComplete.dispatchError();
} else {
this.renderComplete.dispatchInProgress();
@@ -719,7 +835,7 @@ export class Embeddable
,
domNode
);
+
+ const warningsToDisplay = this.getUserMessages('embeddableBadge', {
+ severity: 'warning',
+ });
+
+ if (warningsToDisplay.length && this.warningDomNode) {
+ render(
+
+ message.longMessage)} compressed />
+ ,
+ this.warningDomNode
+ );
+ }
}
private readonly hasCompatibleActions = async (
@@ -902,13 +1031,14 @@ export class Embeddable
newVis.state.visualization = this.onEditAction(newVis.state.visualization, event);
this.savedVis = newVis;
- const { expression, errors } = await getExpressionFromDocument(
+ const { ast } = await getExpressionFromDocument(
this.savedVis,
this.deps.documentToExpression
);
- this.expression = expression;
- this.errors = errors;
+ this.expression = ast;
+
+ this.loadUserMessages();
this.reload();
}
};
@@ -924,81 +1054,36 @@ export class Embeddable
}
private async loadViewUnderlyingDataArgs(): Promise {
- if (!this.savedVis || !this.activeDataInfo.activeData) {
+ if (
+ !this.savedVis ||
+ !this.activeData ||
+ !this.activeDatasource ||
+ !this.activeDatasourceState ||
+ !this.activeVisualization ||
+ !this.activeVisualizationState
+ ) {
return false;
}
+
const mergedSearchContext = this.getMergedSearchContext();
if (!mergedSearchContext.timeRange) {
return false;
}
- const activeDatasourceId = getActiveDatasourceIdFromDoc(this.savedVis);
- if (!activeDatasourceId) {
- return false;
- }
-
- const activeVisualizationId = getActiveVisualizationIdFromDoc(this.savedVis);
- if (!activeVisualizationId) {
- return false;
- }
-
- this.activeDataInfo.activeDatasource = this.deps.datasourceMap[activeDatasourceId];
- this.activeDataInfo.activeVisualization = this.deps.visualizationMap[activeVisualizationId];
-
- const docDatasourceState = this.savedVis?.state.datasourceStates[activeDatasourceId];
- const adHocDataviews = await Promise.all(
- Object.values(this.savedVis?.state.adHocDataViews || {})
- .map((persistedSpec) => {
- return DataViewPersistableStateService.inject(persistedSpec, [
- ...(this.savedVis?.references || []),
- ...(this.savedVis?.state.internalReferences || []),
- ]);
- })
- .map((spec) => this.deps.dataViews.create(spec))
- );
-
- const allIndexPatterns = [...this.indexPatterns, ...adHocDataviews];
-
- const indexPatternsCache = allIndexPatterns.reduce(
- (acc, indexPattern) => ({
- [indexPattern.id!]: convertDataViewIntoLensIndexPattern(indexPattern),
- ...acc,
- }),
- {}
- );
-
- if (!this.activeDataInfo.activeDatasourceState) {
- this.activeDataInfo.activeDatasourceState = this.activeDataInfo.activeDatasource.initialize(
- docDatasourceState,
- [...(this.savedVis?.references || []), ...(this.savedVis?.state.internalReferences || [])],
- undefined,
- undefined,
- indexPatternsCache
- );
- }
-
- if (!this.activeDataInfo.activeVisualizationState) {
- this.activeDataInfo.activeVisualizationState =
- this.activeDataInfo.activeVisualization.initialize(
- () => '',
- this.savedVis?.state.visualization
- );
- }
-
const viewUnderlyingDataArgs = getViewUnderlyingDataArgs({
- activeDatasource: this.activeDataInfo.activeDatasource,
- activeDatasourceState: this.activeDataInfo.activeDatasourceState,
- activeVisualization: this.activeDataInfo.activeVisualization,
- activeVisualizationState: this.activeDataInfo.activeVisualizationState,
- activeData: this.activeDataInfo.activeData,
- dataViews: this.indexPatterns,
+ activeDatasource: this.activeDatasource,
+ activeDatasourceState: this.activeDatasourceState,
+ activeVisualization: this.activeVisualization,
+ activeVisualizationState: this.activeVisualizationState,
+ activeData: this.activeData,
+ dataViews: this.dataViews,
capabilities: this.deps.capabilities,
query: mergedSearchContext.query,
filters: mergedSearchContext.filters || [],
timeRange: mergedSearchContext.timeRange,
esQueryConfig: getEsQueryConfig(this.deps.uiSettings),
- indexPatternsCache,
+ indexPatternsCache: this.indexPatterns,
});
const loaded = typeof viewUnderlyingDataArgs !== 'undefined';
@@ -1038,33 +1123,48 @@ export class Embeddable
)
).forEach((dataView) => indexPatterns.push(dataView));
- this.indexPatterns = uniqBy(indexPatterns, 'id');
+ this.dataViews = uniqBy(indexPatterns, 'id');
// passing edit url and index patterns to the output of this embeddable for
// the container to pick them up and use them to configure filter bar and
// config dropdown correctly.
const input = this.getInput();
- this.errors = this.maybeAddTimeRangeError(this.errors, input, this.indexPatterns);
+ // if at least one indexPattern is time based, then the Lens embeddable requires the timeRange prop
+ if (
+ input.timeRange == null &&
+ indexPatterns.some((indexPattern) => indexPattern.isTimeBased())
+ ) {
+ this.addUserMessages([
+ {
+ uniqueId: 'missing-time-range-on-embeddable',
+ severity: 'error',
+ fixableInEditor: false,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: i18n.translate('xpack.lens.embeddable.missingTimeRangeParam.shortMessage', {
+ defaultMessage: `Missing timeRange property`,
+ }),
+ longMessage: i18n.translate('xpack.lens.embeddable.missingTimeRangeParam.longMessage', {
+ defaultMessage: `The timeRange property is required for the given configuration`,
+ }),
+ },
+ ]);
+ }
- if (this.errors) {
+ if (this.hasAnyErrors) {
this.logError('validation');
}
const title = input.hidePanelTitles ? '' : input.title ?? this.savedVis.title;
const savedObjectId = (input as LensByReferenceInput).savedObjectId;
this.updateOutput({
- ...this.getOutput(),
defaultTitle: this.savedVis.title,
editable: this.getIsEditable(),
title,
editPath: getEditPath(savedObjectId),
editUrl: this.deps.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`),
- indexPatterns: this.indexPatterns,
+ indexPatterns: this.dataViews,
});
-
- // deferred loading of this embeddable is complete
- this.setInitializationFinished();
}
private getIsEditable() {
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts
index 7904ba4c38f14..f57e532bc1380 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts
+++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts
@@ -7,6 +7,7 @@
import type {
Capabilities,
+ CoreStart,
HttpSetup,
IUiSettingsClient,
ThemeServiceStart,
@@ -30,14 +31,14 @@ import type { LensByReferenceInput, LensEmbeddableInput } from './embeddable';
import type { Document } from '../persistence/saved_object_store';
import type { LensAttributeService } from '../lens_attribute_service';
import { DOC_TYPE } from '../../common/constants';
-import type { ErrorMessage } from '../editor_frame_service/types';
import { extract, inject } from '../../common/embeddable_factory';
-import type { DatasourceMap, VisualizationMap } from '../types';
+import type { DatasourceMap, IndexPatternMap, IndexPatternRef, VisualizationMap } from '../types';
export interface LensEmbeddableStartServices {
data: DataPublicPluginStart;
timefilter: TimefilterContract;
coreHttp: HttpSetup;
+ coreStart: CoreStart;
inspector: InspectorStart;
attributeService: LensAttributeService;
capabilities: RecursiveReadonly;
@@ -45,9 +46,11 @@ export interface LensEmbeddableStartServices {
dataViews: DataViewsContract;
uiActions?: UiActionsStart;
usageCollection?: UsageCollectionSetup;
- documentToExpression: (
- doc: Document
- ) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>;
+ documentToExpression: (doc: Document) => Promise<{
+ ast: Ast | null;
+ indexPatterns: IndexPatternMap;
+ indexPatternRefs: IndexPatternRef[];
+ }>;
injectFilterReferences: FilterManager['inject'];
visualizationMap: VisualizationMap;
datasourceMap: DatasourceMap;
@@ -106,6 +109,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
datasourceMap,
uiActions,
coreHttp,
+ coreStart,
attributeService,
dataViews,
capabilities,
@@ -139,6 +143,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
navLinks: capabilities.navLinks,
discover: capabilities.discover,
},
+ coreStart,
usageCollection,
theme,
spaces,
diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx
index d5aaf99788ae7..717e28d94ab7a 100644
--- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx
+++ b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx
@@ -19,13 +19,13 @@ import { ExecutionContextSearch } from '@kbn/data-plugin/public';
import { DefaultInspectorAdapters, RenderMode } from '@kbn/expressions-plugin/common';
import classNames from 'classnames';
import { getOriginalRequestErrorMessages } from '../editor_frame_service/error_helper';
-import { ErrorMessage } from '../editor_frame_service/types';
import { LensInspector } from '../lens_inspector_service';
+import { UserMessage } from '../types';
export interface ExpressionWrapperProps {
ExpressionRenderer: ReactExpressionRendererType;
expression: string | null;
- errors: ErrorMessage[] | undefined;
+ errors: UserMessage[];
variables?: Record;
interactive?: boolean;
searchContext: ExecutionContextSearch;
@@ -57,8 +57,8 @@ interface VisualizationErrorProps {
}
export function VisualizationErrorPanel({ errors, canEdit }: VisualizationErrorProps) {
- const showMore = errors && errors.length > 1;
- const canFixInLens = canEdit && errors?.some(({ type }) => type === 'fixable');
+ const showMore = errors.length > 1;
+ const canFixInLens = canEdit && errors.some(({ fixableInEditor }) => fixableInEditor);
return (
- {errors ? (
+ {errors.length ? (
<>
{errors[0].longMessage}
{showMore && !canFixInLens ? (
@@ -129,7 +129,7 @@ export function ExpressionWrapper({
}: ExpressionWrapperProps) {
return (
- {errors || expression === null || expression === '' ? (
+ {errors.length || expression === null || expression === '' ? (
) : (
diff --git a/x-pack/plugins/lens/public/mocks/datasource_mock.ts b/x-pack/plugins/lens/public/mocks/datasource_mock.ts
index ab4a5fb519ab5..34a0c9197d7e3 100644
--- a/x-pack/plugins/lens/public/mocks/datasource_mock.ts
+++ b/x-pack/plugins/lens/public/mocks/datasource_mock.ts
@@ -63,7 +63,7 @@ export function createMockDatasource(
// this is an additional property which doesn't exist on real datasources
// but can be used to validate whether specific API mock functions are called
publicAPIMock,
- getErrorMessages: jest.fn((_state, _indexPatterns) => undefined),
+ getUserMessages: jest.fn((_state, _deps) => []),
checkIntegrity: jest.fn((_state, _indexPatterns) => []),
isTimeBased: jest.fn(),
isValidColumn: jest.fn(),
diff --git a/x-pack/plugins/lens/public/mocks/visualization_mock.ts b/x-pack/plugins/lens/public/mocks/visualization_mock.ts
index 27a6826e3d08a..a03914dfdd396 100644
--- a/x-pack/plugins/lens/public/mocks/visualization_mock.ts
+++ b/x-pack/plugins/lens/public/mocks/visualization_mock.ts
@@ -49,7 +49,6 @@ export function createMockVisualization(id = 'testVis'): jest.Mocked
undefined),
renderDimensionEditor: jest.fn(),
};
}
diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts
index 2b3dce5583978..6db5d4abb90e8 100644
--- a/x-pack/plugins/lens/public/plugin.ts
+++ b/x-pack/plugins/lens/public/plugin.ts
@@ -300,6 +300,7 @@ export class LensPlugin {
attributeService: getLensAttributeService(coreStart, plugins),
capabilities: coreStart.application.capabilities,
coreHttp: coreStart.http,
+ coreStart,
data: plugins.data,
timefilter: plugins.data.query.timefilter.timefilter,
expressionRenderer: plugins.expressions.ReactExpressionRenderer,
diff --git a/x-pack/plugins/lens/public/shared_components/dimension_trigger/index.tsx b/x-pack/plugins/lens/public/shared_components/dimension_trigger/index.tsx
index 25d81424c58df..320667a73edf2 100644
--- a/x-pack/plugins/lens/public/shared_components/dimension_trigger/index.tsx
+++ b/x-pack/plugins/lens/public/shared_components/dimension_trigger/index.tsx
@@ -31,7 +31,7 @@ export const DimensionTrigger = ({
id: string;
isInvalid?: boolean;
hideTooltip?: boolean;
- invalidMessage?: string | JSX.Element;
+ invalidMessage?: string | React.ReactNode;
}) => {
if (isInvalid) {
return (
diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts
index da64209a8a80f..5f00eed48005f 100644
--- a/x-pack/plugins/lens/public/state_management/lens_slice.ts
+++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import type { ReactNode } from 'react';
import { createAction, createReducer, current, PayloadAction } from '@reduxjs/toolkit';
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
import { mapValues, uniq } from 'lodash';
@@ -125,8 +124,7 @@ export const getPreloadedState = ({
export const setState = createAction>('lens/setState');
export const onActiveDataChange = createAction<{
- activeData?: TableInspectorAdapter;
- requestWarnings?: Array;
+ activeData: TableInspectorAdapter;
}>('lens/onActiveDataChange');
export const setSaveable = createAction('lens/setSaveable');
export const enableAutoApply = createAction('lens/enableAutoApply');
@@ -286,14 +284,11 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
},
[onActiveDataChange.type]: (
state,
- {
- payload: { activeData, requestWarnings },
- }: PayloadAction<{ activeData: TableInspectorAdapter; requestWarnings?: string[] }>
+ { payload: { activeData } }: PayloadAction<{ activeData: TableInspectorAdapter }>
) => {
return {
...state,
- ...(activeData ? { activeData } : {}),
- ...(requestWarnings ? { requestWarnings } : {}),
+ activeData,
};
},
[setSaveable.type]: (state, { payload }: PayloadAction) => {
diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts
index f7402bf2339d3..38286e78a9078 100644
--- a/x-pack/plugins/lens/public/state_management/selectors.ts
+++ b/x-pack/plugins/lens/public/state_management/selectors.ts
@@ -238,3 +238,8 @@ export const selectFramePublicAPI = createSelector(
};
}
);
+
+export const selectFrameDatasourceAPI = createSelector(
+ [selectFramePublicAPI, selectExecutionContext],
+ (framePublicAPI, context) => ({ ...context, ...framePublicAPI })
+);
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index 9e226dcdb1edc..4a2987a6c5746 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -18,7 +18,6 @@ import type {
ExpressionRendererEvent,
} from '@kbn/expressions-plugin/public';
import type { Configuration, NavigateToLensContext } from '@kbn/visualizations-plugin/common';
-import { Adapters } from '@kbn/inspector-plugin/public';
import type { Query } from '@kbn/es-query';
import type {
UiActionsStart,
@@ -105,6 +104,8 @@ export interface EditorFrameProps {
showNoDataPopover: () => void;
lensInspector: LensInspector;
indexPatternService: IndexPatternServiceAPI;
+ getUserMessages: UserMessagesGetter;
+ addUserMessages: AddUserMessages;
}
export type VisualizationMap = Record;
@@ -272,6 +273,49 @@ interface DimensionLink {
};
}
+type UserMessageDisplayLocation =
+ | {
+ // NOTE: We want to move toward more errors that do not block the render!
+ id:
+ | 'toolbar'
+ | 'embeddableBadge'
+ | 'visualization' // blocks render
+ | 'visualizationOnEmbeddable' // blocks render in embeddable only
+ | 'visualizationInEditor' // blocks render in editor only
+ | 'textBasedLanguagesQueryInput'
+ | 'banner';
+ }
+ | { id: 'dimensionTrigger'; dimensionId: string };
+
+export type UserMessagesDisplayLocationId = UserMessageDisplayLocation['id'];
+
+export interface UserMessage {
+ uniqueId?: string;
+ severity: 'error' | 'warning';
+ shortMessage: string;
+ longMessage: React.ReactNode | string;
+ fixableInEditor: boolean;
+ displayLocations: UserMessageDisplayLocation[];
+}
+
+export type RemovableUserMessage = UserMessage & { uniqueId: string };
+
+export interface UserMessageFilters {
+ severity?: UserMessage['severity'];
+ dimensionId?: string;
+}
+
+export type UserMessagesGetter = (
+ locationId: UserMessagesDisplayLocationId | UserMessagesDisplayLocationId[] | undefined,
+ filters: UserMessageFilters
+) => UserMessage[];
+
+export type AddUserMessages = (messages: RemovableUserMessage[]) => () => void;
+
+export function isMessageRemovable(message: UserMessage): message is RemovableUserMessage {
+ return Boolean(message.uniqueId);
+}
+
/**
* Interface for the datasource registry
*/
@@ -291,7 +335,6 @@ export interface Datasource {
// Given the current state, which parts should be saved?
getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] };
- getUnifiedSearchErrors?: (state: T) => Error[];
insertLayer: (state: T, newLayerId: string, linkToLayers?: string[]) => T;
createEmptyLayer: (indexPatternId: string) => T;
@@ -433,28 +476,16 @@ export interface Datasource {
*/
checkIntegrity: (state: T, indexPatterns: IndexPatternMap) => string[];
- getErrorMessages: (
- state: T,
- indexPatterns: Record
- ) =>
- | Array<{
- shortMessage: string;
- longMessage: React.ReactNode;
- fixAction?: { label: string; newState: () => Promise };
- }>
- | undefined;
-
/**
- * The frame calls this function to display warnings about visualization
+ * The frame calls this function to display messages to the user
*/
- getWarningMessages?: (
+ getUserMessages: (
state: T,
- frame: FramePublicAPI,
- adapters: Adapters,
- setState: StateSetter
- ) => React.ReactNode[] | undefined;
-
- getDeprecationMessages?: (state: T) => React.ReactNode[] | undefined;
+ deps: {
+ frame: FrameDatasourceAPI;
+ setState: StateSetter;
+ }
+ ) => UserMessage[];
/**
* The embeddable calls this function to display warnings about visualization on the dashboard
@@ -464,7 +495,7 @@ export interface Datasource {
warning: SearchResponseWarning,
request: SearchRequest,
response: estypes.SearchResponse
- ) => Array | undefined;
+ ) => UserMessage[];
/**
* Checks if the visualization created is time based, for example date histogram
@@ -623,7 +654,7 @@ export type DatasourceDimensionProps = SharedDimensionProps & {
indexPatterns: IndexPatternMap;
hideTooltip?: boolean;
invalid?: boolean;
- invalidMessage?: string;
+ invalidMessage?: string | React.ReactNode;
};
export type ParamEditorCustomProps = Record & {
labels?: string[];
@@ -835,9 +866,6 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & {
// this dimension group in the hierarchy. If not specified, the position of the dimension in the array is used. specified nesting
// orders are always higher in the hierarchy than non-specified ones.
nestingOrder?: number;
- // some type of layers can produce groups even if invalid. Keep this information to visually show the user that.
- invalid?: boolean;
- invalidMessage?: string;
// need a special flag to know when to pass the previous column on duplicating
requiresPreviousColumnOnDuplicate?: boolean;
supportStaticValue?: boolean;
@@ -1214,7 +1242,7 @@ export interface Visualization {
label: string;
hideTooltip?: boolean;
invalid?: boolean;
- invalidMessage?: string;
+ invalidMessage?: string | React.ReactNode;
}) => JSX.Element | null;
/**
* Creates map of columns ids and unique lables. Used only for noDatasource layers
@@ -1246,32 +1274,12 @@ export interface Visualization {
datasourceLayers: DatasourceLayers,
datasourceExpressionsByLayers?: Record
) => ExpressionAstExpression | string | null;
+
/**
* The frame will call this function on all visualizations at few stages (pre-build/build error) in order
* to provide more context to the error and show it to the user
*/
- getErrorMessages: (
- state: T,
- frame?: Pick
- ) =>
- | Array<{
- shortMessage: string;
- longMessage: React.ReactNode;
- }>
- | undefined;
-
- validateColumn?: (
- state: T,
- frame: Pick,
- layerId: string,
- columnId: string,
- group?: VisualizationDimensionGroupConfig
- ) => { invalid: boolean; invalidMessage?: string };
-
- /**
- * The frame calls this function to display warnings about visualization
- */
- getWarningMessages?: (state: T, frame: FramePublicAPI) => React.ReactNode[] | undefined;
+ getUserMessages?: (state: T, deps: { frame: FramePublicAPI }) => UserMessage[];
/**
* On Edit events the frame will call this to know what's going to be the next visualization state
diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts
index c268a79599e77..ad3d6a6178807 100644
--- a/x-pack/plugins/lens/public/utils.ts
+++ b/x-pack/plugins/lens/public/utils.ts
@@ -16,7 +16,6 @@ import type { DatatableUtilitiesService } from '@kbn/data-plugin/common';
import { BrushTriggerEvent, ClickTriggerEvent } from '@kbn/charts-plugin/public';
import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { ISearchStart } from '@kbn/data-plugin/public';
-import React from 'react';
import type { Document } from './persistence/saved_object_store';
import {
Datasource,
@@ -27,6 +26,7 @@ import {
DraggedField,
DragDropOperation,
isOperation,
+ UserMessage,
} from './types';
import type { DatasourceStates, VisualizationState } from './state_management';
import type { IndexPatternServiceAPI } from './data_views_service/service';
@@ -329,8 +329,8 @@ export const getSearchWarningMessages = (
deps: {
searchService: ISearchStart;
}
-) => {
- const warningsMap: Map> = new Map();
+): UserMessage[] => {
+ const warningsMap: Map = new Map();
deps.searchService.showWarnings(adapter, (warning, meta) => {
const { request, response, requestId } = meta;
diff --git a/x-pack/plugins/lens/public/visualization_container.scss b/x-pack/plugins/lens/public/visualization_container.scss
index a20bcef82e17b..ea4a8bdbce994 100644
--- a/x-pack/plugins/lens/public/visualization_container.scss
+++ b/x-pack/plugins/lens/public/visualization_container.scss
@@ -25,8 +25,4 @@
align-items: center;
justify-content: center;
overflow: auto;
-}
-
-.lnsSelectableErrorMessage {
- user-select: text;
}
\ No newline at end of file
diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx
index a38d669d73cd5..a6810e77d4388 100644
--- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx
+++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx
@@ -695,60 +695,6 @@ describe('Datatable Visualization', () => {
});
});
- describe('#getErrorMessages', () => {
- it('returns undefined if the datasource is missing a metric dimension', () => {
- const datasource = createMockDatasource('test');
- const frame = mockFrame();
- frame.datasourceLayers = { a: datasource.publicAPIMock };
- datasource.publicAPIMock.getTableSpec.mockReturnValue([
- { columnId: 'c', fields: [] },
- { columnId: 'b', fields: [] },
- ]);
- datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
- dataType: 'string',
- isBucketed: true, // move it from the metric to the break down by side
- label: 'label',
- isStaticValue: false,
- hasTimeShift: false,
- hasReducedTimeRange: false,
- });
-
- const error = datatableVisualization.getErrorMessages({
- layerId: 'a',
- layerType: LayerTypes.DATA,
- columns: [{ columnId: 'b' }, { columnId: 'c' }],
- });
-
- expect(error).toBeUndefined();
- });
-
- it('returns undefined if the metric dimension is defined', () => {
- const datasource = createMockDatasource('test');
- const frame = mockFrame();
- frame.datasourceLayers = { a: datasource.publicAPIMock };
- datasource.publicAPIMock.getTableSpec.mockReturnValue([
- { columnId: 'c', fields: [] },
- { columnId: 'b', fields: [] },
- ]);
- datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
- dataType: 'string',
- isBucketed: false, // keep it a metric
- label: 'label',
- isStaticValue: false,
- hasTimeShift: false,
- hasReducedTimeRange: false,
- });
-
- const error = datatableVisualization.getErrorMessages({
- layerId: 'a',
- layerType: LayerTypes.DATA,
- columns: [{ columnId: 'b' }, { columnId: 'c' }],
- });
-
- expect(error).toBeUndefined();
- });
- });
-
describe('#onEditAction', () => {
it('should add a sort column to the state', () => {
const currentState: DatatableVisualizationState = {
diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx
index 9e31b0bd0035e..9b4865d597d88 100644
--- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx
+++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx
@@ -499,10 +499,6 @@ export const getDatatableVisualization = ({
};
},
- getErrorMessages(state) {
- return undefined;
- },
-
getRenderEventCounters(state) {
const events = {
color_by_value: false,
diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts
index 29e14c9412bd1..9d90fac937bf3 100644
--- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts
@@ -345,107 +345,6 @@ describe('gauge', () => {
],
});
});
-
- test('resolves configuration when with group error when max < minimum', () => {
- const state: GaugeVisualizationState = {
- ...exampleState(),
- layerId: 'first',
- metricAccessor: 'metric-accessor',
- minAccessor: 'min-accessor',
- maxAccessor: 'max-accessor',
- goalAccessor: 'goal-accessor',
- };
- frame.activeData = {
- first: {
- type: 'datatable',
- columns: [],
- rows: [{ 'min-accessor': 10, 'max-accessor': 0 }],
- },
- };
-
- expect(
- getGaugeVisualization({
- paletteService,
- theme,
- }).getConfiguration({ state, frame, layerId: 'first' })
- ).toEqual({
- groups: [
- {
- layerId: 'first',
- paramEditorCustomProps: {
- headingLabel: 'Value',
- },
- groupId: GROUP_ID.METRIC,
- groupLabel: 'Metric',
- isMetricDimension: true,
- accessors: [{ columnId: 'metric-accessor', triggerIconType: 'none' }],
- filterOperations: isNumericDynamicMetric,
- supportsMoreColumns: false,
- requiredMinDimensionCount: 1,
- dataTestSubj: 'lnsGauge_metricDimensionPanel',
- enableDimensionEditor: true,
- enableFormatSelector: true,
- },
- {
- layerId: 'first',
- paramEditorCustomProps: {
- headingLabel: 'Value',
- labels: ['Minimum value'],
- },
- groupId: GROUP_ID.MIN,
- groupLabel: 'Minimum value',
- isMetricDimension: true,
- accessors: [{ columnId: 'min-accessor' }],
- filterOperations: isNumericMetric,
- supportsMoreColumns: false,
- dataTestSubj: 'lnsGauge_minDimensionPanel',
- prioritizedOperation: 'min',
- suggestedValue: expect.any(Function),
- enableFormatSelector: false,
- supportStaticValue: true,
- invalid: true,
- invalidMessage: 'Minimum value may not be greater than maximum value',
- },
- {
- layerId: 'first',
- paramEditorCustomProps: {
- headingLabel: 'Value',
- labels: ['Maximum value'],
- },
- groupId: GROUP_ID.MAX,
- groupLabel: 'Maximum value',
- isMetricDimension: true,
- accessors: [{ columnId: 'max-accessor' }],
- filterOperations: isNumericMetric,
- supportsMoreColumns: false,
- dataTestSubj: 'lnsGauge_maxDimensionPanel',
- prioritizedOperation: 'max',
- suggestedValue: expect.any(Function),
- enableFormatSelector: false,
- supportStaticValue: true,
- invalid: true,
- invalidMessage: 'Minimum value may not be greater than maximum value',
- },
- {
- layerId: 'first',
- paramEditorCustomProps: {
- headingLabel: 'Value',
- labels: ['Goal value'],
- },
- groupId: GROUP_ID.GOAL,
- groupLabel: 'Goal value',
- isMetricDimension: true,
- accessors: [{ columnId: 'goal-accessor' }],
- filterOperations: isNumericMetric,
- supportsMoreColumns: false,
- requiredMinDimensionCount: 0,
- dataTestSubj: 'lnsGauge_goalDimensionPanel',
- enableFormatSelector: false,
- supportStaticValue: true,
- },
- ],
- });
- });
});
describe('#setDimension', () => {
@@ -607,17 +506,7 @@ describe('gauge', () => {
});
});
- describe('#getErrorMessages', () => {
- it('returns undefined if no error is raised', () => {
- const error = getGaugeVisualization({
- paletteService,
- theme,
- }).getErrorMessages(exampleState());
- expect(error).not.toBeDefined();
- });
- });
-
- describe('#getWarningMessages', () => {
+ describe('#getUserMessages', () => {
beforeEach(() => {
const mockDatasource = createMockDatasource('testDatasource');
mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
@@ -636,6 +525,51 @@ describe('gauge', () => {
maxAccessor: 'max-accessor',
goalAccessor: 'goal-accessor',
};
+
+ it('should report error when max < minimum', () => {
+ const localState: GaugeVisualizationState = {
+ ...exampleState(),
+ layerId: 'first',
+ metricAccessor: 'metric-accessor',
+ minAccessor: 'min-accessor',
+ maxAccessor: 'max-accessor',
+ goalAccessor: 'goal-accessor',
+ };
+ frame.activeData = {
+ first: {
+ type: 'datatable',
+ columns: [],
+ rows: [{ 'min-accessor': 10, 'max-accessor': 0 }],
+ },
+ };
+
+ expect(
+ getGaugeVisualization({
+ paletteService,
+ theme,
+ }).getUserMessages!(localState, { frame })
+ ).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "displayLocations": Array [
+ Object {
+ "dimensionId": "min-accessor",
+ "id": "dimensionTrigger",
+ },
+ Object {
+ "dimensionId": "max-accessor",
+ "id": "dimensionTrigger",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "",
+ "severity": "error",
+ "shortMessage": "Minimum value may not be greater than maximum value",
+ },
+ ]
+ `);
+ });
+
it('should not warn for data in bounds', () => {
frame.activeData = {
first: {
@@ -656,7 +590,7 @@ describe('gauge', () => {
getGaugeVisualization({
paletteService,
theme,
- }).getWarningMessages!(state, frame)
+ }).getUserMessages!(state, { frame })
).toHaveLength(0);
});
it('should warn when minimum value is greater than metric value', () => {
@@ -679,7 +613,7 @@ describe('gauge', () => {
getGaugeVisualization({
paletteService,
theme,
- }).getWarningMessages!(state, frame)
+ }).getUserMessages!(state, { frame })
).toHaveLength(1);
});
@@ -702,7 +636,7 @@ describe('gauge', () => {
getGaugeVisualization({
paletteService,
theme,
- }).getWarningMessages!(state, frame)
+ }).getUserMessages!(state, { frame })
).toHaveLength(1);
});
it('should warn when goal value is greater than maximum value', () => {
@@ -725,7 +659,7 @@ describe('gauge', () => {
getGaugeVisualization({
paletteService,
theme,
- }).getWarningMessages!(state, frame)
+ }).getUserMessages!(state, { frame })
).toHaveLength(1);
});
it('should warn when minimum value is greater than goal value', () => {
@@ -748,7 +682,7 @@ describe('gauge', () => {
getGaugeVisualization({
paletteService,
theme,
- }).getWarningMessages!(state, frame)
+ }).getUserMessages!(state, { frame })
).toHaveLength(1);
});
});
diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx
index 26077fd45c566..d0d999d444196 100644
--- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx
+++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx
@@ -25,7 +25,13 @@ import {
import { IconChartHorizontalBullet, IconChartVerticalBullet } from '@kbn/chart-icons';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
import type { FormBasedPersistedState } from '../../datasources/form_based/types';
-import type { DatasourceLayers, OperationMetadata, Suggestion, Visualization } from '../../types';
+import type {
+ DatasourceLayers,
+ OperationMetadata,
+ Suggestion,
+ UserMessage,
+ Visualization,
+} from '../../types';
import { getSuggestions } from './suggestions';
import { GROUP_ID, LENS_GAUGE_ID, GaugeVisualizationState } from './constants';
import { GaugeToolbar } from './toolbar_component';
@@ -76,35 +82,52 @@ function computePaletteParams(params: CustomPaletteParams) {
};
}
-const checkInvalidConfiguration = (row?: DatatableRow, state?: GaugeVisualizationState) => {
+const getErrorMessages = (row?: DatatableRow, state?: GaugeVisualizationState): UserMessage[] => {
if (!row || !state) {
- return;
+ return [];
}
+
+ const errors: UserMessage[] = [];
+
const minAccessor = state?.minAccessor;
const maxAccessor = state?.maxAccessor;
const minValue = minAccessor ? getValueFromAccessor(minAccessor, row) : undefined;
const maxValue = maxAccessor ? getValueFromAccessor(maxAccessor, row) : undefined;
if (maxValue !== null && maxValue !== undefined && minValue != null && minValue !== undefined) {
if (maxValue < minValue) {
- return {
- invalid: true,
- invalidMessage: i18n.translate(
+ errors.push({
+ severity: 'error',
+ displayLocations: [
+ { id: 'dimensionTrigger', dimensionId: minAccessor! },
+ { id: 'dimensionTrigger', dimensionId: maxAccessor! },
+ ],
+ fixableInEditor: true,
+ shortMessage: i18n.translate(
'xpack.lens.guageVisualization.chartCannotRenderMinGreaterMax',
{
defaultMessage: 'Minimum value may not be greater than maximum value',
}
),
- };
+ longMessage: '',
+ });
}
if (maxValue === minValue) {
- return {
- invalid: true,
- invalidMessage: i18n.translate('xpack.lens.guageVisualization.chartCannotRenderEqual', {
+ errors.push({
+ severity: 'error',
+ displayLocations: [
+ { id: 'dimensionTrigger', dimensionId: minAccessor! },
+ { id: 'dimensionTrigger', dimensionId: maxAccessor! },
+ ],
+ fixableInEditor: true,
+ shortMessage: i18n.translate('xpack.lens.guageVisualization.chartCannotRenderEqual', {
defaultMessage: 'Minimum and maximum values may not be equal',
}),
- };
+ longMessage: '',
+ });
}
}
+
+ return errors;
};
const toExpression = (
@@ -228,7 +251,6 @@ export const getGaugeVisualization = ({
const displayStops = applyPaletteParams(paletteService, state?.palette, currentMinMax);
palette = displayStops.map(({ color }) => color);
}
- const invalidProps = checkInvalidConfiguration(row, state) || {};
return {
groups: [
@@ -290,7 +312,6 @@ export const getGaugeVisualization = ({
dataTestSubj: 'lnsGauge_minDimensionPanel',
prioritizedOperation: 'min',
suggestedValue: () => (state.metricAccessor ? getMinValue(row, accessors) : undefined),
- ...invalidProps,
},
{
supportStaticValue: true,
@@ -317,7 +338,6 @@ export const getGaugeVisualization = ({
dataTestSubj: 'lnsGauge_maxDimensionPanel',
prioritizedOperation: 'max',
suggestedValue: () => (state.metricAccessor ? getMaxValue(row, accessors) : undefined),
- ...invalidProps,
},
{
supportStaticValue: true,
@@ -466,67 +486,92 @@ export const getGaugeVisualization = ({
toPreviewExpression: (state, datasourceLayers, datasourceExpressionsByLayers = {}) =>
toExpression(paletteService, state, datasourceLayers, undefined, datasourceExpressionsByLayers),
- getErrorMessages(state) {
- // not possible to break it?
- return undefined;
- },
-
- getWarningMessages(state, frame) {
+ getUserMessages(state, { frame }) {
const { maxAccessor, minAccessor, goalAccessor, metricAccessor } = state;
if (!maxAccessor && !minAccessor && !goalAccessor && !metricAccessor) {
// nothing configured yet
- return;
+ return [];
}
if (!metricAccessor) {
return [];
}
- const row = frame?.activeData?.[state.layerId]?.rows?.[0];
- if (!row || checkInvalidConfiguration(row, state)) {
+ const row = frame.activeData?.[state.layerId]?.rows?.[0];
+ if (!row) {
return [];
}
+
+ const errors = getErrorMessages(row, state);
+ if (errors.length) {
+ return errors;
+ }
+
const metricValue = row[metricAccessor];
const maxValue = maxAccessor && row[maxAccessor];
const minValue = minAccessor && row[minAccessor];
const goalValue = goalAccessor && row[goalAccessor];
- const warnings = [];
+ const warnings: UserMessage[] = [];
if (typeof minValue === 'number') {
if (minValue > metricValue) {
- warnings.push([
- ,
- ]);
+ warnings.push({
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'toolbar' }],
+ shortMessage: '',
+ longMessage: (
+
+ ),
+ });
}
if (minValue > goalValue) {
- warnings.push([
- ,
- ]);
+ warnings.push({
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'toolbar' }],
+ shortMessage: '',
+ longMessage: (
+
+ ),
+ });
}
}
if (typeof maxValue === 'number') {
if (metricValue > maxValue) {
- warnings.push([
- ,
- ]);
+ warnings.push({
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'toolbar' }],
+ shortMessage: '',
+ longMessage: (
+
+ ),
+ });
}
if (typeof goalValue === 'number' && goalValue > maxValue) {
- warnings.push([
- ,
- ]);
+ warnings.push({
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'toolbar' }],
+ shortMessage: '',
+ longMessage: (
+
+ ),
+ });
}
}
diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts
index 7f6a9caf506fd..2cb2869339697 100644
--- a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts
@@ -21,7 +21,12 @@ import {
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
import { Position } from '@elastic/charts';
import type { HeatmapVisualizationState } from './types';
-import type { DatasourceLayers, OperationDescriptor } from '../../types';
+import type {
+ DatasourceLayers,
+ FramePublicAPI,
+ OperationDescriptor,
+ UserMessage,
+} from '../../types';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { themeServiceMock } from '@kbn/core/public/mocks';
@@ -581,7 +586,7 @@ describe('heatmap', () => {
});
});
- describe('#getErrorMessages', () => {
+ describe('#getUserMessages', () => {
test('should not return an error when chart has empty configuration', () => {
const mockState = {
shape: CHART_SHAPES.HEATMAP,
@@ -590,8 +595,10 @@ describe('heatmap', () => {
getHeatmapVisualization({
paletteService,
theme,
- }).getErrorMessages(mockState)
- ).toEqual(undefined);
+ }).getUserMessages!(mockState, {
+ frame: {} as FramePublicAPI,
+ })
+ ).toHaveLength(0);
});
test('should return an error when the X accessor is missing', () => {
@@ -603,88 +610,108 @@ describe('heatmap', () => {
getHeatmapVisualization({
paletteService,
theme,
- }).getErrorMessages(mockState)
- ).toEqual([
- {
- longMessage: 'Configuration for the horizontal axis is missing.',
- shortMessage: 'Missing Horizontal axis.',
- },
- ]);
+ }).getUserMessages!(mockState, {
+ frame: {} as FramePublicAPI,
+ })
+ ).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "displayLocations": Array [
+ Object {
+ "id": "visualization",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "Configuration for the horizontal axis is missing.",
+ "severity": "error",
+ "shortMessage": "Missing Horizontal axis.",
+ },
+ ]
+ `);
});
- });
- describe('#getWarningMessages', () => {
- beforeEach(() => {
- const mockDatasource = createMockDatasource('testDatasource');
+ describe('warnings', () => {
+ beforeEach(() => {
+ const mockDatasource = createMockDatasource('testDatasource');
- mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
- dataType: 'string',
- label: 'MyOperation',
- } as OperationDescriptor);
+ mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
+ dataType: 'string',
+ label: 'MyOperation',
+ } as OperationDescriptor);
- frame.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- };
- });
+ frame.datasourceLayers = {
+ first: mockDatasource.publicAPIMock,
+ };
+ });
- test('should not return warning messages when the layer it not configured', () => {
- const mockState = {
- shape: CHART_SHAPES.HEATMAP,
- valueAccessor: 'v-accessor',
- } as HeatmapVisualizationState;
- expect(
- getHeatmapVisualization({
- paletteService,
- theme,
- }).getWarningMessages!(mockState, frame)
- ).toEqual(undefined);
- });
+ const onlyWarnings = (messages: UserMessage[]) =>
+ messages.filter(({ severity }) => severity === 'warning');
+
+ test('should not return warning messages when the layer it not configured', () => {
+ const mockState = {
+ shape: CHART_SHAPES.HEATMAP,
+ valueAccessor: 'v-accessor',
+ } as HeatmapVisualizationState;
+ expect(
+ onlyWarnings(
+ getHeatmapVisualization({
+ paletteService,
+ theme,
+ }).getUserMessages!(mockState, { frame })
+ )
+ ).toHaveLength(0);
+ });
- test('should not return warning messages when the data table is empty', () => {
- frame.activeData = {
- first: {
- type: 'datatable',
- rows: [],
- columns: [],
- },
- };
- const mockState = {
- shape: CHART_SHAPES.HEATMAP,
- valueAccessor: 'v-accessor',
- layerId: 'first',
- } as HeatmapVisualizationState;
- expect(
- getHeatmapVisualization({
- paletteService,
- theme,
- }).getWarningMessages!(mockState, frame)
- ).toEqual(undefined);
- });
+ test('should not return warning messages when the data table is empty', () => {
+ frame.activeData = {
+ first: {
+ type: 'datatable',
+ rows: [],
+ columns: [],
+ },
+ };
+ const mockState = {
+ shape: CHART_SHAPES.HEATMAP,
+ valueAccessor: 'v-accessor',
+ layerId: 'first',
+ } as HeatmapVisualizationState;
+ expect(
+ onlyWarnings(
+ getHeatmapVisualization({
+ paletteService,
+ theme,
+ }).getUserMessages!(mockState, { frame })
+ )
+ ).toHaveLength(0);
+ });
- test('should return a warning message when cell value data contains arrays', () => {
- frame.activeData = {
- first: {
- type: 'datatable',
- rows: [
- {
- 'v-accessor': [1, 2, 3],
- },
- ],
- columns: [],
- },
- };
+ test('should return a warning message when cell value data contains arrays', () => {
+ frame.activeData = {
+ first: {
+ type: 'datatable',
+ rows: [
+ {
+ 'v-accessor': [1, 2, 3],
+ },
+ ],
+ columns: [],
+ },
+ };
- const mockState = {
- shape: CHART_SHAPES.HEATMAP,
- valueAccessor: 'v-accessor',
- layerId: 'first',
- } as HeatmapVisualizationState;
- expect(
- getHeatmapVisualization({
- paletteService,
- theme,
- }).getWarningMessages!(mockState, frame)
- ).toHaveLength(1);
+ const mockState = {
+ shape: CHART_SHAPES.HEATMAP,
+ valueAccessor: 'v-accessor',
+ layerId: 'first',
+ } as HeatmapVisualizationState;
+ expect(
+ onlyWarnings(
+ getHeatmapVisualization({
+ paletteService,
+ theme,
+ }).getUserMessages!(mockState, { frame })
+ )
+ ).toHaveLength(1);
+ });
});
});
});
diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx
index 1db54423bf1a0..4450ec558db8b 100644
--- a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx
+++ b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx
@@ -24,7 +24,7 @@ import {
HeatmapLegendExpressionFunctionDefinition,
} from '@kbn/expression-heatmap-plugin/common';
import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/common';
-import type { OperationMetadata, Suggestion, Visualization } from '../../types';
+import type { OperationMetadata, Suggestion, UserMessage, Visualization } from '../../types';
import type { HeatmapVisualizationState } from './types';
import { getSuggestions } from './suggestions';
import {
@@ -433,16 +433,19 @@ export const getHeatmapVisualization = ({
};
},
- getErrorMessages(state) {
+ getUserMessages(state, { frame }) {
if (!state.yAccessor && !state.xAccessor && !state.valueAccessor) {
// nothing configured yet
- return;
+ return [];
}
- const errors: ReturnType = [];
+ const errors: UserMessage[] = [];
if (!state.xAccessor) {
errors.push({
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
shortMessage: i18n.translate(
'xpack.lens.heatmapVisualization.missingXAccessorShortMessage',
{
@@ -455,33 +458,37 @@ export const getHeatmapVisualization = ({
});
}
- return errors.length ? errors : undefined;
- },
-
- getWarningMessages(state, frame) {
- if (!state?.layerId || !frame.activeData || !state.valueAccessor) {
- return;
- }
-
- const rows = frame.activeData[state.layerId] && frame.activeData[state.layerId].rows;
- if (!rows) {
- return;
+ let warnings: UserMessage[] = [];
+
+ if (state?.layerId && frame.activeData && state.valueAccessor) {
+ const rows = frame.activeData[state.layerId] && frame.activeData[state.layerId].rows;
+ if (rows) {
+ const hasArrayValues = rows.some((row) => Array.isArray(row[state.valueAccessor!]));
+
+ const datasource = frame.datasourceLayers[state.layerId];
+ const operation = datasource?.getOperationForColumnId(state.valueAccessor);
+
+ warnings = hasArrayValues
+ ? [
+ {
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'toolbar' }],
+ shortMessage: '',
+ longMessage: (
+ {operation?.label} }}
+ />
+ ),
+ },
+ ]
+ : [];
+ }
}
- const hasArrayValues = rows.some((row) => Array.isArray(row[state.valueAccessor!]));
-
- const datasource = frame.datasourceLayers[state.layerId];
- const operation = datasource?.getOperationForColumnId(state.valueAccessor);
-
- return hasArrayValues
- ? [
- {operation?.label} }}
- />,
- ]
- : undefined;
+ return [...errors, ...warnings];
},
getSuggestionFromConvertToLensContext({ suggestions, context }) {
diff --git a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts
index ebb84b8da1b71..d6a70ddbe7114 100644
--- a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts
@@ -384,12 +384,4 @@ describe('metric_visualization', () => {
`);
});
});
-
- describe('#getErrorMessages', () => {
- it('returns undefined if no error is raised', () => {
- const error = metricVisualization.getErrorMessages(exampleState());
-
- expect(error).not.toBeDefined();
- });
- });
});
diff --git a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx
index 499003dd87319..957010b5b131e 100644
--- a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx
+++ b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx
@@ -312,11 +312,6 @@ export const getLegacyMetricVisualization = ({
);
},
- getErrorMessages(state) {
- // Is it possible to break it?
- return undefined;
- },
-
getVisualizationInfo(state: LegacyMetricState) {
const dimensions = [];
if (state.accessor) {
diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx
index 2a77ce35be48c..380c63975a977 100644
--- a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx
+++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx
@@ -634,11 +634,6 @@ export const getMetricVisualization = ({
);
},
- getErrorMessages(state) {
- // Is it possible to break it?
- return undefined;
- },
-
getDisplayOptions() {
return {
noPanelTitle: true,
diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts
index 8617a2f0f7664..2bf830c0028cd 100644
--- a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts
@@ -72,7 +72,7 @@ function mockFrame(): FramePublicAPI {
describe('pie_visualization', () => {
beforeEach(() => jest.clearAllMocks());
- describe('#getErrorMessages', () => {
+ describe('#getUserMessages', () => {
describe('too many dimensions', () => {
const state = { ...getExampleState(), shape: PieChartTypes.MOSAIC };
const colIds = new Array(PartitionChartsMeta.mosaic.maxBuckets + 1)
@@ -83,7 +83,9 @@ describe('pie_visualization', () => {
state.layers[0].secondaryGroups = colIds.slice(2);
it('returns error', () => {
- expect(pieVisualization.getErrorMessages(state)).toHaveLength(1);
+ expect(
+ pieVisualization.getUserMessages!(state, { frame: {} as FramePublicAPI })
+ ).toHaveLength(1);
});
it("doesn't count collapsed dimensions", () => {
@@ -92,17 +94,23 @@ describe('pie_visualization', () => {
[colIds[0]]: 'some-fn' as CollapseFunction,
};
- expect(pieVisualization.getErrorMessages(localState)).toHaveLength(0);
+ expect(
+ pieVisualization.getUserMessages!(localState, { frame: {} as FramePublicAPI })
+ ).toHaveLength(0);
});
it('counts multiple metrics as an extra bucket dimension', () => {
const localState = cloneDeep(state);
localState.layers[0].primaryGroups.pop();
- expect(pieVisualization.getErrorMessages(localState)).toHaveLength(0);
+ expect(
+ pieVisualization.getUserMessages!(localState, { frame: {} as FramePublicAPI })
+ ).toHaveLength(0);
localState.layers[0].metrics.push('one-metric', 'another-metric');
- expect(pieVisualization.getErrorMessages(localState)).toHaveLength(1);
+ expect(
+ pieVisualization.getUserMessages!(localState, { frame: {} as FramePublicAPI })
+ ).toHaveLength(1);
});
});
});
diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx
index 4c214de970e82..cedfb12f72df7 100644
--- a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx
+++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx
@@ -25,6 +25,7 @@ import type {
Suggestion,
VisualizeEditorContext,
VisualizationInfo,
+ UserMessage,
} from '../../types';
import {
getColumnToLabelMap,
@@ -508,66 +509,6 @@ export const getPieVisualization = ({
);
},
- getWarningMessages(state, frame) {
- if (state?.layers.length === 0 || !frame.activeData) {
- return;
- }
- const warningMessages = [];
-
- for (const layer of state.layers) {
- const { layerId, metrics } = layer;
- const rows = frame.activeData[layerId]?.rows;
- const numericColumn = frame.activeData[layerId]?.columns.find(
- ({ meta }) => meta?.type === 'number'
- );
-
- if (!rows || !metrics.length) {
- break;
- }
-
- if (
- numericColumn &&
- state.shape === 'waffle' &&
- layer.primaryGroups.length &&
- checkTableForContainsSmallValues(frame.activeData[layerId], numericColumn.id, 1)
- ) {
- warningMessages.push(
-
- );
- }
-
- const metricsWithArrayValues = metrics
- .map((metricColId) => {
- if (rows.some((row) => Array.isArray(row[metricColId]))) {
- return metricColId;
- }
- })
- .filter(Boolean) as string[];
-
- if (metricsWithArrayValues.length) {
- const labels = metricsWithArrayValues.map(
- (colId) => frame.datasourceLayers[layerId]?.getOperationForColumnId(colId)?.label || colId
- );
- warningMessages.push(
- {labels.join(', ')},
- }}
- />
- );
- }
- }
-
- return warningMessages;
- },
-
getSuggestionFromConvertToLensContext(props) {
const context = props.context;
if (!isPartitionVisConfiguration(context)) {
@@ -592,7 +533,7 @@ export const getPieVisualization = ({
return suggestion;
},
- getErrorMessages(state) {
+ getUserMessages(state, { frame }) {
const hasTooManyBucketDimensions = state.layers
.map((layer) => {
const totalBucketDimensions =
@@ -605,9 +546,12 @@ export const getPieVisualization = ({
})
.some(Boolean);
- return hasTooManyBucketDimensions
+ const errors: UserMessage[] = hasTooManyBucketDimensions
? [
{
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
shortMessage: i18n.translate('xpack.lens.pie.tooManyDimensions', {
defaultMessage: 'Your visualization has too many dimensions.',
}),
@@ -626,6 +570,75 @@ export const getPieVisualization = ({
},
]
: [];
+
+ const warningMessages: UserMessage[] = [];
+ if (state?.layers.length > 0 && frame.activeData) {
+ for (const layer of state.layers) {
+ const { layerId, metrics } = layer;
+ const rows = frame.activeData[layerId]?.rows;
+ const numericColumn = frame.activeData[layerId]?.columns.find(
+ ({ meta }) => meta?.type === 'number'
+ );
+
+ if (!rows || !metrics.length) {
+ break;
+ }
+
+ if (
+ numericColumn &&
+ state.shape === 'waffle' &&
+ layer.primaryGroups.length &&
+ checkTableForContainsSmallValues(frame.activeData[layerId], numericColumn.id, 1)
+ ) {
+ warningMessages.push({
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'toolbar' }],
+ shortMessage: '',
+ longMessage: (
+
+ ),
+ });
+ }
+
+ const metricsWithArrayValues = metrics
+ .map((metricColId) => {
+ if (rows.some((row) => Array.isArray(row[metricColId]))) {
+ return metricColId;
+ }
+ })
+ .filter(Boolean) as string[];
+
+ if (metricsWithArrayValues.length) {
+ const labels = metricsWithArrayValues.map(
+ (colId) =>
+ frame.datasourceLayers[layerId]?.getOperationForColumnId(colId)?.label || colId
+ );
+ warningMessages.push({
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'toolbar' }],
+ shortMessage: '',
+ longMessage: (
+ {labels.join(', ')},
+ }}
+ />
+ ),
+ });
+ }
+ }
+ }
+
+ return [...errors, ...warningMessages];
},
getVisualizationInfo(state: PieVisualizationState) {
diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx
index 9d1a7a33421e2..c63fc226907c5 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx
+++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx
@@ -44,7 +44,7 @@ export const defaultRangeAnnotationLabel = i18n.translate(
}
);
-const isDateHistogram = (
+export const isDateHistogram = (
dataLayers: XYDataLayerConfig[],
frame?: Pick | undefined
) =>
@@ -475,8 +475,6 @@ export const getAnnotationsConfiguration = ({
frame: Pick;
layer: XYAnnotationLayerConfig;
}) => {
- const hasDateHistogram = isDateHistogram(getDataLayers(state.layers), frame);
-
const groupLabel = getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) });
const emptyButtonLabels = {
@@ -503,10 +501,6 @@ export const getAnnotationsConfiguration = ({
),
accessors: getAnnotationsAccessorColorConfig(layer),
dataTestSubj: 'lnsXY_xAnnotationsPanel',
- invalid: !hasDateHistogram,
- invalidMessage: i18n.translate('xpack.lens.xyChart.addAnnotationsLayerLabelDisabledHelp', {
- defaultMessage: 'Annotations require a time based chart to work. Add a date histogram.',
- }),
requiredMinDimensionCount: 0,
supportsMoreColumns: true,
supportFieldFormat: false,
diff --git a/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts b/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts
index a84168b009f9e..236cd25dd2e2d 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts
+++ b/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts
@@ -12,12 +12,8 @@ import { isQueryAnnotationConfig } from '@kbn/event-annotation-plugin/public';
import { i18n } from '@kbn/i18n';
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
import { validateQuery } from '../../shared_components';
-import type {
- FramePublicAPI,
- DatasourcePublicAPI,
- VisualizationDimensionGroupConfig,
- VisualizeEditorContext,
-} from '../../types';
+import { DataViewsState } from '../../state_management';
+import type { FramePublicAPI, DatasourcePublicAPI, VisualizeEditorContext } from '../../types';
import {
visualizationTypes,
XYLayerConfig,
@@ -27,7 +23,6 @@ import {
YConfig,
XYState,
XYPersistedState,
- State,
XYAnnotationLayerConfig,
} from './types';
import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization_helpers';
@@ -177,29 +172,18 @@ function getIndexPatternIdFromInitialContext(
}
}
-export function validateColumn(
- state: State,
- frame: Pick,
- layerId: string,
+export function getAnnotationLayerErrors(
+ layer: XYAnnotationLayerConfig,
columnId: string,
- group?: VisualizationDimensionGroupConfig
-): { invalid: boolean; invalidMessages?: string[] } {
- if (group?.invalid) {
- return {
- invalid: true,
- invalidMessages: group.invalidMessage ? [group.invalidMessage] : undefined,
- };
- }
- const validColumn = { invalid: false };
- const layer = state.layers.find((l) => l.layerId === layerId);
- if (!layer || !isAnnotationsLayer(layer)) {
- return validColumn;
+ dataViews: DataViewsState
+): string[] {
+ if (!layer) {
+ return [];
}
const annotation = layer.annotations.find(({ id }) => id === columnId);
if (!annotation || !isQueryAnnotationConfig(annotation)) {
- return validColumn;
+ return [];
}
- const { dataViews } = frame || {};
const layerDataView = dataViews.indexPatterns[layer.indexPatternId];
const invalidMessages: string[] = [];
@@ -255,11 +239,5 @@ export function validateColumn(
}
}
- if (!invalidMessages.length) {
- return validColumn;
- }
- return {
- invalid: true,
- invalidMessages,
- };
+ return invalidMessages;
}
diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts
index 2000f0714687f..e7395bf08f81f 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts
@@ -7,7 +7,13 @@
import { getXyVisualization } from './visualization';
import { Position } from '@elastic/charts';
-import { Operation, OperationDescriptor, DatasourcePublicAPI } from '../../types';
+import {
+ Operation,
+ OperationDescriptor,
+ DatasourcePublicAPI,
+ FramePublicAPI,
+ UserMessage,
+} from '../../types';
import type {
State,
XYState,
@@ -15,6 +21,7 @@ import type {
XYDataLayerConfig,
XYReferenceLineLayerConfig,
SeriesType,
+ XYPersistedState,
} from './types';
import { createMockDatasource, createMockFramePublicAPI } from '../../mocks';
import { IconChartBar, IconCircle } from '@kbn/chart-icons';
@@ -31,7 +38,7 @@ import { createMockedIndexPattern } from '../../datasources/form_based/mocks';
import { createMockDataViewsState } from '../../data_views_service/mocks';
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
import { KEEP_GLOBAL_FILTERS_ACTION_ID } from './annotations/actions';
-import { layerTypes } from '../..';
+import { layerTypes, Visualization } from '../..';
const exampleAnnotation: EventAnnotationConfig = {
id: 'an1',
@@ -1990,18 +1997,6 @@ describe('xy_visualization', () => {
triggerIconType: 'custom',
},
]);
- expect(config.groups[0].invalid).toEqual(false);
- });
-
- it('When data layer is empty, should return invalid state', () => {
- const state = getStateWithAnnotationLayer();
- (state.layers[0] as XYDataLayerConfig).xAccessor = undefined;
- const config = xyVisualization.getConfiguration({
- state,
- frame,
- layerId: 'annotations',
- });
- expect(config.groups[0].invalid).toEqual(true);
});
});
@@ -2170,544 +2165,640 @@ describe('xy_visualization', () => {
});
});
- describe('#getErrorMessages', () => {
- let mockDatasource: ReturnType;
- let frame: ReturnType;
-
- beforeEach(() => {
- frame = createMockFramePublicAPI();
- mockDatasource = createMockDatasource('testDatasource');
+ describe('#getUserMessages', () => {
+ describe('errors', () => {
+ let mockDatasource: ReturnType;
+ let frame: ReturnType;
- mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
- dataType: 'string',
- label: 'MyOperation',
- } as OperationDescriptor);
+ beforeEach(() => {
+ frame = createMockFramePublicAPI();
+ mockDatasource = createMockDatasource('testDatasource');
- frame.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- };
- });
+ mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
+ dataType: 'string',
+ label: 'MyOperation',
+ } as OperationDescriptor);
- it("should not return an error when there's only one dimension (X or Y)", () => {
- expect(
- xyVisualization.getErrorMessages({
- ...exampleState(),
- layers: [
- {
- layerId: 'first',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: 'a',
- accessors: [],
- },
- ],
- })
- ).not.toBeDefined();
- });
- it("should not return an error when there's only one dimension on multiple layers (same axis everywhere)", () => {
- expect(
- xyVisualization.getErrorMessages({
- ...exampleState(),
- layers: [
- {
- layerId: 'first',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: 'a',
- accessors: [],
- },
- {
- layerId: 'second',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: 'a',
- accessors: [],
- },
- ],
- })
- ).not.toBeDefined();
- });
- it('should not return an error when mixing different valid configurations in multiple layers', () => {
- expect(
- xyVisualization.getErrorMessages({
- ...exampleState(),
- layers: [
- {
- layerId: 'first',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: 'a',
- accessors: ['a'],
- },
- {
- layerId: 'second',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: undefined,
- accessors: ['a'],
- splitAccessor: 'a',
- },
- ],
- })
- ).not.toBeDefined();
- });
- it("should not return an error when there's only one splitAccessor dimension configured", () => {
- expect(
- xyVisualization.getErrorMessages({
- ...exampleState(),
- layers: [
- {
- layerId: 'first',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: undefined,
- accessors: [],
- splitAccessor: 'a',
- },
- ],
- })
- ).not.toBeDefined();
+ frame.datasourceLayers = {
+ first: mockDatasource.publicAPIMock,
+ };
+ });
- expect(
- xyVisualization.getErrorMessages({
- ...exampleState(),
- layers: [
- {
- layerId: 'first',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: undefined,
- accessors: [],
- splitAccessor: 'a',
- },
- {
- layerId: 'second',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: undefined,
- accessors: [],
- splitAccessor: 'a',
- },
- ],
- })
- ).not.toBeDefined();
- });
- it('should return an error when there are multiple layers, one axis configured for each layer (but different axis from each other)', () => {
- expect(
- xyVisualization.getErrorMessages({
- ...exampleState(),
- layers: [
- {
- layerId: 'first',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: 'a',
- accessors: [],
- },
- {
- layerId: 'second',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: undefined,
- accessors: ['a'],
- },
- ],
- })
- ).toEqual([
- {
- shortMessage: 'Missing Vertical axis.',
- longMessage: 'Layer 1 requires a field for the Vertical axis.',
- },
- ]);
- });
- it('should return an error with batched messages for the same error with multiple layers', () => {
- expect(
- xyVisualization.getErrorMessages({
- ...exampleState(),
- layers: [
- {
- layerId: 'first',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: 'a',
- accessors: ['a'],
- },
- {
- layerId: 'second',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: undefined,
- accessors: [],
- splitAccessor: 'a',
- },
- {
- layerId: 'third',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: undefined,
- accessors: [],
- splitAccessor: 'a',
- },
- ],
- })
- ).toEqual([
- {
- shortMessage: 'Missing Vertical axis.',
- longMessage: 'Layers 2, 3 require a field for the Vertical axis.',
- },
- ]);
- });
- it("should return an error when some layers are complete but other layers aren't", () => {
- expect(
- xyVisualization.getErrorMessages({
- ...exampleState(),
- layers: [
- {
- layerId: 'first',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: 'a',
- accessors: [],
- },
- {
- layerId: 'second',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: 'a',
- accessors: ['a'],
- },
- {
- layerId: 'third',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: 'a',
- accessors: ['a'],
- },
- ],
- })
- ).toEqual([
- {
- shortMessage: 'Missing Vertical axis.',
- longMessage: 'Layer 1 requires a field for the Vertical axis.',
- },
- ]);
- });
+ const getErrorMessages = (
+ vis: Visualization,
+ state: XYState,
+ frameMock = { datasourceLayers: {} } as Partial
+ ) =>
+ vis.getUserMessages!(state, { frame: frameMock as FramePublicAPI })
+ .filter(({ severity }) => severity === 'error')
+ .map((error) => ({ shortMessage: error.shortMessage, longMessage: error.longMessage }));
- it('should return an error when accessor type is of the wrong type', () => {
- expect(
- xyVisualization.getErrorMessages(
- {
+ it("should not return an error when there's only one dimension (X or Y)", () => {
+ expect(
+ getErrorMessages(xyVisualization, {
...exampleState(),
layers: [
{
layerId: 'first',
layerType: layerTypes.DATA,
seriesType: 'area',
- splitAccessor: 'd',
xAccessor: 'a',
- accessors: ['b'], // just use a single accessor to avoid too much noise
+ accessors: [],
},
],
- },
- { datasourceLayers: frame.datasourceLayers, dataViews: {} as DataViewsState }
- )
- ).toEqual([
- {
- shortMessage: 'Wrong data type for Vertical axis.',
- longMessage:
- 'The dimension MyOperation provided for the Vertical axis has the wrong data type. Expected number but have string',
- },
- ]);
- });
-
- it('should return an error if two incompatible xAccessors (multiple layers) are used', () => {
- // current incompatibility is only for date and numeric histograms as xAccessors
- const datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- second: createMockDatasource('testDatasource').publicAPIMock,
- };
- datasourceLayers.first.getOperationForColumnId = jest.fn((id: string) =>
- id === 'a'
- ? ({
- dataType: 'date',
- scale: 'interval',
- } as unknown as OperationDescriptor)
- : null
- );
- datasourceLayers.second.getOperationForColumnId = jest.fn((id: string) =>
- id === 'e'
- ? ({
- dataType: 'number',
- scale: 'interval',
- } as unknown as OperationDescriptor)
- : null
- );
- expect(
- xyVisualization.getErrorMessages(
- {
+ })
+ ).toHaveLength(0);
+ });
+ it("should not return an error when there's only one dimension on multiple layers (same axis everywhere)", () => {
+ expect(
+ getErrorMessages(xyVisualization, {
...exampleState(),
layers: [
{
layerId: 'first',
layerType: layerTypes.DATA,
seriesType: 'area',
- splitAccessor: 'd',
xAccessor: 'a',
- accessors: ['b'],
+ accessors: [],
},
{
layerId: 'second',
layerType: layerTypes.DATA,
seriesType: 'area',
- splitAccessor: 'd',
- xAccessor: 'e',
- accessors: ['b'],
+ xAccessor: 'a',
+ accessors: [],
},
],
- },
- { datasourceLayers, dataViews: {} as DataViewsState }
- )
- ).toEqual([
- {
- shortMessage: 'Wrong data type for Horizontal axis.',
- longMessage:
- 'The Horizontal axis data in layer 1 is incompatible with the data in layer 2. Select a new function for the Horizontal axis.',
- },
- ]);
- });
-
- it('should return an error if string and date histogram xAccessors (multiple layers) are used together', () => {
- // current incompatibility is only for date and numeric histograms as xAccessors
- const datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- second: createMockDatasource('testDatasource').publicAPIMock,
- };
- datasourceLayers.first.getOperationForColumnId = jest.fn((id: string) =>
- id === 'a'
- ? ({
- dataType: 'date',
- scale: 'interval',
- } as unknown as OperationDescriptor)
- : null
- );
- datasourceLayers.second.getOperationForColumnId = jest.fn((id: string) =>
- id === 'e'
- ? ({
- dataType: 'string',
- scale: 'ordinal',
- } as unknown as OperationDescriptor)
- : null
- );
- expect(
- xyVisualization.getErrorMessages(
- {
+ })
+ ).toHaveLength(0);
+ });
+ it('should not return an error when mixing different valid configurations in multiple layers', () => {
+ expect(
+ getErrorMessages(xyVisualization, {
...exampleState(),
layers: [
{
layerId: 'first',
layerType: layerTypes.DATA,
seriesType: 'area',
- splitAccessor: 'd',
xAccessor: 'a',
- accessors: ['b'],
+ accessors: ['a'],
},
{
layerId: 'second',
layerType: layerTypes.DATA,
seriesType: 'area',
- splitAccessor: 'd',
- xAccessor: 'e',
- accessors: ['b'],
+ xAccessor: undefined,
+ accessors: ['a'],
+ splitAccessor: 'a',
},
],
- },
- { datasourceLayers, dataViews: {} as DataViewsState }
- )
- ).toEqual([
- {
- shortMessage: 'Wrong data type for Horizontal axis.',
- longMessage:
- 'The Horizontal axis data in layer 1 is incompatible with the data in layer 2. Select a new function for the Horizontal axis.',
- },
- ]);
- });
+ })
+ ).toHaveLength(0);
+ });
+ it("should not return an error when there's only one splitAccessor dimension configured", () => {
+ expect(
+ getErrorMessages(xyVisualization, {
+ ...exampleState(),
+ layers: [
+ {
+ layerId: 'first',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ xAccessor: undefined,
+ accessors: [],
+ splitAccessor: 'a',
+ },
+ ],
+ })
+ ).toHaveLength(0);
- describe('Annotation layers', () => {
- function createStateWithAnnotationProps(annotation: Partial) {
- return {
- layers: [
+ expect(
+ getErrorMessages(xyVisualization, {
+ ...exampleState(),
+ layers: [
+ {
+ layerId: 'first',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ xAccessor: undefined,
+ accessors: [],
+ splitAccessor: 'a',
+ },
+ {
+ layerId: 'second',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ xAccessor: undefined,
+ accessors: [],
+ splitAccessor: 'a',
+ },
+ ],
+ })
+ ).toHaveLength(0);
+ });
+ it('should return an error when there are multiple layers, one axis configured for each layer (but different axis from each other)', () => {
+ expect(
+ getErrorMessages(xyVisualization, {
+ ...exampleState(),
+ layers: [
+ {
+ layerId: 'first',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ xAccessor: 'a',
+ accessors: [],
+ },
+ {
+ layerId: 'second',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ xAccessor: undefined,
+ accessors: ['a'],
+ },
+ ],
+ })
+ ).toEqual([
+ {
+ shortMessage: 'Missing Vertical axis.',
+ longMessage: 'Layer 1 requires a field for the Vertical axis.',
+ },
+ ]);
+ });
+ it('should return an error with batched messages for the same error with multiple layers', () => {
+ expect(
+ getErrorMessages(xyVisualization, {
+ ...exampleState(),
+ layers: [
+ {
+ layerId: 'first',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ xAccessor: 'a',
+ accessors: ['a'],
+ },
+ {
+ layerId: 'second',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ xAccessor: undefined,
+ accessors: [],
+ splitAccessor: 'a',
+ },
+ {
+ layerId: 'third',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ xAccessor: undefined,
+ accessors: [],
+ splitAccessor: 'a',
+ },
+ ],
+ })
+ ).toEqual([
+ {
+ shortMessage: 'Missing Vertical axis.',
+ longMessage: 'Layers 2, 3 require a field for the Vertical axis.',
+ },
+ ]);
+ });
+ it("should return an error when some layers are complete but other layers aren't", () => {
+ expect(
+ getErrorMessages(xyVisualization, {
+ ...exampleState(),
+ layers: [
+ {
+ layerId: 'first',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ xAccessor: 'a',
+ accessors: [],
+ },
+ {
+ layerId: 'second',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ xAccessor: 'a',
+ accessors: ['a'],
+ },
+ {
+ layerId: 'third',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ xAccessor: 'a',
+ accessors: ['a'],
+ },
+ ],
+ })
+ ).toEqual([
+ {
+ shortMessage: 'Missing Vertical axis.',
+ longMessage: 'Layer 1 requires a field for the Vertical axis.',
+ },
+ ]);
+ });
+
+ it('should return an error when accessor type is of the wrong type', () => {
+ expect(
+ getErrorMessages(
+ xyVisualization,
{
- layerId: 'layerId',
- layerType: 'annotations',
- indexPatternId: 'first',
- annotations: [
+ ...exampleState(),
+ layers: [
{
- label: 'Event',
- id: '1',
- type: 'query',
- timeField: 'start_date',
- ...annotation,
+ layerId: 'first',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ splitAccessor: 'd',
+ xAccessor: 'a',
+ accessors: ['b'], // just use a single accessor to avoid too much noise
},
],
},
- ],
- } as XYState;
- }
+ { datasourceLayers: frame.datasourceLayers, dataViews: {} as DataViewsState }
+ )
+ ).toEqual([
+ {
+ shortMessage: 'Wrong data type for Vertical axis.',
+ longMessage:
+ 'The dimension MyOperation provided for the Vertical axis has the wrong data type. Expected number but have string',
+ },
+ ]);
+ });
- function getFrameMock() {
- return createMockFramePublicAPI({
- datasourceLayers: { first: mockDatasource.publicAPIMock },
- dataViews: createMockDataViewsState({
- indexPatterns: { first: createMockedIndexPattern() },
- }),
- });
- }
- it('should return error if current annotation contains non-existent field as timeField', () => {
- const xyState = createStateWithAnnotationProps({
- timeField: 'non-existent',
- });
- const errors = xyVisualization.getErrorMessages(xyState, getFrameMock());
- expect(errors).toHaveLength(1);
- expect(errors![0]).toEqual(
- expect.objectContaining({
- shortMessage: 'Time field non-existent not found in data view my-fake-index-pattern',
- })
+ it('should return an error if two incompatible xAccessors (multiple layers) are used', () => {
+ // current incompatibility is only for date and numeric histograms as xAccessors
+ const datasourceLayers = {
+ first: mockDatasource.publicAPIMock,
+ second: createMockDatasource('testDatasource').publicAPIMock,
+ };
+ datasourceLayers.first.getOperationForColumnId = jest.fn((id: string) =>
+ id === 'a'
+ ? ({
+ dataType: 'date',
+ scale: 'interval',
+ } as unknown as OperationDescriptor)
+ : null
);
- });
- it('should return error if current annotation contains non existent field as textField', () => {
- const xyState = createStateWithAnnotationProps({
- textField: 'non-existent',
- });
- const errors = xyVisualization.getErrorMessages(xyState, getFrameMock());
- expect(errors).toHaveLength(1);
- expect(errors![0]).toEqual(
- expect.objectContaining({
- shortMessage: 'Text field non-existent not found in data view my-fake-index-pattern',
- })
+ datasourceLayers.second.getOperationForColumnId = jest.fn((id: string) =>
+ id === 'e'
+ ? ({
+ dataType: 'number',
+ scale: 'interval',
+ } as unknown as OperationDescriptor)
+ : null
);
+ expect(
+ getErrorMessages(
+ xyVisualization,
+ {
+ ...exampleState(),
+ layers: [
+ {
+ layerId: 'first',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ splitAccessor: 'd',
+ xAccessor: 'a',
+ accessors: ['b'],
+ },
+ {
+ layerId: 'second',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ splitAccessor: 'd',
+ xAccessor: 'e',
+ accessors: ['b'],
+ },
+ ],
+ },
+ { datasourceLayers, dataViews: {} as DataViewsState }
+ )
+ ).toEqual([
+ {
+ shortMessage: 'Wrong data type for Horizontal axis.',
+ longMessage:
+ 'The Horizontal axis data in layer 1 is incompatible with the data in layer 2. Select a new function for the Horizontal axis.',
+ },
+ ]);
});
- it('should contain error if current annotation contains at least one non-existent field as tooltip field', () => {
- const xyState = createStateWithAnnotationProps({
- extraFields: ['bytes', 'memory', 'non-existent'],
- });
- const errors = xyVisualization.getErrorMessages(xyState, getFrameMock());
- expect(errors).toHaveLength(1);
- expect(errors![0]).toEqual(
- expect.objectContaining({
- shortMessage: 'Tooltip field non-existent not found in data view my-fake-index-pattern',
- })
+
+ it('should return an error if string and date histogram xAccessors (multiple layers) are used together', () => {
+ // current incompatibility is only for date and numeric histograms as xAccessors
+ const datasourceLayers = {
+ first: mockDatasource.publicAPIMock,
+ second: createMockDatasource('testDatasource').publicAPIMock,
+ };
+ datasourceLayers.first.getOperationForColumnId = jest.fn((id: string) =>
+ id === 'a'
+ ? ({
+ dataType: 'date',
+ scale: 'interval',
+ } as unknown as OperationDescriptor)
+ : null
);
- });
- it('should contain error if current annotation contains invalid query', () => {
- const xyState = createStateWithAnnotationProps({
- filter: { type: 'kibana_query', query: 'invalid: "', language: 'kuery' },
- });
- const errors = xyVisualization.getErrorMessages(xyState, getFrameMock());
- expect(errors).toHaveLength(1);
- expect(errors![0]).toEqual(
- expect.objectContaining({
- shortMessage: expect.stringContaining(
- 'Expected "(", "{", value, whitespace but """ found.'
- ),
- })
+ datasourceLayers.second.getOperationForColumnId = jest.fn((id: string) =>
+ id === 'e'
+ ? ({
+ dataType: 'string',
+ scale: 'ordinal',
+ } as unknown as OperationDescriptor)
+ : null
);
+ expect(
+ getErrorMessages(
+ xyVisualization,
+ {
+ ...exampleState(),
+ layers: [
+ {
+ layerId: 'first',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ splitAccessor: 'd',
+ xAccessor: 'a',
+ accessors: ['b'],
+ },
+ {
+ layerId: 'second',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ splitAccessor: 'd',
+ xAccessor: 'e',
+ accessors: ['b'],
+ },
+ ],
+ },
+ { datasourceLayers, dataViews: {} as DataViewsState }
+ )
+ ).toEqual([
+ {
+ shortMessage: 'Wrong data type for Horizontal axis.',
+ longMessage:
+ 'The Horizontal axis data in layer 1 is incompatible with the data in layer 2. Select a new function for the Horizontal axis.',
+ },
+ ]);
});
- it('should contain multiple errors if current annotation contains multiple non-existent fields', () => {
- const xyState = createStateWithAnnotationProps({
- timeField: 'non-existent',
- textField: 'non-existent',
- extraFields: ['bytes', 'memory', 'non-existent'],
- filter: { type: 'kibana_query', query: 'invalid: "', language: 'kuery' },
+
+ describe('Annotation layers', () => {
+ const DATE_HISTORGRAM_COLUMN_ID = 'date_histogram_column';
+
+ function createStateWithAnnotationProps(annotation: Partial) {
+ return {
+ layers: [
+ {
+ layerId: 'first',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ splitAccessor: undefined,
+ xAccessor: DATE_HISTORGRAM_COLUMN_ID,
+ accessors: ['b'],
+ },
+ {
+ layerId: 'layerId',
+ layerType: 'annotations',
+ indexPatternId: 'first',
+ annotations: [
+ {
+ label: 'Event',
+ id: '1',
+ type: 'query',
+ timeField: 'start_date',
+ ...annotation,
+ },
+ ],
+ },
+ ],
+ } as XYState;
+ }
+
+ function getFrameMock() {
+ const datasourceMock = createMockDatasource('testDatasource');
+ datasourceMock.publicAPIMock.getOperationForColumnId.mockImplementation((id) =>
+ id === DATE_HISTORGRAM_COLUMN_ID
+ ? ({
+ label: DATE_HISTORGRAM_COLUMN_ID,
+ dataType: 'date',
+ scale: 'interval',
+ } as OperationDescriptor)
+ : ({
+ dataType: 'number',
+ label: 'MyOperation',
+ } as OperationDescriptor)
+ );
+
+ return createMockFramePublicAPI({
+ datasourceLayers: { first: datasourceMock.publicAPIMock },
+ dataViews: createMockDataViewsState({
+ indexPatterns: { first: createMockedIndexPattern() },
+ }),
+ });
+ }
+ test('When data layer is empty, should return error on dimension', () => {
+ const state: State = {
+ ...exampleState(),
+ layers: [
+ {
+ layerId: 'first',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ splitAccessor: undefined,
+ xAccessor: undefined, // important
+ accessors: ['b'],
+ },
+ {
+ layerId: 'annotations',
+ layerType: layerTypes.ANNOTATIONS,
+ indexPatternId: 'indexPattern1',
+ annotations: [exampleAnnotation],
+ ignoreGlobalFilters: true,
+ },
+ ],
+ };
+ expect(xyVisualization.getUserMessages!(state, { frame: getFrameMock() }))
+ .toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "displayLocations": Array [
+ Object {
+ "dimensionId": "an1",
+ "id": "dimensionTrigger",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage": "",
+ "severity": "error",
+ "shortMessage": "Annotations require a time based chart to work. Add a date histogram.",
+ },
+ ]
+ `);
});
- const errors = xyVisualization.getErrorMessages(xyState, getFrameMock());
- expect(errors).toHaveLength(4);
- });
- it('should contain error if current annotation contains no time field set', () => {
- const xyState = createStateWithAnnotationProps({
- timeField: undefined,
+
+ it('should return error if current annotation contains non-existent field as timeField', () => {
+ const xyState = createStateWithAnnotationProps({
+ timeField: 'non-existent',
+ });
+ const errors = getErrorMessages(xyVisualization, xyState, getFrameMock());
+ expect(errors).toHaveLength(1);
+ expect(errors![0]).toEqual(
+ expect.objectContaining({
+ shortMessage: 'Time field non-existent not found in data view my-fake-index-pattern',
+ })
+ );
+ });
+ it('should return error if current annotation contains non existent field as textField', () => {
+ const xyState = createStateWithAnnotationProps({
+ textField: 'non-existent',
+ });
+ const errors = getErrorMessages(xyVisualization, xyState, getFrameMock());
+ expect(errors).toHaveLength(1);
+ expect(errors![0]).toEqual(
+ expect.objectContaining({
+ shortMessage: 'Text field non-existent not found in data view my-fake-index-pattern',
+ })
+ );
+ });
+ it('should contain error if current annotation contains at least one non-existent field as tooltip field', () => {
+ const xyState = createStateWithAnnotationProps({
+ extraFields: ['bytes', 'memory', 'non-existent'],
+ });
+ const errors = getErrorMessages(xyVisualization, xyState, getFrameMock());
+ expect(errors).toHaveLength(1);
+ expect(errors![0]).toEqual(
+ expect.objectContaining({
+ shortMessage:
+ 'Tooltip field non-existent not found in data view my-fake-index-pattern',
+ })
+ );
+ });
+ it('should contain error if current annotation contains invalid query', () => {
+ const xyState = createStateWithAnnotationProps({
+ filter: { type: 'kibana_query', query: 'invalid: "', language: 'kuery' },
+ });
+ const errors = getErrorMessages(xyVisualization, xyState, getFrameMock());
+ expect(errors).toHaveLength(1);
+ expect(errors![0]).toEqual(
+ expect.objectContaining({
+ shortMessage: expect.stringContaining(
+ 'Expected "(", "{", value, whitespace but """ found.'
+ ),
+ })
+ );
+ });
+ it('should contain multiple errors if current annotation contains multiple non-existent fields', () => {
+ const xyState = createStateWithAnnotationProps({
+ timeField: 'non-existent',
+ textField: 'non-existent',
+ extraFields: ['bytes', 'memory', 'non-existent'],
+ filter: { type: 'kibana_query', query: 'invalid: "', language: 'kuery' },
+ });
+ const errors = getErrorMessages(xyVisualization, xyState, getFrameMock());
+ expect(errors).toHaveLength(4);
+ });
+ it('should contain error if current annotation contains no time field set', () => {
+ const xyState = createStateWithAnnotationProps({
+ timeField: undefined,
+ });
+ const errors = getErrorMessages(xyVisualization, xyState, getFrameMock());
+ expect(errors).toHaveLength(1);
+ expect(errors![0]).toEqual(
+ expect.objectContaining({
+ shortMessage: expect.stringContaining('Time field is missing'),
+ })
+ );
});
- const errors = xyVisualization.getErrorMessages(xyState, getFrameMock());
- expect(errors).toHaveLength(1);
- expect(errors![0]).toEqual(
- expect.objectContaining({
- shortMessage: expect.stringContaining('Time field is missing'),
- })
- );
});
});
- });
- describe('#getWarningMessages', () => {
- let mockDatasource: ReturnType;
- let frame: ReturnType;
+ describe('warnings', () => {
+ let mockDatasource: ReturnType;
+ let frame: ReturnType;
- beforeEach(() => {
- frame = createMockFramePublicAPI();
- mockDatasource = createMockDatasource('testDatasource');
+ beforeEach(() => {
+ frame = createMockFramePublicAPI();
+ mockDatasource = createMockDatasource('testDatasource');
- mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([
- { columnId: 'd', fields: [] },
- { columnId: 'a', fields: [] },
- { columnId: 'b', fields: [] },
- { columnId: 'c', fields: [] },
- ]);
+ mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([
+ { columnId: 'd', fields: [] },
+ { columnId: 'a', fields: [] },
+ { columnId: 'b', fields: [] },
+ { columnId: 'c', fields: [] },
+ ]);
- frame.datasourceLayers = {
- first: mockDatasource.publicAPIMock,
- };
+ frame.datasourceLayers = {
+ first: mockDatasource.publicAPIMock,
+ };
- frame.activeData = {
- first: {
- type: 'datatable',
- columns: [
- { id: 'a', name: 'A', meta: { type: 'number' } },
- { id: 'b', name: 'B', meta: { type: 'number' } },
- ],
- rows: [
- { a: 1, b: [2, 0] },
- { a: 3, b: 4 },
- { a: 5, b: 6 },
- { a: 7, b: 8 },
- ],
- },
- };
- });
- it('should return a warning when numeric accessors contain array', () => {
- const datasourceLayers = frame.datasourceLayers as Record;
- (datasourceLayers.first.getOperationForColumnId as jest.Mock).mockReturnValue({
- label: 'Label B',
+ frame.activeData = {
+ first: {
+ type: 'datatable',
+ columns: [
+ { id: 'a', name: 'A', meta: { type: 'number' } },
+ { id: 'b', name: 'B', meta: { type: 'number' } },
+ ],
+ rows: [
+ { a: 1, b: [2, 0] },
+ { a: 3, b: 4 },
+ { a: 5, b: 6 },
+ { a: 7, b: 8 },
+ ],
+ },
+ };
});
- const warningMessages = xyVisualization.getWarningMessages!(
- {
- ...exampleState(),
- layers: [
+
+ const onlyWarnings = (messages: UserMessage[]) =>
+ messages.filter(({ severity }) => severity === 'warning');
+
+ it('should return a warning when numeric accessors contain array', () => {
+ const datasourceLayers = frame.datasourceLayers as Record;
+ (datasourceLayers.first.getOperationForColumnId as jest.Mock).mockReturnValue({
+ label: 'Label B',
+ });
+ const warningMessages = onlyWarnings(
+ xyVisualization.getUserMessages!(
{
- layerId: 'first',
- layerType: layerTypes.DATA,
- seriesType: 'area',
- xAccessor: 'a',
- accessors: ['b'],
+ ...exampleState(),
+ layers: [
+ {
+ layerId: 'first',
+ layerType: layerTypes.DATA,
+ seriesType: 'area',
+ xAccessor: 'a',
+ accessors: ['b'],
+ },
+ ],
},
- ],
- },
- frame
- );
- expect(warningMessages).toHaveLength(1);
- expect(warningMessages && warningMessages[0]).toMatchInlineSnapshot(`
-
- Label B
- ,
- }
+ { frame }
+ )
+ );
+ expect(warningMessages).toHaveLength(1);
+ expect(warningMessages && warningMessages[0]).toMatchInlineSnapshot(`
+ Object {
+ "displayLocations": Array [
+ Object {
+ "id": "toolbar",
+ },
+ ],
+ "fixableInEditor": true,
+ "longMessage":
+ Label B
+ ,
+ }
+ }
+ />,
+ "severity": "warning",
+ "shortMessage": "",
}
- />
- `);
+ `);
+ });
});
});
+
describe('#getUniqueLabels', () => {
it('creates unique labels for single annotations layer with repeating labels', async () => {
const xyState = {
diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx
index 9ee9572a0c35f..78e2637cedc31 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx
+++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx
@@ -35,7 +35,13 @@ import {
DimensionEditor,
} from './xy_config_panel/dimension_editor';
import { LayerHeader, LayerHeaderContent } from './xy_config_panel/layer_header';
-import type { Visualization, AccessorConfig, FramePublicAPI, Suggestion } from '../../types';
+import type {
+ Visualization,
+ AccessorConfig,
+ FramePublicAPI,
+ Suggestion,
+ UserMessage,
+} from '../../types';
import type { FormBasedPersistedState } from '../../datasources/form_based/types';
import {
type State,
@@ -48,9 +54,9 @@ import {
} from './types';
import {
extractReferences,
+ getAnnotationLayerErrors,
injectReferences,
isHorizontalChart,
- validateColumn,
} from './state_helpers';
import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression';
import { getAccessorColorConfigs, getColorAssignments } from './color_assignment';
@@ -67,6 +73,7 @@ import {
setAnnotationsDimension,
getUniqueLabels,
onAnnotationDrop,
+ isDateHistogram,
} from './annotations/helpers';
import {
checkXAccessorCompatibility,
@@ -692,52 +699,58 @@ export const getXyVisualization = ({
eventAnnotationService
),
- validateColumn(state, frame, layerId, columnId, group) {
- const { invalid, invalidMessages } = validateColumn(state, frame, layerId, columnId, group);
- if (!invalid) {
- return { invalid };
- }
- return { invalid, invalidMessage: invalidMessages![0] };
- },
-
- getErrorMessages(state, frame) {
- const { datasourceLayers, dataViews } = frame || {};
- const errors: Array<{
- shortMessage: string;
- longMessage: React.ReactNode;
- }> = [];
-
+ getUserMessages(state, { frame }) {
+ const { datasourceLayers, dataViews, activeData } = frame;
const annotationLayers = getAnnotationsLayers(state.layers);
+ const errors: UserMessage[] = [];
+
+ const hasDateHistogram = isDateHistogram(getDataLayers(state.layers), frame);
+
+ annotationLayers.forEach((layer) => {
+ layer.annotations.forEach((annotation) => {
+ if (!hasDateHistogram) {
+ errors.push({
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'dimensionTrigger', dimensionId: annotation.id }],
+ shortMessage: i18n.translate(
+ 'xpack.lens.xyChart.addAnnotationsLayerLabelDisabledHelp',
+ {
+ defaultMessage:
+ 'Annotations require a time based chart to work. Add a date histogram.',
+ }
+ ),
+ longMessage: '',
+ });
+ }
- if (dataViews) {
- annotationLayers.forEach((layer) => {
- layer.annotations.forEach((annotation) => {
- const validatedColumn = validateColumn(
- state,
- { dataViews },
- layer.layerId,
- annotation.id
- );
- if (validatedColumn?.invalid && validatedColumn.invalidMessages?.length) {
- errors.push(
- ...validatedColumn.invalidMessages.map((invalidMessage) => ({
- shortMessage: invalidMessage,
- longMessage: (
-
- ),
- }))
- );
- }
- });
+ const errorMessages = getAnnotationLayerErrors(layer, annotation.id, dataViews);
+ errors.push(
+ ...errorMessages.map((errorMessage) => {
+ const message: UserMessage = {
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [
+ { id: 'visualization' },
+ { id: 'dimensionTrigger', dimensionId: annotation.id },
+ ],
+ shortMessage: errorMessage,
+ longMessage: (
+
+ ),
+ };
+ return message;
+ })
+ );
});
- }
+ });
// Data error handling below here
const hasNoAccessors = ({ accessors }: XYDataLayerConfig) =>
@@ -763,84 +776,108 @@ export const getXyVisualization = ({
for (const [dimension, criteria] of checks) {
const result = validateLayersForDimension(dimension, filteredLayers, criteria);
if (!result.valid) {
- errors.push(result.payload);
+ errors.push({
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: result.payload.shortMessage,
+ longMessage: result.payload.longMessage,
+ });
}
}
}
+ // temporary fix for #87068
+ errors.push(
+ ...checkXAccessorCompatibility(state, datasourceLayers).map(
+ ({ shortMessage, longMessage }) =>
+ ({
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage,
+ longMessage,
+ } as UserMessage)
+ )
+ );
- if (datasourceLayers && state) {
- // temporary fix for #87068
- errors.push(...checkXAccessorCompatibility(state, datasourceLayers));
-
- for (const layer of getDataLayers(state.layers)) {
- const datasourceAPI = datasourceLayers[layer.layerId];
- if (datasourceAPI) {
- for (const accessor of layer.accessors) {
- const operation = datasourceAPI.getOperationForColumnId(accessor);
- if (operation && operation.dataType !== 'number') {
- errors.push({
- shortMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureYShort', {
- defaultMessage: `Wrong data type for {axis}.`,
- values: {
- axis: getAxisName('y', { isHorizontal: isHorizontalChart(state.layers) }),
- },
- }),
- longMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureYLong', {
- defaultMessage: `The dimension {label} provided for the {axis} has the wrong data type. Expected number but have {dataType}`,
- values: {
- label: operation.label,
- dataType: operation.dataType,
- axis: getAxisName('y', { isHorizontal: isHorizontalChart(state.layers) }),
- },
- }),
- });
- }
+ for (const layer of getDataLayers(state.layers)) {
+ const datasourceAPI = datasourceLayers[layer.layerId];
+ if (datasourceAPI) {
+ for (const accessor of layer.accessors) {
+ const operation = datasourceAPI.getOperationForColumnId(accessor);
+ if (operation && operation.dataType !== 'number') {
+ errors.push({
+ severity: 'error',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'visualization' }],
+ shortMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureYShort', {
+ defaultMessage: `Wrong data type for {axis}.`,
+ values: {
+ axis: getAxisName('y', { isHorizontal: isHorizontalChart(state.layers) }),
+ },
+ }),
+ longMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureYLong', {
+ defaultMessage: `The dimension {label} provided for the {axis} has the wrong data type. Expected number but have {dataType}`,
+ values: {
+ label: operation.label,
+ dataType: operation.dataType,
+ axis: getAxisName('y', { isHorizontal: isHorizontalChart(state.layers) }),
+ },
+ }),
+ });
}
}
}
}
- return errors.length ? errors : undefined;
- },
-
- getWarningMessages(state, frame) {
- if (state?.layers.length === 0 || !frame.activeData) {
- return;
- }
+ const warnings: UserMessage[] = [];
- const filteredLayers = [
- ...getDataLayers(state.layers),
- ...getReferenceLayers(state.layers),
- ].filter(({ accessors }) => accessors.length > 0);
+ if (state?.layers.length > 0 && activeData) {
+ const filteredLayers = [
+ ...getDataLayers(state.layers),
+ ...getReferenceLayers(state.layers),
+ ].filter(({ accessors }) => accessors.length > 0);
- const accessorsWithArrayValues = [];
+ const accessorsWithArrayValues = [];
- for (const layer of filteredLayers) {
- const { layerId, accessors } = layer;
- const rows = frame.activeData?.[layerId] && frame.activeData[layerId].rows;
- if (!rows) {
- break;
- }
- const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layerId]);
- for (const accessor of accessors) {
- const hasArrayValues = rows.some((row) => Array.isArray(row[accessor]));
- if (hasArrayValues) {
- accessorsWithArrayValues.push(columnToLabel[accessor]);
+ for (const layer of filteredLayers) {
+ const { layerId, accessors } = layer;
+ const rows = activeData?.[layerId] && activeData[layerId].rows;
+ if (!rows) {
+ break;
+ }
+ const columnToLabel = getColumnToLabelMap(layer, datasourceLayers[layerId]);
+ for (const accessor of accessors) {
+ const hasArrayValues = rows.some((row) => Array.isArray(row[accessor]));
+ if (hasArrayValues) {
+ accessorsWithArrayValues.push(columnToLabel[accessor]);
+ }
}
}
+
+ accessorsWithArrayValues.forEach((label) =>
+ warnings.push({
+ severity: 'warning',
+ fixableInEditor: true,
+ displayLocations: [{ id: 'toolbar' }],
+ shortMessage: '',
+ longMessage: (
+ {label},
+ }}
+ />
+ ),
+ })
+ );
}
- return accessorsWithArrayValues.map((label) => (
- {label},
- }}
- />
- ));
+ return [...errors, ...warnings];
},
+
getUniqueLabels(state) {
return getUniqueLabels(state.layers);
},
@@ -852,19 +889,7 @@ export const getXyVisualization = ({
state?.layers.filter(isAnnotationsLayer).map(({ indexPatternId }) => indexPatternId) ?? []
);
},
- renderDimensionTrigger({
- columnId,
- label,
- hideTooltip,
- invalid,
- invalidMessage,
- }: {
- columnId: string;
- label?: string;
- hideTooltip?: boolean;
- invalid?: boolean;
- invalidMessage?: string;
- }) {
+ renderDimensionTrigger({ columnId, label, hideTooltip, invalid, invalidMessage }) {
if (label) {
return (
lns-empty-dimension',
operation: 'cumulative_sum',
});
- expect(await PageObjects.lens.getErrorCount()).to.eql(1);
+ expect(await PageObjects.lens.getWorkspaceErrorCount()).to.eql(1);
await PageObjects.lens.removeDimension('lnsXY_xDimensionPanel');
- expect(await PageObjects.lens.getErrorCount()).to.eql(2);
+ expect(await PageObjects.lens.getWorkspaceErrorCount()).to.eql(2);
await PageObjects.lens.dragFieldToDimensionTrigger(
'@timestamp',
'lnsXY_xDimensionPanel > lns-empty-dimension'
);
- expect(await PageObjects.lens.getErrorCount()).to.eql(1);
+ expect(await PageObjects.lens.getWorkspaceErrorCount()).to.eql(1);
expect(await PageObjects.lens.hasChartSwitchWarning('lnsDatatable')).to.eql(false);
await PageObjects.lens.switchToVisualization('lnsDatatable');
diff --git a/x-pack/test/functional/apps/lens/group3/formula.ts b/x-pack/test/functional/apps/lens/group3/formula.ts
index 2df2af2950909..d5e1c9e38b00f 100644
--- a/x-pack/test/functional/apps/lens/group3/formula.ts
+++ b/x-pack/test/functional/apps/lens/group3/formula.ts
@@ -136,7 +136,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
formula: `asdf`,
});
- expect(await PageObjects.lens.getErrorCount()).to.eql(1);
+ expect(await PageObjects.lens.getWorkspaceErrorCount()).to.eql(1);
});
it('should keep the formula when entering expanded mode', async () => {
@@ -175,7 +175,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
await PageObjects.lens.waitForVisualization('legacyMtrVis');
- expect(await PageObjects.lens.getErrorCount()).to.eql(0);
+ expect(await PageObjects.lens.getWorkspaceErrorCount()).to.eql(0);
});
it('should duplicate a moving average formula and be a valid table with conditional coloring', async () => {
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index 10deb555fa359..9da4bd778e45f 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -840,17 +840,16 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
},
/** Counts the visible warnings in the config panel */
- async getErrorCount() {
- const moreButton = await testSubjects.exists('configuration-failure-more-errors');
+ async getWorkspaceErrorCount() {
+ const moreButton = await testSubjects.exists('workspace-more-errors-button');
if (moreButton) {
await retry.try(async () => {
- await testSubjects.click('configuration-failure-more-errors');
- await testSubjects.missingOrFail('configuration-failure-more-errors');
+ await testSubjects.click('workspace-more-errors-button');
+ await testSubjects.missingOrFail('workspace-more-errors-button');
});
}
- const errors = await testSubjects.findAll('configuration-failure-error');
- const expressionErrors = await testSubjects.findAll('expression-failure');
- return (errors?.length ?? 0) + (expressionErrors?.length ?? 0);
+ const errors = await testSubjects.findAll('workspace-error-message');
+ return errors?.length ?? 0;
},
async searchOnChartSwitch(subVisualizationId: string, searchTerm?: string) {