* [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:
parent
7c259ef914
commit
f187515c1a
|
@ -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 [],
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -69,3 +69,7 @@
|
|||
opacity: 1; /* 2 */
|
||||
}
|
||||
}
|
||||
|
||||
.dscSidebar__indexPatternSwitcher {
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue