[Discover] Runtime Fields Editor integration: Add + Delete operation (#96762) (#97231)

* [Discover] Updating a functional test

* [Discover] Support for edit operation

* Fix unit tests

* Fix typescript

* Fixing failing functional test

* Fixing wrongly commented line

* Uncomment accidentally commented line

* Reintroducing accidnetally removed unit test

* Trigger data refetch onSave

* Remove refreshAppState variable

* Bundling observers together

* Clean state before refetch

* Update formatting in data grid

* [Discover] Runtime fields editor : add operation

* [Discover] Updating a functional test

* Adding a functional test

* Fixing package.json

* Reset fieldCount after data fetch

* [Discover] Updating a functional test

* Don't allow editing of unmapped fields

* Add functionality

* Fix issues with mobile display

* Allow editing if it's a runtime field

* Add a functional test

* [Discover] Updating a functional test

* Add functional test

* Remove unnecessary debugger statement

* Add more tests

* Add delete functionality

* Include runtimeFields in doc search

* Add another functional test

* [Discover] Updating a functional test

* Fix failing i18n check

* Fix package.json

* Addressing PR comments

* Addressing design input

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Maja Grubic 2021-04-15 14:20:08 +01:00 committed by GitHub
parent 7c259ef914
commit f187515c1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 310 additions and 16 deletions

View file

@ -51,6 +51,7 @@ describe('Test of <Doc /> helper / hook', () => {
],
},
},
"runtime_mappings": Object {},
"script_fields": Array [],
"stored_fields": Array [],
}
@ -79,6 +80,55 @@ describe('Test of <Doc /> helper / hook', () => {
],
},
},
"runtime_mappings": Object {},
"script_fields": Array [],
"stored_fields": Array [],
}
`);
});
test('buildSearchBody with runtime fields', () => {
const indexPattern = {
getComputedFields: () => ({
storedFields: [],
scriptFields: [],
docvalueFields: [],
runtimeFields: {
myRuntimeField: {
type: 'double',
script: {
source: 'emit(10.0)',
},
},
},
}),
} as any;
const actual = buildSearchBody('1', indexPattern, true);
expect(actual).toMatchInlineSnapshot(`
Object {
"_source": false,
"docvalue_fields": Array [],
"fields": Array [
Object {
"field": "*",
"include_unmapped": "true",
},
],
"query": Object {
"ids": Object {
"values": Array [
"1",
],
},
},
"runtime_mappings": Object {
"myRuntimeField": Object {
"script": Object {
"source": "emit(10.0)",
},
"type": "double",
},
},
"script_fields": Array [],
"stored_fields": Array [],
}

View file

