Skip to content

Commit

Permalink
improve field browser performance
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelolo24 committed Feb 26, 2025
1 parent 303e5fd commit 8b03ec7
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 165 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,28 +67,30 @@ export const getFieldItemsData = ({
selectedCategoryIds.length > 0 ? selectedCategoryIds : Object.keys(browserFields);
const selectedFieldIds = new Set(columnIds);

const fieldItems = uniqBy(
'name',
categoryIds.reduce<BrowserFieldItem[]>((fieldItemsAcc, categoryId) => {
const getFieldItems = () => {
let fieldItemsAcc: BrowserFieldItem[] = [];
for (let i = 0; i < categoryIds.length; i += 1) {
const categoryId = categoryIds[i];
const categoryBrowserFields = Object.values(browserFields[categoryId]?.fields ?? {});
if (categoryBrowserFields.length > 0) {
fieldItemsAcc.push(
...categoryBrowserFields.map(({ name = '', ...field }) => {
return {
name,
type: field.type,
description: getDescription(name, EcsFlat as Record<string, EcsMetadata>),
example: field.example?.toString(),
category: getCategory(name),
selected: selectedFieldIds.has(name),
isRuntime: !!field.runtimeField,
};
})
);
const categoryFieldItems = categoryBrowserFields.map(({ name = '', ...field }) => {
return {
name,
type: field.type,
description: getDescription(name, EcsFlat as Record<string, EcsMetadata>),
example: field.example?.toString(),
category: getCategory(name),
selected: selectedFieldIds.has(name),
isRuntime: !!field.runtimeField,
};
});
fieldItemsAcc = fieldItemsAcc.concat(categoryFieldItems);
}
return fieldItemsAcc;
}, [])
);
}
return fieldItemsAcc;
};

const fieldItems = uniqBy('name', getFieldItems());
return { fieldItems };
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,151 +5,19 @@
* 2.0.
*/

import type { IndexFieldSearch } from './use_data_view';
import { useDataView } from './use_data_view';
import { mocksSource } from './mock';
import { mockGlobalState, TestProviders } from '../../mock';
import { act, renderHook } from '@testing-library/react';
import { useKibana } from '../../lib/kibana';

const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');

return {
...original,
useDispatch: () => mockDispatch,
};
});
jest.mock('../../lib/kibana');
jest.mock('../../lib/apm/use_track_http_request');
import { mockBrowserFields, mockIndexFields, mockIndexFieldsByName } from './mock';
import * as indexUtils from '.';

