Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0aad1a4
[Discover] Handle increments correctly
jughosta Feb 14, 2025
598f3de
[Discover] Handle form input correctly
jughosta Feb 14, 2025
6fa5d4d
[Discover] Handle getting form data correctly
jughosta Feb 14, 2025
bd3ef1d
[Discover] Handle runtime fields too
jughosta Feb 14, 2025
4ef4354
[Discover] Prevent resetting of counts inside fieldAttrs
jughosta Feb 14, 2025
eeacf4a
Merge branch 'main' into 211109-fix-popularity-scores
jughosta Feb 19, 2025
91978a4
[Discover] Add a test
jughosta Feb 19, 2025
3421be1
[Discover] Add more tests
jughosta Feb 19, 2025
1eaa32d
[Discover] Update utils
jughosta Feb 19, 2025
885ba5f
[Discover] Revert some changes
jughosta Feb 19, 2025
d87f207
[Discover] Update test
jughosta Feb 19, 2025
f79545d
[Discover] Add a comment
jughosta Feb 19, 2025
a59d482
Merge branch 'main' into 211109-fix-popularity-scores
jughosta Feb 21, 2025
f2bfbe3
[Discover] Move the conversion to the initialization code
jughosta Feb 21, 2025
7b1efba
[Discover] Add a test and fix types
jughosta Feb 21, 2025
7af1687
[Discover] Cleaner form handling
jughosta Feb 21, 2025
9e605b3
[Discover] Fix test
jughosta Feb 21, 2025
db47f5c
[Discover] Simplify
jughosta Feb 21, 2025
9a29c06
[Discover] Update tests
jughosta Feb 21, 2025
e4333a3
[Discover] Add a test
jughosta Feb 21, 2025
60339a6
[Discover] Extend the test
jughosta Feb 21, 2025
0ec138d
[Discover] Fix test
jughosta Feb 21, 2025
05b327f
Merge branch 'main' into 211109-fix-popularity-scores
jughosta Feb 24, 2025
83b214c
Merge branch 'main' into 211109-fix-popularity-scores
jughosta Feb 26, 2025
beb2d05
Merge branch 'main' into 211109-fix-popularity-scores
jughosta Feb 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,31 @@ describe('Popularize field', () => {
expect(field.count).toEqual(1);
});

test('should increment', async () => {
const field = {
count: 5,
};
const dataView = {
id: 'id',
fields: {
getByName: () => field,
},
setFieldCount: jest.fn().mockImplementation((fieldName, count) => {
field.count = count;
}),
isPersisted: () => true,
} as unknown as DataView;
const fieldName = '@timestamp';
const updateSavedObjectMock = jest.fn();
const dataViewsService = {
updateSavedObject: updateSavedObjectMock,
} as unknown as DataViewsContract;
const result = await popularizeField(dataView, fieldName, dataViewsService, capabilities);
expect(result).toBeUndefined();
expect(updateSavedObjectMock).toHaveBeenCalled();
expect(field.count).toEqual(6);
});