@ -30,7 +30,7 @@ export function buildSearchBody(
useNewFieldsApi: boolean
): Record<string, any> {
const computedFields = indexPattern.getComputedFields();
const runtimeFields = indexPattern.getComputedFields().runtimeFields;
return {
query: {
ids: {
@ -42,6 +42,7 @@ export function buildSearchBody(
fields: useNewFieldsApi ? [{ field: '*', include_unmapped: 'true' }] : undefined,
script_fields: computedFields.scriptFields,
docvalue_fields: computedFields.docvalueFields,
runtime_mappings: useNewFieldsApi && runtimeFields ? runtimeFields : {}, // needed for index pattern runtime fields in a single doc view
};
}

View file

@ -72,7 +72,17 @@ export interface DiscoverFieldProps {
multiFields?: Array<{ field: IndexPatternField; isSelected: boolean }>;
/**
* Callback to edit a runtime field from index pattern
* @param fieldName name of the field to edit
*/
onEditField?: (fieldName: string) => void;
/**
* Callback to delete a runtime field from index pattern
* @param fieldName name of the field to delete
*/
onDeleteField?: (fieldName: string) => void;
}
export function DiscoverField({
@ -87,6 +97,7 @@ export function DiscoverField({
trackUiMetric,
multiFields,
onEditField,
onDeleteField,
}: DiscoverFieldProps) {
const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', {
defaultMessage: 'Add {field} to table',
@ -255,6 +266,7 @@ export function DiscoverField({
};
const fieldInfoIcon = getFieldInfoIcon();
const shouldRenderMultiFields = !!multiFields;
const renderMultiFields = () => {
if (!multiFields) {
@ -289,13 +301,15 @@ export function DiscoverField({
const isRuntimeField = Boolean(indexPattern.getFieldByName(field.name)?.runtimeField);
const isUnknownField = field.type === 'unknown' || field.type === 'unknown_selected';
const canEditField = onEditField && (!isUnknownField || isRuntimeField);
const displayNameGrow = canEditField ? 9 : 10;
const canDeleteField = onDeleteField && isRuntimeField;
const popoverTitle = (
<EuiPopoverTitle style={{ textTransform: 'none' }} className="eui-textBreakWord">
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={displayNameGrow}>{field.displayName}</EuiFlexItem>
<EuiFlexGroup responsive={false} gutterSize="s">
<EuiFlexItem grow={true}>
<h5>{field.displayName}</h5>
</EuiFlexItem>
{canEditField && (
<EuiFlexItem grow={1} data-test-subj="discoverFieldListPanelEditItem">
<EuiFlexItem grow={false} data-test-subj="discoverFieldListPanelEditItem">
<EuiButtonIcon
onClick={() => {
if (onEditField) {
@ -311,6 +325,29 @@ export function DiscoverField({
/>
</EuiFlexItem>
)}
{canDeleteField && (
<EuiFlexItem grow={false} data-test-subj="discoverFieldListPanelDeleteItem">
<EuiToolTip
content={i18n.translate('discover.fieldChooser.discoverField.deleteFieldLabel', {
defaultMessage: 'Delete index pattern field',
})}
>
<EuiButtonIcon
onClick={() => {
if (onDeleteField) {
onDeleteField(field.name);
}
}}
iconType="trash"
data-test-subj={`discoverFieldListPanelDelete-${field.name}`}
color="danger"
aria-label={i18n.translate('discover.fieldChooser.discoverField.deleteFieldLabel', {
defaultMessage: 'Delete index pattern field',
})}
/>
</EuiToolTip>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiPopoverTitle>
);

View file

@ -69,3 +69,7 @@
opacity: 1; /* 2 */
}
}
.dscSidebar__indexPatternSwitcher {
min-width: 0;
}

View file

@ -19,6 +19,10 @@ import {
EuiSpacer,
EuiNotificationBadge,
EuiPageSideBar,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiPopover,
EuiButtonIcon,
useResizeObserver,
} from '@elastic/eui';
@ -51,7 +55,7 @@ export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps {
setFieldFilter: (next: FieldFilterState) => void;
/**
* Callback to close the flyout sidebar rendered in a flyout, close flyout
* Callback to close the flyout if sidebar is rendered in a flyout
*/
closeFlyout?: () => void;
@ -88,7 +92,8 @@ export function DiscoverSidebar({
closeFlyout,
}: DiscoverSidebarProps) {
const [fields, setFields] = useState<IndexPatternField[] | null>(null);
const { indexPatternFieldEditor } = services;
const [isAddIndexPatternFieldPopoverOpen, setIsAddIndexPatternFieldPopoverOpen] = useState(false);
const { indexPatternFieldEditor, core } = services;
const indexPatternFieldEditPermission = indexPatternFieldEditor?.userPermissions.editIndexPattern();
const canEditIndexPatternField = !!indexPatternFieldEditPermission && useNewFieldsApi;
const [scrollContainer, setScrollContainer] = useState<Element | null>(null);
@ -224,6 +229,37 @@ export function DiscoverSidebar({
return map;
}, [fields, useNewFieldsApi, selectedFields]);
const deleteField = useMemo(
() =>
canEditIndexPatternField && selectedIndexPattern
? async (fieldName: string) => {
const ref = indexPatternFieldEditor.openDeleteModal({
ctx: {
indexPattern: selectedIndexPattern,
},
fieldName,
onDelete: async () => {
onEditRuntimeField();
},
});
if (setFieldEditorRef) {
setFieldEditorRef(ref);
}
if (closeFlyout) {
closeFlyout();
}
}
: undefined,
[
selectedIndexPattern,
canEditIndexPatternField,
setFieldEditorRef,
closeFlyout,
onEditRuntimeField,
indexPatternFieldEditor,
]
);
const getPaginated = useCallback(
(list) => {
return list.slice(0, fieldsToRender);
@ -237,7 +273,7 @@ export function DiscoverSidebar({
return null;
}
const editField = (fieldName: string) => {
const editField = (fieldName?: string) => {
if (!canEditIndexPatternField) {
return;
}
@ -258,6 +294,10 @@ export function DiscoverSidebar({
}
};
const addField = () => {
editField(undefined);
};
if (useFlyout) {
return (
<section
@ -277,6 +317,64 @@ export function DiscoverSidebar({
);
}
const indexPatternActions = (
<EuiPopover
panelPaddingSize="s"
isOpen={isAddIndexPatternFieldPopoverOpen}
closePopover={() => {
setIsAddIndexPatternFieldPopoverOpen(false);
}}
ownFocus
data-test-subj="discover-addRuntimeField-popover"
button={
<EuiButtonIcon
color="text"
iconType="boxesHorizontal"
data-test-subj="discoverIndexPatternActions"
aria-label={i18n.translate('discover.fieldChooser.indexPatterns.actionsPopoverLabel', {
defaultMessage: 'Index pattern settings',
})}
onClick={() => {
setIsAddIndexPatternFieldPopoverOpen(!isAddIndexPatternFieldPopoverOpen);
}}
/>
}
>
<EuiContextMenuPanel
size="s"
items={[
<EuiContextMenuItem
key="add"
icon="indexOpen"
data-test-subj="indexPattern-add-field"
onClick={() => {
setIsAddIndexPatternFieldPopoverOpen(false);
addField();
}}
>
{i18n.translate('discover.fieldChooser.indexPatterns.addFieldButton', {
defaultMessage: 'Add field to index pattern',
})}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="manage"
icon="indexSettings"
onClick={() => {
setIsAddIndexPatternFieldPopoverOpen(false);
core.application.navigateToApp('management', {
path: `/kibana/indexPatterns/patterns/${selectedIndexPattern.id}`,
});
}}
>
{i18n.translate('discover.fieldChooser.indexPatterns.manageFieldButton', {
defaultMessage: 'Manage index pattern fields',
})}
</EuiContextMenuItem>,
]}
/>
</EuiPopover>
);
return (
<EuiPageSideBar
className="dscSidebar"
@ -294,14 +392,19 @@ export function DiscoverSidebar({
responsive={false}
>
<EuiFlexItem grow={false}>
<DiscoverIndexPattern
config={config}
selectedIndexPattern={selectedIndexPattern}
indexPatternList={sortBy(indexPatternList, (o) => o.attributes.title)}
indexPatterns={indexPatterns}
state={state}
setAppState={setAppState}
/>
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
<EuiFlexItem grow={true} className="dscSidebar__indexPatternSwitcher">
<DiscoverIndexPattern
config={config}
selectedIndexPattern={selectedIndexPattern}
indexPatternList={sortBy(indexPatternList, (o) => o.attributes.title)}
indexPatterns={indexPatterns}
state={state}
setAppState={setAppState}
/>
</EuiFlexItem>
{useNewFieldsApi && <EuiFlexItem grow={false}>{indexPatternActions}</EuiFlexItem>}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<form>
@ -370,6 +473,7 @@ export function DiscoverSidebar({
trackUiMetric={trackUiMetric}
multiFields={multiFields?.get(field.name)}
onEditField={canEditIndexPatternField ? editField : undefined}
onDeleteField={canEditIndexPatternField ? deleteField : undefined}
/>
</li>
);
@ -428,6 +532,7 @@ export function DiscoverSidebar({
trackUiMetric={trackUiMetric}
multiFields={multiFields?.get(field.name)}
onEditField={canEditIndexPatternField ? editField : undefined}
onDeleteField={canEditIndexPatternField ? deleteField : undefined}
/>
</li>
);
@ -455,6 +560,7 @@ export function DiscoverSidebar({
trackUiMetric={trackUiMetric}
multiFields={multiFields?.get(field.name)}
onEditField={canEditIndexPatternField ? editField : undefined}
onDeleteField={canEditIndexPatternField ? deleteField : undefined}
/>
</li>
);

View file

@ -11,6 +11,9 @@ import { FtrProviderContext } from './ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const retry = getService('retry');
const docTable = getService('docTable');
const testSubjects = getService('testSubjects');
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
const fieldEditor = getService('fieldEditor');
@ -19,6 +22,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
defaultIndex: 'logstash-*',
'discover:searchFieldsFromSource': false,
};
const createRuntimeField = async (fieldName: string) => {
await PageObjects.discover.clickIndexPatternActions();
await PageObjects.discover.clickAddNewField();
await fieldEditor.setName(fieldName);
await fieldEditor.enableValue();
await fieldEditor.typeScript("emit('abc')");
await fieldEditor.save();
};
describe('discover integration with runtime fields editor', function describeIndexTests() {
before(async function () {
await esArchiver.load('discover');
@ -43,5 +56,68 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.discover.getDocHeader()).to.have.string('megabytes');
expect((await PageObjects.discover.getAllFieldNames()).includes('megabytes')).to.be(true);
});
it('allows creation of a new field', async function () {
await createRuntimeField('runtimefield');
await PageObjects.header.waitUntilLoadingHasFinished();
expect((await PageObjects.discover.getAllFieldNames()).includes('runtimefield')).to.be(true);
});
it('allows editing of a newly created field', async function () {
await PageObjects.discover.editField('runtimefield');
await fieldEditor.setName('runtimefield edited');
await fieldEditor.save();
await fieldEditor.confirmSave();
await PageObjects.header.waitUntilLoadingHasFinished();
expect((await PageObjects.discover.getAllFieldNames()).includes('runtimefield')).to.be(false);
expect((await PageObjects.discover.getAllFieldNames()).includes('runtimefield edited')).to.be(
true
);
});
it('allows creation of a new field and use it in a saved search', async function () {
await createRuntimeField('discover runtimefield');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.clickFieldListItemAdd('discover runtimefield');
expect(await PageObjects.discover.getDocHeader()).to.have.string('discover runtimefield');
expect(await PageObjects.discover.saveSearch('Saved Search with runtimefield'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.clickNewSearchButton();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.loadSavedSearch('Saved Search with runtimefield');
await PageObjects.header.waitUntilLoadingHasFinished();
expect(await PageObjects.discover.getDocHeader()).to.have.string('discover runtimefield');
});
it('deletes a runtime field', async function () {
await createRuntimeField('delete');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.removeField('delete');
await fieldEditor.confirmDelete();
await PageObjects.header.waitUntilLoadingHasFinished();
expect((await PageObjects.discover.getAllFieldNames()).includes('delete')).to.be(false);
});
it('doc view includes runtime fields', async function () {
// navigate to doc view
await docTable.clickRowToggle({ rowIndex: 0 });
// click the open action
await retry.try(async () => {
const rowActions = await docTable.getRowActions({ rowIndex: 0 });
if (!rowActions.length) {
throw new Error('row actions empty, trying again');
}
await rowActions[1].click();
});
const hasDocHit = await testSubjects.exists('doc-hit');
expect(hasDocHit).to.be(true);
const runtimeFieldsRow = await testSubjects.exists('tableDocViewRow-discover runtimefield');
expect(runtimeFieldsRow).to.be(true);
});
});
}

View file

@ -270,6 +270,26 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
});
}
public async removeField(field: string) {
await testSubjects.click(`field-${field}`);
await testSubjects.click(`discoverFieldListPanelDelete-${field}`);
await testSubjects.existOrFail('runtimeFieldDeleteConfirmModal');
}
public async clickIndexPatternActions() {
await retry.try(async () => {
await testSubjects.click('discoverIndexPatternActions');
await testSubjects.existOrFail('discover-addRuntimeField-popover');
});
}
public async clickAddNewField() {
await retry.try(async () => {
await testSubjects.click('indexPattern-add-field');
await find.byClassName('indexPatternFieldEditor__form');
});
}
public async hasNoResults() {
return await testSubjects.exists('discoverNoResults');
}