diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 8bd8edb9424b4..51211705db573 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -15,6 +15,7 @@ export const allowedExperimentalValues = Object.freeze({ metricsEntitiesEnabled: false, ruleRegistryEnabled: false, tGridEnabled: true, + tGridEventRenderedViewEnabled: true, trustedAppsByPolicyEnabled: false, excludePoliciesInFilterEnabled: false, uebaEnabled: false, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index a0433f0cc73e1..5a0e0a0633e00 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -67,7 +67,7 @@ export const getColumns = ({ ), sortable: false, truncateText: false, - width: '180px', + width: '132px', render: (values: string[] | null | undefined, data: EventFieldsData) => { const label = data.isObjectArray ? i18n.NESTED_COLUMN(data.field) diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 2c4241ffbbb16..5496bd2d52c3e 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -58,7 +58,6 @@ export interface OwnProps { scopeId: SourcererScopeName; start: string; showTotalCount?: boolean; - headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; currentFilter?: Status; onRuleChange?: () => void; @@ -88,7 +87,6 @@ const StatefulEventsViewerComponent: React.FC = ({ entityType, excludedRowRendererIds, filters, - headerFilterGroup, id, isLive, itemsPerPage, @@ -120,6 +118,9 @@ const StatefulEventsViewerComponent: React.FC = ({ const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); // TODO: Once we are past experimental phase this code should be removed const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); + const tGridEventRenderedViewEnabled = useIsExperimentalFeatureEnabled( + 'tGridEventRenderedViewEnabled' + ); useEffect(() => { if (createTimeline != null) { createTimeline({ @@ -153,6 +154,7 @@ const StatefulEventsViewerComponent: React.FC = ({ {tGridEnabled ? ( timelinesUi.getTGrid<'embedded'>({ + id, type: 'embedded', browserFields, columns, @@ -165,8 +167,6 @@ const StatefulEventsViewerComponent: React.FC = ({ filters: globalFilters, globalFullScreen, graphOverlay, - headerFilterGroup, - id, indexNames: selectedPatterns, indexPattern, isLive, @@ -186,6 +186,7 @@ const StatefulEventsViewerComponent: React.FC = ({ filterStatus: currentFilter, leadingControlColumns, trailingControlColumns, + tGridEventRenderedViewEnabled, }) ) : ( = ({ end={end} isLoadingIndexPattern={isLoadingIndexPattern} filters={globalFilters} - headerFilterGroup={headerFilterGroup} indexNames={selectedPatterns} indexPattern={indexPattern} isLive={isLive} diff --git a/x-pack/plugins/timelines/public/components/rule_name/index.tsx b/x-pack/plugins/timelines/public/components/rule_name/index.tsx new file mode 100644 index 0000000000000..2bfaf9b03525e --- /dev/null +++ b/x-pack/plugins/timelines/public/components/rule_name/index.tsx @@ -0,0 +1,51 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import React, { useCallback, useMemo } from 'react'; +import { CoreStart } from '../../../../../../src/core/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; + +interface RuleNameProps { + name: string; + id: string; +} + +const appendSearch = (search?: string) => + isEmpty(search) ? '' : `${search?.startsWith('?') ? search : `?${search}`}`; + +const RuleNameComponents = ({ name, id }: RuleNameProps) => { + const { navigateToApp, getUrlForApp } = useKibana().services.application; + + const hrefRuleDetails = useMemo( + () => + getUrlForApp('securitySolution', { + deepLinkId: 'rules', + path: `/id/${id}${appendSearch(window.location.search)}`, + }), + [getUrlForApp, id] + ); + const goToRuleDetails = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp('securitySolution', { + deepLinkId: 'rules', + path: `/id/${id}${appendSearch(window.location.search)}`, + }); + }, + [navigateToApp, id] + ); + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + + {name} + + ); +}; + +export const RuleName = React.memo(RuleNameComponents); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx index 17805b5f03939..2ab5a86fa7ddd 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx @@ -66,8 +66,10 @@ describe('Body', () => { excludedRowRendererIds: [], id: 'timeline-test', isSelectAllChecked: false, + itemsPerPageOptions: [], loadingEventIds: [], loadPage: jest.fn(), + querySize: 25, renderCellValue: TestCellRenderer, rowRenderers: [], selectedEventIds: {}, @@ -75,6 +77,7 @@ describe('Body', () => { sort: mockSort, showCheckboxes: false, tabType: TimelineTabs.query, + tableView: 'gridView', totalPages: 1, totalItems: 1, leadingControlColumns: [], diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 87e31dc1d6c9f..12a0f6bfc2b64 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -13,6 +13,8 @@ import { EuiDataGridStyle, EuiDataGridToolBarVisibilityOptions, EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { getOr } from 'lodash/fp'; import memoizeOne from 'memoize-one'; @@ -67,6 +69,8 @@ import * as i18n from './translations'; import { AlertCount } from '../styles'; import { checkBoxControlColumn } from './control_columns'; import type { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; +import { ViewSelection } from '../event_rendered_view/selector'; +import { EventRenderedView } from '../event_rendered_view'; const StatefulAlertStatusBulkActions = lazy( () => import('../toolbar/bulk_actions/alert_status_bulk_actions') @@ -76,25 +80,28 @@ interface OwnProps { activePage: number; additionalControls?: React.ReactNode; browserFields: BrowserFields; - filterQuery: string; + bulkActions?: BulkActionsProp; data: TimelineItem[]; defaultCellActions?: TGridCellAction[]; + filterQuery: string; + filterStatus?: AlertStatus; id: string; + indexNames: string[]; isEventViewer?: boolean; + itemsPerPageOptions: number[]; + leadingControlColumns?: ControlColumnProps[]; + loadPage: (newActivePage: number) => void; + onRuleChange?: () => void; + querySize: number; + refetch: Refetch; renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; + tableView: ViewSelection; tabType: TimelineTabs; - leadingControlColumns?: ControlColumnProps[]; - loadPage: (newActivePage: number) => void; - trailingControlColumns?: ControlColumnProps[]; - totalPages: number; totalItems: number; - bulkActions?: BulkActionsProp; - filterStatus?: AlertStatus; + totalPages: number; + trailingControlColumns?: ControlColumnProps[]; unit?: (total: number) => React.ReactNode; - onRuleChange?: () => void; - indexNames: string[]; - refetch: Refetch; } const basicUnit = (n: number) => i18n.UNIT(n); @@ -235,34 +242,37 @@ export const BodyComponent = React.memo( activePage, additionalControls, browserFields, - filterQuery, + bulkActions = true, + clearSelected, columnHeaders, data, defaultCellActions, excludedRowRendererIds, + filterQuery, + filterStatus, id, + indexNames, isEventViewer = false, isSelectAllChecked, + itemsPerPageOptions, + leadingControlColumns = EMPTY_CONTROL_COLUMNS, loadingEventIds, loadPage, - selectedEventIds, - setSelected, - clearSelected, onRuleChange, - showCheckboxes, + querySize, + refetch, renderCellValue, rowRenderers, + selectedEventIds, + setSelected, + showCheckboxes, sort, + tableView = 'gridView', tabType, - totalPages, totalItems, - filterStatus, - bulkActions = true, - unit = basicUnit, - leadingControlColumns = EMPTY_CONTROL_COLUMNS, + totalPages, trailingControlColumns = EMPTY_CONTROL_COLUMNS, - indexNames, - refetch, + unit = basicUnit, }) => { const dispatch = useDispatch(); const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); @@ -336,6 +346,43 @@ export const BodyComponent = React.memo( return bulkActions.alertStatusActions ?? true; }, [selectedCount, showCheckboxes, bulkActions]); + const alertToolbar = useMemo( + () => ( + + + {alertCountText} + + {showBulkActions && ( + }> + + + )} + + ), + [ + alertCountText, + filterQuery, + filterStatus, + id, + indexNames, + onAlertStatusActionFailure, + onAlertStatusActionSuccess, + refetch, + showBulkActions, + totalItems, + ] + ); + const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo( () => ({ additionalControls: ( @@ -573,20 +620,39 @@ export const BodyComponent = React.memo( }, [columnHeaders, data, id, renderCellValue, tabType, theme, browserFields, rowRenderers]); return ( - + <> + {tableView === 'gridView' && ( + + )} + {tableView === 'eventRenderedView' && ( + + )} + ); } ); @@ -635,4 +701,4 @@ const connector = connect(makeMapStateToProps, mapDispatchToProps); type PropsFromRedux = ConnectedProps; -export const StatefulBody = connector(BodyComponent); +export const StatefulBody: React.FunctionComponent = connector(BodyComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx new file mode 100644 index 0000000000000..09d46c71d9217 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx @@ -0,0 +1,249 @@ +/* + * 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 { + CriteriaWithPagination, + EuiBasicTable, + EuiBasicTableProps, + EuiDataGridCellValueElementProps, + EuiDataGridControlColumn, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + /* ALERT_REASON, ALERT_RULE_ID, */ ALERT_RULE_NAME, + TIMESTAMP, +} from '@kbn/rule-data-utils'; +import { get } from 'lodash'; +import moment from 'moment'; +import React, { ComponentType, useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; + +import { useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; + +import type { BrowserFields, RowRenderer, TimelineItem } from '../../../../common'; +import { tGridActions } from '../../../store/t_grid'; +import { RuleName } from '../../rule_name'; +import { isEventBuildingBlockType } from '../body/helpers'; + +const EventRenderedFlexItem = styled(EuiFlexItem)` + div:first-child { + padding-left: 0px; + div { + margin: 0px; + } + } +`; + +// Fix typing issue with EuiBasicTable and styled +type BasicTableType = ComponentType>; + +const StyledEuiBasicTable = styled(EuiBasicTable as BasicTableType)` + padding-top: ${({ theme }) => theme.eui.paddingSizes.m}; + .EventRenderedView__buildingBlock { + background: ${({ theme }) => theme.eui.euiColorHighlight}; + } + + & > div:last-child { + height: 72px; + } +`; + +interface EventRenderedViewProps { + alertToolbar: React.ReactNode; + browserFields: BrowserFields; + events: TimelineItem[]; + leadingControlColumns: EuiDataGridControlColumn[]; + onChangePage: (newActivePage: number) => void; + pageIndex: number; + pageSize: number; + pageSizeOptions: number[]; + rowRenderers: RowRenderer[]; + timelineId: string; + totalItemCount: number; +} +const PreferenceFormattedDateComponent = ({ value }: { value: Date }) => { + const tz = useUiSetting('dateFormat:tz'); + const dateFormat = useUiSetting('dateFormat'); + return <>{moment.tz(value, tz).format(dateFormat)}; +}; +export const PreferenceFormattedDate = React.memo(PreferenceFormattedDateComponent); + +const EventRenderedViewComponent = ({ + alertToolbar, + browserFields, + events, + leadingControlColumns, + onChangePage, + pageIndex, + pageSize, + pageSizeOptions, + rowRenderers, + timelineId, + totalItemCount, +}: EventRenderedViewProps) => { + const dispatch = useDispatch(); + + const ActionTitle = useMemo( + () => ( + + {leadingControlColumns.map((action) => { + const ActionHeader = action.headerCellRender; + return ( + + + + ); + })} + + ), + [leadingControlColumns] + ); + + const columns = useMemo( + () => [ + { + field: 'actions', + name: ActionTitle, + truncateText: false, + hideForMobile: false, + render: (name: unknown, item: unknown) => { + const alertId = get(item, '_id'); + const rowIndex = events.findIndex((evt) => evt._id === alertId); + return leadingControlColumns.length > 0 + ? leadingControlColumns.map((action) => { + const getActions = action.rowCellRender as ( + props: EuiDataGridCellValueElementProps + ) => React.ReactNode; + return getActions({ + columnId: 'actions', + isDetails: false, + isExpandable: false, + isExpanded: false, + rowIndex, + setCellProps: () => null, + }); + }) + : null; + }, + }, + { + field: 'ecs.@timestamp', + name: i18n.translate('xpack.timelines.alerts.EventRenderedView.timestamp.column', { + defaultMessage: 'Timestamp', + }), + truncateText: false, + hideForMobile: false, + // eslint-disable-next-line react/display-name + render: (name: unknown, item: TimelineItem) => { + const timestamp = get(item, `ecs.${TIMESTAMP}`); + return ; + }, + }, + { + field: `ecs.${ALERT_RULE_NAME}`, + name: i18n.translate('xpack.timelines.alerts.EventRenderedView.rule.column', { + defaultMessage: 'Rule', + }), + truncateText: false, + hideForMobile: false, + // eslint-disable-next-line react/display-name + render: (name: unknown, item: TimelineItem) => { + const ruleName = get(item, `ecs.signal.rule.name`); /* `ecs.${ALERT_RULE_NAME}`*/ + const ruleId = get(item, `ecs.signal.rule.id}`); /* `ecs.${ALERT_RULE_ID}`*/ + return ; + }, + }, + { + field: 'eventSummary', + name: i18n.translate('xpack.timelines.alerts.EventRenderedView.eventSummary.column', { + defaultMessage: 'Event Summary', + }), + truncateText: false, + hideForMobile: false, + // eslint-disable-next-line react/display-name + render: (name: unknown, item: TimelineItem) => { + const ecsData = get(item, 'ecs'); + const reason = get(item, `ecs.signal.reason`); /* `ecs.${ALERT_REASON}`*/ + const rowRenderersValid = rowRenderers.filter((rowRenderer) => + rowRenderer.isInstance(ecsData) + ); + return ( + + {reason && {reason}} + {rowRenderersValid.length > 0 && + rowRenderersValid.map((rowRenderer) => ( + <> + + + {rowRenderer.renderRow({ + browserFields, + data: ecsData, + isDraggable: false, + timelineId: 'NONE', + })} + + + ))} + + ); + }, + width: '60%', + }, + ], + [ActionTitle, browserFields, events, leadingControlColumns, rowRenderers] + ); + + const handleTableChange = useCallback( + (pageChange: CriteriaWithPagination) => { + if (pageChange.page.index !== pageIndex) { + onChangePage(pageChange.page.index); + } + if (pageChange.page.size !== pageSize) { + dispatch( + tGridActions.updateItemsPerPage({ id: timelineId, itemsPerPage: pageChange.page.size }) + ); + } + }, + [dispatch, onChangePage, pageIndex, pageSize, timelineId] + ); + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions, + hidePerPageOptions: false, + }), + [pageIndex, pageSize, pageSizeOptions, totalItemCount] + ); + + return ( + <> + {alertToolbar} + + isEventBuildingBlockType(ecs) + ? { + className: `EventRenderedView__buildingBlock`, + } + : {} + } + /> + + ); +}; + +export const EventRenderedView = React.memo(EventRenderedViewComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/selector/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/selector/index.tsx new file mode 100644 index 0000000000000..02d20072e7652 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/selector/index.tsx @@ -0,0 +1,161 @@ +/* + * 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 { + EuiButtonEmpty, + EuiPopover, + EuiSelectable, + EuiSelectableOption, + EuiTitle, + EuiTextColor, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; + +export type ViewSelection = 'gridView' | 'eventRenderedView'; + +const ContainerEuiSelectable = styled.div` + width: 300px; + .euiSelectableListItem__text { + white-space: pre-wrap !important; + line-height: normal; + } +`; + +const gridView = i18n.translate('xpack.timelines.alerts.summaryView.gridView.label', { + defaultMessage: 'Grid view', +}); + +const eventRenderedView = i18n.translate( + 'xpack.timelines.alerts.summaryView.eventRendererView.label', + { + defaultMessage: 'Event rendered view', + } +); + +interface SummaryViewSelectorProps { + onViewChange: (viewSelection: ViewSelection) => void; + viewSelected: ViewSelection; +} + +const SummaryViewSelectorComponent = ({ viewSelected, onViewChange }: SummaryViewSelectorProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onButtonClick = useCallback(() => setIsPopoverOpen((currentVal) => !currentVal), []); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const onChangeSelectable = useCallback( + (opts: EuiSelectableOption[]) => { + const selected = opts.filter((i) => i.checked === 'on'); + if (selected.length > 0) { + onViewChange((selected[0]?.key ?? 'gridView') as ViewSelection); + } + setIsPopoverOpen(false); + }, + [onViewChange] + ); + + const button = useMemo( + () => ( + + {viewSelected === 'gridView' ? gridView : eventRenderedView} + + ), + [onButtonClick, viewSelected] + ); + + const options = useMemo( + () => [ + { + label: gridView, + key: 'gridView', + checked: (viewSelected === 'gridView' ? 'on' : undefined) as EuiSelectableOption['checked'], + meta: [ + { + text: i18n.translate('xpack.timelines.alerts.summaryView.options.default.description', { + defaultMessage: + 'View as tabular data with the ability to group and sort by specific fields', + }), + }, + ], + }, + { + label: eventRenderedView, + key: 'eventRenderedView', + checked: (viewSelected === 'eventRenderedView' + ? 'on' + : undefined) as EuiSelectableOption['checked'], + meta: [ + { + text: i18n.translate( + 'xpack.timelines.alerts.summaryView.options.summaryView.description', + { + defaultMessage: 'View a rendering of the event flow for each alert', + } + ), + }, + ], + }, + ], + [viewSelected] + ); + + const renderOption = useCallback((option) => { + return ( + <> + +
{option.label}
+
+ + {option.meta[0].text} + + + ); + }, []); + + const listProps = useMemo( + () => ({ + rowHeight: 80, + showIcons: true, + }), + [] + ); + + return ( + + + + {(list) => list} + + + + ); +}; + +export const SummaryViewSelector = React.memo(SummaryViewSelectorComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/header_section/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/header_section/__snapshots__/index.test.tsx.snap deleted file mode 100644 index d3d20c7183570..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/header_section/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`HeaderSection it renders 1`] = ` -
- - - - - -

- Test title -

-
- -
-
-
-
-
-`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/header_section/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/header_section/index.test.tsx deleted file mode 100644 index c5b4e679fe9f8..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/header_section/index.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount, shallow } from 'enzyme'; -import React from 'react'; -import { TestProviders } from '../../../mock'; - -import { HeaderSection } from './index'; - -describe('HeaderSection', () => { - test('it renders', () => { - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the title', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-section-title"]').first().exists()).toBe(true); - }); - - test('it renders the subtitle when provided', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-section-subtitle"]').first().exists()).toBe(true); - }); - - test('renders the subtitle when not provided (to prevent layout thrash)', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-section-subtitle"]').first().exists()).toBe(true); - }); - - test('it renders supplements when children provided', () => { - const wrapper = mount( - - -

{'Test children'}

-
-
- ); - - expect(wrapper.find('[data-test-subj="header-section-supplements"]').first().exists()).toBe( - true - ); - }); - - test('it DOES NOT render supplements when children not provided', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-section-supplements"]').first().exists()).toBe( - false - ); - }); - - test('it applies border styles when border is true', () => { - const wrapper = mount( - - - - ); - const siemHeaderSection = wrapper.find('.siemHeaderSection').first(); - - expect(siemHeaderSection).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); - expect(siemHeaderSection).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); - }); - - test('it DOES NOT apply border styles when border is false', () => { - const wrapper = mount( - - - - ); - const siemHeaderSection = wrapper.find('.siemHeaderSection').first(); - - expect(siemHeaderSection).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); - expect(siemHeaderSection).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); - }); - - test('it splits the title and supplement areas evenly when split is true', () => { - const wrapper = mount( - - -

{'Test children'}

-
-
- ); - - expect( - wrapper - .find('.euiFlexItem--flexGrowZero[data-test-subj="header-section-supplements"]') - .first() - .exists() - ).toBe(false); - }); - - test('it DOES NOT split the title and supplement areas evenly when split is false', () => { - const wrapper = mount( - - -

{'Test children'}

-
-
- ); - - expect( - wrapper - .find('.euiFlexItem--flexGrowZero[data-test-subj="header-section-supplements"]') - .first() - .exists() - ).toBe(true); - }); - - test('it renders an inspect button when an `id` is provided', () => { - const wrapper = mount( - - -

{'Test children'}

-
-
- ); - - expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(true); - }); - - test('it does NOT an inspect button when an `id` is NOT provided', () => { - const wrapper = mount( - - -

{'Test children'}

-
-
- ); - - expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(false); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/header_section/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/header_section/index.tsx deleted file mode 100644 index 3a6838f4d8640..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/header_section/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle, EuiTitleSize } from '@elastic/eui'; -import React from 'react'; -import styled, { css } from 'styled-components'; -import { InspectQuery } from '../../../store/t_grid/inputs'; -import { InspectButton } from '../../inspect'; - -import { Subtitle } from '../subtitle'; - -interface HeaderProps { - border?: boolean; - height?: number; -} - -const Header = styled.header.attrs(() => ({ - className: 'siemHeaderSection', -}))` - ${({ height }) => - height && - css` - height: ${height}px; - `} - margin-bottom: ${({ height, theme }) => (height ? 0 : theme.eui.euiSizeL)}; - user-select: text; - - ${({ border }) => - border && - css` - border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; - padding-bottom: ${({ theme }) => theme.eui.paddingSizes.l}; - `} -`; -Header.displayName = 'Header'; - -export interface HeaderSectionProps extends HeaderProps { - children?: React.ReactNode; - height?: number; - id?: string; - inspect: InspectQuery | null; - loading: boolean; - split?: boolean; - subtitle?: string | React.ReactNode; - title: string | React.ReactNode; - titleSize?: EuiTitleSize; - tooltip?: string; - growLeftSplit?: boolean; -} - -const HeaderSectionComponent: React.FC = ({ - border, - children, - height, - id, - inspect, - loading, - split, - subtitle, - title, - titleSize = 'm', - tooltip, - growLeftSplit = true, -}) => ( -
- - - - - -

- {title} - {tooltip && ( - <> - {' '} - - - )} -

-
- - -
- - {id && ( - - - - )} -
-
- - {children && ( - - {children} - - )} -
-
-); - -export const HeaderSection = React.memo(HeaderSectionComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index a7a80b5e61d2f..16bb071d5dc08 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -47,19 +47,17 @@ import { } from '../helpers'; import { tGridActions, tGridSelectors } from '../../../store/t_grid'; import { useTimelineEvents } from '../../../container'; -import { HeaderSection } from '../header_section'; import { StatefulBody } from '../body'; import { Footer, footerHeight } from '../footer'; -import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles'; +import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } from '../styles'; import * as i18n from '../translations'; -import { ExitFullScreen } from '../../exit_full_screen'; import { Sort } from '../body/sort'; -import { InspectButtonContainer } from '../../inspect'; +import { InspectButton, InspectButtonContainer } from '../../inspect'; +import { SummaryViewSelector, ViewSelection } from '../event_rendered_view/selector'; const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px -const COMPACT_HEADER_HEIGHT = 36; // px const TitleText = styled.span` margin-right: 12px; @@ -80,13 +78,10 @@ const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` `} `; -const TitleFlexGroup = styled(EuiFlexGroup)` - margin-top: 8px; -`; - const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`, }))` + position: relative; width: 100%; overflow: hidden; flex: 1; @@ -104,14 +99,6 @@ const ScrollableFlexItem = styled(EuiFlexItem)` overflow: auto; `; -/** - * Hides stateful headerFilterGroup implementations, but prevents the component - * from being unmounted, to preserve the state of the component - */ -const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>` - ${({ show }) => (show ? '' : 'visibility: hidden;')} -`; - const SECURITY_ALERTS_CONSUMERS = [AlertConsumers.SIEM]; export interface TGridIntegratedProps { @@ -126,7 +113,6 @@ export interface TGridIntegratedProps { filters: Filter[]; globalFullScreen: boolean; graphOverlay?: React.ReactNode; - headerFilterGroup?: React.ReactNode; filterStatus?: AlertStatus; height?: number; id: TimelineId; @@ -150,6 +136,7 @@ export interface TGridIntegratedProps { leadingControlColumns?: ControlColumnProps[]; trailingControlColumns?: ControlColumnProps[]; data?: DataPublicPluginStart; + tGridEventRenderedViewEnabled: boolean; } const TGridIntegratedComponent: React.FC = ({ @@ -163,7 +150,6 @@ const TGridIntegratedComponent: React.FC = ({ entityType, filters, globalFullScreen, - headerFilterGroup, filterStatus, id, indexNames, @@ -185,6 +171,7 @@ const TGridIntegratedComponent: React.FC = ({ graphEventId, leadingControlColumns, trailingControlColumns, + tGridEventRenderedViewEnabled, data, }) => { const dispatch = useDispatch(); @@ -192,6 +179,7 @@ const TGridIntegratedComponent: React.FC = ({ const { uiSettings } = useKibana().services; const [isQueryLoading, setIsQueryLoading] = useState(false); + const [tableView, setTableView] = useState('gridView'); const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); const unit = useMemo(() => (n: number) => i18n.ALERTS_UNIT(n), []); const { queryFields, title } = useDeepEqualSelector((state) => @@ -203,17 +191,6 @@ const TGridIntegratedComponent: React.FC = ({ }, [dispatch, id, isQueryLoading]); const justTitle = useMemo(() => {title}, [title]); - const titleWithExitFullScreen = useMemo( - () => ( - - {justTitle} - - - - - ), - [globalFullScreen, justTitle, setGlobalFullScreen] - ); const combinedQueries = buildCombinedQuery({ config: esQuery.getEsQueryConfig(uiSettings), @@ -295,23 +272,12 @@ const TGridIntegratedComponent: React.FC = ({ events, ]); - const HeaderSectionContent = useMemo( - () => - headerFilterGroup && ( - - {headerFilterGroup} - - ), - [headerFilterGroup, graphEventId] - ); - useEffect(() => { setIsQueryLoading(loading); }, [loading]); + const alignItems = tableView === 'gridView' ? 'baseline' : 'center'; + return ( = ({ {canQueryTimeline ? ( <> - - {HeaderSectionContent} - - {graphOverlay} - + + + + {!resolverIsShowing(graphEventId) && additionalFilters} - + {tGridEventRenderedViewEnabled && ( + + + + )} + = ({ defaultCellActions={defaultCellActions} id={id} isEventViewer={true} + itemsPerPageOptions={itemsPerPageOptions} loadPage={loadPage} onRuleChange={onRuleChange} + querySize={pageInfo.querySize} renderCellValue={renderCellValue} rowRenderers={rowRenderers} tabType={TimelineTabs.query} + tableView={tableView} totalPages={calculateTotalPages({ itemsCount: totalCountMinusDeleted, itemsPerPage, @@ -399,19 +364,21 @@ const TGridIntegratedComponent: React.FC = ({ refetch={refetch} indexNames={indexNames} /> -