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' && (
-
-
-
- >
- ) : null}
+
+ }
+ />
+ )}
+ {totalCountMinusDeleted > 0 && (
+
+ )}
+ >
+ )}
+
+ )}
);
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 9c755202aea81..ad77ad177a9fe 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
@@ -4,15 +4,17 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
import { isEmpty } from 'lodash/fp';
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useState, useRef } from 'react';
import styled from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { Direction, EntityType } from '../../../../common/search_strategy';
import type { CoreStart } from '../../../../../../../src/core/public';
import { TGridCellAction, TimelineTabs } from '../../../../common/types/timeline';
+
import type {
CellValueElementProps,
ColumnHeaderOptions,
@@ -31,12 +33,11 @@ import {
} from '../../../../../../../src/plugins/data/public';
import { useDeepEqualSelector } from '../../../hooks/use_selector';
import { defaultHeaders } from '../body/column_headers/default_headers';
-import { calculateTotalPages, combineQueries, getCombinedFilterQuery } from '../helpers';
+import { combineQueries, getCombinedFilterQuery } from '../helpers';
import { tGridActions, tGridSelectors } from '../../../store/t_grid';
import type { State } from '../../../store/t_grid';
import { useTimelineEvents } from '../../../container';
import { StatefulBody } from '../body';
-import { Footer, footerHeight } from '../footer';
import { LastUpdatedAt } from '../..';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem, UpdatedFlexGroup } from '../styles';
import { InspectButton, InspectButtonContainer } from '../../inspect';
@@ -152,6 +153,7 @@ const TGridStandaloneComponent: React.FC = ({
itemsPerPage: itemsPerPageStore,
itemsPerPageOptions: itemsPerPageOptionsStore,
queryFields,
+ sort: sortStore,
title,
} = useDeepEqualSelector((state) => getTGrid(state, STANDALONE_ID ?? ''));
@@ -194,12 +196,12 @@ const TGridStandaloneComponent: React.FC = ({
const sortField = useMemo(
() =>
- sort.map(({ columnId, columnType, sortDirection }) => ({
+ sortStore.map(({ columnId, columnType, sortDirection }) => ({
field: columnId,
type: columnType,
direction: sortDirection as Direction,
})),
- [sort]
+ [sortStore]
);
const [
@@ -312,6 +314,7 @@ const TGridStandaloneComponent: React.FC = ({
id: STANDALONE_ID,
defaultColumns: columns,
footerText,
+ sort,
loadingText,
unit,
})
@@ -319,9 +322,17 @@ const TGridStandaloneComponent: React.FC = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+ const isFirstUpdate = useRef(true);
+ useEffect(() => {
+ if (isFirstUpdate.current && !loading) {
+ isFirstUpdate.current = false;
+ }
+ }, [loading]);
+
return (
+ {isFirstUpdate.current && }
{canQueryTimeline ? (
<>
= ({
-
-
-
-
-
-
+ {totalCountMinusDeleted === 0 && loading === false && (
+
+
+
+ }
+ titleSize="s"
+ body={
+
+
+
+ }
+ />
+ )}
+ {totalCountMinusDeleted > 0 && (
+
+
+
+
+
+ )}
>
) : null}
diff --git a/x-pack/plugins/timelines/public/container/index.tsx b/x-pack/plugins/timelines/public/container/index.tsx
index 87359516a9db9..cdc31ce28e380 100644
--- a/x-pack/plugins/timelines/public/container/index.tsx
+++ b/x-pack/plugins/timelines/public/container/index.tsx
@@ -134,7 +134,7 @@ export const useTimelineEvents = ({
const refetch = useRef(noop);
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef(new Subscription());
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(0);
const [timelineRequest, setTimelineRequest] = useState | null>(
null
diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts
index 204b1e5b8bd2e..3a609155ad892 100644
--- a/x-pack/plugins/timelines/public/index.ts
+++ b/x-pack/plugins/timelines/public/index.ts
@@ -50,6 +50,7 @@ export {
skipFocusInContainerTo,
stopPropagationAndPreventDefault,
} from '../common/utils/accessibility';
+export { getPageRowIndex } from '../common/utils/pagination';
export {
addFieldToTimelineColumns,
getTimelineIdFromColumnDroppableId,
diff --git a/x-pack/plugins/timelines/public/methods/index.tsx b/x-pack/plugins/timelines/public/methods/index.tsx
index 55d15a6410fde..91802c4eb10e1 100644
--- a/x-pack/plugins/timelines/public/methods/index.tsx
+++ b/x-pack/plugins/timelines/public/methods/index.tsx
@@ -6,7 +6,7 @@
*/
import React, { lazy, Suspense } from 'react';
-import { EuiLoadingSpinner } from '@elastic/eui';
+import { EuiLoadingContent, EuiLoadingSpinner, EuiPanel } from '@elastic/eui';
import { I18nProvider } from '@kbn/i18n/react';
import type { Store } from 'redux';
import { Provider } from 'react-redux';
@@ -51,7 +51,13 @@ export const getTGridLazy = (
) => {
initializeStore({ store, storage, setStore });
return (
- }>
+
+
+
+ }
+ >
);