[data views / runtime fields] Fix runtime fields with dots in the name (#160458)

## Summary

Composite runtime fields with dots in the name were broken, now fixed. 

To verify - 
1. Create runtime field with dot in the name
2. Make a composite runtime field with subfields with dot in the name
3. Go back and edit those fields

Closes: https://github.com/elastic/kibana/issues/159648

### Release note

Fixes creation and editing of composite runtime fields with dots in the
names.
This commit is contained in:
Matthew Kime 2023-06-29 21:20:05 -05:00 committed by GitHub
parent a91535202a
commit b2200e4d33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 44 additions and 31 deletions

View file

@ -71,7 +71,7 @@ export const CompositeEditor = ({ onReset }: CompositeEditorProps) => {
</EuiFlexGroup>
{Object.entries(subfields).map(([key, itemValue], idx) => {
return (
<div>
<div key={key}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFieldText value={key} disabled={true} />

View file

@ -168,7 +168,7 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props)
useEffect(() => {
const existingCompositeField = !!Object.keys(subfields$.getValue() || {}).length;
const changes$ = getFieldPreviewChanges(fieldPreview$);
const changes$ = getFieldPreviewChanges(fieldPreview$, updatedName);
const subChanges = changes$.subscribe((previewFields) => {
const fields = subfields$.getValue();
@ -199,7 +199,7 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props)
return () => {
subChanges.unsubscribe();
};
}, [form, fieldPreview$, subfields$]);
}, [form, fieldPreview$, subfields$, updatedName]);
useEffect(() => {
if (onChange) {

View file

@ -10,58 +10,68 @@ import { getFieldPreviewChanges } from './lib';
import { BehaviorSubject } from 'rxjs';
import { ChangeType, FieldPreview } from '../preview/types';
// note that periods and overlap in parent and subfield names are to test for corner cases
const parentName = 'composite.field';
const subfieldName = 'composite.field.a';
const appendParentName = (key: string) => `${parentName}.${key}`;
describe('getFieldPreviewChanges', () => {
it('should return new keys', (done) => {
const subj = new BehaviorSubject<FieldPreview[] | undefined>(undefined);
const changes = getFieldPreviewChanges(subj);
const changes = getFieldPreviewChanges(subj, parentName);
changes.subscribe((change) => {
expect(change).toStrictEqual({ hello: { changeType: ChangeType.UPSERT, type: 'keyword' } });
expect(change).toStrictEqual({
[subfieldName]: { changeType: ChangeType.UPSERT, type: 'keyword' },
});
done();
});
subj.next([]);
subj.next([{ key: 'hello', value: 'world', type: 'keyword' }]);
subj.next([{ key: appendParentName(subfieldName), value: 'world', type: 'keyword' }]);
});
it('should return updated type', (done) => {
const subj = new BehaviorSubject<FieldPreview[] | undefined>(undefined);
const changes = getFieldPreviewChanges(subj);
const changes = getFieldPreviewChanges(subj, parentName);
changes.subscribe((change) => {
expect(change).toStrictEqual({ hello: { changeType: ChangeType.UPSERT, type: 'long' } });
expect(change).toStrictEqual({
[subfieldName]: { changeType: ChangeType.UPSERT, type: 'long' },
});
done();
});
subj.next([{ key: 'hello', value: 'world', type: 'keyword' }]);
subj.next([{ key: 'hello', value: 1, type: 'long' }]);
subj.next([{ key: appendParentName(subfieldName), value: 'world', type: 'keyword' }]);
subj.next([{ key: appendParentName(subfieldName), value: 1, type: 'long' }]);
});
it('should remove keys', (done) => {
const subj = new BehaviorSubject<FieldPreview[] | undefined>(undefined);
const changes = getFieldPreviewChanges(subj);
const changes = getFieldPreviewChanges(subj, parentName);
changes.subscribe((change) => {
expect(change).toStrictEqual({ hello: { changeType: ChangeType.DELETE } });
expect(change).toStrictEqual({ [subfieldName]: { changeType: ChangeType.DELETE } });
done();
});
subj.next([{ key: 'hello', value: 'world', type: 'keyword' }]);
subj.next([{ key: appendParentName(subfieldName), value: 'world', type: 'keyword' }]);
subj.next([]);
});
it('should add, update, and remove keys in a single change', (done) => {
const subj = new BehaviorSubject<FieldPreview[] | undefined>(undefined);
const changes = getFieldPreviewChanges(subj);
const changes = getFieldPreviewChanges(subj, parentName);
changes.subscribe((change) => {
expect(change).toStrictEqual({
hello: { changeType: ChangeType.UPSERT, type: 'long' },
[subfieldName]: { changeType: ChangeType.UPSERT, type: 'long' },
hello2: { changeType: ChangeType.DELETE },
hello3: { changeType: ChangeType.UPSERT, type: 'keyword' },
});
done();
});
subj.next([
{ key: 'hello', value: 'world', type: 'keyword' },
{ key: 'hello2', value: 'world', type: 'keyword' },
{ key: appendParentName(subfieldName), value: 'world', type: 'keyword' },
{ key: appendParentName('hello2'), value: 'world', type: 'keyword' },
]);
subj.next([
{ key: 'hello', value: 1, type: 'long' },
{ key: 'hello3', value: 'world', type: 'keyword' },
{ key: appendParentName(subfieldName), value: 1, type: 'long' },
{ key: appendParentName('hello3'), value: 'world', type: 'keyword' },
]);
});
});

View file

@ -88,13 +88,16 @@ export const getNameFieldConfig = (
export const valueToComboBoxOption = (value: string) =>
RUNTIME_FIELD_OPTIONS_PRIMITIVE.find(({ value: optionValue }) => optionValue === value);
export const getFieldPreviewChanges = (subject: BehaviorSubject<FieldPreview[] | undefined>) =>
export const getFieldPreviewChanges = (
subject: BehaviorSubject<FieldPreview[] | undefined>,
parentName: string
) =>
subject.pipe(
filter((preview) => preview !== undefined),
map((items) =>
// reduce the fields to make diffing easier
items!.map((item) => {
const key = item.key.slice(item.key.search('\\.') + 1);
const key = item.key.substring(`${parentName}.`.length);
return { name: key, type: item.type! };
})
),

View file

@ -115,6 +115,13 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
return;
}
// Not sure why this is getting called without currentDocIndex
// would be much better to prevent this function from being called at all
if (!currentDocIndex) {
controller.setIsLoadingPreview(false);
return;
}
controller.setLastExecutePainlessRequestParams({
type,
script: script?.source,

View file

@ -153,14 +153,7 @@ export const getFieldEditorOpener =
let field: Field | undefined;
if (dataViewField) {
if (isExistingRuntimeField && dataViewField.runtimeField!.type === 'composite') {
// Composite runtime subfield
const [compositeName] = fieldNameToEdit!.split('.');
field = {
name: compositeName,
...dataView.getRuntimeField(compositeName)!,
};
} else if (isExistingRuntimeField) {
if (isExistingRuntimeField) {
// Runtime field
field = {
name: fieldNameToEdit!,

View file

@ -32,7 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('create composite runtime field', function describeIndexTests() {
// Starting with '@' to sort toward start of field list
const fieldName = '@composite_test';
const fieldName = '@composite.test';
it('should create runtime field', async function () {
await PageObjects.settings.navigateTo();
@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await log.debug('add runtime field');
await PageObjects.settings.addCompositeRuntimeField(
fieldName,
"emit('a','hello world')",
"emit('a.a','hello world')",
false,
1
);