From a8aa215db5b26b1dcdb049231dad2ae787167048 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 9 Aug 2024 17:08:19 +0200 Subject: [PATCH] [OneDiscover][UnifiedDocViewer] Allow filtering by field type (#189981) - Closes https://github.com/elastic/kibana/issues/188733 ## Summary This PR adds Field type filter to Doc Viewer (same as in UnifiedFieldList as discussed with @MichaelMarcialis). The selected field types would be persisted in Local Storage under `unifiedDocViewer:selectedFieldTypes` key. Screenshot 2024-08-07 at 16 52 46 ### Checklist - [x] [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 - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../public/__mocks__/services.ts | 2 + .../components/doc_viewer_table/table.tsx | 138 +++++++------- .../doc_viewer_table/table_filters.tsx | 171 ++++++++++++++++++ .../doc_viewer_table/test_filters.test.ts | 128 +++++++++++++ .../unified_doc_viewer/public/plugin.tsx | 1 + .../unified_doc_viewer/public/types.ts | 3 + src/plugins/unified_doc_viewer/tsconfig.json | 4 +- .../apps/discover/group3/_doc_viewer.ts | 79 ++++++++ test/functional/page_objects/discover_page.ts | 13 ++ 9 files changed, 463 insertions(+), 76 deletions(-) create mode 100644 src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_filters.tsx create mode 100644 src/plugins/unified_doc_viewer/public/components/doc_viewer_table/test_filters.test.ts diff --git a/src/plugins/unified_doc_viewer/public/__mocks__/services.ts b/src/plugins/unified_doc_viewer/public/__mocks__/services.ts index 5e7222f261532..81e2f084282f7 100644 --- a/src/plugins/unified_doc_viewer/public/__mocks__/services.ts +++ b/src/plugins/unified_doc_viewer/public/__mocks__/services.ts @@ -8,6 +8,7 @@ import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { coreMock } from '@kbn/core/public/mocks'; import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; import { fieldsMetadataPluginPublicMock } from '@kbn/fields-metadata-plugin/public/mocks'; import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; @@ -29,4 +30,5 @@ export const mockUnifiedDocViewerServices: jest.Mocked uiSettings: uiSettingsServiceMock.createStartContract(), unifiedDocViewer: mockUnifiedDocViewer, share: sharePluginMock.createStartContract(), + core: coreMock.createStart(), }; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx index 283c00eabae27..97330ac8ce1a0 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx @@ -13,7 +13,6 @@ import useLocalStorage from 'react-use/lib/useLocalStorage'; import { EuiFlexGroup, EuiFlexItem, - EuiFieldSearch, EuiSpacer, EuiSelectableMessage, EuiDataGrid, @@ -28,7 +27,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; -import { debounce } from 'lodash'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { getFieldIconType } from '@kbn/field-utils/src/utils/get_field_icon_type'; import { @@ -41,7 +39,6 @@ import { } from '@kbn/discover-utils'; import { FieldDescription, - fieldNameWildcardMatcher, getFieldSearchMatchingHighlight, getTextBasedColumnIconType, } from '@kbn/field-utils'; @@ -60,12 +57,14 @@ import { DEFAULT_MARGIN_BOTTOM, getTabContentAvailableHeight, } from '../doc_viewer_source/get_height'; +import { TableFilters, TableFiltersProps, useTableFilters } from './table_filters'; export type FieldRecord = TableRow; interface ItemsEntry { pinnedItems: FieldRecord[]; restItems: FieldRecord[]; + allFields: TableFiltersProps['allFields']; } const MIN_NAME_COLUMN_WIDTH = 150; @@ -74,7 +73,6 @@ const PAGE_SIZE_OPTIONS = [25, 50, 100, 250, 500]; const DEFAULT_PAGE_SIZE = 25; const PINNED_FIELDS_KEY = 'discover:pinnedFields'; const PAGE_SIZE = 'discover:pageSize'; -const SEARCH_TEXT = 'discover:searchText'; const HIDE_NULL_VALUES = 'unifiedDocViewer:hideNullValues'; const GRID_COLUMN_FIELD_NAME = 'name'; @@ -126,14 +124,6 @@ const updatePageSize = (newPageSize: number, storage: Storage) => { storage.set(PAGE_SIZE, newPageSize); }; -const getSearchText = (storage: Storage) => { - return storage.get(SEARCH_TEXT) || ''; -}; -const updateSearchText = debounce( - (newSearchText: string, storage: Storage) => storage.set(SEARCH_TEXT, newSearchText), - 500 -); - export const DocViewerTable = ({ columns, columnsMeta, @@ -151,7 +141,6 @@ export const DocViewerTable = ({ const showMultiFields = uiSettings.get(SHOW_MULTIFIELDS); const currentDataViewId = dataView.id!; - const [searchText, setSearchText] = useState(getSearchText(storage)); const [pinnedFields, setPinnedFields] = useState( getPinnedFields(currentDataViewId, storage) ); @@ -165,10 +154,6 @@ export const DocViewerTable = ({ [flattened, dataView, showMultiFields] ); - const searchPlaceholder = i18n.translate('unifiedDocViewer.docView.table.searchPlaceHolder', { - defaultMessage: 'Search field names', - }); - const mapping = useCallback((name: string) => dataView.fields.getByName(name), [dataView.fields]); const onToggleColumn = useMemo(() => { @@ -196,14 +181,7 @@ export const DocViewerTable = ({ [currentDataViewId, pinnedFields, storage] ); - const onSearch = useCallback( - (event: React.ChangeEvent) => { - const newSearchText = event.currentTarget.value; - updateSearchText(newSearchText, storage); - setSearchText(newSearchText); - }, - [storage] - ); + const { onFilterField, ...tableFiltersProps } = useTableFilters(storage); const fieldToItem = useCallback( (field: string, isPinned: boolean) => { @@ -261,47 +239,64 @@ export const DocViewerTable = ({ ] ); - const { pinnedItems, restItems } = Object.keys(flattened) - .sort((fieldA, fieldB) => { - const mappingA = mapping(fieldA); - const mappingB = mapping(fieldB); - const nameA = !mappingA || !mappingA.displayName ? fieldA : mappingA.displayName; - const nameB = !mappingB || !mappingB.displayName ? fieldB : mappingB.displayName; - return nameA.localeCompare(nameB); - }) - .reduce( - (acc, curFieldName) => { - if (!shouldShowFieldHandler(curFieldName)) { - return acc; - } - const shouldHideNullValue = - areNullValuesHidden && flattened[curFieldName] == null && isEsqlMode; - if (shouldHideNullValue) { - return acc; - } - if (pinnedFields.includes(curFieldName)) { - acc.pinnedItems.push(fieldToItem(curFieldName, true)); - } else { - const fieldMapping = mapping(curFieldName); - if ( - !searchText?.trim() || - fieldNameWildcardMatcher( - { name: curFieldName, displayName: fieldMapping?.displayName }, - searchText - ) - ) { - // filter only unpinned fields - acc.restItems.push(fieldToItem(curFieldName, false)); + const { pinnedItems, restItems, allFields } = useMemo( + () => + Object.keys(flattened) + .sort((fieldA, fieldB) => { + const mappingA = mapping(fieldA); + const mappingB = mapping(fieldB); + const nameA = !mappingA || !mappingA.displayName ? fieldA : mappingA.displayName; + const nameB = !mappingB || !mappingB.displayName ? fieldB : mappingB.displayName; + return nameA.localeCompare(nameB); + }) + .reduce( + (acc, curFieldName) => { + if (!shouldShowFieldHandler(curFieldName)) { + return acc; + } + const shouldHideNullValue = + areNullValuesHidden && flattened[curFieldName] == null && isEsqlMode; + if (shouldHideNullValue) { + return acc; + } + + const isPinned = pinnedFields.includes(curFieldName); + const row = fieldToItem(curFieldName, isPinned); + + if (isPinned) { + acc.pinnedItems.push(row); + } else { + if (onFilterField(curFieldName, row.field.displayName, row.field.fieldType)) { + // filter only unpinned fields + acc.restItems.push(row); + } + } + + acc.allFields.push({ + name: curFieldName, + displayName: row.field.displayName, + type: row.field.fieldType, + }); + + return acc; + }, + { + pinnedItems: [], + restItems: [], + allFields: [], } - } - - return acc; - }, - { - pinnedItems: [], - restItems: [], - } - ); + ), + [ + areNullValuesHidden, + fieldToItem, + flattened, + isEsqlMode, + mapping, + onFilterField, + pinnedFields, + shouldShowFieldHandler, + ] + ); const rows = useMemo(() => [...pinnedItems, ...restItems], [pinnedItems, restItems]); @@ -402,7 +397,7 @@ export const DocViewerTable = ({ scripted={scripted} highlight={getFieldSearchMatchingHighlight( fieldMapping?.displayName ?? field, - searchText + tableFiltersProps.searchTerm )} isPinned={pinned} /> @@ -433,7 +428,7 @@ export const DocViewerTable = ({ return null; }, - [rows, searchText, fieldsMetadata] + [rows, tableFiltersProps.searchTerm, fieldsMetadata] ); const renderCellPopover = useCallback( @@ -489,14 +484,7 @@ export const DocViewerTable = ({ - + {rows.length === 0 ? ( diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_filters.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_filters.tsx new file mode 100644 index 0000000000000..0a030336571e8 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_filters.tsx @@ -0,0 +1,171 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useState, useMemo } from 'react'; +import { EuiFieldSearch } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { debounce } from 'lodash'; +import { fieldNameWildcardMatcher, type FieldTypeKnown } from '@kbn/field-utils'; +import type { FieldListItem } from '@kbn/unified-field-list'; +import { + FieldTypeFilter, + type FieldTypeFilterProps, +} from '@kbn/unified-field-list/src/components/field_list_filters/field_type_filter'; +import { getUnifiedDocViewerServices } from '../../plugin'; + +export const LOCAL_STORAGE_KEY_SEARCH_TERM = 'discover:searchText'; +export const LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES = 'unifiedDocViewer:selectedFieldTypes'; + +const searchPlaceholder = i18n.translate('unifiedDocViewer.docView.table.searchPlaceHolder', { + defaultMessage: 'Search field names', +}); + +interface TableFiltersCommonProps { + // search + searchTerm: string; + onChangeSearchTerm: (searchTerm: string) => void; + // field types + selectedFieldTypes: FieldTypeFilterProps['selectedFieldTypes']; + onChangeFieldTypes: FieldTypeFilterProps['onChange']; +} + +export interface TableFiltersProps extends TableFiltersCommonProps { + allFields: FieldListItem[]; +} + +export const TableFilters: React.FC = ({ + searchTerm, + onChangeSearchTerm, + selectedFieldTypes, + onChangeFieldTypes, + allFields, +}) => { + const { core } = getUnifiedDocViewerServices(); + + const onSearchTermChange = useCallback( + (event: React.ChangeEvent) => { + const newSearchTerm = event.currentTarget.value; + onChangeSearchTerm(newSearchTerm); + }, + [onChangeSearchTerm] + ); + + return ( + + ) : undefined + } + /> + ); +}; + +const persistSearchTerm = debounce( + (newSearchText: string, storage: Storage) => + storage.set(LOCAL_STORAGE_KEY_SEARCH_TERM, newSearchText), + 500, + { leading: true, trailing: true } +); + +const persistSelectedFieldTypes = debounce( + (selectedFieldTypes: FieldTypeKnown[], storage: Storage) => + storage.set(LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES, JSON.stringify(selectedFieldTypes)), + 500, + { leading: true, trailing: true } +); + +const getStoredFieldTypes = (storage: Storage) => { + const storedFieldTypes = storage.get(LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES); + let parsedFieldTypes: FieldTypeKnown[] = []; + + try { + parsedFieldTypes = storedFieldTypes ? JSON.parse(storedFieldTypes) : []; + } catch { + // ignore invalid JSON + } + + return Array.isArray(parsedFieldTypes) ? parsedFieldTypes : []; +}; + +interface UseTableFiltersReturn extends TableFiltersCommonProps { + onFilterField: ( + fieldName: string, + fieldDisplayName: string | undefined, + fieldType: string | undefined + ) => boolean; +} + +export const useTableFilters = (storage: Storage): UseTableFiltersReturn => { + const [searchTerm, setSearchTerm] = useState(storage.get(LOCAL_STORAGE_KEY_SEARCH_TERM) || ''); + const [selectedFieldTypes, setSelectedFieldTypes] = useState( + getStoredFieldTypes(storage) + ); + + const onChangeSearchTerm = useCallback( + (newSearchTerm: string) => { + setSearchTerm(newSearchTerm); + persistSearchTerm(newSearchTerm, storage); + }, + [storage, setSearchTerm] + ); + + const onChangeFieldTypes = useCallback( + (newFieldTypes: FieldTypeKnown[]) => { + setSelectedFieldTypes(newFieldTypes); + persistSelectedFieldTypes(newFieldTypes, storage); + }, + [storage, setSelectedFieldTypes] + ); + + const onFilterField: UseTableFiltersReturn['onFilterField'] = useCallback( + (fieldName, fieldDisplayName, fieldType) => { + const term = searchTerm?.trim(); + if ( + term && + !fieldNameWildcardMatcher({ name: fieldName, displayName: fieldDisplayName }, term) + ) { + return false; + } + + if (selectedFieldTypes.length > 0 && fieldType) { + return selectedFieldTypes.includes(fieldType); + } + + return true; + }, + [searchTerm, selectedFieldTypes] + ); + + return useMemo( + () => ({ + // props for TableFilters component + searchTerm, + onChangeSearchTerm, + selectedFieldTypes, + onChangeFieldTypes, + // the actual filtering function + onFilterField, + }), + [searchTerm, onChangeSearchTerm, selectedFieldTypes, onChangeFieldTypes, onFilterField] + ); +}; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/test_filters.test.ts b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/test_filters.test.ts new file mode 100644 index 0000000000000..77895ab4f9179 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/test_filters.test.ts @@ -0,0 +1,128 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { + useTableFilters, + LOCAL_STORAGE_KEY_SEARCH_TERM, + LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES, +} from './table_filters'; + +const storage = new Storage(window.localStorage); + +describe('useTableFilters', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + + afterEach(() => { + storage.clear(); + }); + + it('should return initial search term and field types', () => { + const { result } = renderHook(() => useTableFilters(storage)); + + expect(result.current.searchTerm).toBe(''); + expect(result.current.selectedFieldTypes).toEqual([]); + expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(true); + expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(true); + + expect(storage.get(LOCAL_STORAGE_KEY_SEARCH_TERM)).toBeNull(); + }); + + it('should filter by search term', () => { + const { result } = renderHook(() => useTableFilters(storage)); + + act(() => { + result.current.onChangeSearchTerm('ext'); + }); + + expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(true); + expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(false); + + expect(storage.get(LOCAL_STORAGE_KEY_SEARCH_TERM)).toBe('ext'); + }); + + it('should filter by field type', () => { + const { result } = renderHook(() => useTableFilters(storage)); + + act(() => { + result.current.onChangeFieldTypes(['number']); + }); + + expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(false); + expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(true); + + act(() => { + result.current.onChangeFieldTypes(['keyword']); + }); + + expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(true); + expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(false); + + act(() => { + result.current.onChangeFieldTypes(['number', 'keyword']); + }); + + expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(true); + expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(true); + + jest.advanceTimersByTime(600); + expect(storage.get(LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES)).toBe('["number","keyword"]'); + }); + + it('should filter by search term and field type', () => { + const { result } = renderHook(() => useTableFilters(storage)); + + act(() => { + result.current.onChangeSearchTerm('ext'); + result.current.onChangeFieldTypes(['keyword']); + }); + + expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(true); + expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(false); + + act(() => { + result.current.onChangeSearchTerm('ext'); + result.current.onChangeFieldTypes(['number']); + }); + + expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(false); + expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(false); + + act(() => { + result.current.onChangeSearchTerm('bytes'); + result.current.onChangeFieldTypes(['number']); + }); + + expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(false); + expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(true); + + jest.advanceTimersByTime(600); + expect(storage.get(LOCAL_STORAGE_KEY_SEARCH_TERM)).toBe('bytes'); + expect(storage.get(LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES)).toBe('["number"]'); + }); + + it('should restore previous filters', () => { + storage.set(LOCAL_STORAGE_KEY_SEARCH_TERM, 'bytes'); + storage.set(LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES, '["number"]'); + + const { result } = renderHook(() => useTableFilters(storage)); + + expect(result.current.searchTerm).toBe('bytes'); + expect(result.current.selectedFieldTypes).toEqual(['number']); + + expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(false); + expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(true); + expect(result.current.onFilterField('bytes_counter', undefined, 'counter')).toBe(false); + }); +}); diff --git a/src/plugins/unified_doc_viewer/public/plugin.tsx b/src/plugins/unified_doc_viewer/public/plugin.tsx index e8a23342e7976..d1e9d1cb86b18 100644 --- a/src/plugins/unified_doc_viewer/public/plugin.tsx +++ b/src/plugins/unified_doc_viewer/public/plugin.tsx @@ -120,6 +120,7 @@ export class UnifiedDocViewerPublicPlugin uiSettings, unifiedDocViewer, share, + core, }; setUnifiedDocViewerServices(services); return unifiedDocViewer; diff --git a/src/plugins/unified_doc_viewer/public/types.ts b/src/plugins/unified_doc_viewer/public/types.ts index 9266328306aaf..42a562944c1e1 100644 --- a/src/plugins/unified_doc_viewer/public/types.ts +++ b/src/plugins/unified_doc_viewer/public/types.ts @@ -5,10 +5,12 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + export type { JsonCodeEditorProps } from './components'; export type { EsDocSearchProps } from './hooks'; export type { UnifiedDocViewerSetup, UnifiedDocViewerStart } from './plugin'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; @@ -27,4 +29,5 @@ export interface UnifiedDocViewerServices { uiSettings: IUiSettingsClient; unifiedDocViewer: UnifiedDocViewerStart; share: SharePluginStart; + core: CoreStart; } diff --git a/src/plugins/unified_doc_viewer/tsconfig.json b/src/plugins/unified_doc_viewer/tsconfig.json index 3b271744ed4af..ef3a7a91153ac 100644 --- a/src/plugins/unified_doc_viewer/tsconfig.json +++ b/src/plugins/unified_doc_viewer/tsconfig.json @@ -35,7 +35,9 @@ "@kbn/core-notifications-browser", "@kbn/deeplinks-observability", "@kbn/share-plugin", - "@kbn/router-utils" + "@kbn/router-utils", + "@kbn/unified-field-list", + "@kbn/core-lifecycle-browser" ], "exclude": [ "target/**/*", diff --git a/test/functional/apps/discover/group3/_doc_viewer.ts b/test/functional/apps/discover/group3/_doc_viewer.ts index 2ce59a332cf67..66f1f74a4ddbe 100644 --- a/test/functional/apps/discover/group3/_doc_viewer.ts +++ b/test/functional/apps/discover/group3/_doc_viewer.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -111,6 +112,84 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); + describe('filter by field type', function () { + beforeEach(async () => { + await dataGrid.clickRowToggle(); + await PageObjects.discover.isShowingDocViewer(); + await retry.waitFor('rendered items', async () => { + return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length > 0; + }); + }); + + it('should reveal and hide the filter form when the toggle is clicked', async function () { + await PageObjects.discover.openFilterByFieldTypeInDocViewer(); + expect(await find.allByCssSelector('[data-test-subj*="typeFilter"]')).to.have.length(6); + await PageObjects.discover.closeFilterByFieldTypeInDocViewer(); + }); + + it('should filter by field type', async function () { + const initialFieldsCount = (await find.allByCssSelector('.kbnDocViewer__fieldName')).length; + + await PageObjects.discover.openFilterByFieldTypeInDocViewer(); + + await testSubjects.click('typeFilter-date'); + + await retry.waitFor('first updates', async () => { + return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 4; + }); + + await testSubjects.click('typeFilter-number'); + + await retry.waitFor('second updates', async () => { + return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 7; + }); + + await testSubjects.click('unifiedDocViewerFieldsTableFieldTypeFilterClearAll'); + + await retry.waitFor('reset', async () => { + return ( + (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === initialFieldsCount + ); + }); + }); + + it('should show filters by type in ES|QL view', async function () { + await PageObjects.discover.selectTextBaseLang(); + + const testQuery = `from logstash-* | limit 10000`; + await monacoEditor.setCodeEditorValue(testQuery); + await testSubjects.click('querySubmitButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + await dataGrid.clickRowToggle(); + await PageObjects.discover.isShowingDocViewer(); + await retry.waitFor('rendered items', async () => { + return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length > 0; + }); + + const initialFieldsCount = (await find.allByCssSelector('.kbnDocViewer__fieldName')).length; + const numberFieldsCount = 6; + + expect(initialFieldsCount).to.above(numberFieldsCount); + + const pinnedFieldsCount = 1; + await dataGrid.clickFieldActionInFlyout('agent', 'togglePinFilterButton'); + + await PageObjects.discover.openFilterByFieldTypeInDocViewer(); + expect(await find.allByCssSelector('[data-test-subj*="typeFilter"]')).to.have.length(6); + + await testSubjects.click('typeFilter-number'); + + await retry.waitFor('updates', async () => { + return ( + (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === + numberFieldsCount + pinnedFieldsCount + ); + }); + }); + }); + describe('hide null values switch - ES|QL mode', function () { beforeEach(async () => { await PageObjects.discover.selectTextBaseLang(); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index c00e2c311e68c..3d364d9ff2c3e 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -448,6 +448,19 @@ export class DiscoverPageObject extends FtrService { await fieldSearch.type(name); } + public async openFilterByFieldTypeInDocViewer() { + await this.testSubjects.click('unifiedDocViewerFieldsTableFieldTypeFilterToggle'); + await this.testSubjects.existOrFail('unifiedDocViewerFieldsTableFieldTypeFilterOptions'); + } + + public async closeFilterByFieldTypeInDocViewer() { + await this.testSubjects.click('unifiedDocViewerFieldsTableFieldTypeFilterToggle'); + + await this.retry.waitFor('doc viewer filter closed', async () => { + return !(await this.testSubjects.exists('unifiedDocViewerFieldsTableFieldTypeFilterOptions')); + }); + } + public async getMarks() { const table = await this.docTable.getTable(); const marks = await table.findAllByTagName('mark');