[Search]Add filter by FieldType component to mappings tab (#181188)

## Summary
Adds filter by field type component in mappings tab, in index management view


f6137117-19e5-453f-a2c0-e124770756c0




### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Saarika Bhasi 2024-05-02 10:18:14 -04:00 committed by GitHub
parent fe41c27995
commit 15d2235a73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 819 additions and 29 deletions

View file

@ -41,6 +41,8 @@ export interface IndexDetailsPageTestBed extends TestBed {
getActiveTabContent: () => string;
mappings: {
addNewMappingFieldNameAndType: (mappingFields?: MappingField[]) => Promise<void>;
clickFilterByFieldType: () => Promise<void>;
selectFilterFieldType: (fieldType: string) => Promise<void>;
clickAddFieldButton: () => Promise<void>;
clickSaveMappingsButton: () => Promise<void>;
getCodeBlockContent: () => string;
@ -51,6 +53,8 @@ export interface IndexDetailsPageTestBed extends TestBed {
getTreeViewContent: (fieldName: string) => string;
clickToggleViewButton: () => Promise<void>;
isSearchBarDisabled: () => boolean;
setSearchBarValue: (searchValue: string) => Promise<void>;
findSearchResult: () => string;
isSemanticTextBannerVisible: () => boolean;
};
settings: {
@ -216,9 +220,35 @@ export const setup = async ({
});
component.update();
},
clickFilterByFieldType: async () => {
expect(exists('indexDetailsMappingsFilterByFieldTypeButton')).toBe(true);
await act(async () => {
find('indexDetailsMappingsFilterByFieldTypeButton').simulate('click');
});
component.update();
},
selectFilterFieldType: async (fieldType: string) => {
expect(testBed.exists('indexDetailsMappingsSelectFilter-text')).toBe(true);
await act(async () => {
find(fieldType).simulate('click');
});
component.update();
},
isSearchBarDisabled: () => {
return find('indexDetailsMappingsFieldSearch').prop('disabled');
},
setSearchBarValue: async (searchValue: string) => {
await act(async () => {
testBed
.find('indexDetailsMappingsFieldSearch')
.simulate('change', { target: { value: searchValue } });
});
component.update();
},
findSearchResult: () => {
expect(testBed.exists('fieldName')).toBe(true);
return testBed.find('fieldName').text();
},
isSemanticTextBannerVisible: () => {
return exists('indexDetailsMappingsSemanticTextBanner');
},

View file

@ -471,10 +471,11 @@ describe('<IndexDetailsPage />', () => {
requestOptions
);
});
it('searchbar, toggle button, add field button exists', async () => {
it('filter, searchbar, toggle button, add field button exists', async () => {
expect(testBed.exists('indexDetailsMappingsAddField')).toBe(true);
expect(testBed.exists('indexDetailsMappingsToggleViewButton')).toBe(true);
expect(testBed.exists('indexDetailsMappingsFieldSearch')).toBe(true);
expect(testBed.exists('indexDetailsMappingsFilter')).toBe(true);
});
it('displays the mappings in the table view', async () => {
@ -508,6 +509,57 @@ describe('<IndexDetailsPage />', () => {
'https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/mapping.html'
);
});
describe('Filter field by filter Type', () => {
const mockIndexMappingResponse: any = {
...testIndexMappings.mappings,
properties: {
...testIndexMappings.mappings.properties,
name: {
type: 'text',
},
},
};
beforeEach(async () => {
httpRequestsMockHelpers.setLoadIndexMappingResponse(testIndexName, {
mappings: mockIndexMappingResponse,
});
await act(async () => {
testBed = await setup({ httpSetup });
});
testBed.component.update();
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Mappings);
});
test('popover is visible and shows list of available field types', async () => {
await testBed.actions.mappings.clickFilterByFieldType();
expect(testBed.exists('euiSelectableList')).toBe(true);
expect(testBed.exists('indexDetailsMappingsFilterByFieldTypeSearch')).toBe(true);
expect(testBed.exists('euiSelectableList')).toBe(true);
});
test('can select a field type and list view changes', async () => {
await testBed.actions.mappings.clickFilterByFieldType();
await testBed.actions.mappings.selectFilterFieldType(
'indexDetailsMappingsSelectFilter-text'
);
expect(testBed.actions.mappings.getTreeViewContent('nameField-fieldName')).toContain(
'name'
);
expect(testBed.find('@timestampField-fieldName')).not.toContain('@timestamp');
});
test('can search field with filter', async () => {
expect(testBed.find('fieldName')).toHaveLength(2);
// set filter
await testBed.actions.mappings.clickFilterByFieldType();
await testBed.actions.mappings.selectFilterFieldType(
'indexDetailsMappingsSelectFilter-text'
);
await testBed.actions.mappings.setSearchBarValue('na');
expect(testBed.find('fieldName')).toHaveLength(1);
expect(testBed.actions.mappings.findSearchResult()).not.toBe('@timestamp');
expect(testBed.actions.mappings.findSearchResult()).toBe('name');
});
});
describe('Add a new field ', () => {
const mockIndexMappingResponse: any = {
...testIndexMappings.mappings,

View file

@ -18,6 +18,7 @@ interface Props {
result: SearchResultType[];
documentFieldsState: State['documentFields'];
style?: React.CSSProperties;
onClearSearch?: () => void;
}
const ITEM_HEIGHT = 64;
@ -50,12 +51,21 @@ const Row = React.memo<RowProps>(({ data, index, style }) => {
}, areEqual);
export const SearchResult = React.memo(
({ result, documentFieldsState: { status, fieldToEdit }, style: virtualListStyle }: Props) => {
({
result,
documentFieldsState: { status, fieldToEdit },
style: virtualListStyle,
onClearSearch,
}: Props) => {
const dispatch = useDispatch();
const listHeight = Math.min(result.length * ITEM_HEIGHT, 600);
const clearSearch = () => {
dispatch({ type: 'search:update', value: '' });
if (onClearSearch !== undefined) {
onClearSearch();
} else {
dispatch({ type: 'search:update', value: '' });
}
};
const itemData = useMemo(

View file

@ -27,6 +27,9 @@ export {
stripUndefinedValues,
normalizeRuntimeFields,
deNormalizeRuntimeFields,
getAllFieldTypesFromState,
getFieldsFromState,
getFieldsMatchingFilterFromState,
} from './utils';
export * from './serializers';

View file

@ -10,7 +10,143 @@ jest.mock('../constants', () => {
return { MAIN_DATA_TYPE_DEFINITION: {}, TYPE_DEFINITION };
});
import { stripUndefinedValues, getTypeLabelFromField, getFieldMeta } from './utils';
import { Fields, NormalizedFields, State } from '../types';
import {
stripUndefinedValues,
getTypeLabelFromField,
getFieldMeta,
getFieldsFromState,
getAllFieldTypesFromState,
getFieldsMatchingFilterFromState,
} from './utils';
const fieldsWithnestedFields: NormalizedFields = {
byId: {
'4459e8f2-3bec-4d17-b50c-f62d1fcbcca0': {
id: '4459e8f2-3bec-4d17-b50c-f62d1fcbcca0',
parentId: 'dd0dd3aa-52c9-472b-a23d-ecec0a1b2420',
nestedDepth: 1,
isMultiField: false,
path: ['multifield', 'flag'],
source: {
name: 'flag',
type: 'boolean',
},
childFieldsName: 'fields',
canHaveChildFields: false,
hasChildFields: false,
canHaveMultiFields: true,
hasMultiFields: false,
isExpanded: false,
},
'20fffee6-2a94-4aa6-b7e4-1cd745d6f775': {
id: '20fffee6-2a94-4aa6-b7e4-1cd745d6f775',
parentId: '97399281-b0b2-4490-9931-6cc92676b305',
nestedDepth: 3,
isMultiField: false,
path: ['multifield', 'points', 'entity', 'entity_1'],
source: {
name: 'entity_1',
type: 'keyword',
},
childFieldsName: 'fields',
canHaveChildFields: false,
hasChildFields: false,
canHaveMultiFields: true,
hasMultiFields: false,
isExpanded: false,
},
'97399281-b0b2-4490-9931-6cc92676b305': {
id: '97399281-b0b2-4490-9931-6cc92676b305',
parentId: '9031c735-a445-491f-948d-41989b51a1a3',
nestedDepth: 2,
isMultiField: false,
path: ['multifield', 'points', 'entity'],
source: {
name: 'entity',
type: 'object',
},
childFieldsName: 'properties',
canHaveChildFields: true,
hasChildFields: true,
canHaveMultiFields: false,
hasMultiFields: false,
isExpanded: false,
childFields: ['20fffee6-2a94-4aa6-b7e4-1cd745d6f775'],
},
'af9b7a29-8c44-4dbe-baa0-c29eb1760d96': {
id: 'af9b7a29-8c44-4dbe-baa0-c29eb1760d96',
parentId: '9031c735-a445-491f-948d-41989b51a1a3',
nestedDepth: 2,
isMultiField: false,
path: ['multifield', 'points', 'name'],
source: {
name: 'name',
type: 'text',
},
childFieldsName: 'fields',
canHaveChildFields: false,
hasChildFields: false,
canHaveMultiFields: true,
hasMultiFields: false,
isExpanded: false,
},
'9031c735-a445-491f-948d-41989b51a1a3': {
id: '9031c735-a445-491f-948d-41989b51a1a3',
parentId: 'dd0dd3aa-52c9-472b-a23d-ecec0a1b2420',
nestedDepth: 1,
isMultiField: false,
path: ['multifield', 'points'],
source: {
name: 'points',
type: 'object',
},
childFieldsName: 'properties',
canHaveChildFields: true,
hasChildFields: true,
canHaveMultiFields: false,
hasMultiFields: false,
isExpanded: false,
childFields: ['97399281-b0b2-4490-9931-6cc92676b305', 'af9b7a29-8c44-4dbe-baa0-c29eb1760d96'],
},
'dd0dd3aa-52c9-472b-a23d-ecec0a1b2420': {
id: 'dd0dd3aa-52c9-472b-a23d-ecec0a1b2420',
nestedDepth: 0,
isMultiField: false,
path: ['multifield'],
source: {
name: 'multifield',
type: 'object',
},
childFieldsName: 'properties',
canHaveChildFields: true,
hasChildFields: true,
canHaveMultiFields: false,
hasMultiFields: false,
isExpanded: false,
childFields: ['4459e8f2-3bec-4d17-b50c-f62d1fcbcca0', '9031c735-a445-491f-948d-41989b51a1a3'],
},
'54204b52-c6a0-4de4-8f82-3e1ea9ad533a': {
id: '54204b52-c6a0-4de4-8f82-3e1ea9ad533a',
nestedDepth: 0,
isMultiField: false,
path: ['title'],
source: {
name: 'title',
type: 'text',
},
childFieldsName: 'fields',
canHaveChildFields: false,
hasChildFields: false,
canHaveMultiFields: true,
hasMultiFields: false,
isExpanded: false,
},
},
aliases: {},
rootLevelFields: ['dd0dd3aa-52c9-472b-a23d-ecec0a1b2420', '54204b52-c6a0-4de4-8f82-3e1ea9ad533a'],
maxNestedDepth: 3,
};
describe('utils', () => {
describe('stripUndefinedValues()', () => {
@ -101,4 +237,210 @@ describe('utils', () => {
).toEqual(false);
});
});
describe('getFieldsFromState', () => {
test('returns all the fields', () => {
expect(getFieldsFromState(fieldsWithnestedFields)).toEqual([
{
id: 'dd0dd3aa-52c9-472b-a23d-ecec0a1b2420',
nestedDepth: 0,
isMultiField: false,
path: ['multifield'],
source: {
name: 'multifield',
type: 'object',
},
childFieldsName: 'properties',
canHaveChildFields: true,
hasChildFields: true,
canHaveMultiFields: false,
hasMultiFields: false,
isExpanded: false,
childFields: [
'4459e8f2-3bec-4d17-b50c-f62d1fcbcca0',
'9031c735-a445-491f-948d-41989b51a1a3',
],
},
{
id: '54204b52-c6a0-4de4-8f82-3e1ea9ad533a',
nestedDepth: 0,
isMultiField: false,
path: ['title'],
source: {
name: 'title',
type: 'text',
},
childFieldsName: 'fields',
canHaveChildFields: false,
hasChildFields: false,
canHaveMultiFields: true,
hasMultiFields: false,
isExpanded: false,
},
]);
});
test('returns only text fields matching filter', () => {
expect(getFieldsFromState(fieldsWithnestedFields, ['Text'])).toEqual([
{
id: 'af9b7a29-8c44-4dbe-baa0-c29eb1760d96',
parentId: '9031c735-a445-491f-948d-41989b51a1a3',
nestedDepth: 2,
isMultiField: false,
path: ['multifield', 'points', 'name'],
source: {
name: 'name',
type: 'text',
},
childFieldsName: 'fields',
canHaveChildFields: false,
hasChildFields: false,
canHaveMultiFields: true,
hasMultiFields: false,
isExpanded: false,
},
{
id: '54204b52-c6a0-4de4-8f82-3e1ea9ad533a',
nestedDepth: 0,
isMultiField: false,
path: ['title'],
source: {
name: 'title',
type: 'text',
},
childFieldsName: 'fields',
canHaveChildFields: false,
hasChildFields: false,
canHaveMultiFields: true,
hasMultiFields: false,
isExpanded: false,
},
]);
});
});
describe('getallFieldsIncludingNestedFields', () => {
const fields: Fields = {
nested_field: {
properties: {
flag: { type: 'boolean' },
points: {
properties: {
name: { type: 'text' },
entity: {
type: 'object',
properties: {
entity_1: { type: 'keyword' },
},
},
},
type: 'object',
},
},
type: 'object',
},
};
test('returns all the data types including nested fields types', () => {
expect(getAllFieldTypesFromState(fields)).toEqual(['object', 'boolean', 'text', 'keyword']);
});
});
describe('getFieldsMatchingFilterFromState', () => {
const sampleState: State = {
isValid: true,
configuration: {
defaultValue: {},
data: {
internal: {},
format: () => ({}),
},
validate: () => Promise.resolve(true),
},
templates: {
defaultValue: {},
data: {
internal: {},
format: () => ({}),
},
validate: () => Promise.resolve(true),
},
fields: fieldsWithnestedFields,
documentFields: {
status: 'disabled',
editor: 'default',
},
runtimeFields: {},
runtimeFieldsList: {
status: 'idle',
},
fieldsJsonEditor: {
format: () => ({}),
isValid: true,
},
search: {
term: 'f',
result: [],
},
filter: {
filteredFields: [
{
id: 'eb903187-c99e-4773-9274-cbefc68bb3f1',
parentId: '5c6287de-7ed0-48f8-bc08-c401bcc26e40',
nestedDepth: 1,
isMultiField: false,
path: ['multifield', 'flag'],
source: {
name: 'flag',
type: 'boolean',
},
childFieldsName: 'fields',
canHaveChildFields: false,
hasChildFields: false,
canHaveMultiFields: true,
hasMultiFields: false,
isExpanded: false,
},
],
selectedOptions: [
{
label: 'Object',
'data-test-subj': 'indexDetailsMappingsSelectFilter-object',
},
{
checked: 'on',
label: 'Boolean',
'data-test-subj': 'indexDetailsMappingsSelectFilter-boolean',
},
{
label: 'Keyword',
'data-test-subj': 'indexDetailsMappingsSelectFilter-keyword',
},
{
label: 'Text',
'data-test-subj': 'indexDetailsMappingsSelectFilter-text',
},
],
selectedDataTypes: ['Boolean'],
},
inferenceToModelIdMap: {},
};
test('returns list of matching fields with search term', () => {
expect(getFieldsMatchingFilterFromState(sampleState, ['Boolean'])).toEqual({
'4459e8f2-3bec-4d17-b50c-f62d1fcbcca0': {
id: '4459e8f2-3bec-4d17-b50c-f62d1fcbcca0',
parentId: 'dd0dd3aa-52c9-472b-a23d-ecec0a1b2420',
nestedDepth: 1,
isMultiField: false,
path: ['multifield', 'flag'],
source: {
name: 'flag',
type: 'boolean',
},
childFieldsName: 'fields',
canHaveChildFields: false,
hasChildFields: false,
canHaveMultiFields: true,
hasMultiFields: false,
isExpanded: false,
},
});
});
});
});

View file

@ -19,6 +19,7 @@ import {
NormalizedField,
NormalizedFields,
NormalizedRuntimeFields,
State,
ParameterName,
RuntimeFields,
SubType,
@ -602,3 +603,85 @@ export const deNormalizeRuntimeFields = (fields: NormalizedRuntimeFields): Runti
};
}, {} as RuntimeFields);
};
/**
* get all the fields from given state which matches selected DataTypes from filter
*
* @param state The state that we are using depending on the context (when adding new fields, static state is used)
* @param filteredDataTypes data types array from which fields are filtered from given state
*/
export const getFieldsMatchingFilterFromState = (
state: State,
filteredDataTypes: string[]
): {
[id: string]: NormalizedField;
} => {
return Object.fromEntries(
Object.entries(state.fields.byId).filter(([_, fieldId]) =>
filteredDataTypes.includes(TYPE_DEFINITION[state.fields.byId[fieldId.id].source.type].label)
)
);
};
/** accepts Generics argument and returns value, if value is not null or undefined
* @param value
*/
function isNotNullish<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
/** returns normalized field that matches the dataTypes from the filteredDataTypes array
* @param normalizedFields fields that we are using, depending on the context (when adding new fields, static state is used)
* @param filteredDataTypes data types array from which fields are filtered from given state. When there are no filter selected, array would be undefined
*/
export const getFieldsFromState = (
normalizedFields: NormalizedFields,
filteredDataTypes?: string[]
): NormalizedField[] => {
const getField = (fieldId: string) => {
if (filteredDataTypes) {
if (
filteredDataTypes.includes(
TYPE_DEFINITION[normalizedFields.byId[fieldId].source.type].label
)
) {
return normalizedFields.byId[fieldId];
}
} else {
return normalizedFields.byId[fieldId];
}
};
const fields: Array<NormalizedField | undefined> = filteredDataTypes
? Object.entries(normalizedFields.byId).map(([key, _]) => getField(key))
: normalizedFields.rootLevelFields.map((id) => getField(id));
return fields.filter(isNotNullish);
};
/**
* returns true if given value is first occurence of array
* useful when filtering unique values of an array
*/
function filterUnique<T>(value: T, index: number, array: T[]) {
return array.indexOf(value) === index;
}
/**
* returns array consisting of all field types from state's fields including nested fields
* @param fields
*/
const getallFieldsIncludingNestedFields = (fields: Fields, fieldsArray: DataType[]) => {
const fieldsValue = Object.values(fields);
for (const field of fieldsValue) {
if (field.type) fieldsArray.push(field.type);
if (field.fields) getallFieldsIncludingNestedFields(field.fields, fieldsArray);
if (field.properties) getallFieldsIncludingNestedFields(field.properties, fieldsArray);
}
return fieldsArray;
};
/** returns all field types from the fields, including multifield and child fields
* @param allFields fields from state
*/
export const getAllFieldTypesFromState = (allFields: Fields): DataType[] => {
const fields: DataType[] = [];
return getallFieldsIncludingNestedFields(allFields, fields).filter(filterUnique);
};

View file

@ -54,6 +54,11 @@ export const StateProvider: React.FC<{ children?: React.ReactNode }> = ({ childr
term: '',
result: [],
},
filter: {
filteredFields: [],
selectedOptions: [],
selectedDataTypes: [],
},
inferenceToModelIdMap: {},
};

View file

@ -9,6 +9,8 @@ import { PARAMETERS_DEFINITION } from './constants';
import {
getAllChildFields,
getFieldMeta,
getFieldsFromState,
getFieldsMatchingFilterFromState,
getMaxNestedDepth,
getUniqueId,
normalize,
@ -205,6 +207,11 @@ export const reducer = (state: State, action: Action): State => {
term: '',
result: [],
},
filter: {
filteredFields: action.value.filter.filteredFields,
selectedOptions: action.value.filter.selectedOptions,
selectedDataTypes: action.value.filter.selectedDataTypes,
},
};
}
case 'configuration.update': {
@ -604,7 +611,12 @@ export const reducer = (state: State, action: Action): State => {
...state,
search: {
term: action.value,
result: searchFields(action.value, state.fields.byId),
result: searchFields(
action.value,
state.filter.selectedDataTypes.length > 0
? getFieldsMatchingFilterFromState(state, state.filter.selectedDataTypes)
: state.fields.byId
),
},
};
}
@ -614,6 +626,22 @@ export const reducer = (state: State, action: Action): State => {
isValid: action.value,
};
}
case 'filter:update': {
const selectedDataTypes: string[] = action.value.selectedOptions
.filter((option) => option.checked === 'on')
.map((option) => option.label);
return {
...state,
filter: {
filteredFields: getFieldsFromState(
state.fields,
selectedDataTypes.length > 0 ? selectedDataTypes : undefined
),
selectedOptions: action.value.selectedOptions,
selectedDataTypes,
},
};
}
case 'inferenceToModelIdMap.update': {
return {
...state,

View file

@ -5,10 +5,12 @@
* 2.0.
*/
import { EuiSelectableOption } from '@elastic/eui';
import { InferenceToModelIdMap } from '../components/document_fields/fields';
import { FormHook, OnFormUpdateArg, RuntimeField } from '../shared_imports';
import {
Field,
NormalizedField,
NormalizedFields,
NormalizedRuntimeField,
NormalizedRuntimeFields,
@ -94,6 +96,11 @@ export interface State {
format(): MappingsFields;
isValid: boolean;
};
filter: {
filteredFields: NormalizedField[];
selectedOptions: EuiSelectableOption[];
selectedDataTypes: string[];
};
search: {
term: string;
result: SearchResult[];
@ -130,6 +137,7 @@ export type Action =
| { type: 'runtimeField.edit'; value: NormalizedRuntimeField }
| { type: 'fieldsJsonEditor.update'; value: { json: { [key: string]: any }; isValid: boolean } }
| { type: 'search:update'; value: string }
| { type: 'validity:update'; value: boolean };
| { type: 'validity:update'; value: boolean }
| { type: 'filter:update'; value: { selectedOptions: EuiSelectableOption[] } };
export type Dispatch = (action: Action) => void;

View file

@ -7,6 +7,7 @@
import { useEffect, useMemo } from 'react';
import { EuiSelectableOption } from '@elastic/eui';
import {
DocumentFieldsStatus,
Field,
@ -22,8 +23,11 @@ import {
stripUndefinedValues,
normalizeRuntimeFields,
deNormalizeRuntimeFields,
getAllFieldTypesFromState,
getFieldsFromState,
} from './lib';
import { useMappingsState, useDispatch } from './mappings_state_context';
import { TYPE_DEFINITION } from './constants';
interface Args {
onChange?: OnUpdateHandler;
@ -47,6 +51,14 @@ export const useMappingsStateListener = ({ onChange, value, status }: Args) => {
() => normalizeRuntimeFields(runtimeFields),
[runtimeFields]
);
const fieldTypesOptions: EuiSelectableOption[] = useMemo(() => {
const allFieldsTypes = getAllFieldTypesFromState(deNormalize(normalize(mappedFields)));
return allFieldsTypes.map((dataType) => ({
checked: undefined,
label: TYPE_DEFINITION[dataType].label,
'data-test-subj': `indexDetailsMappingsSelectFilter-${dataType}`,
}));
}, [mappedFields]);
const calculateStatus = (fieldStatus: string | undefined, rootLevelFields: string | any[]) => {
if (fieldStatus) return fieldStatus;
@ -163,7 +175,19 @@ export const useMappingsStateListener = ({ onChange, value, status }: Args) => {
editor: 'default',
},
runtimeFields: parsedRuntimeFieldsDefaultValue,
filter: {
selectedOptions: fieldTypesOptions,
filteredFields: getFieldsFromState(parsedFieldsDefaultValue),
selectedDataTypes: [],
},
},
});
}, [value, parsedFieldsDefaultValue, dispatch, status, parsedRuntimeFieldsDefaultValue]);
}, [
value,
parsedFieldsDefaultValue,
dispatch,
status,
parsedRuntimeFieldsDefaultValue,
fieldTypesOptions,
]);
};

View file

@ -0,0 +1,160 @@
/*
* 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 {
EuiFilterButton,
EuiFilterGroup,
EuiPopover,
EuiPopoverTitle,
EuiSelectable,
EuiSelectableOption,
} from '@elastic/eui';
import React, { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { useDispatch } from '../../../../components/mappings_editor/mappings_state_context';
import { State } from '../../../../components/mappings_editor/types';
import {
getFieldsFromState,
getFieldsMatchingFilterFromState,
searchFields,
} from '../../../../components/mappings_editor/lib';
interface Props {
isAddingFields: boolean;
isJSONVisible: boolean;
previousState: State;
setPreviousState: (state: State) => void;
state: State;
}
export const MappingsFilter: React.FC<Props> = ({
isAddingFields,
isJSONVisible,
previousState,
setPreviousState,
state,
}) => {
const [isFilterByPopoverVisible, setIsFilterPopoverVisible] = useState<boolean>(false);
const dispatch = useDispatch();
const setSelectedOptions = useCallback(
(options) => {
dispatch({
type: 'filter:update',
value: {
selectedOptions: options,
},
});
dispatch({
type: 'search:update',
value: state.search.term,
});
},
[dispatch, state.search.term]
);
const setPreviousStateSelectedOptions = useCallback(
(options: EuiSelectableOption[]) => {
const selectedDataTypes: string[] = options
.filter((option) => option.checked === 'on')
.map((option) => option.label);
setPreviousState({
...previousState,
filter: {
filteredFields: getFieldsFromState(
previousState.fields,
selectedDataTypes.length > 0 ? selectedDataTypes : undefined
),
selectedOptions: options,
selectedDataTypes,
},
search: {
term: previousState.search.term,
result: searchFields(
previousState.search.term,
selectedDataTypes.length > 0
? getFieldsMatchingFilterFromState(previousState, selectedDataTypes)
: previousState.fields.byId
),
},
});
},
[previousState, setPreviousState]
);
const filterByFieldTypeButton = (
<EuiFilterButton
iconType="arrowDown"
iconSide="right"
isDisabled={isJSONVisible}
onClick={() => setIsFilterPopoverVisible(!isFilterByPopoverVisible)}
numFilters={
!isAddingFields
? state.filter.selectedOptions.length
: previousState.filter.selectedOptions.length
}
hasActiveFilters={
!isAddingFields
? state.filter.selectedDataTypes.length > 0
: previousState.filter.selectedDataTypes.length > 0
}
numActiveFilters={
!isAddingFields
? state.filter.selectedDataTypes.length
: previousState.filter.selectedDataTypes.length
}
isSelected={isFilterByPopoverVisible}
data-test-subj="indexDetailsMappingsFilterByFieldTypeButton"
>
{i18n.translate('xpack.idxMgmt.indexDetails.mappings.filterByFieldType.button', {
defaultMessage: 'Field types',
})}
</EuiFilterButton>
);
return (
<EuiFilterGroup>
<EuiPopover
button={filterByFieldTypeButton}
isOpen={isFilterByPopoverVisible}
closePopover={() => setIsFilterPopoverVisible(!isFilterByPopoverVisible)}
anchorPosition="downCenter"
data-test-subj="indexDetailsMappingsFilter"
>
<EuiSelectable
searchable
data-test-subj="filterItem"
searchProps={{
placeholder: i18n.translate(
'xpack.idxMgmt.indexDetails.mappings.filterByFieldType.searchPlaceholder',
{
defaultMessage: 'Filter list ',
}
),
}}
options={
!isAddingFields ? state.filter.selectedOptions : previousState.filter.selectedOptions
}
onChange={(options) => {
if (!isAddingFields) {
setSelectedOptions(options);
} else {
setPreviousStateSelectedOptions(options);
}
}}
>
{(list, search) => (
<div style={{ width: 200 }}>
<EuiPopoverTitle
paddingSize="s"
data-test-subj="indexDetailsMappingsFilterByFieldTypeSearch"
>
{search}
</EuiPopoverTitle>
{list}
</div>
)}
</EuiSelectable>
</EuiPopover>
</EuiFilterGroup>
);
};

View file

@ -42,10 +42,12 @@ import {
useMappingsState,
} from '../../../../components/mappings_editor/mappings_state_context';
import {
NormalizedField,
NormalizedFields,
State,
} from '../../../../components/mappings_editor/types';
getFieldsFromState,
getFieldsMatchingFilterFromState,
} from '../../../../components/mappings_editor/lib';
import { NormalizedFields, State } from '../../../../components/mappings_editor/types';
import { MappingsFilter } from './details_page_filter_fields';
import { useMappingsStateListener } from '../../../../components/mappings_editor/use_state_listener';
import { documentationService } from '../../../../services';
import { updateIndexMappings } from '../../../../services/api';
@ -54,16 +56,6 @@ import { SemanticTextBanner } from './semantic_text_banner';
import { TrainedModelsDeploymentModal } from './trained_models_deployment_modal';
import { parseMappings } from '../../../../shared/parse_mappings';
const getFieldsFromState = (state: State) => {
const getField = (fieldId: string) => {
return state.fields.byId[fieldId];
};
const fields = () => {
return state.fields.rootLevelFields.map((id) => getField(id));
};
return fields();
};
export const DetailsPageMappingsContent: FunctionComponent<{
index: Index;
data: string;
@ -111,9 +103,13 @@ export const DetailsPageMappingsContent: FunctionComponent<{
}, [state.fields.byId]);
const [previousState, setPreviousState] = useState<State>(state);
const [previousStateFields, setPreviousStateFields] = useState<NormalizedField[]>(
getFieldsFromState(state)
);
const previousStateSelectedDataTypes: string[] = useMemo(() => {
return previousState.filter.selectedOptions
.filter((option) => option.checked === 'on')
.map((option) => option.label);
}, [previousState.filter.selectedOptions]);
const [saveMappingError, setSaveMappingError] = useState<string | undefined>(undefined);
const [isJSONVisible, setIsJSONVisible] = useState<boolean>(false);
const onToggleChange = () => {
@ -151,7 +147,6 @@ export const DetailsPageMappingsContent: FunctionComponent<{
setAddingFields(!isAddingFields);
// when adding new field, save previous state. This state is then used by FieldsList component to show only saved mappings.
setPreviousStateFields(getFieldsFromState(state));
setPreviousState(state);
// reset mappings and change status to create field.
@ -160,6 +155,11 @@ export const DetailsPageMappingsContent: FunctionComponent<{
value: {
...state,
fields: { ...state.fields, byId: {}, rootLevelFields: [] } as NormalizedFields,
filter: {
filteredFields: [],
selectedOptions: [],
selectedDataTypes: [],
},
documentFields: {
status: 'creatingField',
editor: 'default',
@ -235,16 +235,39 @@ export const DetailsPageMappingsContent: FunctionComponent<{
...previousState,
search: {
term: value,
result: searchFields(value, previousState.fields.byId),
result: searchFields(
value,
previousStateSelectedDataTypes.length > 0
? getFieldsMatchingFilterFromState(previousState, previousStateSelectedDataTypes)
: previousState.fields.byId
),
},
});
} else {
dispatch({ type: 'search:update', value });
}
},
[dispatch, previousState, isAddingFields]
[dispatch, previousState, isAddingFields, previousStateSelectedDataTypes]
);
const onClearSearch = useCallback(() => {
setPreviousState({
...previousState,
search: {
term: '',
result: searchFields(
'',
previousState.filter.selectedDataTypes.length > 0
? getFieldsMatchingFilterFromState(
previousState,
previousState.filter.selectedDataTypes
)
: previousState.fields.byId
),
},
});
}, [previousState]);
const searchTerm = isAddingFields ? previousState.search.term.trim() : state.search.term.trim();
const jsonBlock = (
@ -263,6 +286,7 @@ export const DetailsPageMappingsContent: FunctionComponent<{
<SearchResult
result={previousState.search.result}
documentFieldsState={previousState.documentFields}
onClearSearch={onClearSearch}
/>
) : (
<SearchResult result={state.search.result} documentFieldsState={state.documentFields} />
@ -270,13 +294,25 @@ export const DetailsPageMappingsContent: FunctionComponent<{
const fieldsListComponent = isAddingFields ? (
<FieldsList
fields={previousStateFields}
fields={
previousStateSelectedDataTypes.length > 0
? previousState.filter.filteredFields
: getFieldsFromState(previousState.fields)
}
state={previousState}
setPreviousState={setPreviousState}
isAddingFields={isAddingFields}
/>
) : (
<FieldsList fields={getFieldsFromState(state)} state={state} isAddingFields={isAddingFields} />
<FieldsList
fields={
state.filter.selectedDataTypes.length > 0
? state.filter.filteredFields
: getFieldsFromState(state.fields)
}
state={state}
isAddingFields={isAddingFields}
/>
);
const fieldSearchComponent = isAddingFields ? (
<DocumentFieldsSearch
@ -391,6 +427,15 @@ export const DetailsPageMappingsContent: FunctionComponent<{
)}
<EuiFlexGroup direction="column">
<EuiFlexGroup gutterSize="s" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<MappingsFilter
isAddingFields={isAddingFields}
isJSONVisible={isJSONVisible}
previousState={previousState}
setPreviousState={setPreviousState}
state={state}
/>
</EuiFlexItem>
<EuiFlexItem>{fieldSearchComponent}</EuiFlexItem>
{!index.hidden && (
<EuiFlexItem grow={false}>