mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Lens] Runtime field editor (#91882)
This commit is contained in:
parent
90e3013449
commit
52a1ce1723
22 changed files with 488 additions and 36 deletions
|
@ -330,7 +330,7 @@
|
|||
"description": [],
|
||||
"source": {
|
||||
"path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts",
|
||||
"lineNumber": 72
|
||||
"lineNumber": 73
|
||||
},
|
||||
"signature": [
|
||||
"Record<string, Pick<",
|
||||
|
@ -347,7 +347,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts",
|
||||
"lineNumber": 71
|
||||
"lineNumber": 72
|
||||
},
|
||||
"initialIsOpen": false
|
||||
},
|
||||
|
@ -483,7 +483,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "x-pack/plugins/lens/public/plugin.ts",
|
||||
"lineNumber": 88
|
||||
"lineNumber": 90
|
||||
},
|
||||
"signature": [
|
||||
"React.ComponentType<",
|
||||
|
@ -509,7 +509,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "x-pack/plugins/lens/public/plugin.ts",
|
||||
"lineNumber": 97
|
||||
"lineNumber": 99
|
||||
},
|
||||
"signature": [
|
||||
"(input: ",
|
||||
|
@ -533,7 +533,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "x-pack/plugins/lens/public/plugin.ts",
|
||||
"lineNumber": 101
|
||||
"lineNumber": 103
|
||||
},
|
||||
"signature": [
|
||||
"() => boolean"
|
||||
|
@ -542,7 +542,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "x-pack/plugins/lens/public/plugin.ts",
|
||||
"lineNumber": 79
|
||||
"lineNumber": 81
|
||||
},
|
||||
"initialIsOpen": false
|
||||
},
|
||||
|
@ -1553,7 +1553,7 @@
|
|||
"description": [],
|
||||
"source": {
|
||||
"path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts",
|
||||
"lineNumber": 75
|
||||
"lineNumber": 76
|
||||
},
|
||||
"signature": [
|
||||
"{ columns: Record<string, IndexPatternColumn>; columnOrder: string[]; incompleteColumns?: Record<string, IncompleteColumn> | undefined; }"
|
||||
|
|
|
@ -91,7 +91,10 @@ export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxEr
|
|||
const painlessSyntaxErrors = PainlessLang.getSyntaxErrors();
|
||||
// It is possible for there to be more than one editor in a view,
|
||||
// so we need to get the syntax errors based on the editor (aka model) ID
|
||||
const editorHasSyntaxErrors = editorId && painlessSyntaxErrors[editorId].length > 0;
|
||||
const editorHasSyntaxErrors =
|
||||
editorId &&
|
||||
painlessSyntaxErrors[editorId] &&
|
||||
painlessSyntaxErrors[editorId].length > 0;
|
||||
|
||||
if (editorHasSyntaxErrors) {
|
||||
return resolve({
|
||||
|
|
44
test/functional/services/field_editor.ts
Normal file
44
test/functional/services/field_editor.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export function FieldEditorProvider({ getService }: FtrProviderContext) {
|
||||
const browser = getService('browser');
|
||||
const retry = getService('retry');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
class FieldEditor {
|
||||
public async setName(name: string) {
|
||||
await testSubjects.setValue('nameField > input', name);
|
||||
}
|
||||
public async enableValue() {
|
||||
await testSubjects.setEuiSwitch('valueRow > toggle', 'check');
|
||||
}
|
||||
public async disableValue() {
|
||||
await testSubjects.setEuiSwitch('valueRow > toggle', 'uncheck');
|
||||
}
|
||||
public async typeScript(script: string) {
|
||||
const editor = await (await testSubjects.find('valueRow')).findByClassName(
|
||||
'react-monaco-editor-container'
|
||||
);
|
||||
const textarea = await editor.findByClassName('monaco-mouse-cursor-text');
|
||||
|
||||
await textarea.click();
|
||||
await browser.pressKeys(script);
|
||||
}
|
||||
public async save() {
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('fieldSaveButton');
|
||||
await testSubjects.missingOrFail('fieldSaveButton', { timeout: 2000 });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new FieldEditor();
|
||||
}
|
|
@ -31,6 +31,7 @@ import { FilterBarProvider } from './filter_bar';
|
|||
import { FlyoutProvider } from './flyout';
|
||||
import { GlobalNavProvider } from './global_nav';
|
||||
import { InspectorProvider } from './inspector';
|
||||
import { FieldEditorProvider } from './field_editor';
|
||||
import { ManagementMenuProvider } from './management';
|
||||
import { QueryBarProvider } from './query_bar';
|
||||
import { RemoteProvider } from './remote';
|
||||
|
@ -74,6 +75,7 @@ export const services = {
|
|||
browser: BrowserProvider,
|
||||
pieChart: PieChartProvider,
|
||||
inspector: InspectorProvider,
|
||||
fieldEditor: FieldEditorProvider,
|
||||
vegaDebugInspector: VegaDebugInspectorViewProvider,
|
||||
appsMenu: AppsMenuProvider,
|
||||
globalNav: GlobalNavProvider,
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
"uiActions",
|
||||
"embeddable",
|
||||
"share",
|
||||
"presentationUtil"
|
||||
"presentationUtil",
|
||||
"indexPatternFieldEditor"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"usageCollection",
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
padding: $euiSize $euiSize 0;
|
||||
}
|
||||
|
||||
.lnsInnerIndexPatternDataPanel__switcher {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lnsInnerIndexPatternDataPanel__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import React, { ChangeEvent, ReactElement } from 'react';
|
||||
import { createMockedDragDropContext } from './mocks';
|
||||
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
|
||||
import { InnerIndexPatternDataPanel, IndexPatternDataPanel, MemoizedDataPanel } from './datapanel';
|
||||
|
@ -19,6 +19,7 @@ import { ChangeIndexPattern } from './change_indexpattern';
|
|||
import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { documentField } from './document_field';
|
||||
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
|
||||
import { indexPatternFieldEditorPluginMock } from '../../../../../src/plugins/index_pattern_field_editor/public/mocks';
|
||||
import { getFieldByNameFactory } from './pure_helpers';
|
||||
|
||||
const fieldsOne = [
|
||||
|
@ -240,14 +241,16 @@ describe('IndexPattern Data Panel', () => {
|
|||
let defaultProps: Parameters<typeof InnerIndexPatternDataPanel>[0] & {
|
||||
showNoDataPopover: () => void;
|
||||
};
|
||||
let core: ReturnType<typeof coreMock['createSetup']>;
|
||||
let core: ReturnType<typeof coreMock['createStart']>;
|
||||
|
||||
beforeEach(() => {
|
||||
core = coreMock.createSetup();
|
||||
core = coreMock.createStart();
|
||||
defaultProps = {
|
||||
indexPatternRefs: [],
|
||||
existingFields: {},
|
||||
data: dataPluginMock.createStartContract(),
|
||||
indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(),
|
||||
onUpdateIndexPattern: jest.fn(),
|
||||
dragDropContext: createMockedDragDropContext(),
|
||||
currentIndexPatternId: '1',
|
||||
indexPatterns: initialState.indexPatterns,
|
||||
|
@ -806,5 +809,78 @@ describe('IndexPattern Data Panel', () => {
|
|||
'memory',
|
||||
]);
|
||||
});
|
||||
describe('edit field list', () => {
|
||||
beforeEach(() => {
|
||||
props.indexPatternFieldEditor.userPermissions.editIndexPattern = () => true;
|
||||
});
|
||||
it('should call field editor plugin on clicking add button', async () => {
|
||||
const mockIndexPattern = {};
|
||||
(props.data.indexPatterns.get as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve(mockIndexPattern)
|
||||
);
|
||||
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
|
||||
act(() => {
|
||||
(wrapper
|
||||
.find('[data-test-subj="lnsIndexPatternActions-popover"]')
|
||||
.first()
|
||||
.prop('children') as ReactElement).props.items[0].props.onClick();
|
||||
});
|
||||
|
||||
// wait for indx pattern to be loaded
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
expect(props.indexPatternFieldEditor.openEditor).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ctx: expect.objectContaining({
|
||||
indexPattern: mockIndexPattern,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should reload index pattern if callback gets called', async () => {
|
||||
const mockIndexPattern = {
|
||||
id: '1',
|
||||
fields: [
|
||||
{
|
||||
name: 'fieldOne',
|
||||
aggregatable: true,
|
||||
},
|
||||
],
|
||||
metaFields: [],
|
||||
};
|
||||
(props.data.indexPatterns.get as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve(mockIndexPattern)
|
||||
);
|
||||
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
|
||||
act(() => {
|
||||
(wrapper
|
||||
.find('[data-test-subj="lnsIndexPatternActions-popover"]')
|
||||
.first()
|
||||
.prop('children') as ReactElement).props.items[0].props.onClick();
|
||||
});
|
||||
// wait for indx pattern to be loaded
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
await (props.indexPatternFieldEditor.openEditor as jest.Mock).mock.calls[0][0].onSave();
|
||||
// wait for indx pattern to be loaded
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(props.onUpdateIndexPattern).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fields: [
|
||||
expect.objectContaining({
|
||||
name: 'fieldOne',
|
||||
}),
|
||||
expect.anything(),
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not render add button without permissions', () => {
|
||||
props.indexPatternFieldEditor.userPermissions.editIndexPattern = () => false;
|
||||
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
|
||||
expect(wrapper.find('[data-test-subj="indexPattern-add-field"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import './datapanel.scss';
|
||||
import { uniq, groupBy } from 'lodash';
|
||||
import React, { useState, memo, useCallback, useMemo } from 'react';
|
||||
import React, { useState, memo, useCallback, useMemo, useRef, useEffect } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -20,9 +20,11 @@ import {
|
|||
EuiFilterGroup,
|
||||
EuiFilterButton,
|
||||
EuiScreenReaderOnly,
|
||||
EuiButtonIcon,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { DataPublicPluginStart, EsQueryConfig, Query, Filter } from 'src/plugins/data/public';
|
||||
import { htmlIdGenerator } from '@elastic/eui';
|
||||
import { DatasourceDataPanelProps, DataType, StateSetter } from '../types';
|
||||
|
@ -34,12 +36,13 @@ import {
|
|||
IndexPatternRef,
|
||||
} from './types';
|
||||
import { trackUiEvent } from '../lens_ui_telemetry';
|
||||
import { syncExistingFields } from './loader';
|
||||
import { loadIndexPatterns, syncExistingFields } from './loader';
|
||||
import { fieldExists } from './pure_helpers';
|
||||
import { Loader } from '../loader';
|
||||
import { esQuery, IIndexPattern } from '../../../../../src/plugins/data/public';
|
||||
import { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public';
|
||||
|
||||
export type Props = DatasourceDataPanelProps<IndexPatternPrivateState> & {
|
||||
export type Props = Omit<DatasourceDataPanelProps<IndexPatternPrivateState>, 'core'> & {
|
||||
data: DataPublicPluginStart;
|
||||
changeIndexPattern: (
|
||||
id: string,
|
||||
|
@ -47,6 +50,8 @@ export type Props = DatasourceDataPanelProps<IndexPatternPrivateState> & {
|
|||
setState: StateSetter<IndexPatternPrivateState>
|
||||
) => void;
|
||||
charts: ChartsPluginSetup;
|
||||
core: CoreStart;
|
||||
indexPatternFieldEditor: IndexPatternFieldEditorStart;
|
||||
};
|
||||
import { LensFieldIcon } from './lens_field_icon';
|
||||
import { ChangeIndexPattern } from './change_indexpattern';
|
||||
|
@ -112,6 +117,7 @@ export function IndexPatternDataPanel({
|
|||
dateRange,
|
||||
changeIndexPattern,
|
||||
charts,
|
||||
indexPatternFieldEditor,
|
||||
showNoDataPopover,
|
||||
dropOntoWorkspace,
|
||||
hasSuggestionForField,
|
||||
|
@ -122,6 +128,19 @@ export function IndexPatternDataPanel({
|
|||
[state, setState, changeIndexPattern]
|
||||
);
|
||||
|
||||
const onUpdateIndexPattern = useCallback(
|
||||
(indexPattern: IndexPattern) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
indexPatterns: {
|
||||
...prevState.indexPatterns,
|
||||
[indexPattern.id]: indexPattern,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setState]
|
||||
);
|
||||
|
||||
const indexPatternList = uniq(
|
||||
Object.values(state.layers)
|
||||
.map((l) => l.indexPatternId)
|
||||
|
@ -165,6 +184,7 @@ export function IndexPatternDataPanel({
|
|||
dateRange.fromDate,
|
||||
dateRange.toDate,
|
||||
indexPatternList.map((x) => `${x.title}:${x.timeFieldName}`).join(','),
|
||||
state.indexPatterns,
|
||||
]}
|
||||
/>
|
||||
|
||||
|
@ -205,7 +225,9 @@ export function IndexPatternDataPanel({
|
|||
core={core}
|
||||
data={data}
|
||||
charts={charts}
|
||||
indexPatternFieldEditor={indexPatternFieldEditor}
|
||||
onChangeIndexPattern={onChangeIndexPattern}
|
||||
onUpdateIndexPattern={onUpdateIndexPattern}
|
||||
existingFields={state.existingFields}
|
||||
existenceFetchFailed={state.existenceFetchFailed}
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
|
@ -254,21 +276,26 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
filters,
|
||||
dragDropContext,
|
||||
onChangeIndexPattern,
|
||||
onUpdateIndexPattern,
|
||||
core,
|
||||
data,
|
||||
indexPatternFieldEditor,
|
||||
existingFields,
|
||||
charts,
|
||||
dropOntoWorkspace,
|
||||
hasSuggestionForField,
|
||||
}: Omit<DatasourceDataPanelProps, 'state' | 'setState' | 'showNoDataPopover'> & {
|
||||
}: Omit<DatasourceDataPanelProps, 'state' | 'setState' | 'showNoDataPopover' | 'core'> & {
|
||||
data: DataPublicPluginStart;
|
||||
core: CoreStart;
|
||||
currentIndexPatternId: string;
|
||||
indexPatternRefs: IndexPatternRef[];
|
||||
indexPatterns: Record<string, IndexPattern>;
|
||||
dragDropContext: DragContextState;
|
||||
onChangeIndexPattern: (newId: string) => void;
|
||||
onUpdateIndexPattern: (indexPattern: IndexPattern) => void;
|
||||
existingFields: IndexPatternPrivateState['existingFields'];
|
||||
charts: ChartsPluginSetup;
|
||||
indexPatternFieldEditor: IndexPatternFieldEditorStart;
|
||||
existenceFetchFailed?: boolean;
|
||||
}) {
|
||||
const [localState, setLocalState] = useState<DataPanelState>({
|
||||
|
@ -289,6 +316,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
|
||||
const fieldInfoUnavailable = existenceFetchFailed || currentIndexPattern.hasRestrictions;
|
||||
|
||||
const editPermission = indexPatternFieldEditor.userPermissions.editIndexPattern();
|
||||
|
||||
const unfilteredFieldGroups: FieldGroups = useMemo(() => {
|
||||
const containsData = (field: IndexPatternField) => {
|
||||
const overallField = currentIndexPattern.getFieldByName(field.name);
|
||||
|
@ -456,6 +485,48 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
[nameFilter, typeFilter]
|
||||
);
|
||||
|
||||
const closeFieldEditor = useRef<() => void | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Make sure to close the editor when unmounting
|
||||
if (closeFieldEditor.current) {
|
||||
closeFieldEditor.current();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const editField = useMemo(
|
||||
() =>
|
||||
editPermission
|
||||
? async (fieldName?: string, uiAction: 'edit' | 'add' = 'edit') => {
|
||||
trackUiEvent(`open_field_editor_${uiAction}`);
|
||||
const indexPatternInstance = await data.indexPatterns.get(currentIndexPattern.id);
|
||||
closeFieldEditor.current = indexPatternFieldEditor.openEditor({
|
||||
ctx: {
|
||||
indexPattern: indexPatternInstance,
|
||||
},
|
||||
fieldName,
|
||||
onSave: async () => {
|
||||
trackUiEvent(`save_field_${uiAction}`);
|
||||
const newlyMappedIndexPattern = await loadIndexPatterns({
|
||||
indexPatternsService: data.indexPatterns,
|
||||
cache: {},
|
||||
patterns: [currentIndexPattern.id],
|
||||
});
|
||||
onUpdateIndexPattern(newlyMappedIndexPattern[currentIndexPattern.id]);
|
||||
},
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
[data, indexPatternFieldEditor, currentIndexPattern, editPermission, onUpdateIndexPattern]
|
||||
);
|
||||
|
||||
const addField = useMemo(
|
||||
() => (editPermission && editField ? () => editField(undefined, 'add') : undefined),
|
||||
[editField, editPermission]
|
||||
);
|
||||
|
||||
const fieldProps = useMemo(
|
||||
() => ({
|
||||
core,
|
||||
|
@ -479,6 +550,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
]
|
||||
);
|
||||
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ChildDragDropProvider {...dragDropContext}>
|
||||
<EuiFlexGroup
|
||||
|
@ -488,23 +561,88 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div className="lnsInnerIndexPatternDataPanel__header">
|
||||
<ChangeIndexPattern
|
||||
data-test-subj="indexPattern-switcher"
|
||||
trigger={{
|
||||
label: currentIndexPattern.title,
|
||||
title: currentIndexPattern.title,
|
||||
'data-test-subj': 'indexPattern-switch-link',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
indexPatternId={currentIndexPatternId}
|
||||
indexPatternRefs={indexPatternRefs}
|
||||
onChangeIndexPattern={(newId: string) => {
|
||||
onChangeIndexPattern(newId);
|
||||
clearLocalState();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
className="lnsInnerIndexPatternDataPanel__header"
|
||||
>
|
||||
<EuiFlexItem grow={true} className="lnsInnerIndexPatternDataPanel__switcher">
|
||||
<ChangeIndexPattern
|
||||
data-test-subj="indexPattern-switcher"
|
||||
trigger={{
|
||||
label: currentIndexPattern.title,
|
||||
title: currentIndexPattern.title,
|
||||
'data-test-subj': 'indexPattern-switch-link',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
indexPatternId={currentIndexPatternId}
|
||||
indexPatternRefs={indexPatternRefs}
|
||||
onChangeIndexPattern={(newId: string) => {
|
||||
onChangeIndexPattern(newId);
|
||||
clearLocalState();
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{addField && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
panelPaddingSize="s"
|
||||
isOpen={popoverOpen}
|
||||
closePopover={() => {
|
||||
setPopoverOpen(false);
|
||||
}}
|
||||
ownFocus
|
||||
data-test-subj="lnsIndexPatternActions-popover"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
iconType="boxesHorizontal"
|
||||
data-test-subj="lnsIndexPatternActions"
|
||||
aria-label={i18n.translate('xpack.lens.indexPatterns.actionsPopoverLabel', {
|
||||
defaultMessage: 'Index pattern settings',
|
||||
})}
|
||||
onClick={() => {
|
||||
setPopoverOpen(!popoverOpen);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
size="s"
|
||||
items={[
|
||||
<EuiContextMenuItem
|
||||
key="add"
|
||||
icon="indexOpen"
|
||||
data-test-subj="indexPattern-add-field"
|
||||
onClick={() => {
|
||||
setPopoverOpen(false);
|
||||
addField();
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.lens.indexPatterns.addFieldButton', {
|
||||
defaultMessage: 'Add field to index pattern',
|
||||
})}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key="manage"
|
||||
icon="indexSettings"
|
||||
onClick={() => {
|
||||
setPopoverOpen(false);
|
||||
core.application.navigateToApp('management', {
|
||||
path: `/kibana/indexPatterns/patterns/${currentIndexPattern.id}`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.lens.indexPatterns.manageFieldButton', {
|
||||
defaultMessage: 'Manage index pattern fields',
|
||||
})}
|
||||
</EuiContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormControlLayout
|
||||
|
@ -626,6 +764,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
existFieldsInIndex={!!allFields.length}
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
hasSuggestionForField={hasSuggestionForField}
|
||||
editField={editField}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { MouseEvent, ReactElement } from 'react';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { EuiLoadingSpinner, EuiPopover } from '@elastic/eui';
|
||||
|
@ -125,6 +125,26 @@ describe('IndexPattern Field Item', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should render edit field button if callback is set', () => {
|
||||
core.http.post.mockImplementation(() => {
|
||||
return new Promise(() => {});
|
||||
});
|
||||
const editFieldSpy = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<InnerFieldItem {...defaultProps} editField={editFieldSpy} hideDetails />
|
||||
);
|
||||
clickField(wrapper, 'bytes');
|
||||
wrapper.update();
|
||||
const popoverContent = wrapper.find(EuiPopover).prop('children');
|
||||
act(() => {
|
||||
mountWithIntl(popoverContent as ReactElement)
|
||||
.find('[data-test-subj="lnsFieldListPanelEdit"]')
|
||||
.first()
|
||||
.prop('onClick')!({} as MouseEvent);
|
||||
});
|
||||
expect(editFieldSpy).toHaveBeenCalledWith('bytes');
|
||||
});
|
||||
|
||||
it('should request field stats every time the button is clicked', async () => {
|
||||
let resolveFunction: (arg: unknown) => void;
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ export interface FieldItemProps {
|
|||
itemIndex: number;
|
||||
groupIndex: number;
|
||||
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
|
||||
editField?: (name: string) => void;
|
||||
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
|
||||
}
|
||||
|
||||
|
@ -105,10 +106,22 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
itemIndex,
|
||||
groupIndex,
|
||||
dropOntoWorkspace,
|
||||
editField,
|
||||
} = props;
|
||||
|
||||
const [infoIsOpen, setOpen] = useState(false);
|
||||
|
||||
const closeAndEdit = useMemo(
|
||||
() =>
|
||||
editField
|
||||
? (name: string) => {
|
||||
editField(name);
|
||||
setOpen(false);
|
||||
}
|
||||
: undefined,
|
||||
[editField, setOpen]
|
||||
);
|
||||
|
||||
const dropOntoWorkspaceAndClose = useCallback(
|
||||
(droppedField: DragDropIdentifier) => {
|
||||
dropOntoWorkspace(droppedField);
|
||||
|
@ -256,6 +269,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
<FieldItemPopoverContents
|
||||
{...state}
|
||||
{...props}
|
||||
editField={closeAndEdit}
|
||||
dropOntoWorkspace={dropOntoWorkspaceAndClose}
|
||||
/>
|
||||
</EuiPopover>
|
||||
|
@ -270,11 +284,13 @@ function FieldPanelHeader({
|
|||
field,
|
||||
hasSuggestionForField,
|
||||
dropOntoWorkspace,
|
||||
editField,
|
||||
}: {
|
||||
field: IndexPatternField;
|
||||
indexPatternId: string;
|
||||
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
|
||||
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
|
||||
editField?: (name: string) => void;
|
||||
}) {
|
||||
const draggableField = {
|
||||
indexPatternId,
|
||||
|
@ -298,6 +314,22 @@ function FieldPanelHeader({
|
|||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
field={draggableField}
|
||||
/>
|
||||
{editField && (
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.lens.indexPattern.editFieldLabel', {
|
||||
defaultMessage: 'Edit index pattern field',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
onClick={() => editField(field.name)}
|
||||
iconType="pencil"
|
||||
data-test-subj="lnsFieldListPanelEdit"
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.editFieldLabel', {
|
||||
defaultMessage: 'Edit index pattern field',
|
||||
})}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
@ -314,6 +346,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
|
|||
chartsThemeService,
|
||||
data: { fieldFormats },
|
||||
dropOntoWorkspace,
|
||||
editField,
|
||||
hasSuggestionForField,
|
||||
hideDetails,
|
||||
} = props;
|
||||
|
@ -345,6 +378,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
|
|||
field={field}
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
hasSuggestionForField={hasSuggestionForField}
|
||||
editField={editField}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ export const FieldList = React.memo(function FieldList({
|
|||
existFieldsInIndex,
|
||||
dropOntoWorkspace,
|
||||
hasSuggestionForField,
|
||||
editField,
|
||||
}: {
|
||||
exists: (field: IndexPatternField) => boolean;
|
||||
fieldGroups: FieldGroups;
|
||||
|
@ -66,6 +67,7 @@ export const FieldList = React.memo(function FieldList({
|
|||
existFieldsInIndex: boolean;
|
||||
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
|
||||
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
|
||||
editField?: (name: string) => void;
|
||||
}) {
|
||||
const [pageSize, setPageSize] = useState(PAGINATION_SIZE);
|
||||
const [scrollContainer, setScrollContainer] = useState<Element | undefined>(undefined);
|
||||
|
@ -141,6 +143,7 @@ export const FieldList = React.memo(function FieldList({
|
|||
{...fieldProps}
|
||||
exists={exists(field)}
|
||||
field={field}
|
||||
editField={editField}
|
||||
hideDetails={true}
|
||||
key={field.name}
|
||||
itemIndex={index}
|
||||
|
@ -165,6 +168,7 @@ export const FieldList = React.memo(function FieldList({
|
|||
label={fieldGroup.title}
|
||||
helpTooltip={fieldGroup.helpText}
|
||||
exists={exists}
|
||||
editField={editField}
|
||||
hideDetails={fieldGroup.hideDetails}
|
||||
hasLoaded={!!hasSyncedExistingFields}
|
||||
fieldsCount={fieldGroup.fields.length}
|
||||
|
|
|
@ -54,6 +54,7 @@ export interface FieldsAccordionProps {
|
|||
groupIndex: number;
|
||||
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
|
||||
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
|
||||
editField?: (name: string) => void;
|
||||
}
|
||||
|
||||
export const FieldsAccordion = memo(function InnerFieldsAccordion({
|
||||
|
@ -74,6 +75,7 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({
|
|||
groupIndex,
|
||||
dropOntoWorkspace,
|
||||
hasSuggestionForField,
|
||||
editField,
|
||||
}: FieldsAccordionProps) {
|
||||
const renderField = useCallback(
|
||||
(field: IndexPatternField, index) => (
|
||||
|
@ -87,9 +89,18 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({
|
|||
groupIndex={groupIndex}
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
hasSuggestionForField={hasSuggestionForField}
|
||||
editField={editField}
|
||||
/>
|
||||
),
|
||||
[fieldProps, exists, hideDetails, dropOntoWorkspace, hasSuggestionForField, groupIndex]
|
||||
[
|
||||
fieldProps,
|
||||
exists,
|
||||
hideDetails,
|
||||
dropOntoWorkspace,
|
||||
hasSuggestionForField,
|
||||
groupIndex,
|
||||
editField,
|
||||
]
|
||||
);
|
||||
|
||||
const renderButton = useMemo(() => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { CoreSetup } from 'kibana/public';
|
|||
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public';
|
||||
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
|
||||
import { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public';
|
||||
import {
|
||||
DataPublicPluginSetup,
|
||||
DataPublicPluginStart,
|
||||
|
@ -24,6 +25,7 @@ export interface IndexPatternDatasourceSetupPlugins {
|
|||
|
||||
export interface IndexPatternDatasourceStartPlugins {
|
||||
data: DataPublicPluginStart;
|
||||
indexPatternFieldEditor: IndexPatternFieldEditorStart;
|
||||
}
|
||||
|
||||
export class IndexPatternDatasource {
|
||||
|
@ -42,7 +44,7 @@ export class IndexPatternDatasource {
|
|||
getTimeScaleFunction,
|
||||
getSuffixFormatter,
|
||||
} = await import('../async_services');
|
||||
return core.getStartServices().then(([coreStart, { data }]) => {
|
||||
return core.getStartServices().then(([coreStart, { data, indexPatternFieldEditor }]) => {
|
||||
data.fieldFormats.register([getSuffixFormatter(data.fieldFormats.deserialize)]);
|
||||
expressions.registerFunction(getTimeScaleFunction(data));
|
||||
expressions.registerFunction(counterRate);
|
||||
|
@ -53,6 +55,7 @@ export class IndexPatternDatasource {
|
|||
storage: new Storage(localStorage),
|
||||
data,
|
||||
charts,
|
||||
indexPatternFieldEditor,
|
||||
});
|
||||
}) as Promise<Datasource>;
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'
|
|||
import { getFieldByNameFactory } from './pure_helpers';
|
||||
import { operationDefinitionMap, getErrorMessages } from './operations';
|
||||
import { createMockedReferenceOperation } from './operations/mocks';
|
||||
import { indexPatternFieldEditorPluginMock } from 'src/plugins/index_pattern_field_editor/public/mocks';
|
||||
|
||||
jest.mock('./loader');
|
||||
jest.mock('../id_generator');
|
||||
|
@ -170,6 +171,7 @@ describe('IndexPattern Data Source', () => {
|
|||
core: coreMock.createStart(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
charts: chartPluginMock.createSetupContract(),
|
||||
indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(),
|
||||
});
|
||||
|
||||
baseState = {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { I18nProvider } from '@kbn/i18n/react';
|
|||
import { CoreStart, SavedObjectReference } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public';
|
||||
import {
|
||||
DatasourceDimensionEditorProps,
|
||||
DatasourceDimensionTriggerProps,
|
||||
|
@ -76,11 +77,13 @@ export function getIndexPatternDatasource({
|
|||
storage,
|
||||
data,
|
||||
charts,
|
||||
indexPatternFieldEditor,
|
||||
}: {
|
||||
core: CoreStart;
|
||||
storage: IStorageWrapper;
|
||||
data: DataPublicPluginStart;
|
||||
charts: ChartsPluginSetup;
|
||||
indexPatternFieldEditor: IndexPatternFieldEditorStart;
|
||||
}) {
|
||||
const uiSettings = core.uiSettings;
|
||||
const onIndexPatternLoadError = (err: Error) =>
|
||||
|
@ -191,7 +194,9 @@ export function getIndexPatternDatasource({
|
|||
changeIndexPattern={handleChangeIndexPattern}
|
||||
data={data}
|
||||
charts={charts}
|
||||
indexPatternFieldEditor={indexPatternFieldEditor}
|
||||
{...props}
|
||||
core={core}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
domElement
|
||||
|
|
|
@ -68,6 +68,7 @@ export async function loadIndexPatterns({
|
|||
meta: indexPattern.metaFields.includes(field.name),
|
||||
esTypes: field.esTypes,
|
||||
scripted: field.scripted,
|
||||
runtime: Boolean(field.runtimeField),
|
||||
};
|
||||
|
||||
// Simplifies tests by hiding optional properties instead of undefined
|
||||
|
|
|
@ -57,6 +57,7 @@ export type IndexPatternField = IFieldType & {
|
|||
displayName: string;
|
||||
aggregationRestrictions?: Partial<IndexPatternAggRestrictions>;
|
||||
meta?: boolean;
|
||||
runtime?: boolean;
|
||||
};
|
||||
|
||||
export interface IndexPatternLayer {
|
||||
|
|
|
@ -21,6 +21,7 @@ import { ChartsPluginSetup, ChartsPluginStart } from '../../../../src/plugins/ch
|
|||
import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public';
|
||||
import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public';
|
||||
import { EditorFrameService } from './editor_frame_service';
|
||||
import { IndexPatternFieldEditorStart } from '../../../../src/plugins/index_pattern_field_editor/public';
|
||||
import {
|
||||
IndexPatternDatasource,
|
||||
IndexPatternDatasourceSetupPlugins,
|
||||
|
@ -74,6 +75,7 @@ export interface LensPluginStartDependencies {
|
|||
charts: ChartsPluginStart;
|
||||
savedObjectsTagging?: SavedObjectTaggingPluginStart;
|
||||
presentationUtil: PresentationUtilPluginStart;
|
||||
indexPatternFieldEditor: IndexPatternFieldEditorStart;
|
||||
}
|
||||
|
||||
export interface LensPublicStart {
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
{ "path": "../global_search/tsconfig.json"},
|
||||
{ "path": "../saved_objects_tagging/tsconfig.json"},
|
||||
{ "path": "../../../src/plugins/data/tsconfig.json"},
|
||||
{ "path": "../../../src/plugins/index_pattern_field_editor/tsconfig.json"},
|
||||
{ "path": "../../../src/plugins/charts/tsconfig.json"},
|
||||
{ "path": "../../../src/plugins/expressions/tsconfig.json"},
|
||||
{ "path": "../../../src/plugins/navigation/tsconfig.json" },
|
||||
|
|
|
@ -31,6 +31,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./smokescreen'));
|
||||
loadTestFile(require.resolve('./add_to_dashboard'));
|
||||
loadTestFile(require.resolve('./table'));
|
||||
loadTestFile(require.resolve('./runtime_fields'));
|
||||
loadTestFile(require.resolve('./dashboard'));
|
||||
loadTestFile(require.resolve('./persistent_context'));
|
||||
loadTestFile(require.resolve('./colors'));
|
||||
|
|
66
x-pack/test/functional/apps/lens/runtime_fields.ts
Normal file
66
x-pack/test/functional/apps/lens/runtime_fields.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']);
|
||||
const filterBar = getService('filterBar');
|
||||
const fieldEditor = getService('fieldEditor');
|
||||
const retry = getService('retry');
|
||||
|
||||
describe('lens runtime fields', () => {
|
||||
it('should be able to add runtime field and use it', async () => {
|
||||
await PageObjects.visualize.navigateToNewVisualization();
|
||||
await PageObjects.visualize.clickVisType('lens');
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
await PageObjects.lens.switchToVisualization('lnsDatatable');
|
||||
await PageObjects.lens.clickAddField();
|
||||
await fieldEditor.setName('runtimefield');
|
||||
await fieldEditor.enableValue();
|
||||
await fieldEditor.typeScript("emit('abc')");
|
||||
await fieldEditor.save();
|
||||
await PageObjects.lens.searchField('runtime');
|
||||
await PageObjects.lens.waitForField('runtimefield');
|
||||
await PageObjects.lens.dragFieldToWorkspace('runtimefield');
|
||||
await PageObjects.lens.waitForVisualization();
|
||||
expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal(
|
||||
'Top values of runtimefield'
|
||||
);
|
||||
expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('abc');
|
||||
});
|
||||
|
||||
it('should able to filter runtime fields', async () => {
|
||||
await retry.try(async () => {
|
||||
await PageObjects.lens.clickTableCellAction(0, 0, 'lensDatatableFilterOut');
|
||||
await PageObjects.lens.waitForVisualization();
|
||||
expect(await PageObjects.lens.isShowingNoResults()).to.equal(true);
|
||||
});
|
||||
await filterBar.removeAllFilters();
|
||||
await PageObjects.lens.waitForVisualization();
|
||||
});
|
||||
|
||||
it('should able to edit field', async () => {
|
||||
await PageObjects.lens.clickField('runtimefield');
|
||||
await PageObjects.lens.editField();
|
||||
await fieldEditor.setName('runtimefield2');
|
||||
await fieldEditor.save();
|
||||
await PageObjects.lens.searchField('runtime');
|
||||
await PageObjects.lens.waitForField('runtimefield2');
|
||||
await PageObjects.lens.dragFieldToDimensionTrigger(
|
||||
'runtimefield2',
|
||||
'lnsDatatable_rows > lns-dimensionTrigger'
|
||||
);
|
||||
await PageObjects.lens.waitForVisualization();
|
||||
expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal(
|
||||
'Top values of runtimefield2'
|
||||
);
|
||||
expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('abc');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -172,6 +172,32 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
||||
/**
|
||||
* Drags field to workspace
|
||||
*
|
||||
* @param field - the desired field for the dimension
|
||||
* */
|
||||
async clickField(field: string) {
|
||||
await testSubjects.click(`lnsFieldListPanelField-${field}`);
|
||||
},
|
||||
|
||||
async editField() {
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('lnsFieldListPanelEdit');
|
||||
await testSubjects.missingOrFail('lnsFieldListPanelEdit');
|
||||
});
|
||||
},
|
||||
|
||||
async searchField(name: string) {
|
||||
await testSubjects.setValue('lnsIndexPatternFieldSearch', name);
|
||||
},
|
||||
|
||||
async waitForField(field: string) {
|
||||
await retry.try(async () => {
|
||||
await testSubjects.existOrFail(`lnsFieldListPanelField-${field}`);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Copies field to chosen destination that is defined by distance of `steps`
|
||||
* (right arrow presses) from it
|
||||
|
@ -772,5 +798,11 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
return firstCount === secondCount;
|
||||
});
|
||||
},
|
||||
|
||||
async clickAddField() {
|
||||
await testSubjects.click('lnsIndexPatternActions');
|
||||
await testSubjects.existOrFail('indexPattern-add-field');
|
||||
await testSubjects.click('indexPattern-add-field');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue