[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.

<img width="685" alt="Screenshot 2024-08-07 at 16 52 46"
src="https://github.com/user-attachments/assets/7591aa69-c1b4-4485-ad9f-baac809d7fe5">



### 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>
This commit is contained in:
Julia Rechkunova 2024-08-09 17:08:19 +02:00 committed by GitHub
parent eaf674db88
commit a8aa215db5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 463 additions and 76 deletions

View file

@ -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<UnifiedDocViewerServices>
uiSettings: uiSettingsServiceMock.createStartContract(),
unifiedDocViewer: mockUnifiedDocViewer,
share: sharePluginMock.createStartContract(),
core: coreMock.createStart(),
};

View file

@ -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<string[]>(
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<HTMLInputElement>) => {
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<ItemsEntry>(
(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<ItemsEntry>(
(acc, curFieldName) => {
if (!shouldShowFieldHandler(curFieldName)) {
return acc;
}
const shouldHideNullValue =
areNullValuesHidden && flattened[curFieldName] == null && isEsqlMode;
if (shouldHideNullValue) {
return acc;
}
return acc;
},
{
pinnedItems: [],
restItems: [],
}
);
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: [],
}
),
[
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 = ({
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFieldSearch
aria-label={searchPlaceholder}
fullWidth
onChange={onSearch}
placeholder={searchPlaceholder}
value={searchText}
data-test-subj="unifiedDocViewerFieldsSearchInput"
/>
<TableFilters {...tableFiltersProps} allFields={allFields} />
</EuiFlexItem>
{rows.length === 0 ? (

View file

@ -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<FieldListItem>['selectedFieldTypes'];
onChangeFieldTypes: FieldTypeFilterProps<FieldListItem>['onChange'];
}
export interface TableFiltersProps extends TableFiltersCommonProps {
allFields: FieldListItem[];
}
export const TableFilters: React.FC<TableFiltersProps> = ({
searchTerm,
onChangeSearchTerm,
selectedFieldTypes,
onChangeFieldTypes,
allFields,
}) => {
const { core } = getUnifiedDocViewerServices();
const onSearchTermChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const newSearchTerm = event.currentTarget.value;
onChangeSearchTerm(newSearchTerm);
},
[onChangeSearchTerm]
);
return (
<EuiFieldSearch
data-test-subj="unifiedDocViewerFieldsSearchInput"
aria-label={searchPlaceholder}
placeholder={searchPlaceholder}
fullWidth
compressed
value={searchTerm}
onChange={onSearchTermChange}
append={
allFields && selectedFieldTypes && onChangeFieldTypes ? (
<FieldTypeFilter
data-test-subj="unifiedDocViewerFieldsTable"
docLinks={core.docLinks}
selectedFieldTypes={selectedFieldTypes}
allFields={allFields}
onChange={onChangeFieldTypes}
/>
) : 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<FieldTypeKnown[]>(
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]
);
};

View file

@ -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);
});
});

View file

@ -120,6 +120,7 @@ export class UnifiedDocViewerPublicPlugin
uiSettings,
unifiedDocViewer,
share,
core,
};
setUnifiedDocViewerServices(services);
return unifiedDocViewer;

View file

@ -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;
}

View file

@ -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/**/*",

View file

@ -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();

View file

@ -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');