describe('source/index.tsx', () => {
describe('useDataView hook', () => {
const mockSearchResponse = {
...mocksSource,
indicesExist: ['auditbeat-*', mockGlobalState.sourcerer.signalIndexName],
isRestore: false,
rawResponse: {},
runtimeMappings: {},
};

beforeEach(() => {
jest.clearAllMocks();
const mock = {
subscribe: ({ next }: { next: Function }) => next(mockSearchResponse),
unsubscribe: jest.fn(),
};

(useKibana as jest.Mock).mockReturnValue({
services: {
data: {
dataViews: {
...useKibana().services.data.dataViews,
get: async (dataViewId: string, displayErrors?: boolean, refreshFields = false) => {
const dataViewMock = {
id: dataViewId,
matchedIndices: refreshFields
? ['hello', 'world', 'refreshed']
: ['hello', 'world'],
fields: mocksSource.indexFields,
getIndexPattern: () =>
refreshFields ? 'hello*,world*,refreshed*' : 'hello*,world*',
getRuntimeMappings: () => ({
myfield: {
type: 'keyword',
},
}),
};
return Promise.resolve({
toSpec: () => dataViewMock,
...dataViewMock,
});
},
getFieldsForWildcard: async () => Promise.resolve(),
getExistingIndices: async (indices: string[]) => Promise.resolve(indices),
},
search: {
search: jest.fn().mockReturnValue({
subscribe: ({ next }: { next: Function }) => {
next(mockSearchResponse);
return mock;
},
unsubscribe: jest.fn(),
}),
},
},
},
});
});
it('sets field data for data view', async () => {
const { result } = renderHook(() => useDataView(), {
wrapper: TestProviders,
});

await act(async () => {
await result.current.indexFieldsSearch({ dataViewId: 'neato' });
});
expect(mockDispatch.mock.calls[0][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_DATA_VIEW_LOADING',
payload: { id: 'neato', loading: true },
});
const { type: sourceType, payload } = mockDispatch.mock.calls[1][0];
expect(sourceType).toEqual('x-pack/security_solution/local/sourcerer/SET_DATA_VIEW');
expect(payload.id).toEqual('neato');
});

it('should reuse the result for dataView info when cleanCache not passed', async () => {
let indexFieldsSearch: IndexFieldSearch;

const { result } = renderHook(() => useDataView(), {
wrapper: TestProviders,
});

await act(async () => {
indexFieldsSearch = result.current.indexFieldsSearch;
});

await indexFieldsSearch!({ dataViewId: 'neato' });
const {
payload: { browserFields, indexFields },
} = mockDispatch.mock.calls[1][0];

mockDispatch.mockClear();

await indexFieldsSearch!({ dataViewId: 'neato' });
const {
payload: { browserFields: newBrowserFields, indexFields: newIndexFields },
} = mockDispatch.mock.calls[1][0];

expect(browserFields).toBe(newBrowserFields);
expect(indexFields).toBe(newIndexFields);
describe('getAllBrowserFields', () => {
it('should return the expected browser fields list', () => {
expect(indexUtils.getAllBrowserFields(mockBrowserFields)).toEqual(mockIndexFields);
});
});

it('should not reuse the result for dataView info when cleanCache passed', async () => {
let indexFieldsSearch: IndexFieldSearch;
const { result } = renderHook(() => useDataView(), {
wrapper: TestProviders,
});

await act(async () => {
indexFieldsSearch = result.current.indexFieldsSearch;
});

await indexFieldsSearch!({ dataViewId: 'neato' });
const {
payload: { patternList },
} = mockDispatch.mock.calls[1][0];

mockDispatch.mockClear();

await indexFieldsSearch!({ dataViewId: 'neato', cleanCache: true });
const {
payload: { patternList: newPatternList },
} = mockDispatch.mock.calls[1][0];
expect(patternList).not.toBe(newPatternList);
expect(patternList).not.toContain('refreshed*');
expect(newPatternList).toContain('refreshed*');
describe('getAllFieldsByName', () => {
it('should return the expected browser fields list', () => {
expect(indexUtils.getAllFieldsByName(mockBrowserFields)).toEqual(mockIndexFieldsByName);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ import type { ENDPOINT_FIELDS_SEARCH_STRATEGY } from '../../../../common/endpoin
export type { BrowserFields };

export function getAllBrowserFields(browserFields: BrowserFields): Array<Partial<FieldSpec>> {
const result: Array<Partial<FieldSpec>> = [];
let result: Array<Partial<FieldSpec>> = [];
for (const namespace of Object.values(browserFields)) {
if (namespace.fields) {
result.push(...Object.values(namespace.fields));
const namespaceFields = Object.values(namespace.fields);
result = result.concat(namespaceFields);
}
}
return result;
Expand All @@ -36,9 +37,11 @@ export function getAllBrowserFields(browserFields: BrowserFields): Array<Partial
* @param browserFields
* @returns
*/
export const getAllFieldsByName = (
browserFields: BrowserFields
): { [fieldName: string]: Partial<FieldSpec> } => keyBy('name', getAllBrowserFields(browserFields));
export const getAllFieldsByName = memoizeOne(
(browserFields: BrowserFields): { [fieldName: string]: Partial<FieldSpec> } =>
keyBy('name', getAllBrowserFields(browserFields)),
(newArgs, lastArgs) => newArgs[0] === lastArgs[0]
);

export const getIndexFields = memoizeOne(
(title: string, fields: IIndexPatternFieldList): DataViewBase =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* 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 type { IndexFieldSearch } from './use_data_view';
import { useDataView } from './use_data_view';
import { mocksSource } from './mock';
import { mockGlobalState, TestProviders } from '../../mock';
import { act, renderHook } from '@testing-library/react';
import { useKibana } from '../../lib/kibana';

const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');

return {
...original,
useDispatch: () => mockDispatch,
};
});
jest.mock('../../lib/kibana');
jest.mock('../../lib/apm/use_track_http_request');

describe('useDataView', () => {
const mockSearchResponse = {
...mocksSource,
indicesExist: ['auditbeat-*', mockGlobalState.sourcerer.signalIndexName],
isRestore: false,
rawResponse: {},
runtimeMappings: {},
};

beforeEach(() => {
jest.clearAllMocks();
const mock = {
subscribe: ({ next }: { next: Function }) => next(mockSearchResponse),
unsubscribe: jest.fn(),
};

(useKibana as jest.Mock).mockReturnValue({
services: {
data: {
dataViews: {
...useKibana().services.data.dataViews,
get: async (dataViewId: string, displayErrors?: boolean, refreshFields = false) => {
const dataViewMock = {
id: dataViewId,
matchedIndices: refreshFields
? ['hello', 'world', 'refreshed']
: ['hello', 'world'],
fields: mocksSource.indexFields,
getIndexPattern: () =>
refreshFields ? 'hello*,world*,refreshed*' : 'hello*,world*',
getRuntimeMappings: () => ({
myfield: {
type: 'keyword',
},
}),
};
return Promise.resolve({
toSpec: () => dataViewMock,
...dataViewMock,
});
},
getFieldsForWildcard: async () => Promise.resolve(),
getExistingIndices: async (indices: string[]) => Promise.resolve(indices),
},
search: {
search: jest.fn().mockReturnValue({
subscribe: ({ next }: { next: Function }) => {
next(mockSearchResponse);
return mock;
},
unsubscribe: jest.fn(),
}),
},
},
},
});
});
it('sets field data for data view', async () => {
const { result } = renderHook(() => useDataView(), {
wrapper: TestProviders,
});

await act(async () => {
await result.current.indexFieldsSearch({ dataViewId: 'neato' });
});
expect(mockDispatch.mock.calls[0][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_DATA_VIEW_LOADING',
payload: { id: 'neato', loading: true },
});
const { type: sourceType, payload } = mockDispatch.mock.calls[1][0];
expect(sourceType).toEqual('x-pack/security_solution/local/sourcerer/SET_DATA_VIEW');
expect(payload.id).toEqual('neato');
});

it('should reuse the result for dataView info when cleanCache not passed', async () => {
let indexFieldsSearch: IndexFieldSearch;

const { result } = renderHook(() => useDataView(), {
wrapper: TestProviders,
});

await act(async () => {
indexFieldsSearch = result.current.indexFieldsSearch;
});

await indexFieldsSearch!({ dataViewId: 'neato' });
const {
payload: { browserFields, indexFields },
} = mockDispatch.mock.calls[1][0];

mockDispatch.mockClear();

await indexFieldsSearch!({ dataViewId: 'neato' });
const {
payload: { browserFields: newBrowserFields, indexFields: newIndexFields },
} = mockDispatch.mock.calls[1][0];

expect(browserFields).toBe(newBrowserFields);
expect(indexFields).toBe(newIndexFields);
});

it('should not reuse the result for dataView info when cleanCache passed', async () => {
let indexFieldsSearch: IndexFieldSearch;
const { result } = renderHook(() => useDataView(), {
wrapper: TestProviders,
});

await act(async () => {
indexFieldsSearch = result.current.indexFieldsSearch;
});

await indexFieldsSearch!({ dataViewId: 'neato' });
const {
payload: { patternList },
} = mockDispatch.mock.calls[1][0];

mockDispatch.mockClear();

await indexFieldsSearch!({ dataViewId: 'neato', cleanCache: true });
const {
payload: { patternList: newPatternList },
} = mockDispatch.mock.calls[1][0];
expect(patternList).not.toBe(newPatternList);
expect(patternList).not.toContain('refreshed*');
expect(newPatternList).toContain('refreshed*');
});
});

0 comments on commit 8b03ec7

Please sign in to comment.