mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Lens] introduce unified user messages system (#147818)
This commit is contained in:
parent
2a18937ef7
commit
2ed0123c4a
65 changed files with 3368 additions and 2582 deletions
|
@ -181,9 +181,4 @@ export const getRotatingNumberVisualization = ({
|
|||
domElement
|
||||
);
|
||||
},
|
||||
|
||||
getErrorMessages(state) {
|
||||
// Is it possible to break it?
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
|
104
x-pack/plugins/lens/public/app_plugin/__snapshots__/get_application_user_messages.test.tsx.snap
generated
Normal file
104
x-pack/plugins/lens/public/app_plugin/__snapshots__/get_application_user_messages.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,104 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`application-level user messages missing index pattern errors generates error if missing an index pattern 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "visualizationInEditor",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
"longMessage": <React.Fragment>
|
||||
<p
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="missing-refs-failure"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Data view not found"
|
||||
id="xpack.lens.editorFrame.dataViewNotFound"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
className="eui-textBreakWord"
|
||||
style={
|
||||
Object {
|
||||
"userSelect": "text",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="The {count, plural, one {data view} other {data views}} ({count, plural, one {id} other {ids}}: {indexpatterns}) cannot be found."
|
||||
id="xpack.lens.indexPattern.missingDataView"
|
||||
values={
|
||||
Object {
|
||||
"count": 1,
|
||||
"indexpatterns": "missing_pattern",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<RedirectAppLinks
|
||||
coreStart={
|
||||
Object {
|
||||
"application": Object {
|
||||
"capabilities": Object {
|
||||
"management": Object {
|
||||
"kibana": Object {
|
||||
"indexPatterns": true,
|
||||
},
|
||||
},
|
||||
"navLinks": Object {
|
||||
"management": true,
|
||||
},
|
||||
},
|
||||
"getUrlForApp": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"management",
|
||||
Object {
|
||||
"path": "/kibana/indexPatterns/create",
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": "fake/url",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<a
|
||||
data-test-subj="configuration-failure-reconfigure-indexpatterns"
|
||||
href="fake/url"
|
||||
style={
|
||||
Object {
|
||||
"display": "block",
|
||||
}
|
||||
}
|
||||
>
|
||||
Recreate it in the data view management page.
|
||||
</a>
|
||||
</RedirectAppLinks>
|
||||
</p>
|
||||
</React.Fragment>,
|
||||
"severity": "error",
|
||||
"shortMessage": "",
|
||||
},
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "visualizationOnEmbeddable",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
"longMessage": "Could not find the data view: missing_pattern",
|
||||
"severity": "error",
|
||||
"shortMessage": "",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -36,6 +36,7 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
|||
import moment from 'moment';
|
||||
|
||||
import { setState, LensAppState } from '../state_management';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
jest.mock('../editor_frame_service/editor_frame/expression_helpers');
|
||||
jest.mock('@kbn/core/public');
|
||||
jest.mock('../persistence/saved_objects_utils/check_for_duplicate_title', () => ({
|
||||
|
@ -85,6 +86,7 @@ describe('Lens App', () => {
|
|||
visualizationMap,
|
||||
topNavMenuEntryGenerators: [],
|
||||
theme$: new Observable(),
|
||||
coreStart: coreMock.createStart(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -144,6 +146,8 @@ describe('Lens App', () => {
|
|||
expect(frame.EditorFrameContainer).toHaveBeenLastCalledWith(
|
||||
{
|
||||
indexPatternService: expect.any(Object),
|
||||
getUserMessages: expect.any(Function),
|
||||
addUserMessages: expect.any(Function),
|
||||
lensInspector: {
|
||||
adapters: {
|
||||
expression: expect.any(Object),
|
||||
|
|
|
@ -16,7 +16,7 @@ import type { LensAppLocatorParams } from '../../common/locator/locator';
|
|||
import { LensAppProps, LensAppServices } from './types';
|
||||
import { LensTopNavMenu } from './lens_top_nav';
|
||||
import { LensByReferenceInput } from '../embeddable';
|
||||
import { EditorFrameInstance } from '../types';
|
||||
import { AddUserMessages, EditorFrameInstance, UserMessage, UserMessagesGetter } from '../types';
|
||||
import { Document } from '../persistence/saved_object_store';
|
||||
|
||||
import {
|
||||
|
@ -28,6 +28,9 @@ import {
|
|||
DispatchSetState,
|
||||
selectSavedObjectFormat,
|
||||
updateIndexPatterns,
|
||||
updateDatasourceState,
|
||||
selectActiveDatasourceId,
|
||||
selectFrameDatasourceAPI,
|
||||
} from '../state_management';
|
||||
import { SaveModalContainer, runSaveLensVisualization } from './save_modal_container';
|
||||
import { LensInspector } from '../lens_inspector_service';
|
||||
|
@ -38,6 +41,7 @@ import {
|
|||
createIndexPatternService,
|
||||
} from '../data_views_service/service';
|
||||
import { replaceIndexpattern } from '../state_management/lens_slice';
|
||||
import { filterUserMessages, getApplicationUserMessages } from './get_application_user_messages';
|
||||
|
||||
export type SaveProps = Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & {
|
||||
returnToOrigin: boolean;
|
||||
|
@ -62,6 +66,7 @@ export function App({
|
|||
topNavMenuEntryGenerators,
|
||||
initialContext,
|
||||
theme$,
|
||||
coreStart,
|
||||
}: LensAppProps) {
|
||||
const lensAppServices = useKibana<LensAppServices>().services;
|
||||
|
||||
|
@ -98,8 +103,10 @@ export function App({
|
|||
sharingSavedObjectProps,
|
||||
isLinkedToOriginatingApp,
|
||||
searchSessionId,
|
||||
datasourceStates,
|
||||
isLoading,
|
||||
isSaveable,
|
||||
visualization,
|
||||
} = useLensSelector((state) => state.lens);
|
||||
|
||||
const selectorDependencies = useMemo(
|
||||
|
@ -473,6 +480,98 @@ export function App({
|
|||
})
|
||||
: undefined;
|
||||
|
||||
const activeDatasourceId = useLensSelector(selectActiveDatasourceId);
|
||||
|
||||
const frameDatasourceAPI = useLensSelector((state) =>
|
||||
selectFrameDatasourceAPI(state, datasourceMap)
|
||||
);
|
||||
|
||||
const [userMessages, setUserMessages] = useState<UserMessage[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setUserMessages([
|
||||
...(activeDatasourceId
|
||||
? datasourceMap[activeDatasourceId].getUserMessages(
|
||||
datasourceStates[activeDatasourceId].state,
|
||||
{
|
||||
frame: frameDatasourceAPI,
|
||||
setState: (newStateOrUpdater) =>
|
||||
dispatch(
|
||||
updateDatasourceState({
|
||||
updater: newStateOrUpdater,
|
||||
datasourceId: activeDatasourceId,
|
||||
})
|
||||
),
|
||||
}
|
||||
)
|
||||
: []),
|
||||
...(visualization.activeId && visualization.state
|
||||
? visualizationMap[visualization.activeId]?.getUserMessages?.(visualization.state, {
|
||||
frame: frameDatasourceAPI,
|
||||
}) ?? []
|
||||
: []),
|
||||
...getApplicationUserMessages({
|
||||
visualizationType: persistedDoc?.visualizationType,
|
||||
visualizationMap,
|
||||
visualization,
|
||||
activeDatasource: activeDatasourceId ? datasourceMap[activeDatasourceId] : null,
|
||||
activeDatasourceState: activeDatasourceId ? datasourceStates[activeDatasourceId] : null,
|
||||
core: coreStart,
|
||||
dataViews: frameDatasourceAPI.dataViews,
|
||||
}),
|
||||
]);
|
||||
}, [
|
||||
activeDatasourceId,
|
||||
coreStart,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
dispatch,
|
||||
frameDatasourceAPI,
|
||||
persistedDoc?.visualizationType,
|
||||
visualization,
|
||||
visualizationMap,
|
||||
]);
|
||||
|
||||
// these are messages managed from other parts of Lens
|
||||
const [additionalUserMessages, setAdditionalUserMessages] = useState<Record<string, UserMessage>>(
|
||||
{}
|
||||
);
|
||||
|
||||
const getUserMessages: UserMessagesGetter = (locationId, filterArgs) =>
|
||||
filterUserMessages(
|
||||
[...userMessages, ...Object.values(additionalUserMessages)],
|
||||
locationId,
|
||||
filterArgs
|
||||
);
|
||||
|
||||
const addUserMessages: AddUserMessages = (messages) => {
|
||||
const newMessageMap = {
|
||||
...additionalUserMessages,
|
||||
};
|
||||
|
||||
const addedMessageIds: string[] = [];
|
||||
messages.forEach((message) => {
|
||||
if (!newMessageMap[message.uniqueId]) {
|
||||
addedMessageIds.push(message.uniqueId);
|
||||
newMessageMap[message.uniqueId] = message;
|
||||
}
|
||||
});
|
||||
|
||||
if (addedMessageIds.length) {
|
||||
setAdditionalUserMessages(newMessageMap);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const withMessagesRemoved = {
|
||||
...additionalUserMessages,
|
||||
};
|
||||
|
||||
addedMessageIds.forEach((id) => delete withMessagesRemoved[id]);
|
||||
|
||||
setAdditionalUserMessages(withMessagesRemoved);
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="lnsApp" data-test-subj="lnsApp" role="main">
|
||||
|
@ -506,6 +605,7 @@ export function App({
|
|||
theme$={theme$}
|
||||
indexPatternService={indexPatternService}
|
||||
onTextBasedSavedAndExit={onTextBasedSavedAndExit}
|
||||
getUserMessages={getUserMessages}
|
||||
shortUrlService={shortUrlService}
|
||||
/>
|
||||
{getLegacyUrlConflictCallout()}
|
||||
|
@ -515,6 +615,8 @@ export function App({
|
|||
showNoDataPopover={showNoDataPopover}
|
||||
lensInspector={lensInspector}
|
||||
indexPatternService={indexPatternService}
|
||||
getUserMessages={getUserMessages}
|
||||
addUserMessages={addUserMessages}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -582,18 +684,24 @@ export function App({
|
|||
const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({
|
||||
editorFrame,
|
||||
showNoDataPopover,
|
||||
getUserMessages,
|
||||
addUserMessages,
|
||||
lensInspector,
|
||||
indexPatternService,
|
||||
}: {
|
||||
editorFrame: EditorFrameInstance;
|
||||
lensInspector: LensInspector;
|
||||
showNoDataPopover: () => void;
|
||||
getUserMessages: UserMessagesGetter;
|
||||
addUserMessages: AddUserMessages;
|
||||
indexPatternService: IndexPatternServiceAPI;
|
||||
}) {
|
||||
const { EditorFrameContainer } = editorFrame;
|
||||
return (
|
||||
<EditorFrameContainer
|
||||
showNoDataPopover={showNoDataPopover}
|
||||
getUserMessages={getUserMessages}
|
||||
addUserMessages={addUserMessages}
|
||||
lensInspector={lensInspector}
|
||||
indexPatternService={indexPatternService}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,368 @@
|
|||
/*
|
||||
* 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 { CoreStart } from '@kbn/core/public';
|
||||
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Visualization } from '..';
|
||||
import { DataViewsState } from '../state_management';
|
||||
import { Datasource, UserMessage } from '../types';
|
||||
import { filterUserMessages, getApplicationUserMessages } from './get_application_user_messages';
|
||||
|
||||
describe('application-level user messages', () => {
|
||||
it('should generate error if vis type is not provided', () => {
|
||||
expect(
|
||||
getApplicationUserMessages({
|
||||
visualizationType: undefined,
|
||||
|
||||
visualizationMap: {},
|
||||
visualization: { activeId: '', state: {} },
|
||||
activeDatasource: {} as Datasource,
|
||||
activeDatasourceState: null,
|
||||
dataViews: {} as DataViewsState,
|
||||
core: {} as CoreStart,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "visualization",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
"longMessage": "Visualization type not found.",
|
||||
"severity": "warning",
|
||||
"shortMessage": "",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate error if vis type is unknown', () => {
|
||||
expect(
|
||||
getApplicationUserMessages({
|
||||
visualizationType: '123',
|
||||
visualizationMap: {},
|
||||
visualization: { activeId: 'id_for_type_that_doesnt_exist', state: {} },
|
||||
|
||||
activeDatasource: {} as Datasource,
|
||||
activeDatasourceState: null,
|
||||
dataViews: {} as DataViewsState,
|
||||
core: {} as CoreStart,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "visualization",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": false,
|
||||
"longMessage": "The visualization type id_for_type_that_doesnt_exist could not be resolved.",
|
||||
"severity": "error",
|
||||
"shortMessage": "Unknown visualization type",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate error if datasource type is unknown', () => {
|
||||
expect(
|
||||
getApplicationUserMessages({
|
||||
activeDatasource: null,
|
||||
|
||||
visualizationType: '123',
|
||||
visualizationMap: { 'some-id': {} as Visualization },
|
||||
visualization: { activeId: 'some-id', state: {} },
|
||||
activeDatasourceState: null,
|
||||
dataViews: {} as DataViewsState,
|
||||
core: {} as CoreStart,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "visualization",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": false,
|
||||
"longMessage": "Could not find datasource for the visualization",
|
||||
"severity": "error",
|
||||
"shortMessage": "Unknown datasource type",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe('missing index pattern errors', () => {
|
||||
const defaultPermissions: Record<string, Record<string, boolean | Record<string, boolean>>> = {
|
||||
navLinks: { management: true },
|
||||
management: { kibana: { indexPatterns: true } },
|
||||
};
|
||||
|
||||
function createCoreStartWithPermissions(newCapabilities = defaultPermissions) {
|
||||
const core = {
|
||||
application: {
|
||||
getUrlForApp: jest.fn(() => 'fake/url'),
|
||||
capabilities: {
|
||||
management: {
|
||||
kibana: {
|
||||
indexPatterns: true,
|
||||
},
|
||||
},
|
||||
navLinks: {
|
||||
management: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
(core.application.capabilities as unknown as Record<
|
||||
string,
|
||||
Record<string, boolean | Record<string, boolean>>
|
||||
>) = 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(
|
||||
<div>
|
||||
{
|
||||
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
|
||||
}
|
||||
</div>
|
||||
).exists(RedirectAppLinks)
|
||||
).toBeFalsy();
|
||||
|
||||
expect(
|
||||
shallow(
|
||||
<div>
|
||||
{
|
||||
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
|
||||
}
|
||||
</div>
|
||||
).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!",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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: (
|
||||
<>
|
||||
<p className="eui-textBreakWord" data-test-subj="missing-refs-failure">
|
||||
<FormattedMessage
|
||||
id="xpack.lens.editorFrame.dataViewNotFound"
|
||||
defaultMessage="Data view not found"
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
className="eui-textBreakWord"
|
||||
style={{
|
||||
userSelect: 'text',
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="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(', '),
|
||||
}}
|
||||
/>
|
||||
{canFix && (
|
||||
<RedirectAppLinks coreStart={core}>
|
||||
<a
|
||||
href={core.application.getUrlForApp('management', {
|
||||
path: '/kibana/indexPatterns/create',
|
||||
})}
|
||||
style={{ display: 'block' }}
|
||||
data-test-subj="configuration-failure-reconfigure-indexpatterns"
|
||||
>
|
||||
{i18n.translate('xpack.lens.editorFrame.dataViewReconfigure', {
|
||||
defaultMessage: `Recreate it in the data view management page.`,
|
||||
})}
|
||||
</a>
|
||||
</RedirectAppLinks>
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
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;
|
||||
});
|
||||
};
|
|
@ -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 (
|
||||
<AggregateQueryTopNavMenu
|
||||
|
|
|
@ -364,6 +364,7 @@ export async function mountApp(
|
|||
contextOriginatingApp={originatingApp}
|
||||
topNavMenuEntryGenerators={topNavMenuEntryGenerators}
|
||||
theme$={core.theme.theme$}
|
||||
coreStart={coreStart}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
|
|
|
@ -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<CoreTheme>;
|
||||
coreStart: CoreStart;
|
||||
}
|
||||
|
||||
export type RunSave = (
|
||||
|
@ -121,6 +124,7 @@ export interface LensTopNavMenuProps {
|
|||
theme$: Observable<CoreTheme>;
|
||||
indexPatternService: IndexPatternServiceAPI;
|
||||
onTextBasedSavedAndExit: ({ onSave }: { onSave: () => void }) => Promise<void>;
|
||||
getUserMessages: UserMessagesGetter;
|
||||
shortUrlService: (params: LensAppLocatorParams) => Promise<string>;
|
||||
isCurrentStateDirty: boolean;
|
||||
}
|
||||
|
|
|
@ -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<FormBasedPrivateState> & {
|
||||
|
@ -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 (
|
||||
<DimensionTrigger
|
||||
id={columnId}
|
||||
label={!currentColumnHasErrors ? formattedLabel : selectedColumn.label}
|
||||
isInvalid={Boolean(currentColumnHasErrors)}
|
||||
hideTooltip={hideTooltip}
|
||||
invalidMessage={invalidMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
|
|
|
@ -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: {},
|
||||
},
|
||||
},
|
||||
currentIndexPatternId: '1',
|
||||
};
|
||||
expect(FormBasedDatasource.getErrorMessages(state, indexPatterns)).toEqual([
|
||||
{ longMessage: 'error 1', shortMessage: '' },
|
||||
{ longMessage: 'error 2', 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: {},
|
||||
},
|
||||
},
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getWarningMessages', () => {
|
||||
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',
|
||||
},
|
||||
};
|
||||
|
||||
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',
|
||||
},
|
||||
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,
|
||||
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',
|
||||
};
|
||||
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);
|
||||
});
|
||||
|
||||
framePublicAPI = {
|
||||
activeData: {
|
||||
first: {
|
||||
type: 'datatable',
|
||||
rows: [],
|
||||
columns: [
|
||||
{
|
||||
id: 'col1',
|
||||
name: 'col1',
|
||||
meta: {
|
||||
type: 'date',
|
||||
source: 'esaggs',
|
||||
sourceParams: {
|
||||
type: 'date_histogram',
|
||||
params: {
|
||||
used_interval: '12h',
|
||||
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.getUserMessages(state, {
|
||||
frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
|
||||
setState: () => {},
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "visualization",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
"longMessage": <FormattedMessage
|
||||
defaultMessage="Layer {position} error: {wrappedMessage}"
|
||||
id="xpack.lens.indexPattern.layerErrorWrapper"
|
||||
values={
|
||||
Object {
|
||||
"position": 1,
|
||||
"wrappedMessage": <React.Fragment>
|
||||
error 1
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
"severity": "error",
|
||||
"shortMessage": "Layer 1 error: ",
|
||||
},
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "visualization",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
"longMessage": <FormattedMessage
|
||||
defaultMessage="Layer {position} error: {wrappedMessage}"
|
||||
id="xpack.lens.indexPattern.layerErrorWrapper"
|
||||
values={
|
||||
Object {
|
||||
"position": 1,
|
||||
"wrappedMessage": <React.Fragment>
|
||||
error 2
|
||||
</React.Fragment>,
|
||||
}
|
||||
}
|
||||
/>,
|
||||
"severity": "error",
|
||||
"shortMessage": "Layer 1 error: ",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(getErrorMessages).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('warning messages', () => {
|
||||
let state: FormBasedPrivateState;
|
||||
let framePublicAPI: FramePublicAPI;
|
||||
|
||||
beforeEach(() => {
|
||||
(getErrorMessages as jest.Mock).mockReturnValueOnce([]);
|
||||
|
||||
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',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should show different types of warning messages', () => {
|
||||
framePublicAPI.activeData!.first.columns[1].meta.sourceParams!.hasPrecisionError = true;
|
||||
|
||||
const warnings = FormBasedDatasource.getWarningMessages!(state, framePublicAPI, {}, () => {});
|
||||
|
||||
expect(extractTranslationIdsFromWarnings(warnings)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"xpack.lens.indexPattern.timeShiftSmallWarning",
|
||||
"xpack.lens.indexPattern.timeShiftMultipleWarning",
|
||||
"xpack.lens.indexPattern.precisionErrorWarning.accuracyDisabled",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
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']);
|
||||
|
||||
state = {
|
||||
layers: {
|
||||
first: {
|
||||
indexPatternId: '1',
|
||||
columnOrder: [],
|
||||
columns: {},
|
||||
dataViews: {
|
||||
...createMockFramePublicAPI().dataViews,
|
||||
indexPatterns: expectedIndexPatterns,
|
||||
indexPatternRefs: Object.values(expectedIndexPatterns).map(({ id, title }) => ({
|
||||
id,
|
||||
title,
|
||||
})),
|
||||
},
|
||||
second: {
|
||||
indexPatternId: '1',
|
||||
columnOrder: [],
|
||||
columns: {},
|
||||
},
|
||||
},
|
||||
currentIndexPatternId: '1',
|
||||
} as unknown as FramePublicAPI;
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
};
|
||||
|
||||
expect(FormBasedDatasource.getErrorMessages(state, indexPatterns)).toEqual([
|
||||
{ longMessage: 'Layer 1 error: error 1', shortMessage: '' },
|
||||
{ longMessage: 'Layer 1 error: error 2', shortMessage: '' },
|
||||
]);
|
||||
expect(getErrorMessages).toHaveBeenCalledTimes(2);
|
||||
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",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should show different types of warning messages', () => {
|
||||
framePublicAPI.activeData!.first.columns[1].meta.sourceParams!.hasPrecisionError = true;
|
||||
|
||||
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",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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<FormBasedPrivateState>
|
||||
) => {
|
||||
const columnLabelMap = formBasedDatasource.uniqueLabels(props.state);
|
||||
const uniqueLabel = columnLabelMap[props.columnId];
|
||||
const formattedLabel = wrapOnDot(uniqueLabel);
|
||||
|
||||
render(
|
||||
<KibanaThemeProvider theme$={core.theme.theme$}>
|
||||
|
@ -542,7 +551,13 @@ export function getFormBasedDatasource({
|
|||
unifiedSearch,
|
||||
}}
|
||||
>
|
||||
<FormBasedDimensionTrigger uniqueLabel={columnLabelMap[props.columnId]} {...props} />
|
||||
<DimensionTrigger
|
||||
id={props.columnId}
|
||||
label={formattedLabel}
|
||||
isInvalid={props.invalid}
|
||||
hideTooltip={props.hideTooltip}
|
||||
invalidMessage={props.invalidMessage}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
</KibanaThemeProvider>,
|
||||
|
@ -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(
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="alert"
|
||||
size="s"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.useFieldExistenceSamplingBody"
|
||||
defaultMessage="Field existence sampling has been deprecated and will be removed in Kibana {version}. You may disable this feature in {link}."
|
||||
values={{
|
||||
version: '8.6.0',
|
||||
link: (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
core.application.navigateToApp('management', {
|
||||
path: `/kibana/settings?query=${useFieldExistenceSamplingKey}`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.useFieldExistenceSampling.advancedSettings"
|
||||
defaultMessage="Advanced Settings"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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<FormBasedPrivateState, unknown>,
|
||||
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 && (
|
||||
<EuiButton
|
||||
data-test-subj="errorFixAction"
|
||||
onClick={async () => {
|
||||
const newState = await error.fixAction?.newState(frameDatasourceAPI);
|
||||
if (newState) {
|
||||
setState(newState);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{error.fixAction?.label}
|
||||
</EuiButton>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
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: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.layerErrorWrapper"
|
||||
defaultMessage="Layer {position} error: {wrappedMessage}"
|
||||
values={{
|
||||
position: index + 1,
|
||||
wrappedMessage: <>{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: (
|
||||
<p>
|
||||
{i18n.translate('xpack.lens.configure.invalidConfigTooltip', {
|
||||
defaultMessage: 'Invalid configuration.',
|
||||
})}
|
||||
<br />
|
||||
{i18n.translate('xpack.lens.configure.invalidConfigTooltipClick', {
|
||||
defaultMessage: 'Click for more details.',
|
||||
})}
|
||||
</p>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
})
|
||||
.flat();
|
||||
|
||||
return columnErrorMessages;
|
||||
}
|
||||
|
|
|
@ -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<string | React.ReactNode> {
|
||||
): 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 [
|
||||
<>
|
||||
<EuiText size="s">
|
||||
<strong>{warning.message}</strong>
|
||||
<p>{warning.text}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
{warning.text ? (
|
||||
<ShardFailureOpenModalButton
|
||||
theme={theme}
|
||||
title={warning.message}
|
||||
size="m"
|
||||
getRequestMeta={() => ({
|
||||
request: request as ShardFailureRequest,
|
||||
response,
|
||||
})}
|
||||
color="primary"
|
||||
isButtonEmpty={true}
|
||||
/>
|
||||
) : null}
|
||||
</>,
|
||||
{
|
||||
uniqueId: `shard_failure`,
|
||||
severity: 'warning',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'toolbar' }, { id: 'embeddableBadge' }],
|
||||
shortMessage: '',
|
||||
longMessage: (
|
||||
<>
|
||||
<EuiText size="s">
|
||||
<strong>{warning.message}</strong>
|
||||
<p>{warning.text}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
{warning.text ? (
|
||||
<ShardFailureOpenModalButton
|
||||
theme={theme}
|
||||
title={warning.message}
|
||||
size="m"
|
||||
getRequestMeta={() => ({
|
||||
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: (
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="alert"
|
||||
size="s"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.useFieldExistenceSamplingBody"
|
||||
defaultMessage="Field existence sampling has been deprecated and will be removed in Kibana {version}. You may disable this feature in {link}."
|
||||
values={{
|
||||
version: '8.6.0',
|
||||
link: (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
core.application.navigateToApp('management', {
|
||||
path: `/kibana/settings?query=${useFieldExistenceSamplingKey}`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.useFieldExistenceSampling.advancedSettings"
|
||||
defaultMessage="Advanced Settings"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
export function getVisualDefaultsForLayer(layer: FormBasedLayer) {
|
||||
return Object.keys(layer.columns).reduce<Record<string, Record<string, unknown>>>(
|
||||
(memo, columnId) => {
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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?,
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
<ColorIndicator accessorConfig={accessorConfig}>{children}</ColorIndicator>
|
||||
</EuiLink>
|
||||
|
|
|
@ -135,6 +135,7 @@ describe('ConfigPanel', () => {
|
|||
isFullscreen: false,
|
||||
toggleFullscreen: jest.fn(),
|
||||
uiActions,
|
||||
getUserMessages: () => [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ActiveDimensionState>(
|
||||
|
@ -509,6 +511,18 @@ export function LayerPanel(
|
|||
<ReorderProvider id={group.groupId} className={'lnsLayerPanel__group'}>
|
||||
{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 (
|
||||
<DraggableDimensionButton
|
||||
activeVisualization={activeVisualization}
|
||||
|
@ -580,8 +594,8 @@ export function LayerPanel(
|
|||
groupId: group.groupId,
|
||||
filterOperations: group.filterOperations,
|
||||
hideTooltip,
|
||||
invalid: group.invalid,
|
||||
invalidMessage: group.invalidMessage,
|
||||
invalid: hasMessages,
|
||||
invalidMessage: messageToDisplay,
|
||||
indexPatterns: dataViews.indexPatterns,
|
||||
}}
|
||||
/>
|
||||
|
@ -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,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -99,6 +99,8 @@ function getDefaultProps() {
|
|||
lensInspector: getLensInspectorService(inspectorPluginMock.createStartContract()),
|
||||
showNoDataPopover: jest.fn(),
|
||||
indexPatternService: createIndexPatternServiceMock(),
|
||||
getUserMessages: () => [],
|
||||
addUserMessages: () => () => {},
|
||||
};
|
||||
return defaultProps;
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<RootDragDropProvider>
|
||||
<FrameLayout
|
||||
bannerMessages={
|
||||
bannerMessages ? (
|
||||
bannerMessages.length ? (
|
||||
<ErrorBoundary onError={onError}>
|
||||
<BannerWrapper nodes={bannerMessages} />
|
||||
<BannerWrapper nodes={bannerMessages.map(({ longMessage }) => longMessage)} />
|
||||
</ErrorBoundary>
|
||||
) : undefined
|
||||
}
|
||||
|
@ -139,6 +142,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
framePublicAPI={framePublicAPI}
|
||||
uiActions={props.plugins.uiActions}
|
||||
indexPatternService={props.indexPatternService}
|
||||
getUserMessages={props.getUserMessages}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
@ -156,6 +160,8 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
visualizationMap={visualizationMap}
|
||||
framePublicAPI={framePublicAPI}
|
||||
getSuggestionForField={getSuggestionForField.current}
|
||||
getUserMessages={props.getUserMessages}
|
||||
addUserMessages={props.addUserMessages}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
@ -169,6 +175,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
datasourceMap={datasourceMap}
|
||||
visualizationMap={visualizationMap}
|
||||
frame={framePublicAPI}
|
||||
getUserMessages={props.getUserMessages}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
|
|
@ -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() }],
|
||||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
};
|
||||
}
|
||||
|
||||
const indexPatternValidation = validateRequiredIndexPatterns(
|
||||
datasourceMap[datasourceId],
|
||||
datasourceStates[datasourceId],
|
||||
indexPatterns
|
||||
);
|
||||
|
||||
if (indexPatternValidation) {
|
||||
return {
|
||||
ast: null,
|
||||
errors: indexPatternValidation,
|
||||
};
|
||||
}
|
||||
|
||||
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<FramePublicAPI, 'datasourceLayers' | 'dataViews'>
|
||||
): 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;
|
||||
};
|
||||
|
|
|
@ -104,6 +104,7 @@ describe('suggestion_panel', () => {
|
|||
},
|
||||
ExpressionRenderer: expressionRendererMock,
|
||||
frame: createMockFramePublicAPI(),
|
||||
getUserMessages: () => [],
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -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 && (
|
||||
<SuggestionPreview
|
||||
preview={{
|
||||
error: currentStateError != null,
|
||||
error: currentStateError,
|
||||
expression: currentStateExpression,
|
||||
icon:
|
||||
visualizationMap[currentVisualization.activeId].getDescription(
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`workspace_panel should show an error message if the expression fails to parse 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"displayLocations": Array [
|
||||
Object {
|
||||
"id": "visualization",
|
||||
},
|
||||
],
|
||||
"fixableInEditor": true,
|
||||
"longMessage": <React.Fragment>
|
||||
<p
|
||||
data-test-subj="expression-failure"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="An error occurred in the expression"
|
||||
id="xpack.lens.editorFrame.expressionFailure"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
Error: Unable to parse expression: Expected "/*", "//", [ ,\\t,\\r,\\n], or function but "|" found.
|
||||
</p>
|
||||
</React.Fragment>,
|
||||
"severity": "error",
|
||||
"shortMessage": "An unexpected error occurred while preparing the chart",
|
||||
"uniqueId": "expression_build_error",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
|
@ -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(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
getUserMessages={() => 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,92 +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(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceMap={{
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
/>,
|
||||
|
||||
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="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(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceMap={{
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
// Use cannot navigate to the management page
|
||||
core={createCoreStartWithPermissions({
|
||||
navLinks: { management: false },
|
||||
management: { kibana: { indexPatterns: true } },
|
||||
})}
|
||||
/>,
|
||||
|
||||
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;
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'visualization' }],
|
||||
shortMessage: 'hey there',
|
||||
longMessage: "i'm another error",
|
||||
},
|
||||
];
|
||||
|
||||
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 getUserMessages = jest.fn(() => messages);
|
||||
|
||||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
|
@ -755,167 +697,35 @@ describe('workspace_panel', () => {
|
|||
datasourceMap={{
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
// user can go to management, but indexPatterns management is not accessible
|
||||
core={createCoreStartWithPermissions({
|
||||
navLinks: { management: true },
|
||||
management: { kibana: { indexPatterns: false } },
|
||||
})}
|
||||
/>,
|
||||
|
||||
{
|
||||
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(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceMap={{
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
testVis: { ...mockVisualization, toExpression: () => '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(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
datasourceMap={{
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
testVis: mockVisualization,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
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 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 mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
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(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
getUserMessages={() => 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<UserMessage[], unknown[]>(() => []);
|
||||
|
||||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...defaultProps}
|
||||
|
@ -988,11 +798,13 @@ describe('workspace_panel', () => {
|
|||
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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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<unknown>;
|
||||
}>;
|
||||
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<WorkspacePanelProps, 'getSuggestionForField'> & {
|
||||
suggestionForDraggedField: Suggestion | undefined;
|
||||
}) {
|
||||
|
@ -166,12 +162,10 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
const searchSessionId = useLensSelector(selectSearchSessionId);
|
||||
|
||||
const [localState, setLocalState] = useState<WorkspaceState>({
|
||||
expressionBuildError: undefined,
|
||||
expandError: false,
|
||||
expressionToRender: undefined,
|
||||
});
|
||||
|
||||
// const expressionToRender = useRef<null | undefined | string>();
|
||||
const initialRenderComplete = useRef<boolean>();
|
||||
|
||||
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<DefaultInspectorAdapters>) => {
|
||||
(_data: unknown, adapters?: Partial<DefaultInspectorAdapters>) => {
|
||||
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<React.ReactNode | string> = [];
|
||||
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<Record<string, Datatable>>(
|
||||
(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<Record<string, Datatable>>(
|
||||
(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: (
|
||||
<>
|
||||
<p data-test-subj="expression-failure">
|
||||
<FormattedMessage
|
||||
id="xpack.lens.editorFrame.expressionFailure"
|
||||
defaultMessage="An error occurred in the expression"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>{e.toString()}</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
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()}
|
||||
</WorkspacePanelWrapper>
|
||||
|
@ -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<boolean>;
|
||||
setLocalState: (dispatch: (prevState: WorkspaceState) => WorkspaceState) => void;
|
||||
localState: WorkspaceState & {
|
||||
configurationValidationError?: Array<{
|
||||
shortMessage: string;
|
||||
longMessage: React.ReactNode;
|
||||
fixAction?: DatasourceFixAction<unknown>;
|
||||
}>;
|
||||
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();
|
||||
if (errors?.length) {
|
||||
const showExtraErrorsAction =
|
||||
!localState.expandError && errors.length > 1 ? (
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
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 },
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
) : null;
|
||||
|
||||
function renderFixAction(
|
||||
validationError:
|
||||
| {
|
||||
shortMessage: string;
|
||||
longMessage: React.ReactNode;
|
||||
fixAction?: DatasourceFixAction<unknown>;
|
||||
}
|
||||
| undefined
|
||||
) {
|
||||
return (
|
||||
validationError &&
|
||||
validationError.fixAction &&
|
||||
activeDatasourceId && (
|
||||
<>
|
||||
<EuiButton
|
||||
data-test-subj="errorFixAction"
|
||||
onClick={async () => {
|
||||
const newState = await validationError.fixAction?.newState({
|
||||
...framePublicAPI,
|
||||
...context,
|
||||
});
|
||||
dispatchLens(
|
||||
updateDatasourceState({
|
||||
updater: newState,
|
||||
datasourceId: activeDatasourceId,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{validationError.fixAction.label}
|
||||
</EuiButton>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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) => (
|
||||
<>
|
||||
<p
|
||||
key={validationError.shortMessage}
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="configuration-failure-error"
|
||||
>
|
||||
{validationError.longMessage}
|
||||
</p>
|
||||
{renderFixAction(validationError)}
|
||||
</>
|
||||
));
|
||||
} else {
|
||||
showExtraErrorsAction = (
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
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 },
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
}
|
||||
const [firstMessage, ...rest] = errors;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj="configuration-failure">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiEmptyPrompt
|
||||
actions={showExtraErrorsAction}
|
||||
body={
|
||||
<>
|
||||
<p className="eui-textBreakWord" data-test-subj="configuration-failure-error">
|
||||
{localState.configurationValidationError[0].longMessage}
|
||||
</p>
|
||||
{renderFixAction(localState.configurationValidationError?.[0])}
|
||||
|
||||
{showExtraErrors}
|
||||
</>
|
||||
}
|
||||
iconColor="danger"
|
||||
iconType="alert"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<EuiFlexGroup data-test-subj="configuration-failure">
|
||||
<EuiFlexItem>
|
||||
<EuiEmptyPrompt
|
||||
actions={
|
||||
isManagementEnabled && isIndexPatternManagementEnabled ? (
|
||||
<RedirectAppLinks coreStart={core}>
|
||||
<a
|
||||
href={core.application.getUrlForApp('management', {
|
||||
path: '/kibana/indexPatterns/create',
|
||||
})}
|
||||
style={{ width: '100%' }}
|
||||
data-test-subj="configuration-failure-reconfigure-indexpatterns"
|
||||
>
|
||||
{i18n.translate('xpack.lens.editorFrame.dataViewReconfigure', {
|
||||
defaultMessage: `Recreate it in the data view management page`,
|
||||
})}
|
||||
</a>
|
||||
</RedirectAppLinks>
|
||||
) : null
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p className="eui-textBreakWord" data-test-subj="missing-refs-failure">
|
||||
<FormattedMessage
|
||||
id="xpack.lens.editorFrame.dataViewNotFound"
|
||||
defaultMessage="Data view not found"
|
||||
/>
|
||||
</p>
|
||||
<p className="eui-textBreakWord lnsSelectableErrorMessage">
|
||||
{localState.missingRefsErrors[0].longMessage}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
iconColor="danger"
|
||||
iconType="alert"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (localState.expressionBuildError?.length) {
|
||||
const firstError = localState.expressionBuildError[0];
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiEmptyPrompt
|
||||
body={
|
||||
<>
|
||||
<p data-test-subj="expression-failure">
|
||||
<FormattedMessage
|
||||
id="xpack.lens.editorFrame.expressionFailure"
|
||||
defaultMessage="An error occurred in the expression"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>{firstError.longMessage}</p>
|
||||
<div data-test-subj="workspace-error-message">{firstMessage.longMessage}</div>
|
||||
{localState.expandError && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
{rest.map((message) => (
|
||||
<div data-test-subj="workspace-error-message">
|
||||
{message.longMessage}
|
||||
<EuiSpacer />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
iconColor="danger"
|
||||
|
|
|
@ -41,6 +41,7 @@ describe('workspace_panel_wrapper', () => {
|
|||
datasourceStates={{}}
|
||||
isFullscreen={false}
|
||||
lensInspector={{} as unknown as LensInspector}
|
||||
getUserMessages={() => []}
|
||||
>
|
||||
<MyChild />
|
||||
</WorkspacePanelWrapper>
|
||||
|
@ -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={() => []}
|
||||
>
|
||||
<div />
|
||||
</WorkspacePanelWrapper>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 (
|
||||
<div className="lnsApp__frame">
|
||||
<EditorFrame
|
||||
|
@ -127,6 +133,8 @@ export class EditorFrameService {
|
|||
plugins={plugins}
|
||||
lensInspector={lensInspector}
|
||||
showNoDataPopover={showNoDataPopover}
|
||||
getUserMessages={getUserMessages}
|
||||
addUserMessages={addUserMessages}
|
||||
indexPatternService={indexPatternService}
|
||||
datasourceMap={resolvedDatasources}
|
||||
visualizationMap={resolvedVisualizations}
|
||||
|
|
|
@ -8,9 +8,3 @@
|
|||
import type { Datatable } from '@kbn/expressions-plugin/common';
|
||||
|
||||
export type TableInspectorAdapter = Record<string, Datatable>;
|
||||
|
||||
export interface ErrorMessage {
|
||||
shortMessage: string;
|
||||
longMessage: React.ReactNode;
|
||||
type?: 'fixable' | 'critical';
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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<Document, 'savedObjectId' | 'type'>;
|
||||
|
||||
|
@ -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<string, UserMessage> = {};
|
||||
|
||||
// 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: (
|
||||
<this.deps.spaces.ui.components.getEmbeddableLegacyUrlConflict
|
||||
targetType={DOC_TYPE}
|
||||
sourceId={sharingSavedObjectProps.sourceId!}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
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: (
|
||||
<this.deps.spaces.ui.components.getEmbeddableLegacyUrlConflict
|
||||
targetType={DOC_TYPE}
|
||||
sourceId={metaInfo?.sharingSavedObjectProps?.sourceId!}
|
||||
/>
|
||||
),
|
||||
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<DefaultInspectorAdapters>) {
|
||||
const activeDatasourceId = getActiveDatasourceIdFromDoc(this.savedVis);
|
||||
|
||||
if (!activeDatasourceId || !adapters?.requests) {
|
||||
return;
|
||||
private getSearchWarningMessages(adapters?: Partial<DefaultInspectorAdapters>): 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(
|
||||
<KibanaThemeProvider theme$={this.deps.theme.theme$}>
|
||||
<Warnings warnings={requestWarnings} compressed />
|
||||
</KibanaThemeProvider>,
|
||||
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();
|
||||
|
||||
this.updateOutput({
|
||||
...this.getOutput(),
|
||||
loading: true,
|
||||
error,
|
||||
const errors = this.getUserMessages(['visualization', 'visualizationOnEmbeddable'], {
|
||||
severity: 'error',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
this.updateOutput({
|
||||
loading: true,
|
||||
error: errors.length
|
||||
? new Error(
|
||||
typeof errors[0].longMessage === 'string'
|
||||
? errors[0].longMessage
|
||||
: errors[0].shortMessage
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
this.renderComplete.dispatchError();
|
||||
} else {
|
||||
this.renderComplete.dispatchInProgress();
|
||||
|
@ -719,7 +835,7 @@ export class Embeddable
|
|||
<ExpressionWrapper
|
||||
ExpressionRenderer={this.expressionRenderer}
|
||||
expression={this.expression || null}
|
||||
errors={this.errors}
|
||||
errors={errors}
|
||||
lensInspector={this.lensInspector}
|
||||
searchContext={this.getMergedSearchContext()}
|
||||
variables={{
|
||||
|
@ -763,6 +879,19 @@ export class Embeddable
|
|||
</KibanaThemeProvider>,
|
||||
domNode
|
||||
);
|
||||
|
||||
const warningsToDisplay = this.getUserMessages('embeddableBadge', {
|
||||
severity: 'warning',
|
||||
});
|
||||
|
||||
if (warningsToDisplay.length && this.warningDomNode) {
|
||||
render(
|
||||
<KibanaThemeProvider theme$={this.deps.theme.theme$}>
|
||||
<Warnings warnings={warningsToDisplay.map((message) => message.longMessage)} compressed />
|
||||
</KibanaThemeProvider>,
|
||||
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<boolean> {
|
||||
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() {
|
||||
|
|
|
@ -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<Capabilities>;
|
||||
|
@ -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,
|
||||
|
|
|
@ -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<string, unknown>;
|
||||
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 (
|
||||
<div className="lnsEmbeddedError">
|
||||
<EuiEmptyPrompt
|
||||
|
@ -67,7 +67,7 @@ export function VisualizationErrorPanel({ errors, canEdit }: VisualizationErrorP
|
|||
data-test-subj="embeddable-lens-failure"
|
||||
body={
|
||||
<>
|
||||
{errors ? (
|
||||
{errors.length ? (
|
||||
<>
|
||||
<p>{errors[0].longMessage}</p>
|
||||
{showMore && !canFixInLens ? (
|
||||
|
@ -129,7 +129,7 @@ export function ExpressionWrapper({
|
|||
}: ExpressionWrapperProps) {
|
||||
return (
|
||||
<I18nProvider>
|
||||
{errors || expression === null || expression === '' ? (
|
||||
{errors.length || expression === null || expression === '' ? (
|
||||
<VisualizationErrorPanel errors={errors} canEdit={canEdit} />
|
||||
) : (
|
||||
<div className={classNames('lnsExpressionRenderer', className)} style={style}>
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -49,7 +49,6 @@ export function createMockVisualization(id = 'testVis'): jest.Mocked<Visualizati
|
|||
|
||||
setDimension: jest.fn(),
|
||||
removeDimension: jest.fn(),
|
||||
getErrorMessages: jest.fn((_state) => undefined),
|
||||
renderDimensionEditor: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -31,7 +31,7 @@ export const DimensionTrigger = ({
|
|||
id: string;
|
||||
isInvalid?: boolean;
|
||||
hideTooltip?: boolean;
|
||||
invalidMessage?: string | JSX.Element;
|
||||
invalidMessage?: string | React.ReactNode;
|
||||
}) => {
|
||||
if (isInvalid) {
|
||||
return (
|
||||
|
|
|
@ -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<Partial<LensAppState>>('lens/setState');
|
||||
export const onActiveDataChange = createAction<{
|
||||
activeData?: TableInspectorAdapter;
|
||||
requestWarnings?: Array<ReactNode | string>;
|
||||
activeData: TableInspectorAdapter;
|
||||
}>('lens/onActiveDataChange');
|
||||
export const setSaveable = createAction<boolean>('lens/setSaveable');
|
||||
export const enableAutoApply = createAction<void>('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<boolean>) => {
|
||||
|
|
|
@ -238,3 +238,8 @@ export const selectFramePublicAPI = createSelector(
|
|||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const selectFrameDatasourceAPI = createSelector(
|
||||
[selectFramePublicAPI, selectExecutionContext],
|
||||
(framePublicAPI, context) => ({ ...context, ...framePublicAPI })
|
||||
);
|
||||
|
|
|
@ -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<string, Visualization>;
|
||||
|
@ -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<T = unknown, P = unknown> {
|
|||
|
||||
// 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<T = unknown, P = unknown> {
|
|||
*/
|
||||
checkIntegrity: (state: T, indexPatterns: IndexPatternMap) => string[];
|
||||
|
||||
getErrorMessages: (
|
||||
state: T,
|
||||
indexPatterns: Record<string, IndexPattern>
|
||||
) =>
|
||||
| Array<{
|
||||
shortMessage: string;
|
||||
longMessage: React.ReactNode;
|
||||
fixAction?: { label: string; newState: () => Promise<T> };
|
||||
}>
|
||||
| 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<T>
|
||||
) => React.ReactNode[] | undefined;
|
||||
|
||||
getDeprecationMessages?: (state: T) => React.ReactNode[] | undefined;
|
||||
deps: {
|
||||
frame: FrameDatasourceAPI;
|
||||
setState: StateSetter<T>;
|
||||
}
|
||||
) => UserMessage[];
|
||||
|
||||
/**
|
||||
* The embeddable calls this function to display warnings about visualization on the dashboard
|
||||
|
@ -464,7 +495,7 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
warning: SearchResponseWarning,
|
||||
request: SearchRequest,
|
||||
response: estypes.SearchResponse
|
||||
) => Array<string | React.ReactNode> | undefined;
|
||||
) => UserMessage[];
|
||||
|
||||
/**
|
||||
* Checks if the visualization created is time based, for example date histogram
|
||||
|
@ -623,7 +654,7 @@ export type DatasourceDimensionProps<T> = SharedDimensionProps & {
|
|||
indexPatterns: IndexPatternMap;
|
||||
hideTooltip?: boolean;
|
||||
invalid?: boolean;
|
||||
invalidMessage?: string;
|
||||
invalidMessage?: string | React.ReactNode;
|
||||
};
|
||||
export type ParamEditorCustomProps = Record<string, unknown> & {
|
||||
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<T = unknown, P = unknown> {
|
|||
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<T = unknown, P = unknown> {
|
|||
datasourceLayers: DatasourceLayers,
|
||||
datasourceExpressionsByLayers?: Record<string, Ast>
|
||||
) => 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<FramePublicAPI, 'datasourceLayers' | 'dataViews'>
|
||||
) =>
|
||||
| Array<{
|
||||
shortMessage: string;
|
||||
longMessage: React.ReactNode;
|
||||
}>
|
||||
| undefined;
|
||||
|
||||
validateColumn?: (
|
||||
state: T,
|
||||
frame: Pick<FramePublicAPI, 'dataViews'>,
|
||||
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
|
||||
|
|
|
@ -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<string, Array<string | React.ReactNode>> = new Map();
|
||||
): UserMessage[] => {
|
||||
const warningsMap: Map<string, UserMessage[]> = new Map();
|
||||
|
||||
deps.searchService.showWarnings(adapter, (warning, meta) => {
|
||||
const { request, response, requestId } = meta;
|
||||
|
|
|
@ -25,8 +25,4 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.lnsSelectableErrorMessage {
|
||||
user-select: text;
|
||||
}
|
|
@ -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 = {
|
||||
|
|
|
@ -499,10 +499,6 @@ export const getDatatableVisualization = ({
|
|||
};
|
||||
},
|
||||
|
||||
getErrorMessages(state) {
|
||||
return undefined;
|
||||
},
|
||||
|
||||
getRenderEventCounters(state) {
|
||||
const events = {
|
||||
color_by_value: false,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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([
|
||||
<FormattedMessage
|
||||
id="xpack.lens.gaugeVisualization.minValueGreaterMetricShortMessage"
|
||||
defaultMessage="Minimum value is greater than metric value."
|
||||
/>,
|
||||
]);
|
||||
warnings.push({
|
||||
severity: 'warning',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'toolbar' }],
|
||||
shortMessage: '',
|
||||
longMessage: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.gaugeVisualization.minValueGreaterMetricShortMessage"
|
||||
defaultMessage="Minimum value is greater than metric value."
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
if (minValue > goalValue) {
|
||||
warnings.push([
|
||||
<FormattedMessage
|
||||
id="xpack.lens.gaugeVisualization.minimumValueGreaterGoalShortMessage"
|
||||
defaultMessage="Minimum value is greater than goal value."
|
||||
/>,
|
||||
]);
|
||||
warnings.push({
|
||||
severity: 'warning',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'toolbar' }],
|
||||
shortMessage: '',
|
||||
longMessage: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.gaugeVisualization.minimumValueGreaterGoalShortMessage"
|
||||
defaultMessage="Minimum value is greater than goal value."
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof maxValue === 'number') {
|
||||
if (metricValue > maxValue) {
|
||||
warnings.push([
|
||||
<FormattedMessage
|
||||
id="xpack.lens.gaugeVisualization.metricValueGreaterMaximumShortMessage"
|
||||
defaultMessage="Metric value is greater than maximum value."
|
||||
/>,
|
||||
]);
|
||||
warnings.push({
|
||||
severity: 'warning',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'toolbar' }],
|
||||
shortMessage: '',
|
||||
longMessage: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.gaugeVisualization.metricValueGreaterMaximumShortMessage"
|
||||
defaultMessage="Metric value is greater than maximum value."
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof goalValue === 'number' && goalValue > maxValue) {
|
||||
warnings.push([
|
||||
<FormattedMessage
|
||||
id="xpack.lens.gaugeVisualization.goalValueGreaterMaximumShortMessage"
|
||||
defaultMessage="Goal value is greater than maximum value."
|
||||
/>,
|
||||
]);
|
||||
warnings.push({
|
||||
severity: 'warning',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'toolbar' }],
|
||||
shortMessage: '',
|
||||
longMessage: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.gaugeVisualization.goalValueGreaterMaximumShortMessage"
|
||||
defaultMessage="Goal value is greater than maximum value."
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getWarningMessages', () => {
|
||||
beforeEach(() => {
|
||||
const mockDatasource = createMockDatasource('testDatasource');
|
||||
|
||||
mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
|
||||
dataType: 'string',
|
||||
label: 'MyOperation',
|
||||
} as OperationDescriptor);
|
||||
|
||||
frame.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
};
|
||||
}).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.",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
describe('warnings', () => {
|
||||
beforeEach(() => {
|
||||
const mockDatasource = createMockDatasource('testDatasource');
|
||||
|
||||
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);
|
||||
});
|
||||
mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
|
||||
dataType: 'string',
|
||||
label: 'MyOperation',
|
||||
} as OperationDescriptor);
|
||||
|
||||
test('should return a warning message when cell value data contains arrays', () => {
|
||||
frame.activeData = {
|
||||
first: {
|
||||
type: 'datatable',
|
||||
rows: [
|
||||
{
|
||||
'v-accessor': [1, 2, 3],
|
||||
},
|
||||
],
|
||||
columns: [],
|
||||
},
|
||||
};
|
||||
frame.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
};
|
||||
});
|
||||
|
||||
const mockState = {
|
||||
shape: CHART_SHAPES.HEATMAP,
|
||||
valueAccessor: 'v-accessor',
|
||||
layerId: 'first',
|
||||
} as HeatmapVisualizationState;
|
||||
expect(
|
||||
getHeatmapVisualization({
|
||||
paletteService,
|
||||
theme,
|
||||
}).getWarningMessages!(mockState, frame)
|
||||
).toHaveLength(1);
|
||||
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(
|
||||
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: [],
|
||||
},
|
||||
};
|
||||
|
||||
const mockState = {
|
||||
shape: CHART_SHAPES.HEATMAP,
|
||||
valueAccessor: 'v-accessor',
|
||||
layerId: 'first',
|
||||
} as HeatmapVisualizationState;
|
||||
expect(
|
||||
onlyWarnings(
|
||||
getHeatmapVisualization({
|
||||
paletteService,
|
||||
theme,
|
||||
}).getUserMessages!(mockState, { frame })
|
||||
)
|
||||
).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<Visualization['getErrorMessages']> = [];
|
||||
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;
|
||||
},
|
||||
let warnings: UserMessage[] = [];
|
||||
|
||||
getWarningMessages(state, frame) {
|
||||
if (!state?.layerId || !frame.activeData || !state.valueAccessor) {
|
||||
return;
|
||||
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: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.heatmapVisualization.arrayValuesWarningMessage"
|
||||
defaultMessage="{label} contains array values. Your visualization may not render as expected."
|
||||
values={{ label: <strong>{operation?.label}</strong> }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
const rows = frame.activeData[state.layerId] && frame.activeData[state.layerId].rows;
|
||||
if (!rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasArrayValues = rows.some((row) => Array.isArray(row[state.valueAccessor!]));
|
||||
|
||||
const datasource = frame.datasourceLayers[state.layerId];
|
||||
const operation = datasource?.getOperationForColumnId(state.valueAccessor);
|
||||
|
||||
return hasArrayValues
|
||||
? [
|
||||
<FormattedMessage
|
||||
id="xpack.lens.heatmapVisualization.arrayValuesWarningMessage"
|
||||
defaultMessage="{label} contains array values. Your visualization may not render as expected."
|
||||
values={{ label: <strong>{operation?.label}</strong> }}
|
||||
/>,
|
||||
]
|
||||
: undefined;
|
||||
return [...errors, ...warnings];
|
||||
},
|
||||
|
||||
getSuggestionFromConvertToLensContext({ suggestions, context }) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -634,11 +634,6 @@ export const getMetricVisualization = ({
|
|||
);
|
||||
},
|
||||
|
||||
getErrorMessages(state) {
|
||||
// Is it possible to break it?
|
||||
return undefined;
|
||||
},
|
||||
|
||||
getDisplayOptions() {
|
||||
return {
|
||||
noPanelTitle: true,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
<FormattedMessage
|
||||
id="xpack.lens.pie.smallValuesWarningMessage"
|
||||
defaultMessage="Waffle charts are unable to effectively display small field values. To display all field values, use the Data table or Treemap."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
<FormattedMessage
|
||||
key={labels.join(',')}
|
||||
id="xpack.lens.pie.arrayValues"
|
||||
defaultMessage="The following dimensions contain array values: {label}. Your visualization may not render as
|
||||
expected."
|
||||
values={{
|
||||
label: <strong>{labels.join(', ')}</strong>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.pie.smallValuesWarningMessage"
|
||||
defaultMessage="Waffle charts are unable to effectively display small field values. To display all field values, use the Data table or Treemap."
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
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: (
|
||||
<FormattedMessage
|
||||
key={labels.join(',')}
|
||||
id="xpack.lens.pie.arrayValues"
|
||||
defaultMessage="The following dimensions contain array values: {label}. Your visualization may not render as
|
||||
expected."
|
||||
values={{
|
||||
label: <strong>{labels.join(', ')}</strong>,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...errors, ...warningMessages];
|
||||
},
|
||||
|
||||
getVisualizationInfo(state: PieVisualizationState) {
|
||||
|
|
|
@ -44,7 +44,7 @@ export const defaultRangeAnnotationLabel = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
const isDateHistogram = (
|
||||
export const isDateHistogram = (
|
||||
dataLayers: XYDataLayerConfig[],
|
||||
frame?: Pick<FramePublicAPI, 'activeData' | 'datasourceLayers'> | undefined
|
||||
) =>
|
||||
|
@ -475,8 +475,6 @@ export const getAnnotationsConfiguration = ({
|
|||
frame: Pick<FramePublicAPI, 'datasourceLayers'>;
|
||||
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,
|
||||
|
|
|
@ -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<FramePublicAPI, 'dataViews'>,
|
||||
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;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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[] = [];
|
||||
|
||||
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: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.xyChart.annotationError"
|
||||
defaultMessage="Annotation {annotationName} has an error: {errorMessage}"
|
||||
values={{
|
||||
annotationName: annotation.label,
|
||||
errorMessage: invalidMessage,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
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: '',
|
||||
});
|
||||
}
|
||||
|
||||
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: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.xyChart.annotationError"
|
||||
defaultMessage="Annotation {annotationName} has an error: {errorMessage}"
|
||||
values={{
|
||||
annotationName: annotation.label,
|
||||
errorMessage,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
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;
|
||||
},
|
||||
const warnings: UserMessage[] = [];
|
||||
|
||||
getWarningMessages(state, frame) {
|
||||
if (state?.layers.length === 0 || !frame.activeData) {
|
||||
return;
|
||||
}
|
||||
if (state?.layers.length > 0 && activeData) {
|
||||
const filteredLayers = [
|
||||
...getDataLayers(state.layers),
|
||||
...getReferenceLayers(state.layers),
|
||||
].filter(({ accessors }) => accessors.length > 0);
|
||||
|
||||
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: (
|
||||
<FormattedMessage
|
||||
key={label}
|
||||
id="xpack.lens.xyVisualization.arrayValues"
|
||||
defaultMessage="{label} contains array values. Your visualization may not render as expected."
|
||||
values={{
|
||||
label: <strong>{label}</strong>,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return accessorsWithArrayValues.map((label) => (
|
||||
<FormattedMessage
|
||||
key={label}
|
||||
id="xpack.lens.xyVisualization.arrayValues"
|
||||
defaultMessage="{label} contains array values. Your visualization may not render as expected."
|
||||
values={{
|
||||
label: <strong>{label}</strong>,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
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 (
|
||||
<DimensionTrigger
|
||||
|
|
|
@ -211,8 +211,4 @@ export const getVisualization = ({
|
|||
);
|
||||
}
|
||||
},
|
||||
|
||||
getErrorMessages(state) {
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -540,16 +540,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
dimension: 'lnsXY_yDimensionPanel > 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');
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue