From a74a3a20673ed4445533be9daab3821f4dcc4b8d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 20 Jul 2021 14:12:28 -0400 Subject: [PATCH] [RAC] [TGrid] Field browser implemented in EuiDataGrid toolbar (#105207) (#106278) * tGid header using EuiDataGrid * useFetchIndex migrated and column_headers refactor * removed useless mock * add badges translations * i18n translations keys fixed * code format * filter default columns not present in field browser * reset button to initial columns * cleaning * dependencies moved * fix functional test with missing data service * remove unused code (unrelated) * fieldBrowser integration with security solutions timeline * lint and translations cleaned * timeline toolbar removed for merge & some test fixes * type fix * type fixes * timeline static default colums * limit size temporary increase * limit size temporary increase Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Sergi Massaneda --- packages/kbn-optimizer/limits.yml | 2 +- .../timelines/fields_browser.spec.ts | 14 - .../events_viewer/events_viewer.test.tsx | 2 +- .../navigation/breadcrumbs/index.ts | 5 - .../components/alerts_table/actions.test.tsx | 2 + .../components/fields_browser/field_items.tsx | 203 +------- .../components/open_timeline/helpers.test.ts | 445 +++--------------- .../components/open_timeline/helpers.ts | 1 + .../timeline/body/actions/header_actions.tsx | 17 +- .../timeline/body/column_headers/helpers.ts | 22 +- .../timeline/eql_tab_content/index.test.tsx | 1 + .../pinned_tab_content/index.test.tsx | 1 + .../timeline/query_tab_content/index.test.tsx | 1 + .../__snapshots__/empty_value.test.tsx.snap | 7 + .../empty_value/empty_value.test.tsx | 166 +++++++ .../public/components/empty_value/index.tsx | 49 ++ .../components/empty_value/translations.ts | 12 + .../components/fields_browser/index.tsx | 48 ++ .../timelines/public/components/index.tsx | 9 +- .../components/t_grid/standalone/index.tsx | 37 +- .../fields_browser/categories_pane.test.tsx | 2 +- .../fields_browser/categories_pane.tsx | 7 +- .../toolbar}/fields_browser/category.test.tsx | 17 +- .../toolbar}/fields_browser/category.tsx | 11 +- .../fields_browser/category_columns.test.tsx | 2 +- .../fields_browser/category_columns.tsx | 13 +- .../fields_browser/category_title.test.tsx | 3 +- .../fields_browser/category_title.tsx | 6 +- .../fields_browser/field_browser.test.tsx | 4 +- .../toolbar}/fields_browser/field_browser.tsx | 13 +- .../fields_browser/field_items.test.tsx | 39 +- .../toolbar/fields_browser/field_items.tsx | 156 ++++++ .../fields_browser/field_name.test.tsx | 61 +++ .../toolbar/fields_browser/field_name.tsx | 25 + .../fields_browser/fields_pane.test.tsx | 8 +- .../toolbar}/fields_browser/fields_pane.tsx | 15 +- .../toolbar}/fields_browser/header.test.tsx | 7 +- .../t_grid/toolbar}/fields_browser/header.tsx | 12 +- .../toolbar}/fields_browser/helpers.test.tsx | 44 +- .../toolbar}/fields_browser/helpers.tsx | 41 +- .../toolbar}/fields_browser/index.test.tsx | 7 +- .../t_grid/toolbar}/fields_browser/index.tsx | 13 +- .../toolbar}/fields_browser/translations.ts | 68 ++- .../t_grid/toolbar}/fields_browser/types.ts | 4 +- .../components/t_grid/toolbar/index.tsx | 78 +++ .../public/components/utils/helpers.ts | 54 +++ .../public/container/source/index.tsx | 187 ++++++++ .../public/container/source/translations.ts | 19 + x-pack/plugins/timelines/public/index.ts | 1 + .../timelines/public/methods/index.tsx | 24 +- .../timelines/public/mock/plugin_mock.tsx | 2 + x-pack/plugins/timelines/public/plugin.ts | 18 +- x-pack/plugins/timelines/public/types.ts | 2 + .../translations/translations/ja-JP.json | 17 - .../translations/translations/zh-CN.json | 20 - .../plugins/timelines_test/kibana.json | 6 +- .../applications/timelines_test/index.tsx | 8 +- .../plugins/timelines_test/public/plugin.ts | 6 +- 58 files changed, 1195 insertions(+), 869 deletions(-) create mode 100644 x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap create mode 100644 x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx create mode 100644 x-pack/plugins/timelines/public/components/empty_value/index.tsx create mode 100644 x-pack/plugins/timelines/public/components/empty_value/translations.ts create mode 100644 x-pack/plugins/timelines/public/components/fields_browser/index.tsx rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/categories_pane.test.tsx (94%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/categories_pane.tsx (96%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/category.test.tsx (81%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/category.tsx (91%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/category_columns.test.tsx (98%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/category_columns.tsx (88%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/category_title.test.tsx (94%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/category_title.tsx (88%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/field_browser.test.tsx (97%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/field_browser.tsx (95%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/field_items.test.tsx (86%) create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/fields_pane.test.tsx (91%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/fields_pane.tsx (88%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/header.test.tsx (97%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/header.tsx (91%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/helpers.test.tsx (88%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/helpers.tsx (92%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/index.test.tsx (95%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/index.tsx (94%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/translations.ts (51%) rename x-pack/plugins/{security_solution/public/timelines/components => timelines/public/components/t_grid/toolbar}/fields_browser/types.ts (86%) create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/index.tsx create mode 100644 x-pack/plugins/timelines/public/container/source/index.tsx create mode 100644 x-pack/plugins/timelines/public/container/source/translations.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 277b7fd215aac..847d788d328d6 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -107,7 +107,7 @@ pageLoadAssetSize: dataVisualizer: 27530 banners: 17946 mapsEms: 26072 - timelines: 251886 + timelines: 330000 screenshotMode: 17856 visTypePie: 35583 expressionRevealImage: 25675 diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts index 35f38db4f38d2..be726f0323d48 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts @@ -10,7 +10,6 @@ import { FIELDS_BROWSER_FIELDS_COUNT, FIELDS_BROWSER_HOST_CATEGORIES_COUNT, FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER, - FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_HEADER, FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER, FIELDS_BROWSER_MESSAGE_HEADER, FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, @@ -24,7 +23,6 @@ import { cleanKibana } from '../../tasks/common'; import { addsHostGeoCityNameToTimeline, addsHostGeoContinentNameToTimeline, - addsHostGeoCountryNameToTimelineDraggingIt, clearFieldsBrowser, closeFieldsBrowser, filterFieldsBrowser, @@ -156,18 +154,6 @@ describe('Fields Browser', () => { cy.get(FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER).should('exist'); }); - it('adds a field to the timeline when the user drags and drops a field', () => { - const filterInput = 'host.geo.c'; - - filterFieldsBrowser(filterInput); - - cy.get(FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_HEADER).should('not.exist'); - - addsHostGeoCountryNameToTimelineDraggingIt(); - - cy.get(FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_HEADER).should('exist'); - }); - it('resets all fields in the timeline when `Reset Fields` is clicked', () => { const filterInput = 'host.geo.c'; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index ccba97f6a7942..e324a54745c25 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -209,7 +209,7 @@ describe('EventsViewer', () => { ); - expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true); + expect(wrapper.find(`[data-test-subj="field-browser"]`).first().exists()).toBe(true); }); test('it renders the footer containing the pagination', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 03ee38473e58d..4ad26533cb58c 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -181,11 +181,6 @@ export const getBreadcrumbsForRoute = ( } if (isAdminRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'administration', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } return [siemRootBreadcrumb, ...getAdminBreadcrumbs(spyState)]; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 08e88567b0fd0..69160d90a011e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -26,6 +26,7 @@ import { import { ISearchStart } from '../../../../../../../src/plugins/data/public'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { getTimelineTemplate } from '../../../timelines/containers/api'; +import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers'; jest.mock('../../../timelines/containers/api', () => ({ getTimelineTemplate: jest.fn(), @@ -139,6 +140,7 @@ describe('alert actions', () => { initialWidth: 180, }, ], + defaultColumns: defaultHeaders, dataProviders: [], dateRange: { end: '2018-11-05T19:03:25.937Z', diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx index 89a91ee6da305..73fb7c19a6f46 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx @@ -5,71 +5,21 @@ * 2.0. */ -import { - EuiCheckbox, - EuiIcon, - EuiToolTip, - EuiFlexGroup, - EuiFlexItem, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { isEmpty, uniqBy } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import React, { useCallback, useRef, useState } from 'react'; import { Draggable } from 'react-beautiful-dnd'; -import styled from 'styled-components'; -import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid'; -import { BrowserField, BrowserFields } from '../../../common/containers/source'; -import { DragEffects } from '../../../common/components/drag_and_drop/draggable_wrapper'; -import { DroppableWrapper } from '../../../common/components/drag_and_drop/droppable_wrapper'; import { - DRAG_TYPE_FIELD, + DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, getDraggableFieldId, - getDroppableId, -} from '../../../common/components/drag_and_drop/helpers'; -import { DraggableFieldBadge } from '../../../common/components/draggables/field_badge'; -import { getEmptyValue } from '../../../common/components/empty_value'; -import { - getColumnsWithTimestamp, - getExampleText, - getIconFromType, -} from '../../../common/components/event_details/helpers'; -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; -import { OnUpdateColumns } from '../timeline/events'; -import { TruncatableText } from '../../../common/components/truncatable_text'; +} from '@kbn/securitysolution-t-grid'; +import type { BrowserFields } from '../../../common/containers/source'; +import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; +import type { OnUpdateColumns } from '../timeline/events'; import { FieldName } from './field_name'; -import * as i18n from './translations'; -import { getAlertColumnHeader } from './helpers'; -import { ColumnHeaderOptions } from '../../../../common'; +import type { ColumnHeaderOptions } from '../../../../common'; import { useKibana } from '../../../common/lib/kibana'; -const TypeIcon = styled(EuiIcon)` - margin: 0 4px; - position: relative; - top: -1px; -`; - -TypeIcon.displayName = 'TypeIcon'; - -export const Description = styled.span` - user-select: text; - width: 400px; -`; - -Description.displayName = 'Description'; - -/** - * An item rendered in the table - */ -export interface FieldItem { - ariaRowindex?: number; - checkbox: React.ReactNode; - description: React.ReactNode; - field: React.ReactNode; - fieldId: string; -} - const DraggableFieldsBrowserFieldComponent = ({ browserFields, categoryId, @@ -191,142 +141,3 @@ const DraggableFieldsBrowserFieldComponent = ({ export const DraggableFieldsBrowserField = React.memo(DraggableFieldsBrowserFieldComponent); DraggableFieldsBrowserField.displayName = 'DraggableFieldsBrowserFieldComponent'; - -/** - * Returns the draggable fields, values, and descriptions shown when a user expands an event - */ -export const getFieldItems = ({ - browserFields, - category, - categoryId, - columnHeaders, - highlight = '', - onUpdateColumns, - timelineId, - toggleColumn, -}: { - browserFields: BrowserFields; - category: Partial; - categoryId: string; - columnHeaders: ColumnHeaderOptions[]; - highlight?: string; - timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; - onUpdateColumns: OnUpdateColumns; -}): FieldItem[] => - uniqBy('name', [ - ...Object.values(category != null && category.fields != null ? category.fields : {}), - ]).map((field) => ({ - checkbox: ( - - c.id === field.name) !== -1} - data-test-subj={`field-${field.name}-checkbox`} - data-colindex={1} - id={field.name ?? ''} - onChange={() => - toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field.name ?? '', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - ...getAlertColumnHeader(timelineId, field.name ?? ''), - }) - } - /> - - ), - field: ( - - - - - - - - - ( -
- - - -
- )} - > - -
-
-
- ), - description: ( -
- - <> - -

{i18n.DESCRIPTION_FOR_FIELD(field.name ?? '')}

-
- - - {`${field.description ?? getEmptyValue()} ${getExampleText(field.example)}`} - - - -
-
- ), - fieldId: field.name ?? '', - })); - -/** - * Returns a table column template provided to the `EuiInMemoryTable`'s - * `columns` prop - */ -export const getFieldColumns = () => [ - { - field: 'checkbox', - name: '', - render: (checkbox: React.ReactNode, _: FieldItem) => checkbox, - sortable: false, - width: '25px', - }, - { - field: 'field', - name: i18n.FIELD, - render: (field: React.ReactNode, _: FieldItem) => field, - sortable: false, - width: '225px', - }, - { - field: 'description', - name: i18n.DESCRIPTION, - render: (description: React.ReactNode, _: FieldItem) => description, - sortable: false, - truncateText: true, - width: '400px', - }, -]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index c0fea1f210a8a..ae15768d26e70 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -51,6 +51,7 @@ import { mockTemplate as mockSelectedTemplate, } from './__mocks__'; import { getTimeline } from '../../containers/api'; +import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; jest.mock('../../../common/store/inputs/actions'); jest.mock('../../../common/components/url_state/normalize_time_range.ts'); @@ -236,6 +237,49 @@ describe('helpers', () => { }); describe('#defaultTimelineToTimelineModel', () => { + const columns = [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + type: 'number', + initialWidth: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + initialWidth: 180, + }, + ]; test('if title is null, we should get the default title', () => { const timeline = { savedObjectId: 'savedObject-1', @@ -247,49 +291,8 @@ describe('helpers', () => { expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.query, - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - type: 'number', - initialWidth: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - initialWidth: 180, - }, - ], + columns, + defaultColumns: defaultHeaders, dataProviders: [], dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', @@ -358,49 +361,8 @@ describe('helpers', () => { expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.query, - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - type: 'number', - initialWidth: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - initialWidth: 180, - }, - ], + columns, + defaultColumns: defaultHeaders, dataProviders: [], dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', @@ -469,49 +431,8 @@ describe('helpers', () => { expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.query, - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - type: 'number', - initialWidth: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - initialWidth: 180, - }, - ], + columns, + defaultColumns: defaultHeaders, dataProviders: [], dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', @@ -578,49 +499,8 @@ describe('helpers', () => { expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.query, - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - type: 'number', - initialWidth: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - initialWidth: 180, - }, - ], + columns, + defaultColumns: defaultHeaders, dataProviders: [], dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', @@ -677,9 +557,12 @@ describe('helpers', () => { }); test('should merge columns when event.action is deleted without two extra column names of user.name', () => { + const columnsWithoutEventAction = timelineDefaults.columns.filter( + (column) => column.id !== 'event.action' + ); const timeline = { savedObjectId: 'savedObject-1', - columns: timelineDefaults.columns.filter((column) => column.id !== 'event.action'), + columns: columnsWithoutEventAction, version: '1', }; @@ -688,85 +571,8 @@ describe('helpers', () => { activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.query, savedObjectId: 'savedObject-1', - columns: [ - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: '@timestamp', - placeholder: undefined, - type: 'number', - initialWidth: 190, - }, - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: 'message', - placeholder: undefined, - type: undefined, - initialWidth: 180, - }, - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: 'event.category', - placeholder: undefined, - type: undefined, - initialWidth: 180, - }, - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: 'host.name', - placeholder: undefined, - type: undefined, - initialWidth: 180, - }, - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: 'source.ip', - placeholder: undefined, - type: undefined, - initialWidth: 180, - }, - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: 'destination.ip', - placeholder: undefined, - type: undefined, - initialWidth: 180, - }, - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: 'user.name', - placeholder: undefined, - type: undefined, - initialWidth: 180, - }, - ], + columns: columnsWithoutEventAction, + defaultColumns: defaultHeaders, version: '1', dataProviders: [], dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, @@ -822,9 +628,12 @@ describe('helpers', () => { }); test('should merge filters object back with json object', () => { + const columnsWithoutEventAction = timelineDefaults.columns.filter( + (column) => column.id !== 'event.action' + ); const timeline = { savedObjectId: 'savedObject-1', - columns: timelineDefaults.columns.filter((column) => column.id !== 'event.action'), + columns: columnsWithoutEventAction, filters: [ { meta: { @@ -865,44 +674,8 @@ describe('helpers', () => { activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.query, savedObjectId: 'savedObject-1', - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - type: 'number', - initialWidth: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - initialWidth: 180, - }, - ], + columns: columnsWithoutEventAction, + defaultColumns: defaultHeaders, version: '1', dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, dataProviders: [], @@ -1013,49 +786,8 @@ describe('helpers', () => { expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.query, - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - type: 'number', - initialWidth: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - initialWidth: 180, - }, - ], + columns, + defaultColumns: defaultHeaders, dataProviders: [], dateRange: { end: '2020-10-28T11:37:31.655Z', start: '2020-10-27T11:37:31.655Z' }, description: '', @@ -1124,49 +856,8 @@ describe('helpers', () => { expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.query, - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - type: 'number', - initialWidth: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - initialWidth: 180, - }, - ], + columns, + defaultColumns: defaultHeaders, dataProviders: [], dateRange: { end: '2020-07-08T08:20:18.966Z', start: '2020-07-07T08:20:18.966Z' }, description: '', diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 03ac0b3d14342..0dda12d612777 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -257,6 +257,7 @@ export const defaultTimelineToTimelineModel = ( const timelineEntries = { ...timeline, columns: timeline.columns != null ? timeline.columns.map(setTimelineColumn) : defaultHeaders, + defaultColumns: defaultHeaders, dateRange: timeline.status === TimelineStatus.immutable && timeline.timelineType === TimelineType.template diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx index 2daebdf37e77f..d5c1085d506b5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx @@ -29,14 +29,13 @@ import { useTimelineFullScreen, } from '../../../../../common/containers/use_full_screen'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; -import { StatefulFieldsBrowser } from '../../../fields_browser'; import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser'; -import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers'; import { EventsTh, EventsThContent } from '../../styles'; import { EventsSelect } from '../column_headers/events_select'; import * as i18n from '../column_headers/translations'; import { timelineActions } from '../../../../store/timeline'; import { isFullScreen } from '../column_headers'; +import { useKibana } from '../../../../../common/lib/kibana'; const SortingColumnsContainer = styled.div` button { @@ -65,6 +64,7 @@ const HeaderActionsComponent: React.FC = ({ tabType, timelineId, }) => { + const { timelines: timelinesUi } = useKibana().services; const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); const dispatch = useDispatch(); @@ -154,14 +154,11 @@ const HeaderActionsComponent: React.FC = ({ )} - + {timelinesUi.getFieldBrowser({ + browserFields, + columnHeaders, + timelineId, + })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts index c49d088d6241d..760c132cd1824 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts @@ -23,17 +23,19 @@ export const getColumnHeaders = ( headers: ColumnHeaderOptions[], browserFields: BrowserFields ): ColumnHeaderOptions[] => { - return headers.map((header) => { - const splitHeader = header.id.split('.'); // source.geo.city_name -> [source, geo, city_name] + return headers + ? headers.map((header) => { + const splitHeader = header.id.split('.'); // source.geo.city_name -> [source, geo, city_name] - return { - ...header, - ...get( - [splitHeader.length > 1 ? splitHeader[0] : 'base', 'fields', header.id], - browserFields - ), - }; - }); + return { + ...header, + ...get( + [splitHeader.length > 1 ? splitHeader[0] : 'base', 'fields', header.id], + browserFields + ), + }; + }) + : []; }; export const getColumnWidthFromType = (type: string): number => diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx index 5f08bf5a016f5..815f8f43d5c14 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx @@ -60,6 +60,7 @@ jest.mock('../../../../common/lib/kibana', () => { }, timelines: { getLastUpdated: jest.fn(), + getFieldBrowser: jest.fn(), getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper, }, }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx index f4d5570ce40d3..b8e99718fa933 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx @@ -60,6 +60,7 @@ jest.mock('../../../../common/lib/kibana', () => { }, timelines: { getLastUpdated: jest.fn(), + getFieldBrowser: jest.fn(), getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper, }, }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index 9bf7ee28f3934..cd9693313b4f9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -62,6 +62,7 @@ jest.mock('../../../../common/lib/kibana', () => { timelines: { getLastUpdated: jest.fn(), getLoadingPanel: jest.fn(), + getFieldBrowser: jest.fn(), getUseDraggableKeyboardWrapper: () => jest.fn().mockReturnValue({ onBlur: jest.fn(), diff --git a/x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap b/x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap new file mode 100644 index 0000000000000..142ed7a0d7175 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EmptyValue it renders against snapshot 1`] = ` +

+ (Empty String) +

+`; diff --git a/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx b/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx new file mode 100644 index 0000000000000..be9a086d8dc5b --- /dev/null +++ b/x-pack/plugins/timelines/public/components/empty_value/empty_value.test.tsx @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount, shallow } from 'enzyme'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mountWithIntl } from '@kbn/test/jest'; + +import { + defaultToEmptyTag, + getEmptyString, + getEmptyStringTag, + getEmptyTagValue, + getEmptyValue, + getOrEmptyTag, +} from '.'; +import { getMockTheme } from '../../mock/kibana_react.mock'; + +describe('EmptyValue', () => { + const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); + + test('it renders against snapshot', () => { + const wrapper = shallow(

{getEmptyString()}

); + expect(wrapper).toMatchSnapshot(); + }); + + describe('#getEmptyValue', () => { + test('should return an empty value', () => expect(getEmptyValue()).toBe('—')); + }); + + describe('#getEmptyString', () => { + test('should turn into an empty string place holder', () => { + const wrapper = mountWithIntl( + +

{getEmptyString()}

+
+ ); + expect(wrapper.text()).toBe('(Empty String)'); + }); + }); + + describe('#getEmptyTagValue', () => { + const wrapper = mount( + +

{getEmptyTagValue()}

+
+ ); + test('should return an empty tag value', () => expect(wrapper.text()).toBe('—')); + }); + + describe('#getEmptyStringTag', () => { + test('should turn into an span that has length of 1', () => { + const wrapper = mountWithIntl( + +

{getEmptyStringTag()}

+
+ ); + expect(wrapper.find('span')).toHaveLength(1); + }); + + test('should turn into an empty string tag place holder', () => { + const wrapper = mountWithIntl( + +

{getEmptyStringTag()}

+
+ ); + expect(wrapper.text()).toBe(getEmptyString()); + }); + }); + + describe('#defaultToEmptyTag', () => { + test('should default to an empty value when a value is null', () => { + const wrapper = mount( + +

{defaultToEmptyTag(null)}

+
+ ); + expect(wrapper.text()).toBe(getEmptyValue()); + }); + + test('should default to an empty value when a value is undefined', () => { + const wrapper = mount( + +

{defaultToEmptyTag(undefined)}

+
+ ); + expect(wrapper.text()).toBe(getEmptyValue()); + }); + + test('should return a deep path value', () => { + const test = { + a: { + b: { + c: 1, + }, + }, + }; + const wrapper = mount(

{defaultToEmptyTag(test.a.b.c)}

); + expect(wrapper.text()).toBe('1'); + }); + }); + + describe('#getOrEmptyTag', () => { + test('should default empty value when a deep rooted value is null', () => { + const test = { + a: { + b: { + c: null, + }, + }, + }; + const wrapper = mount( + +

{getOrEmptyTag('a.b.c', test)}

+
+ ); + expect(wrapper.text()).toBe(getEmptyValue()); + }); + + test('should default empty value when a deep rooted value is undefined', () => { + const test = { + a: { + b: { + c: undefined, + }, + }, + }; + const wrapper = mount( + +

{getOrEmptyTag('a.b.c', test)}

+
+ ); + expect(wrapper.text()).toBe(getEmptyValue()); + }); + + test('should default empty value when a deep rooted value is missing', () => { + const test = { + a: { + b: {}, + }, + }; + const wrapper = mount( + +

{getOrEmptyTag('a.b.c', test)}

+
+ ); + expect(wrapper.text()).toBe(getEmptyValue()); + }); + + test('should return a deep path value', () => { + const test = { + a: { + b: { + c: 1, + }, + }, + }; + const wrapper = mount(

{getOrEmptyTag('a.b.c', test)}

); + expect(wrapper.text()).toBe('1'); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/empty_value/index.tsx b/x-pack/plugins/timelines/public/components/empty_value/index.tsx new file mode 100644 index 0000000000000..86efb4a78277a --- /dev/null +++ b/x-pack/plugins/timelines/public/components/empty_value/index.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get, isString } from 'lodash/fp'; +import React from 'react'; +import styled from 'styled-components'; + +import * as i18n from './translations'; + +const EmptyWrapper = styled.span` + color: ${(props) => props.theme.eui.euiColorMediumShade}; +`; + +EmptyWrapper.displayName = 'EmptyWrapper'; + +export const getEmptyValue = () => '—'; +export const getEmptyString = () => `(${i18n.EMPTY_STRING})`; + +export const getEmptyTagValue = () => {getEmptyValue()}; +export const getEmptyStringTag = () => {getEmptyString()}; + +export const defaultToEmptyTag = (item: T): JSX.Element => { + if (item == null) { + return getEmptyTagValue(); + } else if (isString(item) && item === '') { + return getEmptyStringTag(); + } else { + return <>{item}; + } +}; + +export const getOrEmptyTag = (path: string, item: unknown): JSX.Element => { + const text = get(path, item); + return getOrEmptyTagFromValue(text); +}; + +export const getOrEmptyTagFromValue = (value: string | number | null | undefined): JSX.Element => { + if (value == null) { + return getEmptyTagValue(); + } else if (value === '') { + return getEmptyStringTag(); + } else { + return <>{value}; + } +}; diff --git a/x-pack/plugins/timelines/public/components/empty_value/translations.ts b/x-pack/plugins/timelines/public/components/empty_value/translations.ts new file mode 100644 index 0000000000000..20c822c67dfb3 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/empty_value/translations.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const EMPTY_STRING = i18n.translate('xpack.timelines.emptyString.emptyStringDescription', { + defaultMessage: 'Empty String', +}); diff --git a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx new file mode 100644 index 0000000000000..ac121f9afdd58 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Store } from 'redux'; +import { Provider } from 'react-redux'; +import { I18nProvider } from '@kbn/i18n/react'; +import type { FieldBrowserProps } from '../t_grid/toolbar/fields_browser/types'; +import { StatefulFieldsBrowser } from '../t_grid/toolbar/fields_browser'; +import { + FIELD_BROWSER_WIDTH, + FIELD_BROWSER_HEIGHT, +} from '../t_grid/toolbar/fields_browser/helpers'; + +const EMPTY_BROWSER_FIELDS = {}; +export type FieldBrowserWrappedProps = Omit & { + width?: FieldBrowserProps['width']; + height?: FieldBrowserProps['height']; +}; +export type FieldBrowserWrappedComponentProps = FieldBrowserWrappedProps & { + store: Store; +}; + +export const FieldBrowserWrappedComponent = (props: FieldBrowserWrappedComponentProps) => { + const { store, ...restProps } = props; + const fieldsBrowseProps = { + width: FIELD_BROWSER_WIDTH, + height: FIELD_BROWSER_HEIGHT, + ...restProps, + browserFields: restProps.browserFields ?? EMPTY_BROWSER_FIELDS, + }; + return ( + + + + + + ); +}; + +FieldBrowserWrappedComponent.displayName = 'FieldBrowserWrappedComponent'; + +// eslint-disable-next-line import/no-default-export +export { FieldBrowserWrappedComponent as default }; diff --git a/x-pack/plugins/timelines/public/components/index.tsx b/x-pack/plugins/timelines/public/components/index.tsx index 8bb4e6cb45853..9959574464836 100644 --- a/x-pack/plugins/timelines/public/components/index.tsx +++ b/x-pack/plugins/timelines/public/components/index.tsx @@ -8,17 +8,17 @@ import React from 'react'; import { Provider } from 'react-redux'; import { I18nProvider } from '@kbn/i18n/react'; -import { Store } from 'redux'; +import type { Store } from 'redux'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import type { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { createStore } from '../store/t_grid'; import { TGrid as TGridComponent } from './tgrid'; -import { TGridProps } from '../types'; +import type { TGridProps } from '../types'; import { DragDropContextWrapper } from './drag_and_drop'; import { initialTGridState } from '../store/t_grid/reducer'; -import { TGridIntegratedProps } from './t_grid/integrated'; +import type { TGridIntegratedProps } from './t_grid/integrated'; const EMPTY_BROWSER_FIELDS = {}; @@ -58,3 +58,4 @@ export * from './drag_and_drop'; export * from './draggables'; export * from './last_updated'; export * from './loading'; +export * from './fields_browser'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index c267a0e57dd2c..fd0b0a5eef75d 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -41,13 +41,12 @@ import { Footer, footerHeight } from '../footer'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../styles'; import * as i18n from './translations'; import { InspectButtonContainer } from '../../inspect'; +import { useFetchIndex } from '../../../container/source'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px const UTILITY_BAR_HEIGHT = 19; // px const COMPACT_HEADER_HEIGHT = EVENTS_VIEWER_HEADER_HEIGHT - UTILITY_BAR_HEIGHT; // px const STANDALONE_ID = 'standalone-t-grid'; -const EMPTY_BROWSER_FIELDS = {}; -const EMPTY_INDEX_PATTERN = { title: '', fields: [] }; const EMPTY_DATA_PROVIDERS: DataProvider[] = []; const UtilityBar = styled.div` @@ -157,6 +156,7 @@ const TGridStandaloneComponent: React.FC = ({ const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const { uiSettings } = useKibana().services; const [isQueryLoading, setIsQueryLoading] = useState(false); + const [indexPatternsLoading, { browserFields, indexPatterns }] = useFetchIndex(indexNames); const getTGrid = useMemo(() => tGridSelectors.getTGridByIdSelector(), []); const { @@ -171,20 +171,24 @@ const TGridStandaloneComponent: React.FC = ({ const justTitle = useMemo(() => {title}, [title]); - const combinedQueries = combineQueries({ - config: esQuery.getEsQueryConfig(uiSettings), - dataProviders: EMPTY_DATA_PROVIDERS, - indexPattern: EMPTY_INDEX_PATTERN, - browserFields: EMPTY_BROWSER_FIELDS, - filters, - kqlQuery: query, - kqlMode: 'search', - isEventViewer: true, - }); + const combinedQueries = useMemo( + () => + combineQueries({ + config: esQuery.getEsQueryConfig(uiSettings), + dataProviders: EMPTY_DATA_PROVIDERS, + indexPattern: indexPatterns, + browserFields, + filters, + kqlQuery: query, + kqlMode: 'search', + isEventViewer: true, + }), + [uiSettings, indexPatterns, browserFields, filters, query] + ); const canQueryTimeline = useMemo( - () => combinedQueries != null && !isEmpty(start) && !isEmpty(end), - [combinedQueries, start, end] + () => !indexPatternsLoading && combinedQueries != null && !isEmpty(start) && !isEmpty(end), + [indexPatternsLoading, combinedQueries, start, end] ); const fields = useMemo( @@ -280,8 +284,9 @@ const TGridStandaloneComponent: React.FC = ({ ); dispatch( tGridActions.initializeTGridSettings({ - footerText, id: STANDALONE_ID, + defaultColumns: columns, + footerText, loadingText, unit, }) @@ -316,7 +321,7 @@ const TGridStandaloneComponent: React.FC = ({ { const timelineId = 'test'; const selectedCategoryId = 'client'; @@ -33,12 +29,9 @@ describe('Category', () => { data-test-subj="category" filteredBrowserFields={mockBrowserFields} fieldItems={getFieldItems({ - browserFields: mockBrowserFields, category: mockBrowserFields[selectedCategoryId], - categoryId: selectedCategoryId, columnHeaders: [], highlight: '', - onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), })} @@ -63,12 +56,9 @@ describe('Category', () => { data-test-subj="category" filteredBrowserFields={mockBrowserFields} fieldItems={getFieldItems({ - browserFields: mockBrowserFields, category: mockBrowserFields[selectedCategoryId], - categoryId: selectedCategoryId, columnHeaders: [], highlight: '', - onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), })} @@ -91,12 +81,9 @@ describe('Category', () => { data-test-subj="category" filteredBrowserFields={mockBrowserFields} fieldItems={getFieldItems({ - browserFields: mockBrowserFields, category: mockBrowserFields[selectedCategoryId], - categoryId: selectedCategoryId, columnHeaders: [], highlight: '', - onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx similarity index 91% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx index deafda95ceab2..45f4b5d12c513 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx @@ -14,13 +14,14 @@ import { DATA_COLINDEX_ATTRIBUTE, DATA_ROWINDEX_ATTRIBUTE, onKeyDownFocusHandler, -} from '../../../../../timelines/public'; - -import { BrowserFields } from '../../../common/containers/source'; -import { OnUpdateColumns } from '../timeline/events'; +} from '../../../../../common'; +// eslint-disable-next-line no-duplicate-imports +import type { BrowserFields, OnUpdateColumns } from '../../../../../common'; import { CategoryTitle } from './category_title'; -import { FieldItem, getFieldColumns } from './field_items'; +import { getFieldColumns } from './field_items'; +// eslint-disable-next-line no-duplicate-imports +import type { FieldItem } from './field_items'; import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx index 7b00b768b56a0..30a6c44a08a16 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx @@ -8,7 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { mockBrowserFields } from '../../../../mock'; import { CATEGORY_PANE_WIDTH, getFieldCount } from './helpers'; import { CategoriesPane } from './categories_pane'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx similarity index 88% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx index 528791328fdb9..4a2805839024e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx @@ -18,19 +18,18 @@ import { import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; -import { BrowserFields } from '../../../common/containers/source'; -import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; -import { CountBadge } from '../../../common/components/page'; -import { OnUpdateColumns } from '../timeline/events'; +import { useDeepEqualSelector } from '../../../../hooks/use_selector'; import { LoadingSpinner, getCategoryPaneCategoryClassName, getFieldCount, VIEW_ALL_BUTTON_CLASS_NAME, + CountBadge, } from './helpers'; import * as i18n from './translations'; -import { timelineSelectors } from '../../store/timeline'; +import { tGridSelectors } from '../../../../store/t_grid'; +import { getColumnsWithTimestamp } from '../../../utils/helpers'; +import type { OnUpdateColumns, BrowserFields } from '../../../../../common'; const CategoryName = styled.span<{ bold: boolean }>` .euiText { @@ -68,7 +67,7 @@ interface ViewAllButtonProps { export const ViewAllButton = React.memo( ({ categoryId, browserFields, onUpdateColumns, timelineId }) => { - const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); const { isLoading } = useDeepEqualSelector((state) => getManageTimeline(state, timelineId ?? '') ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx similarity index 94% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx index 6af4b5c5c312e..746668491abb8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx @@ -8,8 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; +import { mockBrowserFields, TestProviders } from '../../../../mock'; import { CategoryTitle } from './category_title'; import { getFieldCount } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx similarity index 88% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx index e2599ec250a2d..2296b4c855d42 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx @@ -8,10 +8,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly, EuiTitle } from '@elastic/eui'; import React from 'react'; -import { BrowserFields } from '../../../common/containers/source'; -import { getFieldBrowserCategoryTitleClassName, getFieldCount } from './helpers'; -import { CountBadge } from '../../../common/components/page'; -import { OnUpdateColumns } from '../timeline/events'; +import { CountBadge, getFieldBrowserCategoryTitleClassName, getFieldCount } from './helpers'; +import type { BrowserFields, OnUpdateColumns } from '../../../../../common'; import { ViewAllButton } from './category_columns'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx index 9e374752bd302..96a76df19cd93 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx @@ -8,9 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import '../../../common/mock/match_media'; -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; +import { TestProviders, mockBrowserFields } from '../../../../mock'; import { FieldsBrowser } from './field_browser'; import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx similarity index 95% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx index 0496b9d7c8886..6529cf8776125 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx @@ -19,8 +19,9 @@ import { noop } from 'lodash/fp'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import { isEscape, isTab, stopPropagationAndPreventDefault } from '../../../../../timelines/public'; -import { BrowserFields } from '../../../common/containers/source'; +import type { BrowserFields, ColumnHeaderOptions } from '../../../../../common'; +// eslint-disable-next-line no-duplicate-imports +import { isEscape, isTab, stopPropagationAndPreventDefault } from '../../../../../common'; import { CategoriesPane } from './categories_pane'; import { FieldsPane } from './fields_pane'; import { Header } from './header'; @@ -33,11 +34,10 @@ import { PANES_FLEX_GROUP_WIDTH, scrollCategoriesPane, } from './helpers'; -import { FieldBrowserProps, OnHideFieldBrowser } from './types'; -import { timelineActions } from '../../store/timeline'; +import type { FieldBrowserProps, OnHideFieldBrowser } from './types'; +import { tGridActions } from '../../../../store/t_grid'; import * as i18n from './translations'; -import { ColumnHeaderOptions } from '../../../../common'; const FieldsBrowserContainer = styled.div<{ width: number }>` background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; @@ -116,7 +116,6 @@ type Props = Pick< * set focus to the search input, scroll to the selected category, etc */ const FieldsBrowserComponent: React.FC = ({ - browserFields, columnHeaders, filteredBrowserFields, isSearching, @@ -135,7 +134,7 @@ const FieldsBrowserComponent: React.FC = ({ const containerElement = useRef(null); const onUpdateColumns = useCallback( - (columns) => dispatch(timelineActions.updateColumns({ id: timelineId, columns })), + (columns) => dispatch(tGridActions.updateColumns({ id: timelineId, columns })), [dispatch, timelineId] ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx similarity index 86% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx index e40807dc85dc7..789aeeeb187fd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx @@ -8,20 +8,15 @@ import { omit } from 'lodash/fp'; import React from 'react'; import { waitFor } from '@testing-library/react'; - -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; -import '../../../common/mock/match_media'; -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; -import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; +import { mockBrowserFields, TestProviders } from '../../../../mock'; +import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; +import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../body/constants'; import { Category } from './category'; import { getFieldColumns, getFieldItems } from './field_items'; import { FIELDS_PANE_WIDTH } from './helpers'; -import { useMountAppended } from '../../../common/utils/use_mount_appended'; -import { ColumnHeaderOptions } from '../../../../common'; - -jest.mock('../../../common/lib/kibana'); +import { useMountAppended } from '../../../utils/use_mount_appended'; +import { ColumnHeaderOptions } from '../../../../../common'; const selectedCategoryId = 'base'; const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields; @@ -54,12 +49,9 @@ describe('field_items', () => { data-test-subj="category" filteredBrowserFields={mockBrowserFields} fieldItems={getFieldItems({ - browserFields: mockBrowserFields, category: mockBrowserFields[selectedCategoryId], - categoryId: selectedCategoryId, columnHeaders: [], highlight: '', - onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), })} @@ -86,12 +78,9 @@ describe('field_items', () => { data-test-subj="category" filteredBrowserFields={mockBrowserFields} fieldItems={getFieldItems({ - browserFields: mockBrowserFields, category: mockBrowserFields[selectedCategoryId], - categoryId: selectedCategoryId, columnHeaders: [], highlight: '', - onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), })} @@ -117,12 +106,9 @@ describe('field_items', () => { data-test-subj="category" filteredBrowserFields={mockBrowserFields} fieldItems={getFieldItems({ - browserFields: mockBrowserFields, category: mockBrowserFields[selectedCategoryId], - categoryId: selectedCategoryId, columnHeaders, highlight: '', - onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), })} @@ -148,12 +134,9 @@ describe('field_items', () => { data-test-subj="category" filteredBrowserFields={mockBrowserFields} fieldItems={getFieldItems({ - browserFields: mockBrowserFields, category: mockBrowserFields[selectedCategoryId], - categoryId: selectedCategoryId, columnHeaders: columnHeaders.filter((header) => header.id !== timestampFieldId), highlight: '', - onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), })} @@ -181,12 +164,9 @@ describe('field_items', () => { data-test-subj="category" filteredBrowserFields={mockBrowserFields} fieldItems={getFieldItems({ - browserFields: mockBrowserFields, category: mockBrowserFields[selectedCategoryId], - categoryId: selectedCategoryId, columnHeaders: [], highlight: '', - onUpdateColumns: jest.fn(), timelineId, toggleColumn, })} @@ -241,12 +221,9 @@ describe('field_items', () => { data-test-subj="category" filteredBrowserFields={mockBrowserFieldsWithSignal} fieldItems={getFieldItems({ - browserFields: mockBrowserFieldsWithSignal, category: mockBrowserFieldsWithSignal[mockSelectedCategoryId], - categoryId: mockSelectedCategoryId, columnHeaders, highlight: '', - onUpdateColumns: jest.fn(), timelineId, toggleColumn, })} @@ -281,12 +258,9 @@ describe('field_items', () => { data-test-subj="category" filteredBrowserFields={mockBrowserFields} fieldItems={getFieldItems({ - browserFields: mockBrowserFields, category: mockBrowserFields[selectedCategoryId], - categoryId: selectedCategoryId, columnHeaders, highlight: '', - onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), })} @@ -311,12 +285,9 @@ describe('field_items', () => { data-test-subj="category" filteredBrowserFields={mockBrowserFields} fieldItems={getFieldItems({ - browserFields: mockBrowserFields, category: mockBrowserFields[selectedCategoryId], - categoryId: selectedCategoryId, columnHeaders, highlight: '', - onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), })} diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx new file mode 100644 index 0000000000000..8015be2bcc857 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiCheckbox, + EuiIcon, + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + EuiScreenReaderOnly, +} from '@elastic/eui'; +import { uniqBy } from 'lodash/fp'; +import styled from 'styled-components'; + +import { getEmptyValue } from '../../../empty_value'; +import { getExampleText, getIconFromType } from '../../../utils/helpers'; +import type { ColumnHeaderOptions, BrowserField } from '../../../../../common'; +import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../../body/constants'; +import { TruncatableText } from '../../../truncatable_text'; +import { FieldName } from './field_name'; +import * as i18n from './translations'; +import { getAlertColumnHeader } from './helpers'; + +const TypeIcon = styled(EuiIcon)` + margin: 0 4px; + position: relative; + top: -1px; +`; + +TypeIcon.displayName = 'TypeIcon'; + +export const Description = styled.span` + user-select: text; + width: 400px; +`; + +Description.displayName = 'Description'; + +/** + * An item rendered in the table + */ +export interface FieldItem { + ariaRowindex?: number; + checkbox: React.ReactNode; + description: React.ReactNode; + field: React.ReactNode; + fieldId: string; +} + +/** + * Returns the fields items, values, and descriptions shown when a user expands an event + */ +export const getFieldItems = ({ + category, + columnHeaders, + highlight = '', + timelineId, + toggleColumn, +}: { + category: Partial; + columnHeaders: ColumnHeaderOptions[]; + highlight?: string; + timelineId: string; + toggleColumn: (column: ColumnHeaderOptions) => void; +}): FieldItem[] => + uniqBy('name', [ + ...Object.values(category != null && category.fields != null ? category.fields : {}), + ]).map((field) => ({ + checkbox: ( + + c.id === field.name) !== -1} + data-test-subj={`field-${field.name}-checkbox`} + data-colindex={1} + id={field.name ?? ''} + onChange={() => + toggleColumn({ + columnHeaderType: defaultColumnHeaderType, + id: field.name ?? '', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + ...getAlertColumnHeader(timelineId, field.name ?? ''), + }) + } + /> + + ), + field: ( + + + + + + + + + + + + ), + description: ( +
+ + <> + +

{i18n.DESCRIPTION_FOR_FIELD(field.name ?? '')}

+
+ + + {`${field.description ?? getEmptyValue()} ${getExampleText(field.example)}`} + + + +
+
+ ), + fieldId: field.name ?? '', + })); + +/** + * Returns a table column template provided to the `EuiInMemoryTable`'s + * `columns` prop + */ +export const getFieldColumns = () => [ + { + field: 'checkbox', + name: '', + render: (checkbox: React.ReactNode, _: FieldItem) => checkbox, + sortable: false, + width: '25px', + }, + { + field: 'field', + name: i18n.FIELD, + render: (field: React.ReactNode, _: FieldItem) => field, + sortable: false, + width: '225px', + }, + { + field: 'description', + name: i18n.DESCRIPTION, + render: (description: React.ReactNode, _: FieldItem) => description, + sortable: false, + truncateText: true, + width: '400px', + }, +]; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx new file mode 100644 index 0000000000000..05f093eaf1805 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { mockBrowserFields, TestProviders } from '../../../../mock'; +import { getColumnsWithTimestamp } from '../../../utils/helpers'; + +import { FieldName } from './field_name'; + +const categoryId = 'base'; +const timestampFieldId = '@timestamp'; + +const defaultProps = { + categoryId, + categoryColumns: getColumnsWithTimestamp({ + browserFields: mockBrowserFields, + category: categoryId, + }), + closePopOverTrigger: false, + fieldId: timestampFieldId, + handleClosePopOverTrigger: jest.fn(), + hoverActionsOwnFocus: false, + onCloseRequested: jest.fn(), + onUpdateColumns: jest.fn(), + setClosePopOverTrigger: jest.fn(), +}; + +describe('FieldName', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + test('it renders the field name', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="field-name-${timestampFieldId}"]`).first().text() + ).toEqual(timestampFieldId); + }); + + test('it highlights the text specified by the `highlight` prop', () => { + const highlight = 'stamp'; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('mark').first().text()).toEqual(highlight); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx new file mode 100644 index 0000000000000..5781211058d3c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiHighlight, EuiText } from '@elastic/eui'; + +/** Renders a field name in it's non-dragging state */ +export const FieldName = React.memo<{ + fieldId: string; + highlight?: string; +}>(({ fieldId, highlight = '' }) => { + return ( + + + {fieldId} + + + ); +}); + +FieldName.displayName = 'FieldName'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx similarity index 91% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx index 6d17f148aa1dc..275ce4907435f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx @@ -7,16 +7,12 @@ import React from 'react'; -import '../../../common/mock/match_media'; -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; -import { useMountAppended } from '../../../common/utils/use_mount_appended'; +import { useMountAppended } from '../../../utils/use_mount_appended'; +import { mockBrowserFields, TestProviders } from '../../../../mock'; import { FIELDS_PANE_WIDTH } from './helpers'; import { FieldsPane } from './fields_pane'; -jest.mock('../../../common/lib/kibana'); - const timelineId = 'test'; describe('FieldsPane', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx similarity index 88% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx index dfb4edad17414..633d1c536035a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx @@ -10,16 +10,14 @@ import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import { BrowserFields } from '../../../common/containers/source'; -import { timelineActions } from '../../../timelines/store/timeline'; -import { OnUpdateColumns } from '../timeline/events'; import { Category } from './category'; -import { FieldBrowserProps } from './types'; +import type { FieldBrowserProps } from './types'; import { getFieldItems } from './field_items'; import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers'; import * as i18n from './translations'; -import { ColumnHeaderOptions } from '../../../../common'; +import type { BrowserFields, ColumnHeaderOptions, OnUpdateColumns } from '../../../../../common'; +import { tGridActions } from '../../../../store/t_grid'; const NoFieldsPanel = styled.div` background-color: ${(props) => props.theme.eui.euiColorLightestShade}; @@ -76,14 +74,14 @@ export const FieldsPane = React.memo( (column: ColumnHeaderOptions) => { if (columnHeaders.some((c) => c.id === column.id)) { dispatch( - timelineActions.removeColumn({ + tGridActions.removeColumn({ columnId: column.id, id: timelineId, }) ); } else { dispatch( - timelineActions.upsertColumn({ + tGridActions.upsertColumn({ column, id: timelineId, index: 1, @@ -106,12 +104,9 @@ export const FieldsPane = React.memo( data-test-subj="category" filteredBrowserFields={filteredBrowserFields} fieldItems={getFieldItems({ - browserFields: filteredBrowserFields, category: filteredBrowserFields[selectedCategoryId], - categoryId: selectedCategoryId, columnHeaders, highlight: searchInput, - onUpdateColumns, timelineId, toggleColumn, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.test.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.test.tsx index 89b361e86422e..0270540fc491d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.test.tsx @@ -7,8 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; +import { mockBrowserFields, TestProviders } from '../../../../mock'; import { Header } from './header'; const timelineId = 'test'; @@ -29,9 +28,7 @@ describe('Header', () => { ); - expect(wrapper.find('[data-test-subj="field-browser-title"]').first().text()).toEqual( - 'Customize Columns' - ); + expect(wrapper.find('[data-test-subj="field-browser-title"]').first().text()).toEqual('Fields'); }); test('it renders the Reset Fields button', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.tsx similarity index 91% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.tsx index b52c6cd672ac7..42ea20f1dfab8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.tsx @@ -15,11 +15,9 @@ import { } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; - -import { BrowserFields } from '../../../common/containers/source'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; -import { timelineSelectors } from '../../store/timeline'; -import { OnUpdateColumns } from '../timeline/events'; +import type { BrowserFields, OnUpdateColumns } from '../../../../../common'; +import { useDeepEqualSelector } from '../../../../hooks/use_selector'; +import { tGridSelectors } from '../../../../store/t_grid'; import { getFieldBrowserSearchInputClassName, @@ -102,7 +100,7 @@ const TitleRow = React.memo<{ onOutsideClick: () => void; onUpdateColumns: OnUpdateColumns; }>(({ id, onOutsideClick, onUpdateColumns }) => { - const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); const { defaultColumns } = useDeepEqualSelector((state) => getManageTimeline(state, id)); const handleResetColumns = useCallback(() => { @@ -119,7 +117,7 @@ const TitleRow = React.memo<{ > -

{i18n.CUSTOMIZE_COLUMNS}

+

{i18n.FIELDS_BROWSER}

diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx similarity index 88% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.test.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx index 202020607a3d6..ee7d0a334ed23 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { mockBrowserFields } from '../../../../mock'; import { categoryHasFields, @@ -16,7 +16,7 @@ import { getFieldCount, filterBrowserFieldsByFieldName, } from './helpers'; -import { BrowserFields } from '../../../common/containers/source'; +import { BrowserFields } from '../../../../../common'; const timelineId = 'test'; @@ -373,5 +373,45 @@ describe('helpers', () => { }) ).toEqual(expectedMatchingFields); }); + + test('it combines the specified fields into a virtual category omitting the fields missing in the browser fields', () => { + const expectedMatchingFields = { + fields: { + '@timestamp': { + aggregatable: true, + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + }, + 'client.domain': { + aggregatable: true, + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + }, + }, + }; + + const fieldIds = ['agent.hostname', '@timestamp', 'client.domain']; + const { agent, ...mockBrowserFieldsWithoutAgent } = mockBrowserFields; + + expect( + createVirtualCategory({ + browserFields: mockBrowserFieldsWithoutAgent, + fieldIds, + }) + ).toEqual(expectedMatchingFields); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx similarity index 92% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx index 256c172721d35..9a3559c51ef87 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiBadge, EuiLoadingSpinner } from '@elastic/eui'; import { filter, get, pickBy } from 'lodash/fp'; import styled from 'styled-components'; @@ -13,14 +13,11 @@ import { elementOrChildrenHasFocus, skipFocusInContainerTo, stopPropagationAndPreventDefault, -} from '../../../../../timelines/public'; -import { TimelineId } from '../../../../common/types/timeline'; -import { BrowserField, BrowserFields } from '../../../common/containers/source'; -import { alertsHeaders } from '../../../common/components/alerts_viewer/default_headers'; -import { - DEFAULT_CATEGORY_NAME, - defaultHeaders, -} from '../timeline/body/column_headers/default_headers'; +} from '../../../../../public'; +import { TimelineId } from '../../../../../public/types'; +import type { BrowserField, BrowserFields } from '../../../../../common'; +import { defaultHeaders } from '../../../../store/t_grid/defaults'; +import { DEFAULT_CATEGORY_NAME } from '../../body/column_headers/default_headers'; export const LoadingSpinner = styled(EuiLoadingSpinner)` cursor: pointer; @@ -126,15 +123,23 @@ export const createVirtualCategory = ({ browserFields: BrowserFields; fieldIds: string[]; }): Partial => ({ - fields: fieldIds.reduce>>>((fields, fieldId) => { + fields: fieldIds.reduce>((fields, fieldId) => { const splitId = fieldId.split('.'); // source.geo.city_name -> [source, geo, city_name] + const browserField = get( + [splitId.length > 1 ? splitId[0] : 'base', 'fields', fieldId], + browserFields + ); return { ...fields, - [fieldId]: { - ...get([splitId.length > 1 ? splitId[0] : 'base', 'fields', fieldId], browserFields), - name: fieldId, - }, + ...(browserField + ? { + [fieldId]: { + ...browserField, + name: fieldId, + }, + } + : {}), }; }, {}), }); @@ -152,7 +157,7 @@ export const mergeBrowserFieldsWithDefaultCategory = ( export const getAlertColumnHeader = (timelineId: string, fieldId: string) => timelineId === TimelineId.detectionsPage || timelineId === TimelineId.detectionsRulesDetailsPage - ? alertsHeaders.find((c) => c.id === fieldId) ?? {} + ? defaultHeaders.find((c) => c.id === fieldId) ?? {} : {}; export const CATEGORIES_PANE_CLASS_NAME = 'categories-pane'; @@ -393,3 +398,9 @@ export const onFieldsBrowserTabPressed = ({ }); } }; + +export const CountBadge = (styled(EuiBadge)` + margin-left: 5px; +` as unknown) as typeof EuiBadge; + +CountBadge.displayName = 'CountBadge'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx similarity index 95% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx index 381017f7f6260..60c1e8da08b78 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx @@ -9,17 +9,12 @@ import { mount } from 'enzyme'; import React from 'react'; import { waitFor } from '@testing-library/react'; -import '../../../common/mock/match_media'; -import '../../../common/mock/react_beautiful_dnd'; -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; +import { mockBrowserFields, TestProviders } from '../../../../mock'; import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; import { StatefulFieldsBrowserComponent } from '.'; -jest.mock('../../../common/lib/kibana'); - describe('StatefulFieldsBrowser', () => { const timelineId = 'test'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx similarity index 94% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx index 2bdf6862a8f8c..3f2d8c490d215 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx @@ -10,12 +10,12 @@ import { noop } from 'lodash/fp'; import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { BrowserFields } from '../../../common/containers/source'; -import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers'; +import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; +import { DEFAULT_CATEGORY_NAME } from '../../body/column_headers/default_headers'; import { FieldsBrowser } from './field_browser'; import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; import * as i18n from './translations'; -import { FieldBrowserProps } from './types'; +import type { FieldBrowserProps } from './types'; const fieldsButtonClassName = 'fields-button'; @@ -23,6 +23,7 @@ const fieldsButtonClassName = 'fields-button'; export const INPUT_TIMEOUT = 250; const FieldsBrowserButtonContainer = styled.div` + display: inline-block; position: relative; width: 24px; `; @@ -125,13 +126,13 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ return ( - + {i18n.FIELDS} diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts similarity index 51% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/translations.ts rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts index a392aa2191422..21bf3a1479fc7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/translations.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts @@ -7,107 +7,95 @@ import { i18n } from '@kbn/i18n'; -export const CATEGORY = i18n.translate('xpack.securitySolution.fieldBrowser.categoryLabel', { +export const CATEGORY = i18n.translate('xpack.timelines.fieldBrowser.categoryLabel', { defaultMessage: 'Category', }); -export const CATEGORIES = i18n.translate('xpack.securitySolution.fieldBrowser.categoriesTitle', { +export const CATEGORIES = i18n.translate('xpack.timelines.fieldBrowser.categoriesTitle', { defaultMessage: 'Categories', }); export const CATEGORIES_COUNT = (totalCount: number) => - i18n.translate('xpack.securitySolution.fieldBrowser.categoriesCountTitle', { + i18n.translate('xpack.timelines.fieldBrowser.categoriesCountTitle', { values: { totalCount }, defaultMessage: '{totalCount} {totalCount, plural, =1 {category} other {categories}}', }); export const CATEGORY_LINK = ({ category, totalCount }: { category: string; totalCount: number }) => - i18n.translate('xpack.securitySolution.fieldBrowser.categoryLinkAriaLabel', { + i18n.translate('xpack.timelines.fieldBrowser.categoryLinkAriaLabel', { values: { category, totalCount }, defaultMessage: '{category} {totalCount} {totalCount, plural, =1 {field} other {fields}}. Click this button to select the {category} category.', }); export const CATEGORY_FIELDS_TABLE_CAPTION = (categoryId: string) => - i18n.translate('xpack.securitySolution.fieldBrowser.categoryFieldsTableCaption', { + i18n.translate('xpack.timelines.fieldBrowser.categoryFieldsTableCaption', { defaultMessage: 'category {categoryId} fields', values: { categoryId, }, }); -export const COPY_TO_CLIPBOARD = i18n.translate( - 'xpack.securitySolution.fieldBrowser.copyToClipboard', - { - defaultMessage: 'Copy to Clipboard', - } -); +export const COPY_TO_CLIPBOARD = i18n.translate('xpack.timelines.fieldBrowser.copyToClipboard', { + defaultMessage: 'Copy to Clipboard', +}); -export const CLOSE = i18n.translate('xpack.securitySolution.fieldBrowser.closeButton', { +export const CLOSE = i18n.translate('xpack.timelines.fieldBrowser.closeButton', { defaultMessage: 'Close', }); -export const CUSTOMIZE_COLUMNS = i18n.translate( - 'xpack.securitySolution.fieldBrowser.customizeColumnsTitle', - { - defaultMessage: 'Customize Columns', - } -); +export const FIELDS_BROWSER = i18n.translate('xpack.timelines.fieldBrowser.fieldBrowserTitle', { + defaultMessage: 'Fields', +}); -export const DESCRIPTION = i18n.translate('xpack.securitySolution.fieldBrowser.descriptionLabel', { +export const DESCRIPTION = i18n.translate('xpack.timelines.fieldBrowser.descriptionLabel', { defaultMessage: 'Description', }); export const DESCRIPTION_FOR_FIELD = (field: string) => - i18n.translate('xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly', { + i18n.translate('xpack.timelines.fieldBrowser.descriptionForScreenReaderOnly', { values: { field, }, defaultMessage: 'Description for field {field}:', }); -export const FIELD = i18n.translate('xpack.securitySolution.fieldBrowser.fieldLabel', { +export const FIELD = i18n.translate('xpack.timelines.fieldBrowser.fieldLabel', { defaultMessage: 'Field', }); -export const FIELDS = i18n.translate('xpack.securitySolution.fieldBrowser.fieldsTitle', { +export const FIELDS = i18n.translate('xpack.timelines.fieldBrowser.fieldsTitle', { defaultMessage: 'Columns', }); export const FIELDS_COUNT = (totalCount: number) => - i18n.translate('xpack.securitySolution.fieldBrowser.fieldsCountTitle', { + i18n.translate('xpack.timelines.fieldBrowser.fieldsCountTitle', { values: { totalCount }, defaultMessage: '{totalCount} {totalCount, plural, =1 {field} other {fields}}', }); -export const FILTER_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.fieldBrowser.filterPlaceholder', - { - defaultMessage: 'Field name', - } -); +export const FILTER_PLACEHOLDER = i18n.translate('xpack.timelines.fieldBrowser.filterPlaceholder', { + defaultMessage: 'Field name', +}); -export const NO_FIELDS_MATCH = i18n.translate( - 'xpack.securitySolution.fieldBrowser.noFieldsMatchLabel', - { - defaultMessage: 'No fields match', - } -); +export const NO_FIELDS_MATCH = i18n.translate('xpack.timelines.fieldBrowser.noFieldsMatchLabel', { + defaultMessage: 'No fields match', +}); export const NO_FIELDS_MATCH_INPUT = (searchInput: string) => - i18n.translate('xpack.securitySolution.fieldBrowser.noFieldsMatchInputLabel', { + i18n.translate('xpack.timelines.fieldBrowser.noFieldsMatchInputLabel', { defaultMessage: 'No fields match {searchInput}', values: { searchInput, }, }); -export const RESET_FIELDS = i18n.translate('xpack.securitySolution.fieldBrowser.resetFieldsLink', { +export const RESET_FIELDS = i18n.translate('xpack.timelines.fieldBrowser.resetFieldsLink', { defaultMessage: 'Reset Fields', }); export const VIEW_ALL_CATEGORY_FIELDS = (categoryId: string) => - i18n.translate('xpack.securitySolution.fieldBrowser.viewCategoryTooltip', { + i18n.translate('xpack.timelines.fieldBrowser.viewCategoryTooltip', { defaultMessage: 'View all {categoryId} fields', values: { categoryId, @@ -115,14 +103,14 @@ export const VIEW_ALL_CATEGORY_FIELDS = (categoryId: string) => }); export const YOU_ARE_IN_A_POPOVER = i18n.translate( - 'xpack.securitySolution.fieldBrowser.youAreInAPopoverScreenReaderOnly', + 'xpack.timelines.fieldBrowser.youAreInAPopoverScreenReaderOnly', { defaultMessage: 'You are in the Customize Columns popup. To exit this popup, press Escape.', } ); export const VIEW_COLUMN = (field: string) => - i18n.translate('xpack.securitySolution.fieldBrowser.viewColumnCheckboxAriaLabel', { + i18n.translate('xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel', { values: { field }, defaultMessage: 'View {field} column', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts similarity index 86% rename from x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts index ea71a8860ab01..ebd72083a2bfe 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ColumnHeaderOptions } from '../../../../common'; -import { BrowserFields } from '../../../common/containers/source'; +import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; +import type { ColumnHeaderOptions } from '../../../../../common/types/timeline/columns'; export type OnFieldSelected = (fieldId: string) => void; export type OnHideFieldBrowser = () => void; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/index.tsx new file mode 100644 index 0000000000000..d7ac089c17133 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/index.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDataGrid } from '@elastic/eui'; +import deepEqual from 'fast-deep-equal'; +import React, { useState, useEffect, useMemo } from 'react'; +import type { ColumnHeaderOptions } from '../../../../common'; + +import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; + +import { StatefulFieldsBrowser } from './fields_browser'; +import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './fields_browser/helpers'; + +interface Props { + browserFields: BrowserFields; + columnHeaders: ColumnHeaderOptions[]; + timelineId: string; +} + +/** Renders the timeline header columns */ +export const TimelineToolbarComponent = ({ browserFields, columnHeaders, timelineId }: Props) => { + const [visibleColumns, setVisibleColumns] = useState([]); + useEffect(() => setVisibleColumns(columnHeaders.map(({ id }) => id)), [columnHeaders]); + + const columns = useMemo( + () => + columnHeaders.map((column) => ({ + ...column, + actions: { showHide: false }, + })), + [columnHeaders] + ); + + return ( + `${rowIndex}, ${columnId}`} + columns={columns} + toolbarVisibility={{ + showStyleSelector: true, + showSortSelector: true, + showFullScreenSelector: true, + showColumnSelector: { + allowHide: false, + allowReorder: true, + }, + additionalControls: ( + + ), + }} + /> + ); +}; + +export const TimelineToolbar = React.memo( + TimelineToolbarComponent, + (prevProps, nextProps) => + prevProps.timelineId === nextProps.timelineId && + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && + deepEqual(prevProps.browserFields, nextProps.browserFields) +); diff --git a/x-pack/plugins/timelines/public/components/utils/helpers.ts b/x-pack/plugins/timelines/public/components/utils/helpers.ts index 29d83eb1bd7aa..0c531a9c483bc 100644 --- a/x-pack/plugins/timelines/public/components/utils/helpers.ts +++ b/x-pack/plugins/timelines/public/components/utils/helpers.ts @@ -5,6 +5,56 @@ * 2.0. */ +import { get, getOr, isEmpty, uniqBy } from 'lodash/fp'; +import { BrowserField, BrowserFields, ColumnHeaderOptions } from '../../../common'; +import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../t_grid/body/constants'; + +export const getColumnHeaderFromBrowserField = ({ + browserField, + width = DEFAULT_COLUMN_MIN_WIDTH, +}: { + browserField: Partial; + width?: number; +}): ColumnHeaderOptions => ({ + category: browserField.category, + columnHeaderType: 'not-filtered', + description: browserField.description != null ? browserField.description : undefined, + example: browserField.example != null ? `${browserField.example}` : undefined, + id: browserField.name || '', + type: browserField.type, + aggregatable: browserField.aggregatable, + initialWidth: width, +}); + +/** + * Returns a collection of columns, where the first column in the collection + * is a timestamp, and the remaining columns are all the columns in the + * specified category + */ +export const getColumnsWithTimestamp = ({ + browserFields, + category, +}: { + browserFields: BrowserFields; + category: string; +}): ColumnHeaderOptions[] => { + const emptyFields: Record> = {}; + const timestamp = get('base.fields.@timestamp', browserFields); + const categoryFields: Array> = [ + ...Object.values(getOr(emptyFields, `${category}.fields`, browserFields)), + ]; + + return timestamp != null && categoryFields.length + ? uniqBy('id', [ + getColumnHeaderFromBrowserField({ + browserField: timestamp, + width: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }), + ...categoryFields.map((f) => getColumnHeaderFromBrowserField({ browserField: f })), + ]) + : []; +}; + export const getIconFromType = (type: string | null) => { switch (type) { case 'string': // fall through @@ -26,3 +76,7 @@ export const getIconFromType = (type: string | null) => { return 'questionInCircle'; } }; + +/** Returns example text, or an empty string if the field does not have an example */ +export const getExampleText = (example: string | number | null | undefined): string => + !isEmpty(example) ? `Example: ${example}` : ''; diff --git a/x-pack/plugins/timelines/public/container/source/index.tsx b/x-pack/plugins/timelines/public/container/source/index.tsx new file mode 100644 index 0000000000000..94bbd2a874178 --- /dev/null +++ b/x-pack/plugins/timelines/public/container/source/index.tsx @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useRef, useState } from 'react'; +import { isEmpty, isEqual, pick } from 'lodash/fp'; +import { Subscription } from 'rxjs/internal/Subscription'; + +import memoizeOne from 'memoize-one'; +import { + BrowserField, + BrowserFields, + DocValueFields, + IndexField, + IndexFieldsStrategyRequest, + IndexFieldsStrategyResponse, +} from '../../../common'; +import * as i18n from './translations'; + +import { + IIndexPattern, + DataPublicPluginStart, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../src/plugins/data/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useAppToasts } from '../../hooks/use_app_toasts'; + +const DEFAULT_BROWSER_FIELDS = {}; +const DEFAULT_INDEX_PATTERNS = { fields: [], title: '' }; +const DEFAULT_DOC_VALUE_FIELDS: DocValueFields[] = []; +interface FetchIndexReturn { + browserFields: BrowserFields; + docValueFields: DocValueFields[]; + indexes: string[]; + indexExists: boolean; + indexPatterns: IIndexPattern; +} + +/** + * HOT Code path where the fields can be 16087 in length or larger. This is + * VERY mutatious on purpose to improve the performance of the transform. + */ +export const getBrowserFields = memoizeOne( + (_title: string, fields: IndexField[]): BrowserFields => { + // Adds two dangerous casts to allow for mutations within this function + type DangerCastForMutation = Record; + type DangerCastForBrowserFieldsMutation = Record< + string, + Omit & { fields: Record } + >; + + // We mutate this instead of using lodash/set to keep this as fast as possible + return fields.reduce((accumulator, field) => { + if (accumulator[field.category] == null) { + (accumulator as DangerCastForMutation)[field.category] = {}; + } + if (accumulator[field.category].fields == null) { + accumulator[field.category].fields = {}; + } + accumulator[field.category].fields[field.name] = (field as unknown) as BrowserField; + return accumulator; + }, {}); + }, + // Update the value only if _title has changed + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] +); + +export const getDocValueFields = memoizeOne( + (_title: string, fields: IndexField[]): DocValueFields[] => + fields && fields.length > 0 + ? fields.reduce((accumulator: DocValueFields[], field: IndexField) => { + if (field.readFromDocValues && accumulator.length < 100) { + return [ + ...accumulator, + { + field: field.name, + format: field.format ? field.format : undefined, + }, + ]; + } + return accumulator; + }, []) + : [], + // Update the value only if _title has changed + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] +); + +export const getIndexFields = memoizeOne( + (title: string, fields: IndexField[]): IIndexPattern => + fields && fields.length > 0 + ? { + fields: fields.map((field) => + pick(['name', 'searchable', 'type', 'aggregatable', 'esTypes', 'subType'], field) + ), + title, + } + : { fields: [], title }, + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length +); + +export const useFetchIndex = ( + indexNames: string[], + onlyCheckIfIndicesExist: boolean = false +): [boolean, FetchIndexReturn] => { + const { data } = useKibana<{ data: DataPublicPluginStart }>().services; + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(new Subscription()); + const previousIndexesName = useRef([]); + const [isLoading, setLoading] = useState(false); + + const [state, setState] = useState({ + browserFields: DEFAULT_BROWSER_FIELDS, + docValueFields: DEFAULT_DOC_VALUE_FIELDS, + indexes: indexNames, + indexExists: true, + indexPatterns: DEFAULT_INDEX_PATTERNS, + }); + const { addError, addWarning } = useAppToasts(); + + const indexFieldsSearch = useCallback( + (iNames) => { + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + searchSubscription$.current = data.search + .search( + { indices: iNames, onlyCheckIfIndicesExist }, + { + abortSignal: abortCtrl.current.signal, + strategy: 'indexFields', + } + ) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + const stringifyIndices = response.indicesExist.sort().join(); + + previousIndexesName.current = response.indicesExist; + setLoading(false); + + setState({ + browserFields: getBrowserFields(stringifyIndices, response.indexFields), + docValueFields: getDocValueFields(stringifyIndices, response.indexFields), + indexes: response.indicesExist, + indexExists: response.indicesExist.length > 0, + indexPatterns: getIndexFields(stringifyIndices, response.indexFields), + }); + + searchSubscription$.current.unsubscribe(); + } else if (isErrorResponse(response)) { + setLoading(false); + addWarning(i18n.ERROR_BEAT_FIELDS); + searchSubscription$.current.unsubscribe(); + } + }, + error: (msg) => { + setLoading(false); + addError(msg, { + title: i18n.FAIL_BEAT_FIELDS, + }); + searchSubscription$.current.unsubscribe(); + }, + }); + }; + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + asyncSearch(); + }, + [data.search, addError, addWarning, onlyCheckIfIndicesExist, setLoading, setState] + ); + + useEffect(() => { + if (!isEmpty(indexNames) && !isEqual(previousIndexesName.current, indexNames)) { + indexFieldsSearch(indexNames); + } + return () => { + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + }; + }, [indexNames, indexFieldsSearch, previousIndexesName]); + + return [isLoading, state]; +}; diff --git a/x-pack/plugins/timelines/public/container/source/translations.ts b/x-pack/plugins/timelines/public/container/source/translations.ts new file mode 100644 index 0000000000000..f1fa461983116 --- /dev/null +++ b/x-pack/plugins/timelines/public/container/source/translations.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_BEAT_FIELDS = i18n.translate( + 'xpack.timelines.beatFields.errorSearchDescription', + { + defaultMessage: `An error has occurred on getting beat fields`, + } +); + +export const FAIL_BEAT_FIELDS = i18n.translate('xpack.timelines.beatFields.failSearchDescription', { + defaultMessage: `Failed to run search on beat fields`, +}); diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts index 9fe022a670399..6f4de1dd2559e 100644 --- a/x-pack/plugins/timelines/public/index.ts +++ b/x-pack/plugins/timelines/public/index.ts @@ -51,6 +51,7 @@ export { addFieldToTimelineColumns, getTimelineIdFromColumnDroppableId, } from './components/drag_and_drop/helpers'; +export { StatefulFieldsBrowser } from './components/t_grid/toolbar/fields_browser'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. diff --git a/x-pack/plugins/timelines/public/methods/index.tsx b/x-pack/plugins/timelines/public/methods/index.tsx index a11485f32b6a5..a358e29c39e49 100644 --- a/x-pack/plugins/timelines/public/methods/index.tsx +++ b/x-pack/plugins/timelines/public/methods/index.tsx @@ -5,13 +5,17 @@ * 2.0. */ -import { Store } from 'redux'; import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import type { Store } from 'redux'; +import type { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import type { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import type { TGridProps } from '../types'; -import { LastUpdatedAtProps, LoadingPanelProps } from '../components'; +import type { + LastUpdatedAtProps, + LoadingPanelProps, + FieldBrowserWrappedProps, +} from '../components'; const TimelineLazy = lazy(() => import('../components')); export const getTGridLazy = ( @@ -52,3 +56,15 @@ export const getLoadingPanelLazy = (props: LoadingPanelProps) => { ); }; + +const FieldsBrowserLazy = lazy(() => import('../components/fields_browser')); +export const getFieldsBrowserLazy = ( + props: FieldBrowserWrappedProps, + { store }: { store: Store } +) => { + return ( + }> + + + ); +}; diff --git a/x-pack/plugins/timelines/public/mock/plugin_mock.tsx b/x-pack/plugins/timelines/public/mock/plugin_mock.tsx index 8d2141d62f253..64be3306e7aa6 100644 --- a/x-pack/plugins/timelines/public/mock/plugin_mock.tsx +++ b/x-pack/plugins/timelines/public/mock/plugin_mock.tsx @@ -18,6 +18,8 @@ export const createTGridMocks = () => ({ // eslint-disable-next-line react/display-name getTGrid: () => <>{'hello grid'}, // eslint-disable-next-line react/display-name + getFieldBrowser: () =>
, + // eslint-disable-next-line react/display-name getLastUpdated: (props: LastUpdatedAtProps) => , // eslint-disable-next-line react/display-name getLoadingPanel: (props: LoadingPanelProps) => , diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index 38fdc6149839b..b1bff6fd85032 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -8,16 +8,21 @@ import { Store } from 'redux'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -import { +import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import type { CoreSetup, Plugin, PluginInitializerContext, CoreStart, } from '../../../../src/core/public'; import type { TimelinesUIStart, TGridProps } from './types'; -import { getLastUpdatedLazy, getLoadingPanelLazy, getTGridLazy } from './methods'; -import type { LastUpdatedAtProps, LoadingPanelProps } from './components'; +import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserWrappedProps } from './components'; +import { + getLastUpdatedLazy, + getLoadingPanelLazy, + getTGridLazy, + getFieldsBrowserLazy, +} from './methods'; import { tGridReducer } from './store/t_grid/reducer'; import { useDraggableKeyboardWrapper } from './components/drag_and_drop/draggable_keyboard_wrapper_hook'; import { useAddToTimeline, useAddToTimelineSensor } from './hooks/use_add_to_timeline'; @@ -55,6 +60,11 @@ export class TimelinesPlugin implements Plugin { getLastUpdated: (props: LastUpdatedAtProps) => { return getLastUpdatedLazy(props); }, + getFieldBrowser: (props: FieldBrowserWrappedProps) => { + return getFieldsBrowserLazy(props, { + store: this._store!, + }); + }, getUseAddToTimeline: () => { return useAddToTimeline; }, diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts index b5a4b54cf5eb5..1342f277b6878 100644 --- a/x-pack/plugins/timelines/public/types.ts +++ b/x-pack/plugins/timelines/public/types.ts @@ -11,6 +11,7 @@ import { Store } from 'redux'; import type { LastUpdatedAtProps, LoadingPanelProps, + FieldBrowserWrappedProps, UseDraggableKeyboardWrapper, UseDraggableKeyboardWrapperProps, } from './components'; @@ -28,6 +29,7 @@ export interface TimelinesUIStart { getTGridReducer: () => any; getLoadingPanel: (props: LoadingPanelProps) => ReactElement; getLastUpdated: (props: LastUpdatedAtProps) => ReactElement; + getFieldBrowser: (props: FieldBrowserWrappedProps) => ReactElement; getUseAddToTimeline: () => (props: UseAddToTimelineProps) => UseAddToTimeline; getUseAddToTimelineSensor: () => (api: SensorAPI) => void; getUseDraggableKeyboardWrapper: () => ( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7508548906b17..2287d74e199b0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20262,23 +20262,6 @@ "xpack.securitySolution.featureCatalogueDescription2": "検出して対応します。", "xpack.securitySolution.featureCatalogueDescription3": "インシデントを調査します。", "xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle": "セキュリティ", - "xpack.securitySolution.fieldBrowser.categoriesTitle": "カテゴリー", - "xpack.securitySolution.fieldBrowser.categoryFieldsTableCaption": "カテゴリ {categoryId} フィールド", - "xpack.securitySolution.fieldBrowser.categoryLabel": "カテゴリー", - "xpack.securitySolution.fieldBrowser.closeButton": "閉じる", - "xpack.securitySolution.fieldBrowser.copyToClipboard": "クリップボードにコピー", - "xpack.securitySolution.fieldBrowser.customizeColumnsTitle": "列のカスタマイズ", - "xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly": "フィールド {field} の説明:", - "xpack.securitySolution.fieldBrowser.descriptionLabel": "説明", - "xpack.securitySolution.fieldBrowser.fieldLabel": "フィールド", - "xpack.securitySolution.fieldBrowser.fieldsTitle": "列", - "xpack.securitySolution.fieldBrowser.filterPlaceholder": "フィールド名", - "xpack.securitySolution.fieldBrowser.noFieldsMatchInputLabel": "{searchInput} に一致するフィールドがありません", - "xpack.securitySolution.fieldBrowser.noFieldsMatchLabel": "一致するフィールドがありません", - "xpack.securitySolution.fieldBrowser.resetFieldsLink": "フィールドをリセット", - "xpack.securitySolution.fieldBrowser.viewCategoryTooltip": "すべての {categoryId} フィールドを表示します", - "xpack.securitySolution.fieldBrowser.viewColumnCheckboxAriaLabel": "{field} 列を表示", - "xpack.securitySolution.fieldBrowser.youAreInAPopoverScreenReaderOnly": "[列のカスタマイズ]ポップアップが表示されています。このポップアップを閉じるには、Esc キーを押してください。", "xpack.securitySolution.fieldRenderers.moreLabel": "詳細", "xpack.securitySolution.firstLastSeenHost.errorSearchDescription": "最初の前回確認されたホスト検索でエラーが発生しました", "xpack.securitySolution.firstLastSeenHost.failSearchDescription": "最初の前回確認されたホストで検索を実行できませんでした", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5877818a5f102..5fa40af4cb375 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20568,26 +20568,6 @@ "xpack.securitySolution.featureCatalogueDescription2": "检测和响应。", "xpack.securitySolution.featureCatalogueDescription3": "调查事件。", "xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle": "安全", - "xpack.securitySolution.fieldBrowser.categoriesCountTitle": "{totalCount} 个{totalCount, plural, other {类别}}", - "xpack.securitySolution.fieldBrowser.categoriesTitle": "类别", - "xpack.securitySolution.fieldBrowser.categoryFieldsTableCaption": "类别 {categoryId} 字段", - "xpack.securitySolution.fieldBrowser.categoryLabel": "类别", - "xpack.securitySolution.fieldBrowser.categoryLinkAriaLabel": "{category} {totalCount} 个{totalCount, plural, other {字段}}。单击此按钮可选择 {category} 类别。", - "xpack.securitySolution.fieldBrowser.closeButton": "关闭", - "xpack.securitySolution.fieldBrowser.copyToClipboard": "复制到剪贴板", - "xpack.securitySolution.fieldBrowser.customizeColumnsTitle": "定制列", - "xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly": "{field} 字段的描述:", - "xpack.securitySolution.fieldBrowser.descriptionLabel": "描述", - "xpack.securitySolution.fieldBrowser.fieldLabel": "字段", - "xpack.securitySolution.fieldBrowser.fieldsCountTitle": "{totalCount} 个{totalCount, plural, other {字段}}", - "xpack.securitySolution.fieldBrowser.fieldsTitle": "列", - "xpack.securitySolution.fieldBrowser.filterPlaceholder": "字段名称", - "xpack.securitySolution.fieldBrowser.noFieldsMatchInputLabel": "没有字段匹配“{searchInput}”", - "xpack.securitySolution.fieldBrowser.noFieldsMatchLabel": "没有字段匹配", - "xpack.securitySolution.fieldBrowser.resetFieldsLink": "重置字段", - "xpack.securitySolution.fieldBrowser.viewCategoryTooltip": "查看所有 {categoryId} 字段", - "xpack.securitySolution.fieldBrowser.viewColumnCheckboxAriaLabel": "查看 {field} 列", - "xpack.securitySolution.fieldBrowser.youAreInAPopoverScreenReaderOnly": "您当前位于“定制列”弹出式窗口中。按 Esc 键可退出此弹出式窗口。", "xpack.securitySolution.fieldRenderers.moreLabel": "更多", "xpack.securitySolution.firstLastSeenHost.errorSearchDescription": "搜索上次看到的首个主机时发生错误", "xpack.securitySolution.firstLastSeenHost.failSearchDescription": "无法对上次看到的首个主机执行搜索", diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json b/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json index 85c2639ef7d47..e6465142d4b5d 100644 --- a/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json +++ b/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json @@ -3,10 +3,8 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "timelinesTest"], - "requiredPlugins": ["timelines"], - "requiredBundles": [ - "kibanaReact" - ], + "requiredPlugins": ["timelines", "data", "dataEnhanced"], + "requiredBundles": ["kibanaReact"], "server": false, "ui": true } diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx index 072944dd9d78e..e21ca4754e6cf 100644 --- a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx @@ -12,12 +12,15 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { TimelinesUIStart } from '../../../../../../../plugins/timelines/public'; +import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public'; + +type CoreStartTimelines = CoreStart & { data: DataPublicPluginStart }; /** * Render the Timeline Test app. Returns a cleanup function. */ export function renderApp( - coreStart: CoreStart, + coreStart: CoreStartTimelines, parameters: AppMountParameters, timelinesPluginSetup: TimelinesUIStart | null ) { @@ -41,7 +44,7 @@ const AppRoot = React.memo( parameters, timelinesPluginSetup, }: { - coreStart: CoreStart; + coreStart: CoreStartTimelines; parameters: AppMountParameters; timelinesPluginSetup: TimelinesUIStart | null; }) => { @@ -50,6 +53,7 @@ const AppRoot = React.memo( const setRefetch = useCallback((_refetch) => { refetch.current = _refetch; }, []); + return ( diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts index b3b9c7ecbf6e0..e5f9aceace3ba 100644 --- a/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts @@ -9,6 +9,7 @@ import { Plugin, CoreStart, CoreSetup, AppMountParameters } from 'kibana/public' import { i18n } from '@kbn/i18n'; import { TimelinesUIStart } from '../../../../../plugins/timelines/public'; import { renderApp } from './applications/timelines_test'; +import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; export type TimelinesTestPluginSetup = void; export type TimelinesTestPluginStart = void; @@ -17,6 +18,7 @@ export interface TimelinesTestPluginSetupDependencies {} export interface TimelinesTestPluginStartDependencies { timelines: TimelinesUIStart; + data: DataPublicPluginStart; } export class TimelinesTestPlugin @@ -39,8 +41,8 @@ export class TimelinesTestPlugin }), mount: async (params: AppMountParameters) => { const startServices = await core.getStartServices(); - const [coreStart] = startServices; - return renderApp(coreStart, params, this.timelinesPlugin); + const [coreStart, { data }] = startServices; + return renderApp({ ...coreStart, data }, params, this.timelinesPlugin); }, }); }