test('hides errors', async () => {
const field = {
count: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ describe('<FieldEditorFlyoutContent />', () => {
script: { source: 'emit(true)' },
customLabel: 'cool demo test field',
format: { id: 'boolean' },
popularity: 1,
};

const { find } = await setup({ fieldToCreate });
Expand All @@ -70,6 +71,7 @@ describe('<FieldEditorFlyoutContent />', () => {
expect(find('nameField.input').props().value).toBe(fieldToCreate.name);
expect(find('typeField').props().value).toBe(fieldToCreate.type);
expect(find('scriptField').props().value).toBe(fieldToCreate.script.source);
expect(find('editorFieldCount').props().value).toBe(fieldToCreate.popularity.toString());
});

test('should accept an "onSave" prop', async () => {
Expand Down Expand Up @@ -172,6 +174,26 @@ describe('<FieldEditorFlyoutContent />', () => {
script: { source: 'echo("hello")' },
format: null,
});

await toggleFormRow('popularity');
await fields.updatePopularity('5');

await waitForUpdates();

await act(async () => {
find('fieldSaveButton').simulate('click');
jest.advanceTimersByTime(0); // advance timers to allow the form to validate
});

fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0];

expect(fieldReturned).toEqual({
name: 'someName',
type: 'date',
script: { source: 'echo("hello")' },
format: null,
popularity: 5,
});
});

test('should not block validation if no documents could be fetched from server', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const waitForUpdates = async (testBed?: TestBed) => {

export const getCommonActions = (testBed: TestBed) => {
const toggleFormRow = async (
row: 'customLabel' | 'customDescription' | 'value' | 'format',
row: 'customLabel' | 'customDescription' | 'value' | 'format' | 'popularity',
value: 'on' | 'off' = 'on'
) => {
const testSubj = `${row}Row.toggle`;
Expand Down Expand Up @@ -102,6 +102,15 @@ export const getCommonActions = (testBed: TestBed) => {
testBed.component.update();
};

const updatePopularity = async (value: string) => {
await act(async () => {
testBed.form.setInputValue('editorFieldCount', value);
jest.advanceTimersByTime(0); // advance timers to allow the form to validate
});

testBed.component.update();
};

const getScriptError = () => {
const scriptError = testBed.component.find('#runtimeFieldScript-error-0');

Expand All @@ -123,6 +132,7 @@ export const getCommonActions = (testBed: TestBed) => {
updateType,
updateScript,
updateFormat,
updatePopularity,
getScriptError,
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ export interface FieldEditorFormState {
submit: FormHook<Field>['submit'];
}

export interface FieldFormInternal extends Omit<Field, 'type' | 'internalType' | 'fields'> {
export interface FieldFormInternal
extends Omit<Field, 'type' | 'internalType' | 'fields' | 'popularity'> {
fields?: Record<string, { type: RuntimePrimitiveTypes }>;
type: TypeSelection;
popularity?: string;
__meta__: {
isCustomLabelVisible: boolean;
isCustomDescriptionVisible: boolean;
Expand Down Expand Up @@ -84,6 +86,7 @@ const formDeserializer = (field: Field): FieldFormInternal => {

return {
...field,
popularity: typeof field.popularity === 'number' ? String(field.popularity) : field.popularity,
type: fieldType,
format,
__meta__: {
Expand All @@ -97,12 +100,15 @@ const formDeserializer = (field: Field): FieldFormInternal => {
};

const formSerializer = (field: FieldFormInternal): Field => {
const { __meta__, type, format, ...rest } = field;
const { __meta__, type, format, popularity, ...rest } = field;

return {
type: type && type[0].value!,
// By passing "null" we are explicitly telling DataView to remove the
// format if there is one defined for the field.
format: format === undefined ? null : format,
// convert from the input string value into a number
popularity: typeof popularity === 'string' ? Number(popularity) || 0 : popularity,
...rest,
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,17 @@ export const getFieldEditorOpener =
};
};

const dataViewLazy =
dataViewLazyOrNot instanceof DataViewLazy
? dataViewLazyOrNot
: await dataViews.toDataViewLazy(dataViewLazyOrNot);
let dataViewLazy: DataViewLazy;

if (dataViewLazyOrNot instanceof DataViewLazy) {
dataViewLazy = dataViewLazyOrNot;
} else {
if (dataViewLazyOrNot.id) {
// force cache reset to have the latest field attributes
dataViews.clearDataViewLazyCache(dataViewLazyOrNot.id);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like a flaw in the data view lazy implementation that we have to do this. Maybe we should consider clearing the corresponding lazy cache instance internally whenever a regular data view is saved, and vice versa. But this probably needs more investigation and it would be great to get this fixed, so this should be fine for now.

}
dataViewLazy = await dataViews.toDataViewLazy(dataViewLazyOrNot);
}

const dataViewField = fieldNameToEdit
? (await dataViewLazy.getFieldByName(fieldNameToEdit, true)) ||
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,15 @@ export abstract class AbstractDataView {
getAsSavedObjectBody(): DataViewAttributes {
const stringifyOrUndefined = (obj: any) => (obj ? JSON.stringify(obj) : undefined);

const fieldAttrsWithValues: Record<string, FieldAttrSet> = {};
this.fieldAttrs.forEach((attrs, fieldName) => {
if (Object.keys(attrs).length) {
fieldAttrsWithValues[fieldName] = attrs;
}
});
Comment on lines +368 to +373
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious which issue this addresses? I couldn't tell from the code.

Copy link
Copy Markdown
Contributor Author

@jughosta jughosta Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davismcphee To make the output smaller. For example, instead of { bytes: { count: 5 }, message: {}, agent: {} } only { bytes: { count: 5} }


return {
fieldAttrs: stringifyOrUndefined(Object.fromEntries(this.fieldAttrs.entries())),
fieldAttrs: stringifyOrUndefined(fieldAttrsWithValues),
title: this.getIndexPattern(),
timeFieldName: this.timeFieldName,
sourceFilters: stringifyOrUndefined(this.sourceFilters),
Expand All @@ -382,8 +389,10 @@ export abstract class AbstractDataView {
}

protected toSpecShared(includeFields = true): DataViewSpec {
// `cloneDeep` is added to make sure that the original fieldAttrs map is not modified with the following `delete` operation.
const fieldAttrs = cloneDeep(Object.fromEntries(this.fieldAttrs.entries()));
Comment on lines +392 to +393
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't Object.fromEntries(this.fieldAttrs.entries()) already change the original reference? Also if we switch to cloneDeep instead, couldn't we just use cloneDeep(this.fieldAttrs)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.fromEntries(this.fieldAttrs.entries()) does not change the original this.fieldAttrs, it only here to transform from Map type to an object. The "second layer" objects inside are still the ones from this.fieldAttrs so we need to call cloneDeep additionally.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining! I was both thinking fieldAttrs was an object and not considering nested objects, but it makes sense why this is needed now.


// if fields aren't included, don't include count
const fieldAttrs = Object.fromEntries(this.fieldAttrs.entries());
if (!includeFields) {
Object.keys(fieldAttrs).forEach((key) => {
delete fieldAttrs[key].count;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,22 @@ describe('IndexPattern', () => {
indexPattern.removeRuntimeField(newField);
});

test('add and remove a popularity score from a runtime field', () => {
const newField = 'new_field_test';
indexPattern.addRuntimeField(newField, {
...runtimeWithAttrs,
popularity: 10,
});
expect(indexPattern.getFieldByName(newField)?.count).toEqual(10);
indexPattern.setFieldCount(newField, 20);
expect(indexPattern.getFieldByName(newField)?.count).toEqual(20);
indexPattern.setFieldCount(newField, null);
expect(indexPattern.getFieldByName(newField)?.count).toEqual(0);
indexPattern.setFieldCount(newField, undefined);
expect(indexPattern.getFieldByName(newField)?.count).toEqual(0);
indexPattern.removeRuntimeField(newField);
});

test('add and remove composite runtime field as new fields', () => {
const fieldCount = indexPattern.fields.length;
indexPattern.addRuntimeField('new_field', runtimeCompositeWithAttrs);
Expand Down Expand Up @@ -505,6 +521,19 @@ describe('IndexPattern', () => {
const dataView2 = create('test2', spec);
expect(dataView1.sourceFilters).not.toBe(dataView2.sourceFilters);
});

test('getting spec without fields does not modify fieldAttrs', () => {
const fieldAttrs = { bytes: { count: 5, customLabel: 'test_bytes' }, agent: { count: 1 } };
const dataView = new DataView({
fieldFormats: fieldFormatsMock,
spec: {
fieldAttrs,
},
});
const spec = dataView.toSpec(false);
expect(spec.fieldAttrs).toEqual({ bytes: { customLabel: fieldAttrs.bytes.customLabel } });
expect(JSON.parse(dataView.getAsSavedObjectBody().fieldAttrs!)).toEqual(fieldAttrs);
});
});

describe('should initialize from spec with field attributes', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,23 @@ describe('DataViewLazy', () => {
dataViewLazy.removeRuntimeField(newField);
});

test('add and remove a popularity score from a runtime field', async () => {
const newField = 'new_field_test';
fieldCapsResponse = [];
dataViewLazy.addRuntimeField(newField, {
...runtimeWithAttrs,
popularity: 10,
});
expect((await dataViewLazy.getFieldByName(newField))?.count).toEqual(10);
dataViewLazy.setFieldCount(newField, 20);
expect((await dataViewLazy.getFieldByName(newField))?.count).toEqual(20);
dataViewLazy.setFieldCount(newField, null);
expect((await dataViewLazy.getFieldByName(newField))?.count).toEqual(0);
dataViewLazy.setFieldCount(newField, undefined);
expect((await dataViewLazy.getFieldByName(newField))?.count).toEqual(0);
dataViewLazy.removeRuntimeField(newField);
});

test('add and remove composite runtime field as new fields', async () => {
const fieldMap = (await dataViewLazy.getFields({ fieldName: ['*'] })).getFieldMap();
const fieldCount = Object.values(fieldMap).length;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ const savedObject = {
typeMeta: '{}',
type: '',
runtimeFieldMap:
'{"aRuntimeField": { "type": "keyword", "script": {"source": "emit(\'hello\')"}}}',
fieldAttrs: '{"aRuntimeField": { "count": 5, "customLabel": "A Runtime Field"}}',
'{"aRuntimeField": { "type": "keyword", "script": {"source": "emit(\'hello\')"}}, "wrongCountType": { "type": "keyword", "script": {"source": "emit(\'test\')"}}}',
fieldAttrs:
'{"aRuntimeField": { "count": 5, "customLabel": "A Runtime Field" }, "wrongCountType": { "count": "50" }}',
},
type: 'index-pattern',
references: [],
Expand Down Expand Up @@ -682,6 +683,23 @@ describe('IndexPatterns', () => {
expect(attrs.fieldFormatMap).toMatchInlineSnapshot(`"{}"`);
});

test('gets the correct field attrs', async () => {
const id = 'id';
setDocsourcePayload(id, savedObject);
const dataView = await indexPatterns.get(id);
expect(dataView.getFieldByName('aRuntimeField')).toEqual(
expect.objectContaining({
count: 5,
customLabel: 'A Runtime Field',
})
);
expect(dataView.getFieldByName('wrongCountType')).toEqual(
expect.objectContaining({
count: 50,
})
);
});

describe('defaultDataViewExists', () => {
beforeEach(() => {
indexPatterns.clearCache();
Expand Down
Loading