mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
eaf674db88
commit
a8aa215db5
9 changed files with 463 additions and 76 deletions
|
@ -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(),
|
||||
};
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -120,6 +120,7 @@ export class UnifiedDocViewerPublicPlugin
|
|||
uiSettings,
|
||||
unifiedDocViewer,
|
||||
share,
|
||||
core,
|
||||
};
|
||||
setUnifiedDocViewerServices(services);
|
||||
return unifiedDocViewer;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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/**/*",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue