[Lens] Runtime field editor (#91882) (#94892)

Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
This commit is contained in:
Kibana Machine 2021-03-18 05:36:16 -04:00 committed by GitHub
parent b388d36971
commit 184b01ad97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 488 additions and 36 deletions

View file

@ -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; }"

View file

@ -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({

View 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();
}

View file

@ -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,

View file

@ -15,7 +15,8 @@
"uiActions",
"embeddable",
"share",
"presentationUtil"
"presentationUtil",
"indexPatternFieldEditor"
],
"optionalPlugins": [
"usageCollection",

View file

@ -4,6 +4,10 @@
padding: $euiSize $euiSize 0;
}
.lnsInnerIndexPatternDataPanel__switcher {
min-width: 0;
}
.lnsInnerIndexPatternDataPanel__header {
display: flex;
align-items: center;

View file

@ -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);
});
});
});
});

View file

@ -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>

View file

@ -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;

View file

@ -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}
/>
);

View file

@ -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}

View file

@ -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(() => {

View file

@ -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>;
});

View file

@ -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 = {

View file

@ -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

View file

@ -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

View file

@ -57,6 +57,7 @@ export type IndexPatternField = IFieldType & {
displayName: string;
aggregationRestrictions?: Partial<IndexPatternAggRestrictions>;
meta?: boolean;
runtime?: boolean;
};
export interface IndexPatternLayer {

View file

@ -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 {

View file

@ -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" },

View file

@ -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'));

View 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');
});
});
}

View file

@ -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');
},
});
}