diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index d8a964decb87..0b90aaaf6f11 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -107,7 +107,7 @@ pageLoadAssetSize: dataVisualizer: 27530 banners: 17946 mapsEms: 26072 - timelines: 330000 + timelines: 327300 screenshotMode: 17856 visTypePie: 35583 expressionRevealImage: 25675 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 77c2b5cbca0c..cd8965de36f1 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 @@ -21,7 +21,12 @@ import { import type { TimelinesUIStart } from '../../../../timelines/public'; import type { TopAlert } from './'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import type { ActionProps, ColumnHeaderOptions, RowRenderer } from '../../../../timelines/common'; +import type { + ActionProps, + AlertStatus, + ColumnHeaderOptions, + RowRenderer, +} from '../../../../timelines/common'; import { getRenderCellValue } from './render_cell_value'; import { usePluginContext } from '../../hooks/use_plugin_context'; @@ -213,6 +218,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { sortDirection: 'desc', }, ], + filterStatus: status as AlertStatus, leadingControlColumns, trailingControlColumns, unit: (totalAlerts: number) => 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 c4da4e8d4506..4b91122103d1 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 @@ -15,7 +15,8 @@ import { inputsModel, inputsSelectors, State } from '../../store'; import { inputsActions } from '../../store/actions'; import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline'; import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; -import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model'; +import type { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model'; +import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter } from '../../../../../../../src/plugins/data/public'; import { InspectButtonContainer } from '../inspect'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; @@ -54,6 +55,7 @@ export interface OwnProps { showTotalCount?: boolean; headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; + currentFilter?: Status; onRuleChange?: () => void; renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; @@ -83,6 +85,7 @@ const StatefulEventsViewerComponent: React.FC = ({ itemsPerPageOptions, kqlMode, pageFilters, + currentFilter, onRuleChange, query, renderCellValue, @@ -160,6 +163,7 @@ const StatefulEventsViewerComponent: React.FC = ({ sort, utilityBar, graphEventId, + filterStatus: currentFilter, leadingControlColumns, trailingControlColumns, }) 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 b67fd9aeb81b..4b91e3b1e35f 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 @@ -390,6 +390,7 @@ export const AlertsTableComponent: React.FC = ({ pageFilters={defaultFiltersMemo} defaultModel={defaultTimelineModel} end={to} + currentFilter={filterGroup} headerFilterGroup={headerFilterGroup} id={timelineId} onRuleChange={onRuleChange} diff --git a/x-pack/plugins/timelines/common/constants.ts b/x-pack/plugins/timelines/common/constants.ts index 86ff9d501f14..8c4e1700a54d 100644 --- a/x-pack/plugins/timelines/common/constants.ts +++ b/x-pack/plugins/timelines/common/constants.ts @@ -5,4 +5,11 @@ * 2.0. */ +import { AlertStatus } from './types/timeline/actions'; + export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; +export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern'; + +export const FILTER_OPEN: AlertStatus = 'open'; +export const FILTER_CLOSED: AlertStatus = 'closed'; +export const FILTER_IN_PROGRESS: AlertStatus = 'in-progress'; diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index 8d3f212fd6bc..e61361233cda 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -90,3 +90,14 @@ export type ControlColumnProps = Omit< keyof AdditionalControlColumnProps > & Partial; + +export type OnAlertStatusActionSuccess = (status: AlertStatus) => void; +export type OnAlertStatusActionFailure = (status: AlertStatus, error: string) => void; +export interface BulkActionsObjectProp { + alertStatusActions?: boolean; + onAlertStatusActionSuccess?: OnAlertStatusActionSuccess; + onAlertStatusActionFailure?: OnAlertStatusActionFailure; +} +export type BulkActionsProp = boolean | BulkActionsObjectProp; + +export type AlertStatus = 'open' | 'closed' | 'in-progress'; 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 a3de8654ec1c..81fe117e08cc 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 @@ -75,8 +75,11 @@ describe('Body', () => { showCheckboxes: false, tabType: TimelineTabs.query, totalPages: 1, + totalItems: 1, leadingControlColumns: [], trailingControlColumns: [], + filterStatus: 'open', + refetch: jest.fn(), }; describe('rendering', () => { 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 1efee943c645..91667e74ae15 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 @@ -11,19 +11,34 @@ import { EuiDataGridControlColumn, EuiDataGridStyle, EuiDataGridToolBarVisibilityOptions, + EuiLoadingSpinner, } from '@elastic/eui'; import { getOr } from 'lodash/fp'; import memoizeOne from 'memoize-one'; -import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { + ComponentType, + lazy, + Suspense, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { SortColumnTimeline, TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +import { + TimelineId, + TimelineTabs, + BulkActionsProp, + SortColumnTimeline, +} from '../../../../common/types/timeline'; import type { CellValueElementProps, ColumnHeaderOptions, ControlColumnProps, RowRenderer, + AlertStatus, } from '../../../../common/types/timeline'; import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; @@ -32,15 +47,21 @@ import { getEventIdToDataMapping } from './helpers'; import { Sort } from './sort'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; -import { BrowserFields } from '../../../../common/search_strategy/index_fields'; -import { OnRowSelected, OnSelectAll } from '../types'; -import { StatefulFieldsBrowser, tGridActions } from '../../../'; -import { TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid'; +import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; +import type { OnRowSelected, OnSelectAll } from '../types'; +import type { Refetch } from '../../../store/t_grid/inputs'; +import { StatefulFieldsBrowser } from '../../../'; +import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { RowAction } from './row_action'; import * as i18n from './translations'; +import { AlertCount } from '../styles'; import { checkBoxControlColumn } from './control_columns'; +const StatefulAlertStatusBulkActions = lazy( + () => import('../toolbar/bulk_actions/alert_status_bulk_actions') +); + interface OwnProps { activePage: number; additionalControls?: React.ReactNode; @@ -48,16 +69,22 @@ interface OwnProps { data: TimelineItem[]; id: string; isEventViewer?: boolean; - leadingControlColumns: ControlColumnProps[]; renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; sort: Sort[]; tabType: TimelineTabs; - trailingControlColumns: ControlColumnProps[]; + leadingControlColumns?: ControlColumnProps[]; + trailingControlColumns?: ControlColumnProps[]; totalPages: number; + totalItems: number; + bulkActions?: BulkActionsProp; + filterStatus?: AlertStatus; + unit?: (total: number) => React.ReactNode; onRuleChange?: () => void; + refetch: Refetch; } +const basicUnit = (n: number) => i18n.UNIT(n); const NUM_OF_ICON_IN_TIMELINE_ROW = 2; export const hasAdditionalActions = (id: TimelineId): boolean => @@ -200,31 +227,42 @@ export const BodyComponent = React.memo( sort, tabType, totalPages, + totalItems, + filterStatus, + bulkActions = true, + unit = basicUnit, leadingControlColumns = EMPTY_CONTROL_COLUMNS, trailingControlColumns = EMPTY_CONTROL_COLUMNS, + refetch, }) => { const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); const { queryFields, selectAll } = useDeepEqualSelector((state) => getManageTimeline(state, id) ); + const alertCountText = useMemo(() => `${totalItems.toLocaleString()} ${unit(totalItems)}`, [ + totalItems, + unit, + ]); + + const selectedCount = useMemo(() => Object.keys(selectedEventIds).length, [selectedEventIds]); + const onRowSelected: OnRowSelected = useCallback( ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { - setSelected!({ + setSelected({ id, eventIds: getEventIdToDataMapping(data, eventIds, queryFields), isSelected, - isSelectAllChecked: - isSelected && Object.keys(selectedEventIds).length + 1 === data.length, + isSelectAllChecked: isSelected && selectedCount + 1 === data.length, }); }, - [setSelected, id, data, selectedEventIds, queryFields] + [setSelected, id, data, selectedCount, queryFields] ); const onSelectPage: OnSelectAll = useCallback( ({ isSelected }: { isSelected: boolean }) => isSelected - ? setSelected!({ + ? setSelected({ id, eventIds: getEventIdToDataMapping( data, @@ -234,7 +272,7 @@ export const BodyComponent = React.memo( isSelected, isSelectAllChecked: isSelected, }) - : clearSelected!({ id }), + : clearSelected({ id }), [setSelected, clearSelected, id, data, queryFields] ); @@ -245,25 +283,87 @@ export const BodyComponent = React.memo( } }, [isSelectAllChecked, onSelectPage, selectAll]); + const onAlertStatusActionSuccess = useMemo(() => { + if (bulkActions && bulkActions !== true) { + return bulkActions.onAlertStatusActionSuccess; + } + }, [bulkActions]); + + const onAlertStatusActionFailure = useMemo(() => { + if (bulkActions && bulkActions !== true) { + return bulkActions.onAlertStatusActionFailure; + } + }, [bulkActions]); + + const showBulkActions = useMemo(() => { + if (selectedCount === 0 || !showCheckboxes) { + return false; + } + if (typeof bulkActions === 'boolean') { + return bulkActions; + } + return bulkActions.alertStatusActions ?? true; + }, [selectedCount, showCheckboxes, bulkActions]); + const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo( () => ({ additionalControls: ( <> - {additionalControls ?? null} - { - - } + {alertCountText} + {showBulkActions ? ( + <> + }> + + + {additionalControls ?? null} + + ) : ( + <> + {additionalControls ?? null} + + + )} ), - showColumnSelector: { allowHide: false, allowReorder: true }, + ...(showBulkActions + ? { + showColumnSelector: false, + showSortSelector: false, + showFullScreenSelector: false, + } + : { + showColumnSelector: { allowHide: true, allowReorder: true }, + showSortSelector: true, + showFullScreenSelector: true, + }), showStyleSelector: false, }), - [additionalControls, browserFields, columnHeaders, id] + [ + id, + alertCountText, + totalItems, + filterStatus, + browserFields, + columnHeaders, + additionalControls, + showBulkActions, + onAlertStatusActionSuccess, + onAlertStatusActionFailure, + refetch, + ] ); const [sortingColumns, setSortingColumns] = useState([]); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts index e2d13fe49f2b..4eb47afdc192 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts @@ -216,3 +216,9 @@ export const INVESTIGATE_IN_RESOLVER_DISABLED = i18n.translate( defaultMessage: 'This event cannot be analyzed since it has incompatible field mappings', } ); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.timelines.timeline.body.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {event} other {events}}`, + }); 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 3f987e1f75e6..924b83ab6a9e 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 @@ -22,6 +22,7 @@ import type { ControlColumnProps, DataProvider, RowRenderer, + AlertStatus, } from '../../../../common/types/timeline'; import { esQuery, @@ -40,20 +41,15 @@ import { HeaderSection } from '../header_section'; import { StatefulBody } from '../body'; import { Footer, footerHeight } from '../footer'; import { LastUpdatedAt } from '../..'; -import { AlertCount, SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles'; -import * as i18n from './translations'; +import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles'; +import * as i18n from '../translations'; import { ExitFullScreen } from '../../exit_full_screen'; import { Sort } from '../body/sort'; import { InspectButtonContainer } from '../../inspect'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px -const UTILITY_BAR_HEIGHT = 19; // px const COMPACT_HEADER_HEIGHT = 36; // px -const UtilityBar = styled.div` - height: ${UTILITY_BAR_HEIGHT}px; -`; - const TitleText = styled.span` margin-right: 12px; `; @@ -114,6 +110,7 @@ export interface TGridIntegratedProps { filters: Filter[]; globalFullScreen: boolean; headerFilterGroup?: React.ReactNode; + filterStatus?: AlertStatus; height?: number; id: TimelineId; indexNames: string[]; @@ -133,8 +130,8 @@ export interface TGridIntegratedProps { utilityBar?: (refetch: Refetch, totalCount: number) => React.ReactNode; // If truthy, the graph viewer (Resolver) is showing graphEventId: string | undefined; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; + leadingControlColumns?: ControlColumnProps[]; + trailingControlColumns?: ControlColumnProps[]; data?: DataPublicPluginStart; } @@ -148,6 +145,7 @@ const TGridIntegratedComponent: React.FC = ({ filters, globalFullScreen, headerFilterGroup, + filterStatus, id, indexNames, indexPattern, @@ -255,13 +253,6 @@ const TGridIntegratedComponent: React.FC = ({ [deletedEventIds.length, totalCount] ); - const subtitle = useMemo( - () => `${totalCountMinusDeleted.toLocaleString()} ${unit && unit(totalCountMinusDeleted)}`, - [totalCountMinusDeleted, unit] - ); - - const additionalControls = useMemo(() => {subtitle}, [subtitle]); - const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [ deletedEventIds, events, @@ -301,9 +292,7 @@ const TGridIntegratedComponent: React.FC = ({ > {HeaderSectionContent} - {utilityBar && !resolverIsShowing(graphEventId) && ( - {utilityBar?.(refetch, totalCountMinusDeleted)} - )} + = ({ = ({ itemsCount: totalCountMinusDeleted, itemsPerPage, })} + totalItems={totalCountMinusDeleted} + unit={unit} + filterStatus={filterStatus} leadingControlColumns={leadingControlColumns} trailingControlColumns={trailingControlColumns} + refetch={refetch} />