diff --git a/packages/kbn-unified-data-table/src/types.ts b/packages/kbn-unified-data-table/src/types.ts index ca5f4f8a838825d..372cff4dd642d5c 100644 --- a/packages/kbn-unified-data-table/src/types.ts +++ b/packages/kbn-unified-data-table/src/types.ts @@ -12,7 +12,7 @@ import type { EuiDataGridCellValueElementProps, EuiDataGridColumn } from '@elast import type { DataTableRecord } from '@kbn/discover-utils/src/types'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -export type { DataTableColumnsMeta } from '@kbn/discover-utils/src/types'; +export type { DataTableColumnsMeta } from '@kbn/discover-utils/types'; export type { DataGridDensity } from './constants'; /** diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap index aecef4797f9bdf2..a71babb2977d110 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap @@ -4,42 +4,14 @@ exports[`TableActions getFieldCellActions should render correctly for undefined Array [ , @@ -52,83 +24,27 @@ exports[`TableActions getFieldCellActions should render the panels correctly for Array [ , , @@ -141,83 +57,27 @@ exports[`TableActions getFieldValueCellActions should render the panels correctl Array [ , , diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/field_row.ts b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/field_row.ts new file mode 100644 index 000000000000000..d020692bf4871fa --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/field_row.ts @@ -0,0 +1,129 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import type { DataTableColumnsMeta, DataTableRecord } from '@kbn/discover-utils/types'; +import { + formatFieldValue, + getIgnoredReason, + IgnoredReason, + isNestedFieldParent, +} from '@kbn/discover-utils'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { getFieldIconType, getTextBasedColumnIconType } from '@kbn/field-utils'; + +export class FieldRow { + readonly name: string; + readonly flattenedValue: unknown; + readonly dataViewField: DataViewField | undefined; + readonly isPinned: boolean; + readonly columnsMeta: DataTableColumnsMeta | undefined; + + readonly #hit: DataTableRecord; + readonly #dataView: DataView; + readonly #fieldFormats: FieldFormatsStart; + + #isFormattedAsHtml: boolean; + #isFormattedAsText: boolean; + + #formattedAsHtml: string | undefined; + #formattedAsText: string | undefined; + + #fieldType: string | undefined; + + constructor({ + name, + flattenedValue, + hit, + dataView, + fieldFormats, + isPinned, + columnsMeta, + }: { + name: string; + flattenedValue: unknown; + hit: DataTableRecord; + dataView: DataView; + fieldFormats: FieldFormatsStart; + isPinned: boolean; + columnsMeta: DataTableColumnsMeta | undefined; + }) { + this.#hit = hit; + this.#dataView = dataView; + this.#fieldFormats = fieldFormats; + this.#isFormattedAsHtml = false; + this.#isFormattedAsText = false; + + this.name = name; + this.flattenedValue = flattenedValue; + this.dataViewField = dataView.getFieldByName(name); + this.isPinned = isPinned; + this.columnsMeta = columnsMeta; + } + + // format as html in a lazy way + public get formattedAsHtml(): string | undefined { + if (!this.#isFormattedAsHtml) { + this.#formattedAsHtml = formatFieldValue( + this.flattenedValue, + this.#hit.raw, + this.#fieldFormats, + this.#dataView, + this.dataViewField, + 'html' + ); + this.#isFormattedAsHtml = true; + } + + return this.#formattedAsHtml; + } + + // format as text in a lazy way + public get formattedAsText(): string | undefined { + if (!this.#isFormattedAsText) { + this.#formattedAsText = String( + formatFieldValue( + this.flattenedValue, + this.#hit.raw, + this.#fieldFormats, + this.#dataView, + this.dataViewField, + 'text' + ) + ); + this.#isFormattedAsText = true; + } + + return this.#formattedAsText; + } + + public get fieldType(): string | undefined { + if (!this.#fieldType) { + const columnMeta = this.columnsMeta?.[this.name]; + const columnIconType = getTextBasedColumnIconType(columnMeta); + const fieldType = columnIconType + ? columnIconType // for text-based results types come separately + : isNestedFieldParent(this.name, this.#dataView) + ? 'nested' + : this.dataViewField + ? getFieldIconType(this.dataViewField) + : undefined; + + this.#fieldType = fieldType; + } + + return this.#fieldType; + } + + public get ignoredReason(): IgnoredReason | undefined { + return this.dataViewField + ? getIgnoredReason(this.dataViewField, this.#hit.raw._ignored) + : undefined; + } +} diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.test.tsx index da4afe578fcce36..897910377c3bf5e 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.test.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.test.tsx @@ -9,42 +9,39 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { DataViewField } from '@kbn/data-views-plugin/common'; -import { TableRow } from './table_cell_actions'; +import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import type { EuiDataGridCellValueElementProps } from '@elastic/eui/src/components/datagrid/data_grid_types'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { FieldRow } from './field_row'; import { getPinColumnControl } from './get_pin_control'; -import { EuiDataGridCellValueElementProps } from '@elastic/eui/src/components/datagrid/data_grid_types'; +import { buildDataTableRecord } from '@kbn/discover-utils'; describe('getPinControl', () => { - const rows: TableRow[] = [ - { - action: { - onFilter: jest.fn(), - flattenedField: 'flattenedField', - onToggleColumn: jest.fn(), - }, - field: { - pinned: true, - onTogglePinned: jest.fn(), - field: 'message', - fieldMapping: new DataViewField({ - type: 'keyword', - name: 'message', - searchable: true, - aggregatable: true, - }), - fieldType: 'keyword', - displayName: 'message', - scripted: false, - }, - value: { - ignored: undefined, - formattedValue: 'test', - }, - }, + const rows: FieldRow[] = [ + new FieldRow({ + name: 'message', + flattenedValue: 'flattenedField', + hit: buildDataTableRecord( + { + _ignored: [], + _index: 'test', + _id: '1', + _source: { + message: 'test', + }, + }, + dataView + ), + dataView, + fieldFormats: {} as FieldFormatsStart, + isPinned: false, + columnsMeta: undefined, + }), ]; it('should render correctly', () => { - const control = getPinColumnControl({ rows }); + const onTogglePinned = jest.fn(); + const control = getPinColumnControl({ rows, onTogglePinned }); const Cell = control.rowCellRender as React.FC; render( { screen.getByTestId('unifiedDocViewer_pinControlButton_message').click(); - expect(rows[0].field.onTogglePinned).toHaveBeenCalledWith('message'); + expect(onTogglePinned).toHaveBeenCalledWith('message'); }); }); diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.tsx index e455d80a058107d..fee3cf15e15cb63 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.tsx @@ -17,17 +17,18 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; -import type { TableRow } from './table_cell_actions'; +import type { FieldRow } from './field_row'; interface PinControlCellProps { - row: TableRow; + row: FieldRow; + onTogglePinned: (fieldName: string) => void; } -const PinControlCell: React.FC = React.memo(({ row }) => { +const PinControlCell: React.FC = React.memo(({ row, onTogglePinned }) => { const { euiTheme } = useEuiTheme(); - const fieldName = row.field.field; - const isPinned = row.field.pinned; + const fieldName = row.name; + const isPinned = row.isPinned; const label = isPinned ? i18n.translate('unifiedDocViewer.docViews.table.unpinFieldLabel', { defaultMessage: 'Unpin field', @@ -55,7 +56,7 @@ const PinControlCell: React.FC = React.memo(({ row }) => { color="text" aria-label={label} onClick={() => { - row.field.onTogglePinned(fieldName); + onTogglePinned(fieldName); }} /> @@ -63,7 +64,13 @@ const PinControlCell: React.FC = React.memo(({ row }) => { ); }); -export const getPinColumnControl = ({ rows }: { rows: TableRow[] }): EuiDataGridControlColumn => { +export const getPinColumnControl = ({ + rows, + onTogglePinned, +}: { + rows: FieldRow[]; + onTogglePinned: (fieldName: string) => void; +}): EuiDataGridControlColumn => { return { id: 'pin_field', width: 32, @@ -81,7 +88,9 @@ export const getPinColumnControl = ({ rows }: { rows: TableRow[] }): EuiDataGrid if (!row) { return null; } - return ; + return ( + + ); }, }; }; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.scss b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.scss index 25a41710e48183a..19d556b0b142a4d 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.scss +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.scss @@ -62,6 +62,10 @@ color: $euiColorFullShade; vertical-align: top; + &--highlighted { + font-weight: $euiFontWeightBold; + } + .euiDataGridRowCell__popover & { font-size: $euiFontSizeS; } diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.test.tsx index 833b9db05997596..a8d6d5bb793eefb 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.test.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.test.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import { render, screen, act } from '@testing-library/react'; +import { render, screen, act, fireEvent } from '@testing-library/react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { buildDataTableRecord } from '@kbn/discover-utils'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; @@ -85,6 +85,73 @@ describe('DocViewerTable', () => { storage.clear(); }); + describe('table cells', () => { + it('should render cells', async () => { + render( + + + + ); + + expect(screen.getByText('@timestamp')).toBeInTheDocument(); + expect(screen.getByText(hit.flattened['@timestamp'] as string)).toBeInTheDocument(); + expect(screen.getByText('bytes')).toBeInTheDocument(); + expect(screen.getByText(hit.flattened.bytes as string)).toBeInTheDocument(); + expect(screen.getByText('extension.keyword')).toBeInTheDocument(); + expect(screen.getByText(hit.flattened['extension.keyword'] as string)).toBeInTheDocument(); + }); + }); + + describe('search', () => { + beforeEach(() => { + storage.clear(); + }); + + it('should find by field name', async () => { + render( + + + + ); + + expect(screen.getByText('@timestamp')).toBeInTheDocument(); + expect(screen.getByText('bytes')).toBeInTheDocument(); + expect(screen.getByText('extension.keyword')).toBeInTheDocument(); + + act(() => { + fireEvent.change(screen.getByTestId('unifiedDocViewerFieldsSearchInput'), { + target: { value: 'bytes' }, + }); + }); + + expect(screen.queryByText('@timestamp')).toBeNull(); + expect(screen.queryByText('bytes')).toBeInTheDocument(); + expect(screen.queryByText('extension.keyword')).toBeNull(); + }); + + it('should find by field value', async () => { + render( + + + + ); + + expect(screen.getByText('@timestamp')).toBeInTheDocument(); + expect(screen.getByText('bytes')).toBeInTheDocument(); + expect(screen.getByText('extension.keyword')).toBeInTheDocument(); + + act(() => { + fireEvent.change(screen.getByTestId('unifiedDocViewerFieldsSearchInput'), { + target: { value: hit.flattened['extension.keyword'] as string }, + }); + }); + + expect(screen.queryByText('@timestamp')).toBeNull(); + expect(screen.queryByText('bytes')).toBeNull(); + expect(screen.queryByText('extension.keyword')).toBeInTheDocument(); + }); + }); + describe('switch - show only selected fields', () => { it('should disable the switch if columns is empty', async () => { render( 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 e542a94c5fca82c..cbabdbc25974603 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 @@ -29,23 +29,17 @@ import { import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { getFieldIconType } from '@kbn/field-utils/src/utils/get_field_icon_type'; import { SHOW_MULTIFIELDS, DOC_HIDE_TIME_COLUMN_SETTING, - formatFieldValue, - getIgnoredReason, getShouldShowFieldHandler, - isNestedFieldParent, usePager, getVisibleColumns, canPrependTimeFieldColumn, } from '@kbn/discover-utils'; -import { getTextBasedColumnIconType } from '@kbn/field-utils'; import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; import { getUnifiedDocViewerServices } from '../../plugin'; import { - type TableRow, getFieldCellActions, getFieldValueCellActions, getFilterExistsDisabledWarning, @@ -58,12 +52,11 @@ import { import { TableFilters, TableFiltersProps, useTableFilters } from './table_filters'; import { TableCell } from './table_cell'; import { getPinColumnControl } from './get_pin_control'; - -export type FieldRecord = TableRow; +import { FieldRow } from './field_row'; interface ItemsEntry { - pinnedItems: FieldRecord[]; - restItems: FieldRecord[]; + pinnedRows: FieldRow[]; + restRows: FieldRow[]; allFields: TableFiltersProps['allFields']; } @@ -184,62 +177,21 @@ export const DocViewerTable = ({ [currentDataViewId, pinnedFields, storage] ); - const { onFilterField, ...tableFiltersProps } = useTableFilters(storage); + const { onFilterField, onFindSearchTermMatch, ...tableFiltersProps } = useTableFilters(storage); const fieldToItem = useCallback( - (field: string, isPinned: boolean) => { - const fieldMapping = mapping(field); - const displayName = fieldMapping?.displayName ?? field; - const columnMeta = columnsMeta?.[field]; - const columnIconType = getTextBasedColumnIconType(columnMeta); - const fieldType = columnIconType - ? columnIconType // for text-based results types come separately - : isNestedFieldParent(field, dataView) - ? 'nested' - : fieldMapping - ? getFieldIconType(fieldMapping) - : undefined; - - const ignored = getIgnoredReason(fieldMapping ?? field, hit.raw._ignored); - - return { - action: { - onToggleColumn, - onFilter: filter, - flattenedField: flattened[field], - }, - field: { - field, - displayName, - fieldMapping, - fieldType, - scripted: Boolean(fieldMapping?.scripted), - pinned: isPinned, - onTogglePinned, - }, - value: { - formattedValue: formatFieldValue( - hit.flattened[field], - hit.raw, - fieldFormats, - dataView, - fieldMapping - ), - ignored, - }, - }; + (field: string, isPinned: boolean): FieldRow => { + return new FieldRow({ + name: field, + flattenedValue: flattened[field], + hit, + dataView, + fieldFormats, + isPinned, + columnsMeta, + }); }, - [ - mapping, - dataView, - hit, - onToggleColumn, - filter, - columnsMeta, - flattened, - onTogglePinned, - fieldFormats, - ] + [dataView, hit, columnsMeta, flattened, fieldFormats] ); const fieldsFromColumns = useMemo( @@ -287,7 +239,7 @@ export const DocViewerTable = ({ uiSettings, ]); - const { pinnedItems, restItems, allFields } = useMemo( + const { pinnedRows, restRows, allFields } = useMemo( () => displayedFieldNames.reduce( (acc, curFieldName) => { @@ -304,25 +256,25 @@ export const DocViewerTable = ({ const row = fieldToItem(curFieldName, isPinned); if (isPinned) { - acc.pinnedItems.push(row); + acc.pinnedRows.push(row); } else { - if (onFilterField(curFieldName, row.field.displayName, row.field.fieldType)) { + if (onFilterField(row)) { // filter only unpinned fields - acc.restItems.push(row); + acc.restRows.push(row); } } acc.allFields.push({ name: curFieldName, - displayName: row.field.displayName, - type: row.field.fieldType, + displayName: row.dataViewField?.displayName, + type: row.fieldType, }); return acc; }, { - pinnedItems: [], - restItems: [], + pinnedRows: [], + restRows: [], allFields: [], } ), @@ -339,11 +291,11 @@ export const DocViewerTable = ({ ] ); - const rows = useMemo(() => [...pinnedItems, ...restItems], [pinnedItems, restItems]); + const rows = useMemo(() => [...pinnedRows, ...restRows], [pinnedRows, restRows]); const leadingControlColumns = useMemo(() => { - return [getPinColumnControl({ rows })]; - }, [rows]); + return [getPinColumnControl({ rows, onTogglePinned })]; + }, [rows, onTogglePinned]); const { curPageIndex, pageSize, totalPages, changePageIndex, changePageSize } = usePager({ initialPageSize: getPageSize(storage), @@ -372,11 +324,11 @@ export const DocViewerTable = ({ }, [showPagination, curPageIndex, pageSize, onChangePageSize, changePageIndex]); const fieldCellActions = useMemo( - () => getFieldCellActions({ rows, filter, onToggleColumn }), + () => getFieldCellActions({ rows, onFilter: filter, onToggleColumn }), [rows, filter, onToggleColumn] ); const fieldValueCellActions = useMemo( - () => getFieldValueCellActions({ rows, filter }), + () => getFieldValueCellActions({ rows, onFilter: filter }), [rows, filter] ); @@ -434,10 +386,11 @@ export const DocViewerTable = ({ rowIndex={rowIndex} columnId={columnId} isDetails={isDetails} + onFindSearchTermMatch={onFindSearchTermMatch} /> ); }, - [rows, tableFiltersProps.searchTerm] + [rows, tableFiltersProps.searchTerm, onFindSearchTermMatch] ); const renderCellPopover = useCallback( @@ -447,9 +400,9 @@ export const DocViewerTable = ({ let warningMessage: string | undefined; if (columnId === GRID_COLUMN_FIELD_VALUE) { - warningMessage = getFilterInOutPairDisabledWarning(row); + warningMessage = getFilterInOutPairDisabledWarning(row, filter); } else if (columnId === GRID_COLUMN_FIELD_NAME) { - warningMessage = getFilterExistsDisabledWarning(row); + warningMessage = getFilterExistsDisabledWarning(row, filter); } return ( @@ -465,7 +418,7 @@ export const DocViewerTable = ({ ); }, - [rows] + [rows, filter] ); const containerHeight = containerRef diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell.tsx index 609645956412f61..9e8f748bfdf8322 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell.tsx @@ -7,56 +7,66 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; +import React, { useMemo } from 'react'; import { FieldName } from '@kbn/unified-doc-viewer'; import { FieldDescription, getFieldSearchMatchingHighlight } from '@kbn/field-utils'; import { TableFieldValue } from './table_cell_value'; -import type { TableRow } from './table_cell_actions'; +import type { FieldRow } from './field_row'; +import { TermMatch, type UseTableFiltersReturn } from './table_filters'; import { getUnifiedDocViewerServices } from '../../plugin'; interface TableCellProps { searchTerm: string; - rows: TableRow[]; + rows: FieldRow[]; rowIndex: number; columnId: string; isDetails: boolean; + onFindSearchTermMatch?: UseTableFiltersReturn['onFindSearchTermMatch']; } export const TableCell: React.FC = React.memo( - ({ searchTerm, rows, rowIndex, columnId, isDetails }) => { + ({ searchTerm, rows, rowIndex, columnId, isDetails, onFindSearchTermMatch }) => { const { fieldsMetadata } = getUnifiedDocViewerServices(); const row = rows[rowIndex]; + const searchTermMatch = useMemo(() => { + if (row && onFindSearchTermMatch && searchTerm?.trim()) { + return onFindSearchTermMatch(row, searchTerm); + } + return null; + }, [onFindSearchTermMatch, row, searchTerm]); + + const nameHighlight = useMemo( + () => + row && searchTermMatch && [TermMatch.name, TermMatch.both].includes(searchTermMatch) + ? getFieldSearchMatchingHighlight(row.dataViewField?.displayName || row.name, searchTerm) + : undefined, + [searchTerm, searchTermMatch, row] + ); + if (!row) { return null; } - const { - action: { flattenedField }, - field: { field, fieldMapping, fieldType, scripted }, - value: { formattedValue, ignored }, - } = row; + const { flattenedValue, name, dataViewField, ignoredReason, fieldType } = row; if (columnId === 'name') { return (
- {isDetails && !!fieldMapping ? ( + {isDetails && !!dataViewField ? (
@@ -68,11 +78,14 @@ export const TableCell: React.FC = React.memo( if (columnId === 'value') { return ( ); } diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx index f2829a96b95d31b..9dd4305cd4b004a 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx @@ -8,36 +8,33 @@ */ import React from 'react'; -import { getFieldCellActions, getFieldValueCellActions, TableRow } from './table_cell_actions'; -import { DataViewField } from '@kbn/data-views-plugin/common'; +import { getFieldCellActions, getFieldValueCellActions } from './table_cell_actions'; +import { FieldRow } from './field_row'; +import { buildDataTableRecord } from '@kbn/discover-utils'; +import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; describe('TableActions', () => { - const rows: TableRow[] = [ - { - action: { - onFilter: jest.fn(), - flattenedField: 'flattenedField', - onToggleColumn: jest.fn(), - }, - field: { - pinned: true, - onTogglePinned: jest.fn(), - field: 'message', - fieldMapping: new DataViewField({ - type: 'keyword', - name: 'message', - searchable: true, - aggregatable: true, - }), - fieldType: 'keyword', - displayName: 'message', - scripted: false, - }, - value: { - ignored: undefined, - formattedValue: 'test', - }, - }, + const rows: FieldRow[] = [ + new FieldRow({ + name: 'message', + flattenedValue: 'flattenedField', + hit: buildDataTableRecord( + { + _ignored: [], + _index: 'test', + _id: '1', + _source: { + message: 'test', + }, + }, + dataView + ), + dataView, + fieldFormats: {} as FieldFormatsStart, + isPinned: false, + columnsMeta: undefined, + }), ]; const Component = () =>
Component
; @@ -52,13 +49,13 @@ describe('TableActions', () => { describe('getFieldCellActions', () => { it('should render correctly for undefined functions', () => { expect( - getFieldCellActions({ rows, filter: undefined, onToggleColumn: jest.fn() }).map((item) => + getFieldCellActions({ rows, onFilter: undefined, onToggleColumn: jest.fn() }).map((item) => item(EuiCellParams) ) ).toMatchSnapshot(); expect( - getFieldCellActions({ rows, filter: undefined, onToggleColumn: undefined }).map((item) => + getFieldCellActions({ rows, onFilter: undefined, onToggleColumn: undefined }).map((item) => item(EuiCellParams) ) ).toMatchSnapshot(); @@ -66,7 +63,7 @@ describe('TableActions', () => { it('should render the panels correctly for defined onFilter function', () => { expect( - getFieldCellActions({ rows, filter: jest.fn(), onToggleColumn: jest.fn() }).map((item) => + getFieldCellActions({ rows, onFilter: jest.fn(), onToggleColumn: jest.fn() }).map((item) => item(EuiCellParams) ) ).toMatchSnapshot(); @@ -76,13 +73,13 @@ describe('TableActions', () => { describe('getFieldValueCellActions', () => { it('should render correctly for undefined functions', () => { expect( - getFieldValueCellActions({ rows, filter: undefined }).map((item) => item(EuiCellParams)) + getFieldValueCellActions({ rows, onFilter: undefined }).map((item) => item(EuiCellParams)) ).toMatchSnapshot(); }); it('should render the panels correctly for defined onFilter function', () => { expect( - getFieldValueCellActions({ rows, filter: jest.fn() }).map((item) => item(EuiCellParams)) + getFieldValueCellActions({ rows, onFilter: jest.fn() }).map((item) => item(EuiCellParams)) ).toMatchSnapshot(); }); }); diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx index 408708f67415349..e9c5a70770ca52f 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx @@ -10,45 +10,36 @@ import React from 'react'; import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DocViewFilterFn, FieldRecordLegacy } from '@kbn/unified-doc-viewer/types'; - -export interface TableRow { - action: Omit; - field: { - pinned: boolean; - onTogglePinned: (field: string) => void; - } & FieldRecordLegacy['field']; - value: FieldRecordLegacy['value']; -} +import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import { FieldRow } from './field_row'; interface TableActionsProps { Component: EuiDataGridColumnCellActionProps['Component']; - row: TableRow | undefined; // as we pass `rows[rowIndex]` it's safer to assume that `row` prop can be undefined + row: FieldRow | undefined; // as we pass `rows[rowIndex]` it's safer to assume that `row` prop can be undefined } -export function isFilterInOutPairDisabled(row: TableRow | undefined): boolean { +export function isFilterInOutPairDisabled( + row: FieldRow | undefined, + onFilter: DocViewFilterFn | undefined +): boolean { if (!row) { return false; } - const { - action: { onFilter }, - field: { fieldMapping }, - value: { ignored }, - } = row; + const { dataViewField, ignoredReason } = row; - return Boolean(onFilter && (!fieldMapping || !fieldMapping.filterable || ignored)); + return Boolean(onFilter && (!dataViewField || !dataViewField.filterable || ignoredReason)); } -export function getFilterInOutPairDisabledWarning(row: TableRow | undefined): string | undefined { - if (!row || !isFilterInOutPairDisabled(row)) { +export function getFilterInOutPairDisabledWarning( + row: FieldRow | undefined, + onFilter: DocViewFilterFn | undefined +): string | undefined { + if (!row || !isFilterInOutPairDisabled(row, onFilter)) { return undefined; } - const { - field: { fieldMapping }, - value: { ignored }, - } = row; + const { dataViewField, ignoredReason } = row; - if (ignored) { + if (ignoredReason) { return i18n.translate( 'unifiedDocViewer.docViews.table.ignoredValuesCanNotBeSearchedWarningMessage', { @@ -57,7 +48,7 @@ export function getFilterInOutPairDisabledWarning(row: TableRow | undefined): st ); } - return !fieldMapping + return !dataViewField ? i18n.translate( 'unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedWarningMessage', { @@ -67,15 +58,16 @@ export function getFilterInOutPairDisabledWarning(row: TableRow | undefined): st : undefined; } -export const FilterIn: React.FC = ({ Component, row }) => { +export const FilterIn: React.FC = ({ + Component, + row, + onFilter, +}) => { if (!row) { return null; } - const { - action: { onFilter, flattenedField }, - field: { field, fieldMapping }, - } = row; + const { dataViewField, name, flattenedValue } = row; // Filters pair const filterAddLabel = i18n.translate( @@ -91,27 +83,28 @@ export const FilterIn: React.FC = ({ Component, row }) => { return ( onFilter(fieldMapping, flattenedField, '+')} + onClick={() => onFilter(dataViewField, flattenedValue, '+')} > {filterAddLabel} ); }; -export const FilterOut: React.FC = ({ Component, row }) => { +export const FilterOut: React.FC = ({ + Component, + row, + onFilter, +}) => { if (!row) { return null; } - const { - action: { onFilter, flattenedField }, - field: { field, fieldMapping }, - } = row; + const { dataViewField, name, flattenedValue } = row; // Filters pair const filterOutLabel = i18n.translate( @@ -127,39 +120,42 @@ export const FilterOut: React.FC = ({ Component, row }) => { return ( onFilter(fieldMapping, flattenedField, '-')} + onClick={() => onFilter(dataViewField, flattenedValue, '-')} > {filterOutLabel} ); }; -export function isFilterExistsDisabled(row: TableRow | undefined): boolean { +export function isFilterExistsDisabled( + row: FieldRow | undefined, + onFilter: DocViewFilterFn | undefined +): boolean { if (!row) { return false; } - const { - action: { onFilter }, - field: { fieldMapping }, - } = row; + const { dataViewField } = row; - return Boolean(onFilter && (!fieldMapping || !fieldMapping.filterable || fieldMapping.scripted)); + return Boolean( + onFilter && (!dataViewField || !dataViewField.filterable || dataViewField.scripted) + ); } -export function getFilterExistsDisabledWarning(row: TableRow | undefined): string | undefined { - if (!row || !isFilterExistsDisabled(row)) { +export function getFilterExistsDisabledWarning( + row: FieldRow | undefined, + onFilter: DocViewFilterFn | undefined +): string | undefined { + if (!row || !isFilterExistsDisabled(row, onFilter)) { return undefined; } - const { - field: { fieldMapping }, - } = row; + const { dataViewField } = row; - return fieldMapping?.scripted + return dataViewField?.scripted ? i18n.translate( 'unifiedDocViewer.docViews.table.unableToFilterForPresenceOfScriptedFieldsWarningMessage', { @@ -169,15 +165,14 @@ export function getFilterExistsDisabledWarning(row: TableRow | undefined): strin : undefined; } -export const FilterExist: React.FC = ({ Component, row }) => { +export const FilterExist: React.FC< + TableActionsProps & { onFilter: DocViewFilterFn | undefined } +> = ({ Component, row, onFilter }) => { if (!row) { return null; } - const { - action: { onFilter }, - field: { field }, - } = row; + const { name } = row; // Filter exists const filterExistsLabel = i18n.translate( @@ -191,27 +186,28 @@ export const FilterExist: React.FC = ({ Component, row }) => return ( onFilter('_exists_', field, '+')} + onClick={() => onFilter('_exists_', name, '+')} > {filterExistsLabel} ); }; -export const ToggleColumn: React.FC = ({ Component, row }) => { +export const ToggleColumn: React.FC< + TableActionsProps & { + onToggleColumn: ((field: string) => void) | undefined; + } +> = ({ Component, row, onToggleColumn }) => { if (!row) { return null; } - const { - action: { onToggleColumn }, - field: { field }, - } = row; + const { name } = row; if (!onToggleColumn) { return null; @@ -227,11 +223,11 @@ export const ToggleColumn: React.FC = ({ Component, row }) => return ( onToggleColumn(field)} + onClick={() => onToggleColumn(name)} > {toggleColumnLabel} @@ -240,25 +236,31 @@ export const ToggleColumn: React.FC = ({ Component, row }) => export function getFieldCellActions({ rows, - filter, + onFilter, onToggleColumn, }: { - rows: TableRow[]; - filter?: DocViewFilterFn; + rows: FieldRow[]; + onFilter?: DocViewFilterFn; onToggleColumn: ((field: string) => void) | undefined; }) { return [ - ...(filter + ...(onFilter ? [ ({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => { - return ; + return ; }, ] : []), ...(onToggleColumn ? [ ({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => { - return ; + return ( + + ); }, ] : []), @@ -267,18 +269,18 @@ export function getFieldCellActions({ export function getFieldValueCellActions({ rows, - filter, + onFilter, }: { - rows: TableRow[]; - filter?: DocViewFilterFn; + rows: FieldRow[]; + onFilter?: DocViewFilterFn; }) { - return filter + return onFilter ? [ ({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => { - return ; + return ; }, ({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => { - return ; + return ; }, ] : []; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx index dea6010441308a7..3afe935307ab2fb 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx @@ -22,7 +22,7 @@ import classnames from 'classnames'; import React, { Fragment, useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { IgnoredReason, TRUNCATE_MAX_HEIGHT } from '@kbn/discover-utils'; -import { FieldRecord } from './table'; +import { FieldRecordLegacy } from '@kbn/unified-doc-viewer/types'; import { getUnifiedDocViewerServices } from '../../plugin'; const DOC_VIEWER_DEFAULT_TRUNCATE_MAX_HEIGHT = 110; @@ -96,12 +96,13 @@ const IgnoreWarning: React.FC = React.memo(({ rawValue, reas ); }); -type TableFieldValueProps = Pick & { - formattedValue: FieldRecord['value']['formattedValue']; +type TableFieldValueProps = Pick & { + formattedValue: FieldRecordLegacy['value']['formattedValue']; rawValue: unknown; ignoreReason?: IgnoredReason; isDetails?: boolean; // true when inside EuiDataGrid cell popover isLegacy?: boolean; // true when inside legacy table + isHighlighted?: boolean; // whether it's matching a search term }; export const TableFieldValue = ({ @@ -111,6 +112,7 @@ export const TableFieldValue = ({ ignoreReason, isDetails, isLegacy, + isHighlighted, }: TableFieldValueProps) => { const { euiTheme } = useEuiTheme(); const { uiSettings } = getUnifiedDocViewerServices(); @@ -158,6 +160,7 @@ export const TableFieldValue = ({ const valueClasses = classnames('kbnDocViewer__value', { 'kbnDocViewer__value--truncated': shouldTruncate, + 'kbnDocViewer__value--highlighted': isHighlighted && !isDetails, }); return ( 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 index 5c3e7862ea6c8b0..50a3064b0df4dd0 100644 --- 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 @@ -19,14 +19,21 @@ import { type FieldTypeFilterProps, } from '@kbn/unified-field-list/src/components/field_list_filters/field_type_filter'; import { getUnifiedDocViewerServices } from '../../plugin'; +import { FieldRow } from './field_row'; 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', + defaultMessage: 'Search field names or values', }); +export enum TermMatch { + name = 'name', + value = 'value', + both = 'both', +} + interface TableFiltersCommonProps { // search searchTerm: string; @@ -108,12 +115,9 @@ const getStoredFieldTypes = (storage: Storage) => { return Array.isArray(parsedFieldTypes) ? parsedFieldTypes : []; }; -interface UseTableFiltersReturn extends TableFiltersCommonProps { - onFilterField: ( - fieldName: string, - fieldDisplayName: string | undefined, - fieldType: string | undefined - ) => boolean; +export interface UseTableFiltersReturn extends TableFiltersCommonProps { + onFilterField: (row: FieldRow) => boolean; + onFindSearchTermMatch: (row: FieldRow, term: string) => TermMatch | null; } export const useTableFilters = (storage: Storage): UseTableFiltersReturn => { @@ -138,13 +142,34 @@ export const useTableFilters = (storage: Storage): UseTableFiltersReturn => { [storage, setSelectedFieldTypes] ); - const onFilterField: UseTableFiltersReturn['onFilterField'] = useCallback( - (fieldName, fieldDisplayName, fieldType) => { - const term = searchTerm?.trim(); + const onFindSearchTermMatch: UseTableFiltersReturn['onFindSearchTermMatch'] = useCallback( + (row, term) => { + const { name, dataViewField } = row; + + let termMatch: TermMatch | null = null; + + if (fieldNameWildcardMatcher({ name, displayName: dataViewField?.customLabel }, term)) { + termMatch = TermMatch.name; + } + if ( - term && - !fieldNameWildcardMatcher({ name: fieldName, displayName: fieldDisplayName }, term) + (row.formattedAsText || '').toLowerCase().includes(term.toLowerCase()) || + (JSON.stringify(row.flattenedValue) || '').toLowerCase().includes(term.toLowerCase()) ) { + termMatch = termMatch ? TermMatch.both : TermMatch.value; + } + + return termMatch; + }, + [] + ); + + const onFilterField: UseTableFiltersReturn['onFilterField'] = useCallback( + (row) => { + const { fieldType } = row; + const term = searchTerm?.trim(); + + if (term && !onFindSearchTermMatch(row, term)) { return false; } @@ -154,7 +179,7 @@ export const useTableFilters = (storage: Storage): UseTableFiltersReturn => { return true; }, - [searchTerm, selectedFieldTypes] + [searchTerm, selectedFieldTypes, onFindSearchTermMatch] ); return useMemo( @@ -166,7 +191,15 @@ export const useTableFilters = (storage: Storage): UseTableFiltersReturn => { onChangeFieldTypes, // the actual filtering function onFilterField, + onFindSearchTermMatch, }), - [searchTerm, onChangeSearchTerm, selectedFieldTypes, onChangeFieldTypes, onFilterField] + [ + searchTerm, + onChangeSearchTerm, + selectedFieldTypes, + onChangeFieldTypes, + onFilterField, + onFindSearchTermMatch, + ] ); }; 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 index 546c7fb3a0d0967..6d0a8c5dfc96cdd 100644 --- 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 @@ -14,9 +14,54 @@ import { LOCAL_STORAGE_KEY_SEARCH_TERM, LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES, } from './table_filters'; +import { FieldRow } from './field_row'; +import { buildDataTableRecord } from '@kbn/discover-utils'; +import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; const storage = new Storage(window.localStorage); +const hit = buildDataTableRecord( + { + _ignored: [], + _index: 'test', + _id: '1', + _source: { + 'extension.keyword': 'zip', + bytes: 500, + '@timestamp': '2021-01-01T00:00:00', + }, + }, + dataView +); +const rowExtensionKeyword = new FieldRow({ + name: 'extension.keyword', + flattenedValue: 'zip', + hit, + dataView, + fieldFormats: {} as FieldFormatsStart, + isPinned: false, + columnsMeta: undefined, +}); +const rowBytes = new FieldRow({ + name: 'bytes', + flattenedValue: 500, + hit, + dataView, + fieldFormats: {} as FieldFormatsStart, + isPinned: false, + columnsMeta: undefined, +}); +const rowTimestamp = new FieldRow({ + name: '@timestamp', + flattenedValue: '2021-01-01T00:00:00', + hit, + dataView, + fieldFormats: {} as FieldFormatsStart, + isPinned: false, + columnsMeta: undefined, +}); + describe('useTableFilters', () => { beforeAll(() => { jest.useFakeTimers(); @@ -34,8 +79,8 @@ describe('useTableFilters', () => { 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(result.current.onFilterField(rowExtensionKeyword)).toBe(true); + expect(result.current.onFilterField(rowBytes)).toBe(true); expect(storage.get(LOCAL_STORAGE_KEY_SEARCH_TERM)).toBeNull(); }); @@ -47,8 +92,8 @@ describe('useTableFilters', () => { result.current.onChangeSearchTerm('ext'); }); - expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(true); - expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(false); + expect(result.current.onFilterField(rowExtensionKeyword)).toBe(true); + expect(result.current.onFilterField(rowBytes)).toBe(false); expect(storage.get(LOCAL_STORAGE_KEY_SEARCH_TERM)).toBe('ext'); }); @@ -60,22 +105,22 @@ describe('useTableFilters', () => { result.current.onChangeFieldTypes(['number']); }); - expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(false); - expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(true); + expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false); + expect(result.current.onFilterField(rowBytes)).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); + expect(result.current.onFilterField(rowExtensionKeyword)).toBe(true); + expect(result.current.onFilterField(rowBytes)).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); + expect(result.current.onFilterField(rowExtensionKeyword)).toBe(true); + expect(result.current.onFilterField(rowBytes)).toBe(true); jest.advanceTimersByTime(600); expect(storage.get(LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES)).toBe('["number","keyword"]'); @@ -89,30 +134,69 @@ describe('useTableFilters', () => { result.current.onChangeFieldTypes(['keyword']); }); - expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(true); - expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(false); + expect(result.current.onFilterField(rowExtensionKeyword)).toBe(true); + expect(result.current.onFilterField(rowBytes)).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); + expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false); + expect(result.current.onFilterField(rowBytes)).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); + expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false); + expect(result.current.onFilterField(rowBytes)).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 filter by field value and field type', () => { + const { result } = renderHook(() => useTableFilters(storage)); + + expect(result.current.onFilterField(rowTimestamp)).toBe(true); + expect(result.current.onFilterField(rowExtensionKeyword)).toBe(true); + expect(result.current.onFilterField(rowBytes)).toBe(true); + + act(() => { + result.current.onChangeSearchTerm('500'); + result.current.onChangeFieldTypes(['number']); + }); + + expect(result.current.onFilterField(rowTimestamp)).toBe(false); + expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false); + expect(result.current.onFilterField(rowBytes)).toBe(true); + + act(() => { + result.current.onChangeSearchTerm('2021'); + result.current.onChangeFieldTypes(['number']); + }); + + expect(result.current.onFilterField(rowTimestamp)).toBe(false); + expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false); + expect(result.current.onFilterField(rowBytes)).toBe(false); + + act(() => { + result.current.onChangeSearchTerm('2021'); + result.current.onChangeFieldTypes(['date']); + }); + + expect(result.current.onFilterField(rowTimestamp)).toBe(true); + expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false); + expect(result.current.onFilterField(rowBytes)).toBe(false); + + jest.advanceTimersByTime(600); + expect(storage.get(LOCAL_STORAGE_KEY_SEARCH_TERM)).toBe('2021'); + expect(storage.get(LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES)).toBe('["date"]'); + }); + it('should restore previous filters', () => { storage.set(LOCAL_STORAGE_KEY_SEARCH_TERM, 'bytes'); storage.set(LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES, '["number"]'); @@ -122,8 +206,8 @@ describe('useTableFilters', () => { 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); + expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false); + expect(result.current.onFilterField(rowBytes)).toBe(true); + expect(result.current.onFilterField(rowTimestamp)).toBe(false); }); }); diff --git a/test/functional/apps/context/_filters.ts b/test/functional/apps/context/_filters.ts index bc404b45f22c053..4a3f8b3a9998fec 100644 --- a/test/functional/apps/context/_filters.ts +++ b/test/functional/apps/context/_filters.ts @@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('inclusive filter should be addable via expanded data grid rows', async function () { await retry.waitFor(`filter ${TEST_ANCHOR_FILTER_FIELD} in filterbar`, async () => { await dataGrid.clickRowToggle({ isAnchorRow: true, renderMoreRows: true }); - await PageObjects.discover.findFieldByNameInDocViewer(TEST_ANCHOR_FILTER_FIELD); + await PageObjects.discover.findFieldByNameOrValueInDocViewer(TEST_ANCHOR_FILTER_FIELD); await dataGrid.clickFieldActionInFlyout( TEST_ANCHOR_FILTER_FIELD, 'addFilterForValueButton' diff --git a/test/functional/apps/discover/group3/_doc_viewer.ts b/test/functional/apps/discover/group3/_doc_viewer.ts index 3d3562e10beb4ff..bc71c8228907101 100644 --- a/test/functional/apps/discover/group3/_doc_viewer.ts +++ b/test/functional/apps/discover/group3/_doc_viewer.ts @@ -66,13 +66,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should be able to search by string', async function () { - await discover.findFieldByNameInDocViewer('geo'); + await discover.findFieldByNameOrValueInDocViewer('geo'); await retry.waitFor('first updates', async () => { return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 4; }); - await discover.findFieldByNameInDocViewer('.sr'); + await discover.findFieldByNameOrValueInDocViewer('.sr'); await retry.waitFor('second updates', async () => { return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 2; @@ -80,21 +80,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should be able to search by wildcard', async function () { - await discover.findFieldByNameInDocViewer('relatedContent*image'); + await discover.findFieldByNameOrValueInDocViewer('relatedContent*image'); await retry.waitFor('updates', async () => { return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 2; }); }); it('should be able to search with spaces as wildcard', async function () { - await discover.findFieldByNameInDocViewer('relatedContent image'); + await discover.findFieldByNameOrValueInDocViewer('relatedContent image'); await retry.waitFor('updates', async () => { return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 4; }); }); it('should be able to search with fuzzy search (1 typo)', async function () { - await discover.findFieldByNameInDocViewer('rel4tedContent.art'); + await discover.findFieldByNameOrValueInDocViewer('rel4tedContent.art'); await retry.waitFor('updates', async () => { return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 3; @@ -102,7 +102,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should ignore empty search', async function () { - await discover.findFieldByNameInDocViewer(' '); // only spaces + await discover.findFieldByNameOrValueInDocViewer(' '); // only spaces await retry.waitFor('the clear button', async () => { return await testSubjects.exists('clearSearchButton'); @@ -113,6 +113,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length > 0; }); }); + + it('should be able to search by field value', async function () { + await discover.findFieldByNameOrValueInDocViewer('time'); + + await retry.waitFor('updates', async () => { + return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 5; + }); + }); + + it('should be able to search by field raw value', async function () { + await discover.findFieldByNameOrValueInDocViewer('2015-09-22T23:50:13.253Z'); + + await retry.waitFor('updates', async () => { + return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 3; + }); + }); }); describe('filter by field type', function () { @@ -220,7 +236,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should hide fields with null values ', async function () { - await discover.findFieldByNameInDocViewer('machine'); + await discover.findFieldByNameOrValueInDocViewer('machine'); const results = (await find.allByCssSelector('.kbnDocViewer__fieldName')).length; const hideNullValuesSwitch = await testSubjects.find( 'unifiedDocViewerHideNullValuesSwitch' diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 54dae77ff3839b7..1474e9d3155382f 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -444,7 +444,7 @@ export class DiscoverPageObject extends FtrService { return await this.find.byClassName('monaco-editor'); } - public async findFieldByNameInDocViewer(name: string) { + public async findFieldByNameOrValueInDocViewer(name: string) { const fieldSearch = await this.testSubjects.find('unifiedDocViewerFieldsSearchInput'); await fieldSearch.type(name); } diff --git a/x-pack/test_serverless/functional/test_suites/common/context/_filters.ts b/x-pack/test_serverless/functional/test_suites/common/context/_filters.ts index 672e13052f7d852..3dc28217ca7edaa 100644 --- a/x-pack/test_serverless/functional/test_suites/common/context/_filters.ts +++ b/x-pack/test_serverless/functional/test_suites/common/context/_filters.ts @@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('inclusive filter should be addable via expanded data grid rows', async function () { await retry.waitFor(`filter ${TEST_ANCHOR_FILTER_FIELD} in filterbar`, async () => { await dataGrid.clickRowToggle({ isAnchorRow: true, renderMoreRows: true }); - await PageObjects.discover.findFieldByNameInDocViewer(TEST_ANCHOR_FILTER_FIELD); + await PageObjects.discover.findFieldByNameOrValueInDocViewer(TEST_ANCHOR_FILTER_FIELD); await dataGrid.clickFieldActionInFlyout( TEST_ANCHOR_FILTER_FIELD, 'addFilterForValueButton'