diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index d7f5bf8613299..7cb7395acaa8d 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -322,7 +322,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { return [ { id: 'expand', - width: 96, + width: 120, headerCellRender: () => { return ( diff --git a/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx index 7e166ac99c05f..9c93f05196a1c 100644 --- a/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx @@ -13,7 +13,7 @@ import FilterForValueButton from './filter_for_value'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { TimelineNonEcsData } from '../../../../timelines/common/search_strategy'; import { TGridCellAction } from '../../../../timelines/common/types/timeline'; -import { TimelinesUIStart } from '../../../../timelines/public'; +import { getPageRowIndex, TimelinesUIStart } from '../../../../timelines/public'; export const FILTER_FOR_VALUE = i18n.translate('xpack.observability.hoverActions.filterForValue', { defaultMessage: 'Filter for value', @@ -35,11 +35,15 @@ const useKibanaServices = () => { /** actions common to all cells (e.g. copy to clipboard) */ const commonCellActions: TGridCellAction[] = [ - ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { + ({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) => ({ + rowIndex, + columnId, + Component, + }) => { const { timelines } = useKibanaServices(); const value = getMappedNonEcsValue({ - data: data[rowIndex], + data: data[getPageRowIndex(rowIndex, pageSize)], fieldName: columnId, }); @@ -60,9 +64,13 @@ const commonCellActions: TGridCellAction[] = [ /** actions for adding filters to the search bar */ const buildFilterCellActions = (addToQuery: (value: string) => void): TGridCellAction[] => [ - ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { + ({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) => ({ + rowIndex, + columnId, + Component, + }) => { const value = getMappedNonEcsValue({ - data: data[rowIndex], + data: data[getPageRowIndex(rowIndex, pageSize)], fieldName: columnId, }); diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts index 65778e16771e2..de4acdd721c68 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts @@ -38,4 +38,4 @@ export const LOAD_MORE = export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; export const EVENTS_VIEWER_PAGINATION = - '[data-test-subj="events-viewer-panel"] [data-test-subj="timeline-pagination"]'; + '[data-test-subj="events-viewer-panel"] .euiDataGrid__pagination'; diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx index 623957ab8b66c..1481ae3e4248c 100644 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx @@ -12,6 +12,7 @@ import type { TimelineNonEcsData, } from '../../../../../timelines/common/search_strategy'; import { DataProvider, TGridCellAction } from '../../../../../timelines/common/types'; +import { getPageRowIndex } from '../../../../../timelines/public'; import { getMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns'; import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; import { allowTopN, escapeDataProviderId } from '../../components/drag_and_drop/helpers'; @@ -36,11 +37,20 @@ const useKibanaServices = () => { /** the default actions shown in `EuiDataGrid` cells */ export const defaultCellActions: TGridCellAction[] = [ - ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { + ({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) => ({ + rowIndex, + columnId, + Component, + }) => { const { timelines, filterManager } = useKibanaServices(); + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + if (pageRowIndex >= data.length) { + return null; + } + const value = getMappedNonEcsValue({ - data: data[rowIndex], + data: data[pageRowIndex], fieldName: columnId, }); @@ -58,11 +68,20 @@ export const defaultCellActions: TGridCellAction[] = [ ); }, - ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { + ({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) => ({ + rowIndex, + columnId, + Component, + }) => { const { timelines, filterManager } = useKibanaServices(); + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + if (pageRowIndex >= data.length) { + return null; + } + const value = getMappedNonEcsValue({ - data: data[rowIndex], + data: data[pageRowIndex], fieldName: columnId, }); @@ -80,11 +99,20 @@ export const defaultCellActions: TGridCellAction[] = [ ); }, - ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { + ({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) => ({ + rowIndex, + columnId, + Component, + }) => { const { timelines } = useKibanaServices(); + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + if (pageRowIndex >= data.length) { + return null; + } + const value = getMappedNonEcsValue({ - data: data[rowIndex], + data: data[pageRowIndex], fieldName: columnId, }); @@ -122,16 +150,23 @@ export const defaultCellActions: TGridCellAction[] = [ browserFields, data, timelineId, + pageSize, }: { browserFields: BrowserFields; data: TimelineNonEcsData[][]; timelineId: string; + pageSize: number; }) => ({ rowIndex, columnId, Component }) => { const [showTopN, setShowTopN] = useState(false); const onClick = useCallback(() => setShowTopN(!showTopN), [showTopN]); + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + if (pageRowIndex >= data.length) { + return null; + } + const value = getMappedNonEcsValue({ - data: data[rowIndex], + data: data[pageRowIndex], fieldName: columnId, }); @@ -159,11 +194,20 @@ export const defaultCellActions: TGridCellAction[] = [ ); }, - ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { + ({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) => ({ + rowIndex, + columnId, + Component, + }) => { const { timelines } = useKibanaServices(); + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + if (pageRowIndex >= data.length) { + return null; + } + const value = getMappedNonEcsValue({ - data: data[rowIndex], + data: data[pageRowIndex], fieldName: columnId, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 3c0bb0e38b153..4b3c792319cd1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -14,7 +14,6 @@ import { esQuery, Filter } from '../../../../../../../src/plugins/data/public'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { RowRendererId, TimelineIdLiteral } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; -import { HeaderSection } from '../../../common/components/header_section'; import { displayErrorToast, displaySuccessToast, @@ -371,8 +370,7 @@ export const AlertsTableComponent: React.FC = ({ if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) { return ( - - + ); diff --git a/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx b/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx index d6bc34ca80da9..cc20c856f0e13 100644 --- a/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx +++ b/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx @@ -46,11 +46,13 @@ export type TGridCellAction = ({ browserFields, data, timelineId, + pageSize, }: { browserFields: BrowserFields; /** each row of data is represented as one TimelineNonEcsData[] */ data: TimelineNonEcsData[][]; timelineId: string; + pageSize: number; }) => (props: EuiDataGridColumnCellActionProps) => ReactNode; /** The specification of a column header */ diff --git a/x-pack/plugins/timelines/common/utils/pagination.ts b/x-pack/plugins/timelines/common/utils/pagination.ts new file mode 100644 index 0000000000000..407b62bc4c686 --- /dev/null +++ b/x-pack/plugins/timelines/common/utils/pagination.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * rowIndex is bigger than `data.length` for pages with page numbers bigger than one. + * For that reason, we must calculate `rowIndex % itemsPerPage`. + * + * Ex: + * Given `rowIndex` is `13` and `itemsPerPage` is `10`. + * It means that the `activePage` is `2` and the `pageRowIndex` is `3` + * + * **Warning**: + * Be careful with array out of bounds. `pageRowIndex` can be bigger or equal to `data.length` + * in the scenario where the user changes the event status (Open, Acknowledged, Closed). + */ + +export const getPageRowIndex = (rowIndex: number, itemsPerPage: number) => rowIndex % itemsPerPage; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/height_hack.ts b/x-pack/plugins/timelines/public/components/t_grid/body/height_hack.ts new file mode 100644 index 0000000000000..542be06578d6b --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/height_hack.ts @@ -0,0 +1,54 @@ +/* + * 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 { useState, useLayoutEffect } from 'react'; + +// That could be different from security and observability. Get it as parameter? +const INITIAL_DATA_GRID_HEIGHT = 967; + +// It will recalculate DataGrid height after this time interval. +const TIME_INTERVAL = 50; + +/** + * You are probably asking yourself "Why 3?". But that is the wrong mindset. You should be asking yourself "why not 3?". + * 3 (three) is a number, numeral and digit. It is the natural number following 2 and preceding 4, and is the smallest + * odd prime number and the only prime preceding a square number. It has religious or cultural significance in many societies. + */ +const MAGIC_GAP = 3; + +/** + * HUGE HACK!!! + * DataGrtid height isn't properly calculated when the grid has horizontal scroll. + * https://github.com/elastic/eui/issues/5030 + * + * In order to get around this bug we are calculating `DataGrid` height here and setting it as a prop. + * + * Please delete me and allow DataGrid to calculate its height when the bug is fixed. + */ +export const useDataGridHeightHack = (pageSize: number, rowCount: number) => { + const [height, setHeight] = useState(INITIAL_DATA_GRID_HEIGHT); + + useLayoutEffect(() => { + setTimeout(() => { + const gridVirtualized = document.querySelector('#body-data-grid .euiDataGrid__virtualized'); + + if ( + gridVirtualized && + gridVirtualized.children[0].clientHeight !== gridVirtualized.clientHeight // check if it has vertical scroll + ) { + setHeight( + height + + gridVirtualized.children[0].clientHeight - + gridVirtualized.clientHeight + + MAGIC_GAP + ); + } + }, TIME_INTERVAL); + }, [pageSize, rowCount, height]); + + return height; +}; 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 137fa2625e92a..77dde444f77ca 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,10 +66,11 @@ describe('Body', () => { excludedRowRendererIds: [], id: 'timeline-test', isSelectAllChecked: false, + isLoading: false, itemsPerPageOptions: [], loadingEventIds: [], loadPage: jest.fn(), - querySize: 25, + pageSize: 25, renderCellValue: TestCellRenderer, rowRenderers: [], selectedEventIds: {}, @@ -78,7 +79,6 @@ describe('Body', () => { showCheckboxes: false, tabType: TimelineTabs.query, tableView: 'gridView', - totalPages: 1, totalItems: 1, leadingControlColumns: [], trailingControlColumns: [], 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 f3220cfd8909e..74d5d54e96526 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 @@ -15,6 +15,7 @@ import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, + EuiProgress, } from '@elastic/eui'; import { getOr } from 'lodash/fp'; import memoizeOne from 'memoize-one'; @@ -28,10 +29,9 @@ import React, { useState, useContext, } from 'react'; - import { connect, ConnectedProps, useDispatch } from 'react-redux'; -import { ThemeContext } from 'styled-components'; +import styled, { ThemeContext } from 'styled-components'; import { ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; import { TGridCellAction, @@ -62,6 +62,7 @@ import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; import type { OnRowSelected, OnSelectAll } from '../types'; import type { Refetch } from '../../../store/t_grid/inputs'; +import { getPageRowIndex } from '../../../../common/utils/pagination'; import { StatefulEventContext, StatefulFieldsBrowser } from '../../../'; import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; @@ -72,6 +73,7 @@ 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'; +import { useDataGridHeightHack } from './height_hack'; const StatefulAlertStatusBulkActions = lazy( () => import('../toolbar/bulk_actions/alert_status_bulk_actions') @@ -93,14 +95,13 @@ interface OwnProps { leadingControlColumns?: ControlColumnProps[]; loadPage: (newActivePage: number) => void; onRuleChange?: () => void; - querySize: number; + pageSize: number; refetch: Refetch; renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; tableView: ViewSelection; tabType: TimelineTabs; totalItems: number; - totalPages: number; trailingControlColumns?: ControlColumnProps[]; unit?: (total: number) => React.ReactNode; hasAlertsCrud?: boolean; @@ -118,6 +119,8 @@ export const hasAdditionalActions = (id: TimelineId): boolean => const EXTRA_WIDTH = 4; // px +const ES_LIMIT_COUNT = 9999; + const MIN_ACTION_COLUMN_WIDTH = 96; // px const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; @@ -126,6 +129,14 @@ const EmptyHeaderCellRender: ComponentType = () => null; const gridStyle: EuiDataGridStyle = { border: 'none', fontSize: 's', header: 'underline' }; +const EuiDataGridContainer = styled.div<{ hideLastPage: boolean }>` + ul.euiPagination__list { + li.euiPagination__item:last-child { + ${({ hideLastPage }) => `${hideLastPage ? 'display:none' : ''}`}; + } + } +`; + const transformControlColumns = ({ actionColumnsWidth, columnHeaders, @@ -142,6 +153,7 @@ const transformControlColumns = ({ isSelectAllChecked, onSelectPage, browserFields, + pageSize, sort, theme, setEventsLoading, @@ -163,6 +175,7 @@ const transformControlColumns = ({ isSelectAllChecked: boolean; browserFields: BrowserFields; onSelectPage: OnSelectAll; + pageSize: number; sort: SortColumnTimeline[]; theme: EuiTheme; setEventsLoading: SetEventsLoading; @@ -204,12 +217,20 @@ const transformControlColumns = ({ rowIndex, setCellProps, }: EuiDataGridCellValueElementProps) => { - addBuildingBlockStyle(data[rowIndex].ecs, theme, setCellProps); + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + const rowData = data[pageRowIndex]; + let disabled = false; - if (columnId === 'checkbox-control-column' && hasAlertsCrudPermissions != null) { - const alertConsumers = - data[rowIndex].data.find((d) => d.field === ALERT_RULE_CONSUMER)?.value ?? []; - disabled = alertConsumers.some((consumer) => !hasAlertsCrudPermissions(consumer)); + if (rowData) { + addBuildingBlockStyle(rowData.ecs, theme, setCellProps); + if (columnId === 'checkbox-control-column' && hasAlertsCrudPermissions != null) { + const alertConsumers = + rowData.data.find((d) => d.field === ALERT_RULE_CONSUMER)?.value ?? []; + disabled = alertConsumers.some((consumer) => !hasAlertsCrudPermissions(consumer)); + } + } else { + // disable the cell when it has no data + setCellProps({ style: { display: 'none' } }); } return ( @@ -228,6 +249,7 @@ const transformControlColumns = ({ onRowSelected={onRowSelected} onRuleChange={onRuleChange} rowIndex={rowIndex} + pageRowIndex={pageRowIndex} selectedEventIds={selectedEventIds} setCellProps={setCellProps} showCheckboxes={showCheckboxes} @@ -260,7 +282,6 @@ export const BodyComponent = React.memo( columnHeaders, data, defaultCellActions, - excludedRowRendererIds, filterQuery, filterStatus, id, @@ -270,9 +291,10 @@ export const BodyComponent = React.memo( itemsPerPageOptions, leadingControlColumns = EMPTY_CONTROL_COLUMNS, loadingEventIds, + isLoading, loadPage, onRuleChange, - querySize, + pageSize, refetch, renderCellValue, rowRenderers, @@ -283,7 +305,6 @@ export const BodyComponent = React.memo( tableView = 'gridView', tabType, totalItems, - totalPages, trailingControlColumns = EMPTY_CONTROL_COLUMNS, unit = defaultUnit, hasAlertsCrud, @@ -416,6 +437,7 @@ export const BodyComponent = React.memo( () => ({ additionalControls: ( <> + {isLoading && } {alertCountText} {showBulkActions ? ( <> @@ -472,6 +494,7 @@ export const BodyComponent = React.memo( onAlertStatusActionSuccess, onAlertStatusActionFailure, refetch, + isLoading, additionalControls, browserFields, columnHeaders, @@ -524,8 +547,8 @@ export const BodyComponent = React.memo( }, [columnHeaders]); const setEventsLoading = useCallback( - ({ eventIds, isLoading }) => { - dispatch(tGridActions.setEventsLoading({ id, eventIds, isLoading })); + ({ eventIds, isLoading: loading }) => { + dispatch(tGridActions.setEventsLoading({ id, eventIds, isLoading: loading })); }, [dispatch, id] ); @@ -568,6 +591,7 @@ export const BodyComponent = React.memo( theme, setEventsLoading, setEventsDeleted, + pageSize, hasAlertsCrudPermissions, }) ); @@ -589,6 +613,7 @@ export const BodyComponent = React.memo( browserFields, onSelectPage, theme, + pageSize, setEventsLoading, setEventsDeleted, hasAlertsCrudPermissions, @@ -602,6 +627,7 @@ export const BodyComponent = React.memo( data: data.map((row) => row.data), browserFields, timelineId: id, + pageSize, }); return { @@ -610,7 +636,7 @@ export const BodyComponent = React.memo( header.tGridCellActions?.map(buildAction) ?? defaultCellActions?.map(buildAction), }; }), - [browserFields, columnHeaders, data, defaultCellActions, id] + [browserFields, columnHeaders, data, defaultCellActions, id, pageSize] ); const renderTGridCellValue = useMemo(() => { @@ -619,17 +645,25 @@ export const BodyComponent = React.memo( rowIndex, setCellProps, }): React.ReactElement | null => { - const rowData = rowIndex < data.length ? data[rowIndex].data : null; + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + + const rowData = pageRowIndex < data.length ? data[pageRowIndex].data : null; const header = columnHeaders.find((h) => h.id === columnId); - const eventId = rowIndex < data.length ? data[rowIndex]._id : null; + const eventId = pageRowIndex < data.length ? data[pageRowIndex]._id : null; + const ecs = pageRowIndex < data.length ? data[pageRowIndex].ecs : null; useEffect(() => { const defaultStyles = { overflow: 'hidden' }; setCellProps({ style: { ...defaultStyles } }); - addBuildingBlockStyle(data[rowIndex].ecs, theme, setCellProps, defaultStyles); - }, [rowIndex, setCellProps]); + if (ecs && rowData) { + addBuildingBlockStyle(ecs, theme, setCellProps, defaultStyles); + } else { + // disable the cell when it has no data + setCellProps({ style: { display: 'none' } }); + } + }, [rowIndex, setCellProps, ecs, rowData]); - if (rowData == null || header == null || eventId == null) { + if (rowData == null || header == null || eventId == null || ecs === null) { return null; } @@ -642,17 +676,47 @@ export const BodyComponent = React.memo( isExpandable: true, isExpanded: false, isDetails: false, - linkValues: getOr([], header.linkField ?? '', data[rowIndex].ecs), + linkValues: getOr([], header.linkField ?? '', ecs), rowIndex, setCellProps, timelineId: tabType != null ? `${id}-${tabType}` : id, - ecsData: data[rowIndex].ecs, + ecsData: ecs, browserFields, rowRenderers, }) as React.ReactElement; }; return Cell; - }, [columnHeaders, data, id, renderCellValue, tabType, theme, browserFields, rowRenderers]); + }, [ + columnHeaders, + data, + id, + renderCellValue, + tabType, + theme, + browserFields, + rowRenderers, + pageSize, + ]); + + const onChangeItemsPerPage = useCallback( + (itemsChangedPerPage) => { + clearSelected({ id }); + dispatch(tGridActions.setTGridSelectAll({ id, selectAll: false })); + dispatch(tGridActions.updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage })); + }, + [id, dispatch, clearSelected] + ); + + const onChangePage = useCallback( + (page) => { + clearSelected({ id }); + dispatch(tGridActions.setTGridSelectAll({ id, selectAll: false })); + loadPage(page); + }, + [id, loadPage, dispatch, clearSelected] + ); + + const height = useDataGridHeightHack(pageSize, data.length); // Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created const [activeStatefulEventContext] = useState({ @@ -665,20 +729,30 @@ export const BodyComponent = React.memo( <> {tableView === 'gridView' && ( - + ES_LIMIT_COUNT}> + + )} {tableView === 'eventRenderedView' && ( ( browserFields={browserFields} events={data} leadingControlColumns={leadingTGridControlColumns ?? []} - onChangePage={loadPage} + onChangePage={onChangePage} + onChangeItemsPerPage={onChangeItemsPerPage} pageIndex={activePage} - pageSize={querySize} + pageSize={pageSize} pageSizeOptions={itemsPerPageOptions} rowRenderers={rowRenderers} timelineId={id} @@ -723,6 +798,7 @@ const makeMapStateToProps = () => { selectedEventIds, showCheckboxes, sort, + isLoading, } = timeline; return { @@ -730,6 +806,7 @@ const makeMapStateToProps = () => { excludedRowRendererIds, isSelectAllChecked, loadingEventIds, + isLoading, id, selectedEventIds, showCheckboxes: hasAlertsCrud === true && showCheckboxes, diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx index c5ba88dc36a63..15f4f837be65e 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx @@ -39,6 +39,7 @@ type Props = EuiDataGridCellValueElementProps & { width: number; setEventsLoading: SetEventsLoading; setEventsDeleted: SetEventsDeleted; + pageRowIndex: number; }; const RowActionComponent = ({ @@ -51,6 +52,7 @@ const RowActionComponent = ({ loadingEventIds, onRowSelected, onRuleChange, + pageRowIndex, rowIndex, selectedEventIds, showCheckboxes, @@ -60,15 +62,21 @@ const RowActionComponent = ({ setEventsDeleted, width, }: Props) => { - const { data: timelineNonEcsData, ecs: ecsData, _id: eventId, _index: indexName } = useMemo( - () => data[rowIndex], - [data, rowIndex] - ); + const { + data: timelineNonEcsData, + ecs: ecsData, + _id: eventId, + _index: indexName, + } = useMemo(() => { + const rowData: Partial = data[pageRowIndex]; + return rowData ?? {}; + }, [data, pageRowIndex]); const dispatch = useDispatch(); const columnValues = useMemo( () => + timelineNonEcsData && columnHeaders .map( (header) => @@ -85,7 +93,7 @@ const RowActionComponent = ({ const updatedExpandedDetail: TimelineExpandedDetailType = { panelView: 'eventDetail', params: { - eventId, + eventId: eventId ?? '', indexName: indexName ?? '', ecsData, }, @@ -102,7 +110,7 @@ const RowActionComponent = ({ const Action = controlColumn.rowCellRender; - if (data.length === 0 || rowIndex >= data.length) { + if (!timelineNonEcsData || !ecsData || !eventId) { return ; } @@ -110,10 +118,10 @@ const RowActionComponent = ({ <> {Action && ( void; + onChangeItemsPerPage: (newItemsPerPage: number) => void; pageIndex: number; pageSize: number; pageSizeOptions: number[]; @@ -89,6 +85,7 @@ const EventRenderedViewComponent = ({ events, leadingControlColumns, onChangePage, + onChangeItemsPerPage, pageIndex, pageSize, pageSizeOptions, @@ -96,8 +93,6 @@ const EventRenderedViewComponent = ({ timelineId, totalItemCount, }: EventRenderedViewProps) => { - const dispatch = useDispatch(); - const ActionTitle = useMemo( () => ( @@ -220,12 +215,10 @@ const EventRenderedViewComponent = ({ onChangePage(pageChange.page.index); } if (pageChange.page.size !== pageSize) { - dispatch( - tGridActions.updateItemsPerPage({ id: timelineId, itemsPerPage: pageChange.page.size }) - ); + onChangeItemsPerPage(pageChange.page.size); } }, - [dispatch, onChangePage, pageIndex, pageSize, timelineId] + [onChangePage, pageIndex, pageSize, onChangeItemsPerPage] ); const pagination = useMemo( 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 cc34b32b048ac..779fddcad2562 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 @@ -8,9 +8,9 @@ import type { AlertConsumers as AlertConsumersTyped } from '@kbn/rule-data-utils'; // @ts-expect-error import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiProgress } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiLoadingContent, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; @@ -39,16 +39,10 @@ import { } from '../../../../../../../src/plugins/data/public'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { defaultHeaders } from '../body/column_headers/default_headers'; -import { - calculateTotalPages, - buildCombinedQuery, - getCombinedFilterQuery, - resolverIsShowing, -} from '../helpers'; +import { buildCombinedQuery, getCombinedFilterQuery, resolverIsShowing } from '../helpers'; import { tGridActions, tGridSelectors } from '../../../store/t_grid'; import { useTimelineEvents } from '../../../container'; import { StatefulBody } from '../body'; -import { Footer, footerHeight } from '../footer'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } from '../styles'; import { Sort } from '../body/sort'; import { InspectButton, InspectButtonContainer } from '../../inspect'; @@ -86,16 +80,6 @@ const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ flex-direction: column; `; -const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` - overflow: hidden; - margin: 0; - display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; -`; - -const ScrollableFlexItem = styled(EuiFlexItem)` - overflow: auto; -`; - const SECURITY_ALERTS_CONSUMERS = [AlertConsumers.SIEM]; export interface TGridIntegratedProps { @@ -157,7 +141,6 @@ const TGridIntegratedComponent: React.FC = ({ id, indexNames, indexPattern, - isLive, isLoadingIndexPattern, itemsPerPage, itemsPerPageOptions, @@ -176,7 +159,7 @@ const TGridIntegratedComponent: React.FC = ({ const dispatch = useDispatch(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const { uiSettings } = useKibana().services; - const [isQueryLoading, setIsQueryLoading] = useState(false); + const [isQueryLoading, setIsQueryLoading] = useState(true); const [tableView, setTableView] = useState('gridView'); const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); @@ -279,6 +262,13 @@ const TGridIntegratedComponent: React.FC = ({ const alignItems = tableView === 'gridView' ? 'baseline' : 'center'; + const isFirstUpdate = useRef(true); + useEffect(() => { + if (isFirstUpdate.current && !loading) { + isFirstUpdate.current = false; + } + }, [loading]); + return ( = ({ data-test-subj="events-viewer-panel" $isFullScreen={globalFullScreen} > - {loading && } + {isFirstUpdate.current && } {graphOverlay} - {canQueryTimeline ? ( - <> - - - - - + + {canQueryTimeline && ( + + + + + + + {!resolverIsShowing(graphEventId) && additionalFilters} + + {tGridEventRenderedViewEnabled && entityType === 'alerts' && ( - {!resolverIsShowing(graphEventId) && additionalFilters} + - {tGridEventRenderedViewEnabled && entityType === 'alerts' && ( - - - - )} - + )} + - - - {nonDeletedEvents.length === 0 && loading === false ? ( - - - - } - titleSize="s" - body={ -

- -

- } - /> - ) : ( - <> - - {tableView === 'gridView' && ( -