mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
fe41c27995
commit
15d2235a73
12 changed files with 819 additions and 29 deletions
|
@ -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');
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -27,6 +27,9 @@ export {
|
|||
stripUndefinedValues,
|
||||
normalizeRuntimeFields,
|
||||
deNormalizeRuntimeFields,
|
||||
getAllFieldTypesFromState,
|
||||
getFieldsFromState,
|
||||
getFieldsMatchingFilterFromState,
|
||||
} from './utils';
|
||||
|
||||
export * from './serializers';
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -54,6 +54,11 @@ export const StateProvider: React.FC<{ children?: React.ReactNode }> = ({ childr
|
|||
term: '',
|
||||
result: [],
|
||||
},
|
||||
filter: {
|
||||
filteredFields: [],
|
||||
selectedOptions: [],
|
||||
selectedDataTypes: [],
|
||||
},
|
||||
inferenceToModelIdMap: {},
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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}>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue