mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] Decompose Timelines TGrid component and moved to security_solution (#140151)
Resolves [#143152](https://github.com/elastic/kibana/issues/143152) ### Observability changes This changes is a result of removal some types from `timelines` plugin: - cleaned up timelines plugin related types, - replaced `Pick<ActionProps,'data' | 'eventId' | 'ecsData' | 'setEventsDeleted' >` with the props which were actually used: ``` data: TimelineNonEcsData[]; ecsData: Ecs; eventId: string; ``` In this PR we still have references to `@kbn/timelines-plugin`, which needs to be changed later. Threat Hunting team are going to think about replacing `TimelineNonEcsData` with the other type definition (maybe `NonEcsData`?) and moving `Ecs` type to the non `timelines` related plugin/package. ### Security Solution changes Before the current PR changes the components dependencies around `TGrid` looked like the image below: <img width="848" alt="Screen Shot 2022-11-29 at 6 16 14 AM" src="https://user-images.githubusercontent.com/55110838/204663019-664431fb-f360-4a11-b395-6fa54c35dd6d.png"> After decomposition the `timelines` plugin hosted TGrid HOC and moving all the data tables related sub-components to `security_solution` plugin the new components architecture got the next shape: <img width="842" alt="Screen Shot 2022-11-29 at 6 14 41 AM" src="https://user-images.githubusercontent.com/55110838/204663068-40897f18-1485-4b59-a71b-ce09e660f7db.png"> `security_solution` plugin changes includes the next things: - Moved some data table and actions types to `x-pack/plugins/security_solution/common/types`, which is widely used across the related components. - Due to the movement of the data table with the store to from `timeline` plugin to `security_solution` many test files which had the reference to `tGridReducer` now cleaned up from the unnecessary logic: ``` - import { tGridReducer } from '@kbn/timelines-plugin/public'; ``` and `TableState` references was replaced with the next changes: ``` - import type { TableState } from '@kbn/timelines-plugin/public'; + import type { TableState } from '../common/store/data_table/types'; ``` - Replaced `tGridActions` with `dataTableActions` name. - Moved `control_columns` to `security_solution` common plugin components: `RowCheckBox`, `HeaderCheckBox` and `transformControlColumns`: `RowActionComponent` moved from `timelines` plugin to `x-pack/plugins/security_solution/public/common/components/control_columns/row_action` without changes. `transformControlColumns` moved from timelines plugin to `x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx`. Removed not used property `hasAlertsCrudPermissions`, added unit test. <img width="1222" alt="Screen Shot 2022-11-29 at 8 59 42 PM" src="https://user-images.githubusercontent.com/55110838/204711499-9f90fee2-3c2f-4ff6-af28-c324ab1840d8.png"> - Many translation changes as a result of the owner plugin change: ``` - i18n.translate('xpack.timelines....', { + i18n.translate('xpack.securitySolution....', { ``` - Moved `useDraggableKeyboardWrapper` to security_solution, added reference to `useAddToTimeline`, by using timelines plugin with kibana services. Added unit tests. <img width="1112" alt="Screen Shot 2022-11-30 at 9 06 42 AM" src="https://user-images.githubusercontent.com/55110838/204862298-bcd50a52-dbf7-480b-bf13-8e48d6835746.png"> - Replaced the next references: ``` - type: 'x-pack/timelines/t-grid/UPDATE_COLUMN_WIDTH', + type: 'x-pack/security_solution/data-table/UPDATE_COLUMN_WIDTH', ``` ``` - type: 'x-pack/timelines/t-grid/REMOVE_COLUMN', + type: 'x-pack/security_solution/data-table/REMOVE_COLUMN', ``` - moved TGrid store previously hosted in timeline plugin to `security_solution` as `data_table` store: <img width="1109" alt="Screen Shot 2022-11-29 at 9 24 08 PM" src="https://user-images.githubusercontent.com/55110838/204714668-257a9c50-d722-4a6d-9214-f3ef8a14d0d2.png"> - Migrated TGrid `BodyComponent` to `DataTableComponent` `x-pack/plugins/security_solution/public/common/components/data_table/index.tsx` Removed some unused properties: `hasAlertsCrudPermissions, appId, getRowRenderer, isEventViewer, tableView, totalSelectAllAlerts, trailingControlColumns`. Current DataTableComponent is a subset of the previous BodyComponent, which includes only table related functionality: <img width="1028" alt="Screen Shot 2022-11-30 at 10 44 35 AM" src="https://user-images.githubusercontent.com/55110838/204882561-0950b9ce-5a9f-4bdb-b38f-6ff742fc3f92.png"> - Renamed `TimelineExpandedDetail` to `ExpandedDetail` to make the type more generic for usage. - BulkActions related changes includes: <img width="1288" alt="Screen Shot 2022-11-29 at 9 13 32 PM" src="https://user-images.githubusercontent.com/55110838/204713196-409f3d5e-f752-4fe9-9ae9-e752514cbf99.png"> `AlertBulkActionsComponent` moved from timelines plugin to `x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/alert_bulk_actions.tsx`, just renaming changes. Added `x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/types.ts` to consolidate types `useBulkActionItems` moved from timelines plugin to `x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/use_bulk_action_items.tsx`. Changed links, renamed `AlertsStatus` to `AlertWorkflowStatus`, removed `in-progress` case handling. `useUpdateAlertsStatus` moved from timelines plugin to `x-pack/plugins/security_solution/public/common/components/toolbar/bulk_actions/use_update_alerts.ts`. Cleaned up the code from handling Observability API. - Updated `x-pack/plugins/security_solution/public/common/lib/kuery/index.ts` with the actual implementations of ``` convertKueryToDslFilter, convertKueryToElasticSearchQuery, convertToBuildEsQuery, escapeKuery, escapeQueryValue, combineQueries, ``` instead of referencing timelines plugin. - Moved `EventRenderedView` component to security_solution common components. Later planning to make it as a package. - `EventsViewer` component became the stateful component which is responsible for the data representation managing. Some part from TGridIntegratedComponent and BodyComponent was merged under its logic: <img width="1052" alt="Screen Shot 2022-11-30 at 6 22 22 PM" src="https://user-images.githubusercontent.com/55110838/204950708-a8875acd-eb62-4df5-8ac4-613a0a571de6.png"> <img width="242" alt="Screen Shot 2022-11-30 at 6 24 06 PM" src="https://user-images.githubusercontent.com/55110838/204950819-bca194a4-4309-4cb4-a2ba-0176e9fe6c65.png"> - Moved header actions to common components `x-pack/plugins/security_solution/public/common/components/header_actions` - Renamed component `AlertCount` to `UnitCount`. - Moved to `security_solution` configuration for `APM_USER_INTERACTIONS` - changes `createStore` interface by using the direct reference to `dataTableReducer` instead of passing down it's value through the params. ### Timeline plugin changes - cleaned up timeline plugin interface by removing: ``` getTGrid: <T extends TGridType = 'embedded'>( props: GetTGridProps<T> ) => ReactElement<GetTGridProps<T>>; // eslint-disable-next-line @typescript-eslint/no-explicit-any getTGridReducer: () => any; getUseDraggableKeyboardWrapper: () => ( props: UseDraggableKeyboardWrapperProps ) => UseDraggableKeyboardWrapper; ``` - renamed embedded store ``` - setTGridEmbeddedStore: (store: Store) => void; + setTimelineEmbeddedStore: (store: Store) => void; ``` - removed dependency to triggers_actions_ui plugin - removed duplicated components and types with `security_solution`: ``` TruncatableText SubtitleComponent EventsCountComponent PopoverRowItems PagingControlComponent FooterComponent SortIndicator SortNumber RowRendererContainer plainRowRenderer getColumnRenderer StatefulRowRenderer getMappedNonEcsValue InspectButtonComponent TGridCellAction useMountAppended tgrid store ``` Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3e499922ff
commit
f1dc15ae4b
338 changed files with 4630 additions and 12500 deletions
|
@ -60,7 +60,6 @@ describe('ObservabilityActions component', () => {
|
|||
},
|
||||
data: inventoryThresholdAlert as unknown as TimelineNonEcsData[],
|
||||
observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(),
|
||||
setEventsDeleted: jest.fn(),
|
||||
setFlyoutAlert: jest.fn(),
|
||||
id: pageId,
|
||||
};
|
||||
|
|
|
@ -18,7 +18,8 @@ import React, { useMemo, useState, useCallback } from 'react';
|
|||
|
||||
import { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public';
|
||||
import { CommentType } from '@kbn/cases-plugin/common';
|
||||
import type { ActionProps } from '@kbn/timelines-plugin/common';
|
||||
import { Ecs } from '@kbn/timelines-plugin/common/ecs';
|
||||
import { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
|
||||
import { isAlertDetailsEnabledPerApp } from '../../../utils/is_alert_details_enabled';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { useGetUserCasesPermissions } from '../../../hooks/use_get_user_cases_permissions';
|
||||
|
@ -32,15 +33,15 @@ import { ObservabilityRuleTypeRegistry } from '../../..';
|
|||
import { ALERT_DETAILS_PAGE_ID } from '../../alert_details/types';
|
||||
import { ConfigSchema } from '../../../plugin';
|
||||
|
||||
export type ObservabilityActionsProps = Pick<
|
||||
ActionProps,
|
||||
'data' | 'eventId' | 'ecsData' | 'setEventsDeleted'
|
||||
> & {
|
||||
export interface ObservabilityActionsProps {
|
||||
data: TimelineNonEcsData[];
|
||||
ecsData: Ecs;
|
||||
eventId: string;
|
||||
setFlyoutAlert: React.Dispatch<React.SetStateAction<TopAlert | undefined>>;
|
||||
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
|
||||
id?: string;
|
||||
config: ConfigSchema;
|
||||
};
|
||||
}
|
||||
|
||||
export function ObservabilityActions({
|
||||
data,
|
||||
|
|
|
@ -17,7 +17,6 @@ const buildData = (alerts: EcsFieldsResponse): ObservabilityActionsProps['data']
|
|||
[]
|
||||
);
|
||||
};
|
||||
const fakeSetEventsDeleted = () => [];
|
||||
export const getRowActions = (
|
||||
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry,
|
||||
config: ConfigSchema
|
||||
|
@ -35,7 +34,6 @@ export const getRowActions = (
|
|||
ecsData={{ _id: alert._id, _index: alert._index }}
|
||||
id={id}
|
||||
observabilityRuleTypeRegistry={observabilityRuleTypeRegistry}
|
||||
setEventsDeleted={fakeSetEventsDeleted}
|
||||
setFlyoutAlert={setFlyoutAlert}
|
||||
config={config}
|
||||
/>
|
||||
|
|
|
@ -22,4 +22,7 @@ export type SignalEcsAAD = Exclude<SignalEcs, 'rule' | 'status'> & {
|
|||
severity?: string[];
|
||||
building_block_type?: string[];
|
||||
workflow_status?: string[];
|
||||
suppression?: {
|
||||
docs_count: string[];
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 type { TimelineItem } from '../../search_strategy';
|
||||
export interface CustomBulkAction {
|
||||
key: string;
|
||||
label: string;
|
||||
disableOnQuery?: boolean;
|
||||
disabledLabel?: string;
|
||||
onClick: (items?: TimelineItem[]) => void;
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
export type CustomBulkActionProp = Omit<CustomBulkAction, 'onClick'> & {
|
||||
onClick: (eventIds: string[]) => void;
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as runtimeTypes from 'io-ts';
|
||||
|
||||
export enum Direction {
|
||||
asc = 'asc',
|
||||
desc = 'desc',
|
||||
}
|
||||
|
||||
export type SortDirectionTable = 'none' | 'asc' | 'desc' | Direction;
|
||||
export interface SortColumnTable {
|
||||
columnId: string;
|
||||
columnType: string;
|
||||
esTypes?: string[];
|
||||
sortDirection: SortDirectionTable;
|
||||
}
|
||||
|
||||
export type { TableById } from '../../../public/common/store/data_table/types';
|
||||
|
||||
export enum TableId {
|
||||
usersPageEvents = 'users-page-events',
|
||||
hostsPageEvents = 'hosts-page-events',
|
||||
networkPageEvents = 'network-page-events',
|
||||
hostsPageSessions = 'hosts-page-sessions-v2', // the v2 is to cache bust localstorage settings as default columns were reworked.
|
||||
alertsOnRuleDetailsPage = 'alerts-rules-details-page',
|
||||
alertsOnAlertsPage = 'alerts-page',
|
||||
test = 'table-test', // Reserved for testing purposes
|
||||
alternateTest = 'alternateTest',
|
||||
rulePreview = 'rule-preview',
|
||||
kubernetesPageSessions = 'kubernetes-page-sessions',
|
||||
}
|
||||
|
||||
const TableIdLiteralRt = runtimeTypes.union([
|
||||
runtimeTypes.literal(TableId.usersPageEvents),
|
||||
runtimeTypes.literal(TableId.hostsPageEvents),
|
||||
runtimeTypes.literal(TableId.networkPageEvents),
|
||||
runtimeTypes.literal(TableId.hostsPageSessions),
|
||||
runtimeTypes.literal(TableId.alertsOnRuleDetailsPage),
|
||||
runtimeTypes.literal(TableId.alertsOnAlertsPage),
|
||||
runtimeTypes.literal(TableId.test),
|
||||
runtimeTypes.literal(TableId.rulePreview),
|
||||
runtimeTypes.literal(TableId.kubernetesPageSessions),
|
||||
]);
|
||||
export type TableIdLiteral = runtimeTypes.TypeOf<typeof TableIdLiteralRt>;
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 type { FlowTargetSourceDest } from '../../search_strategy';
|
||||
import type { TimelineTabs } from '../timeline';
|
||||
|
||||
type EmptyObject = Record<string | number, never>;
|
||||
|
||||
export type ExpandedEventType =
|
||||
| {
|
||||
panelView?: 'eventDetail';
|
||||
params?: {
|
||||
eventId: string;
|
||||
indexName: string;
|
||||
refetch?: () => void;
|
||||
};
|
||||
}
|
||||
| EmptyObject;
|
||||
|
||||
export type ExpandedHostType =
|
||||
| {
|
||||
panelView?: 'hostDetail';
|
||||
params?: {
|
||||
hostName: string;
|
||||
};
|
||||
}
|
||||
| EmptyObject;
|
||||
|
||||
export type ExpandedNetworkType =
|
||||
| {
|
||||
panelView?: 'networkDetail';
|
||||
params?: {
|
||||
ip: string;
|
||||
flowTarget: FlowTargetSourceDest;
|
||||
};
|
||||
}
|
||||
| EmptyObject;
|
||||
|
||||
export type ExpandedUserType =
|
||||
| {
|
||||
panelView?: 'userDetail';
|
||||
params?: {
|
||||
userName: string;
|
||||
};
|
||||
}
|
||||
| EmptyObject;
|
||||
|
||||
export type ExpandedDetailType =
|
||||
| ExpandedEventType
|
||||
| ExpandedHostType
|
||||
| ExpandedNetworkType
|
||||
| ExpandedUserType;
|
||||
|
||||
export type ExpandedDetailTimeline = {
|
||||
[tab in TimelineTabs]?: ExpandedDetailType;
|
||||
};
|
||||
|
||||
export type ExpandedDetail = Partial<Record<string, ExpandedDetailType>>;
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* 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 type {
|
||||
EuiDataGridCellValueElementProps,
|
||||
EuiDataGridColumn,
|
||||
EuiDataGridColumnCellActionProps,
|
||||
EuiDataGridControlColumn,
|
||||
} from '@elastic/eui';
|
||||
import type { IFieldSubType } from '@kbn/es-query';
|
||||
import type { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { ComponentType, JSXElementConstructor, ReactNode } from 'react';
|
||||
import type { OnRowSelected, SetEventsDeleted, SetEventsLoading } from '..';
|
||||
import type { Ecs } from '../../ecs';
|
||||
import type { BrowserFields, TimelineNonEcsData } from '../../search_strategy';
|
||||
import type { SortColumnTable } from '../data_table';
|
||||
|
||||
export type ColumnHeaderType = 'not-filtered' | 'text-filter';
|
||||
|
||||
/** Uniquely identifies a column */
|
||||
export type ColumnId = string;
|
||||
|
||||
/**
|
||||
* A `DataTableCellAction` function accepts `data`, where each row of data is
|
||||
* represented as a `TimelineNonEcsData[]`. For example, `data[0]` would
|
||||
* contain a `TimelineNonEcsData[]` with the first row of data.
|
||||
*
|
||||
* A `DataTableCellAction` returns a function that has access to all the
|
||||
* `EuiDataGridColumnCellActionProps`, _plus_ access to `data`,
|
||||
* which enables code like the following example to be written:
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => {
|
||||
* const value = getMappedNonEcsValue({
|
||||
* data: data[rowIndex], // access a specific row's values
|
||||
* fieldName: columnId,
|
||||
* });
|
||||
*
|
||||
* return (
|
||||
* <Component onClick={() => alert(`row ${rowIndex} col ${columnId} has value ${value}`)} iconType="heart">
|
||||
* {'Love it'}
|
||||
* </Component>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export type DataTableCellAction = ({
|
||||
browserFields,
|
||||
data,
|
||||
ecsData,
|
||||
header,
|
||||
pageSize,
|
||||
scopeId,
|
||||
closeCellPopover,
|
||||
}: {
|
||||
browserFields: BrowserFields;
|
||||
/** each row of data is represented as one TimelineNonEcsData[] */
|
||||
data: TimelineNonEcsData[][];
|
||||
ecsData: Ecs[];
|
||||
header?: ColumnHeaderOptions;
|
||||
pageSize: number;
|
||||
scopeId: string;
|
||||
closeCellPopover?: () => void;
|
||||
}) => (props: EuiDataGridColumnCellActionProps) => ReactNode;
|
||||
|
||||
/** The specification of a column header */
|
||||
export type ColumnHeaderOptions = Pick<
|
||||
EuiDataGridColumn,
|
||||
| 'actions'
|
||||
| 'defaultSortDirection'
|
||||
| 'display'
|
||||
| 'displayAsText'
|
||||
| 'id'
|
||||
| 'initialWidth'
|
||||
| 'isSortable'
|
||||
| 'schema'
|
||||
> & {
|
||||
aggregatable?: boolean;
|
||||
dataTableCellActions?: DataTableCellAction[];
|
||||
category?: string;
|
||||
columnHeaderType: ColumnHeaderType;
|
||||
description?: string | null;
|
||||
esTypes?: string[];
|
||||
example?: string | number | null;
|
||||
format?: string;
|
||||
linkField?: string;
|
||||
placeholder?: string;
|
||||
subType?: IFieldSubType;
|
||||
type?: string;
|
||||
};
|
||||
export interface HeaderActionProps {
|
||||
width: number;
|
||||
browserFields: BrowserFields;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
fieldBrowserOptions?: FieldBrowserOptions;
|
||||
isEventViewer?: boolean;
|
||||
isSelectAllChecked: boolean;
|
||||
onSelectAll: ({ isSelected }: { isSelected: boolean }) => void;
|
||||
showEventsSelect: boolean;
|
||||
showSelectAllCheckbox: boolean;
|
||||
sort: SortColumnTable[];
|
||||
tabType: string;
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
export type HeaderCellRender = ComponentType | ComponentType<HeaderActionProps>;
|
||||
|
||||
type GenericActionRowCellRenderProps = Pick<
|
||||
EuiDataGridCellValueElementProps,
|
||||
'rowIndex' | 'columnId'
|
||||
>;
|
||||
|
||||
export type RowCellRender =
|
||||
| JSXElementConstructor<GenericActionRowCellRenderProps>
|
||||
| ((props: GenericActionRowCellRenderProps) => JSX.Element)
|
||||
| JSXElementConstructor<ActionProps>
|
||||
| ((props: ActionProps) => JSX.Element);
|
||||
|
||||
export interface ActionProps {
|
||||
action?: RowCellRender;
|
||||
ariaRowindex: number;
|
||||
checked: boolean;
|
||||
columnId: string;
|
||||
columnValues: string;
|
||||
data: TimelineNonEcsData[];
|
||||
disabled?: boolean;
|
||||
ecsData: Ecs;
|
||||
eventId: string;
|
||||
eventIdToNoteIds?: Readonly<Record<string, string[]>>;
|
||||
index: number;
|
||||
isEventPinned?: boolean;
|
||||
isEventViewer?: boolean;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onEventDetailsPanelOpened: () => void;
|
||||
onRowSelected: OnRowSelected;
|
||||
onRuleChange?: () => void;
|
||||
refetch?: () => void;
|
||||
rowIndex: number;
|
||||
setEventsDeleted: SetEventsDeleted;
|
||||
setEventsLoading: SetEventsLoading;
|
||||
showCheckboxes: boolean;
|
||||
showNotes?: boolean;
|
||||
tabType?: string;
|
||||
timelineId: string;
|
||||
toggleShowNotes?: () => void;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface AdditionalControlColumnProps {
|
||||
ariaRowindex: number;
|
||||
actionsColumnWidth: number;
|
||||
columnValues: string;
|
||||
checked: boolean;
|
||||
onRowSelected: OnRowSelected;
|
||||
eventId: string;
|
||||
id: string;
|
||||
columnId: string;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onEventDetailsPanelOpened: () => void;
|
||||
showCheckboxes: boolean;
|
||||
// Override these type definitions to support either a generic custom component or the one used in security_solution today.
|
||||
headerCellRender: HeaderCellRender;
|
||||
rowCellRender: RowCellRender;
|
||||
}
|
||||
|
||||
export type ControlColumnProps = Omit<
|
||||
EuiDataGridControlColumn,
|
||||
keyof AdditionalControlColumnProps
|
||||
> &
|
||||
Partial<AdditionalControlColumnProps>;
|
|
@ -5,4 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Status } from '../detection_engine/schemas/common';
|
||||
|
||||
export * from './timeline';
|
||||
export * from './data_table';
|
||||
export * from './detail_panel';
|
||||
export * from './header_actions';
|
||||
export * from './session_view';
|
||||
export * from './bulk_actions';
|
||||
|
||||
export const FILTER_OPEN: Status = 'open';
|
||||
export const FILTER_CLOSED: Status = 'closed';
|
||||
export const FILTER_ACKNOWLEDGED: Status = 'acknowledged';
|
||||
|
||||
export type SetEventsLoading = (params: { eventIds: string[]; isLoading: boolean }) => void;
|
||||
export type SetEventsDeleted = (params: { eventIds: string[]; isDeleted: boolean }) => void;
|
||||
|
|
|
@ -4,11 +4,10 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
export type {
|
||||
ActionProps,
|
||||
HeaderActionProps,
|
||||
GenericActionRowCellRenderProps,
|
||||
HeaderCellRender,
|
||||
RowCellRender,
|
||||
ControlColumnProps,
|
||||
} from '@kbn/timelines-plugin/common';
|
||||
|
||||
export interface SessionViewConfig {
|
||||
sessionEntityId: string;
|
||||
jumpToEntityId?: string;
|
||||
jumpToCursor?: string;
|
||||
investigatedAlertId?: string;
|
||||
}
|
|
@ -5,4 +5,30 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export type { CellValueElementProps } from '@kbn/timelines-plugin/common';
|
||||
import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { ColumnHeaderOptions, RowRenderer } from '../..';
|
||||
import type { Ecs } from '../../../ecs';
|
||||
import type { BrowserFields, TimelineNonEcsData } from '../../../search_strategy';
|
||||
|
||||
/** The following props are provided to the function called by `renderCellValue` */
|
||||
export type CellValueElementProps = EuiDataGridCellValueElementProps & {
|
||||
asPlainText?: boolean;
|
||||
browserFields?: BrowserFields;
|
||||
data: TimelineNonEcsData[];
|
||||
ecsData?: Ecs;
|
||||
eventId: string; // _id
|
||||
globalFilters?: Filter[];
|
||||
header: ColumnHeaderOptions;
|
||||
isDraggable: boolean;
|
||||
isTimeline?: boolean; // Default cell renderer is used for both the alert table and timeline. This allows us to cheaply separate concerns
|
||||
linkValues: string[] | undefined;
|
||||
rowRenderers?: RowRenderer[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
setFlyoutAlert?: (data: any) => void;
|
||||
scopeId: string;
|
||||
truncate?: boolean;
|
||||
key?: string;
|
||||
closeCellPopover?: () => void;
|
||||
enableActions?: boolean;
|
||||
};
|
||||
|
|
|
@ -5,9 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export type {
|
||||
ColumnHeaderType,
|
||||
ColumnId,
|
||||
ColumnHeaderOptions,
|
||||
ColumnRenderer,
|
||||
} from '@kbn/timelines-plugin/common';
|
||||
export type { ColumnHeaderOptions } from '../../header_actions';
|
||||
|
|
|
@ -22,12 +22,11 @@ import {
|
|||
success,
|
||||
success_count as successCount,
|
||||
} from '../../detection_engine/schemas/common/schemas';
|
||||
import type { FlowTargetSourceDest } from '../../search_strategy/security_solution/network';
|
||||
import { errorSchema } from '../../detection_engine/schemas/response/error_schema';
|
||||
import type { Maybe } from '../../search_strategy';
|
||||
import { Direction } from '../../search_strategy';
|
||||
import type { ExpandedDetailType } from '../detail_panel';
|
||||
|
||||
export * from './actions';
|
||||
export * from './cells';
|
||||
export * from './columns';
|
||||
export * from './data_provider';
|
||||
|
@ -326,32 +325,6 @@ export enum TimelineId {
|
|||
detectionsAlertDetailsPage = 'detections-alert-details-page',
|
||||
}
|
||||
|
||||
export enum TableId {
|
||||
usersPageEvents = 'users-page-events',
|
||||
hostsPageEvents = 'hosts-page-events',
|
||||
networkPageEvents = 'network-page-events',
|
||||
hostsPageSessions = 'hosts-page-sessions-v2', // the v2 is to cache bust localstorage settings as default columns were reworked.
|
||||
alertsOnRuleDetailsPage = 'alerts-rules-details-page',
|
||||
alertsOnAlertsPage = 'alerts-page',
|
||||
test = 'table-test', // Reserved for testing purposes
|
||||
alternateTest = 'alternateTest',
|
||||
rulePreview = 'rule-preview',
|
||||
kubernetesPageSessions = 'kubernetes-page-sessions',
|
||||
}
|
||||
|
||||
export const TableIdLiteralRt = runtimeTypes.union([
|
||||
runtimeTypes.literal(TableId.usersPageEvents),
|
||||
runtimeTypes.literal(TableId.hostsPageEvents),
|
||||
runtimeTypes.literal(TableId.networkPageEvents),
|
||||
runtimeTypes.literal(TableId.hostsPageSessions),
|
||||
runtimeTypes.literal(TableId.alertsOnRuleDetailsPage),
|
||||
runtimeTypes.literal(TableId.alertsOnAlertsPage),
|
||||
runtimeTypes.literal(TableId.test),
|
||||
runtimeTypes.literal(TableId.rulePreview),
|
||||
runtimeTypes.literal(TableId.kubernetesPageSessions),
|
||||
]);
|
||||
export type TableIdLiteral = runtimeTypes.TypeOf<typeof TableIdLiteralRt>;
|
||||
|
||||
export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection([
|
||||
SavedTimelineRuntimeType,
|
||||
runtimeTypes.type({
|
||||
|
@ -484,59 +457,7 @@ export interface ScrollToTopEvent {
|
|||
timestamp: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type EmptyObject = Record<any, never>;
|
||||
|
||||
export type TimelineExpandedEventType =
|
||||
| {
|
||||
panelView?: 'eventDetail';
|
||||
params?: {
|
||||
eventId: string;
|
||||
indexName: string;
|
||||
refetch?: () => void;
|
||||
};
|
||||
}
|
||||
| EmptyObject;
|
||||
|
||||
export type TimelineExpandedHostType =
|
||||
| {
|
||||
panelView?: 'hostDetail';
|
||||
params?: {
|
||||
hostName: string;
|
||||
};
|
||||
}
|
||||
| EmptyObject;
|
||||
|
||||
export type TimelineExpandedNetworkType =
|
||||
| {
|
||||
panelView?: 'networkDetail';
|
||||
params?: {
|
||||
ip: string;
|
||||
flowTarget: FlowTargetSourceDest;
|
||||
};
|
||||
}
|
||||
| EmptyObject;
|
||||
|
||||
export type TimelineExpandedUserType =
|
||||
| {
|
||||
panelView?: 'userDetail';
|
||||
params?: {
|
||||
userName: string;
|
||||
};
|
||||
}
|
||||
| EmptyObject;
|
||||
|
||||
export type TimelineExpandedDetailType =
|
||||
| TimelineExpandedEventType
|
||||
| TimelineExpandedHostType
|
||||
| TimelineExpandedNetworkType
|
||||
| TimelineExpandedUserType;
|
||||
|
||||
export type TimelineExpandedDetail = {
|
||||
[tab in TimelineTabs]?: TimelineExpandedDetailType;
|
||||
};
|
||||
|
||||
export type ToggleDetailPanel = TimelineExpandedDetailType & {
|
||||
export type ToggleDetailPanel = ExpandedDetailType & {
|
||||
tabType?: TimelineTabs;
|
||||
id: string;
|
||||
};
|
||||
|
|
|
@ -6,15 +6,11 @@
|
|||
*/
|
||||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type {
|
||||
ColumnHeaderOptions,
|
||||
ColumnId,
|
||||
RowRendererId,
|
||||
TimelineExpandedDetail,
|
||||
TimelineTypeLiteral,
|
||||
} from '.';
|
||||
import type { RowRendererId, TimelineTypeLiteral } from '.';
|
||||
|
||||
import type { Direction } from '../../search_strategy';
|
||||
import type { ExpandedDetailTimeline } from '../detail_panel';
|
||||
import type { ColumnHeaderOptions, ColumnId } from '../header_actions';
|
||||
import type { DataProvider } from './data_provider';
|
||||
|
||||
export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql';
|
||||
|
@ -47,7 +43,7 @@ export interface TimelinePersistInput {
|
|||
};
|
||||
defaultColumns?: ColumnHeaderOptions[];
|
||||
excludedRowRendererIds?: RowRendererId[];
|
||||
expandedDetail?: TimelineExpandedDetail;
|
||||
expandedDetail?: ExpandedDetailTimeline;
|
||||
filters?: Filter[];
|
||||
id: string;
|
||||
indexNames: string[];
|
||||
|
|
|
@ -26,7 +26,6 @@ import { TimelineId } from '../../../../common/types/timeline';
|
|||
import { createStore } from '../../../common/store';
|
||||
import { kibanaObservable } from '@kbn/timelines-plugin/public/mock';
|
||||
import { sourcererPaths } from '../../../common/containers/sourcerer';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
|
@ -73,13 +72,7 @@ describe('global header', () => {
|
|||
},
|
||||
};
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
beforeEach(() => {
|
||||
useVariationMock.mockReset();
|
||||
|
@ -176,13 +169,7 @@ describe('global header', () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const mockStore = createStore(
|
||||
mockstate,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const mockStore = createStore(mockstate, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue({ pathname: sourcererPaths[2] });
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@ import type { TimelineUrl } from '../../timelines/store/timeline/model';
|
|||
import { timelineDefaults } from '../../timelines/store/timeline/defaults';
|
||||
import { URL_PARAM_KEY } from '../../common/hooks/use_url_state';
|
||||
import { InputsModelId } from '../../common/store/inputs/constants';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
|
||||
jest.mock('../../common/store/inputs/actions');
|
||||
|
||||
|
@ -299,13 +298,7 @@ describe('HomePage', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const mockStore = createStore(
|
||||
mockstate,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const mockStore = createStore(mockstate, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
render(
|
||||
<TestProviders store={mockStore}>
|
||||
|
@ -459,13 +452,7 @@ describe('HomePage', () => {
|
|||
};
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const mockStore = createStore(
|
||||
mockstate,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const mockStore = createStore(mockstate, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
const TestComponent = () => (
|
||||
<TestProviders store={mockStore}>
|
||||
|
@ -522,13 +509,7 @@ describe('HomePage', () => {
|
|||
};
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const mockStore = createStore(
|
||||
mockstate,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const mockStore = createStore(mockstate, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
const TestComponent = () => (
|
||||
<TestProviders store={mockStore}>
|
||||
|
@ -588,13 +569,7 @@ describe('HomePage', () => {
|
|||
|
||||
it('it removes empty timeline state from URL', async () => {
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
mockUseInitializeUrlParam(URL_PARAM_KEY.timeline, {
|
||||
id: 'testSavedTimelineId',
|
||||
|
@ -621,13 +596,7 @@ describe('HomePage', () => {
|
|||
|
||||
it('it updates URL when timeline store changes', async () => {
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const savedObjectId = 'testTimelineId';
|
||||
|
||||
mockUseInitializeUrlParam(URL_PARAM_KEY.timeline, {
|
||||
|
|
|
@ -18,8 +18,6 @@ import type {
|
|||
import type { RouteProps } from 'react-router-dom';
|
||||
import type { AppMountParameters } from '@kbn/core/public';
|
||||
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import type { TableState } from '@kbn/timelines-plugin/public';
|
||||
|
||||
import type { StartServices } from '../types';
|
||||
|
||||
/**
|
||||
|
@ -35,6 +33,7 @@ export interface RenderAppProps extends AppMountParameters {
|
|||
import type { State, SubPluginsInitReducer } from '../common/store';
|
||||
import type { Immutable } from '../../common/endpoint/types';
|
||||
import type { AppAction } from '../common/store/actions';
|
||||
import type { TableState } from '../common/store/data_table/types';
|
||||
|
||||
export { SecurityPageName } from '../../common/constants';
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import type { State } from '../../../../store';
|
|||
import { createStore } from '../../../../store';
|
||||
import * as i18n from './translations';
|
||||
import { useChartSettingsPopoverConfiguration } from '.';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
|
||||
const mockHandleClick = jest.fn();
|
||||
|
||||
|
@ -33,13 +32,7 @@ describe('useChartSettingsPopoverConfiguration', () => {
|
|||
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<TestProviders store={store}>{children}</TestProviders>
|
||||
);
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { ActionProps, HeaderActionProps } from '../../../../../common/types';
|
||||
import { HeaderCheckBox, RowCheckBox } from './checkbox';
|
||||
import React from 'react';
|
||||
import type { ActionProps, HeaderActionProps } from '../../../../common/types';
|
||||
import { TimelineTabs } from '../../../../common/types';
|
||||
|
||||
describe('checkbox control column', () => {
|
||||
describe('RowCheckBox', () => {
|
||||
|
@ -61,7 +62,7 @@ describe('checkbox control column', () => {
|
|||
showEventsSelect: true,
|
||||
showSelectAllCheckbox: true,
|
||||
sort: [],
|
||||
tabType: 'query',
|
||||
tabType: TimelineTabs.query,
|
||||
timelineId: 'test-timelineId',
|
||||
};
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { EuiCheckbox, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import type { ActionProps, HeaderActionProps } from '../../../../../common/types';
|
||||
import type { ActionProps, HeaderActionProps } from '../../../../common/types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const RowCheckBox = ({
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ControlColumnProps } from '../../../../../common/types';
|
||||
import type { ControlColumnProps } from '../../../../common/types';
|
||||
import { HeaderCheckBox, RowCheckBox } from './checkbox';
|
||||
|
||||
export const checkBoxControlColumn: ControlColumnProps = {
|
||||
|
@ -14,3 +14,5 @@ export const checkBoxControlColumn: ControlColumnProps = {
|
|||
headerCellRender: HeaderCheckBox,
|
||||
rowCellRender: RowCheckBox,
|
||||
};
|
||||
|
||||
export * from './transform_control_columns';
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { TableId } from '../../../../../common/types';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { RowAction } from '.';
|
||||
import { defaultHeaders, TestProviders } from '../../../mock';
|
||||
import { getDefaultControlColumn } from '../../../../timelines/components/timeline/body/control_columns';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
|
||||
|
||||
jest.mock('../../../hooks/use_experimental_features', () => ({
|
||||
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
describe('RowAction', () => {
|
||||
const sampleData = {
|
||||
_id: '1',
|
||||
data: [],
|
||||
ecs: {
|
||||
_id: '1',
|
||||
},
|
||||
};
|
||||
const defaultProps = {
|
||||
columnHeaders: defaultHeaders,
|
||||
controlColumn: getDefaultControlColumn(5)[0],
|
||||
data: [sampleData],
|
||||
disabled: false,
|
||||
index: 1,
|
||||
isEventViewer: false,
|
||||
loadingEventIds: [],
|
||||
onRowSelected: jest.fn(),
|
||||
onRuleChange: jest.fn(),
|
||||
selectedEventIds: {},
|
||||
tableId: TableId.test,
|
||||
width: 100,
|
||||
setEventsLoading: jest.fn(),
|
||||
setEventsDeleted: jest.fn(),
|
||||
pageRowIndex: 0,
|
||||
columnId: 'test-columnId',
|
||||
isDetails: false,
|
||||
isExpanded: false,
|
||||
isExpandable: false,
|
||||
rowIndex: 0,
|
||||
colIndex: 0,
|
||||
setCellProps: jest.fn(),
|
||||
tabType: 'query',
|
||||
showCheckboxes: false,
|
||||
};
|
||||
test('displays expand events button', () => {
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<RowAction {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.getAllByTestId('expand-event')).not.toBeNull();
|
||||
});
|
||||
});
|
|
@ -5,21 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiDataGridCellValueElementProps } from '@elastic/eui';
|
||||
import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy';
|
||||
import type {
|
||||
ColumnHeaderOptions,
|
||||
ControlColumnProps,
|
||||
OnRowSelected,
|
||||
SetEventsLoading,
|
||||
SetEventsDeleted,
|
||||
DataExpandedDetailType,
|
||||
} from '../../../../../common/types/timeline';
|
||||
import { getMappedNonEcsValue } from '../data_driven_columns';
|
||||
import { tGridActions } from '../../../../store/t_grid';
|
||||
SetEventsLoading,
|
||||
ControlColumnProps,
|
||||
ExpandedDetailType,
|
||||
} from '../../../../../common/types';
|
||||
import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
|
||||
|
||||
import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy';
|
||||
import type { ColumnHeaderOptions, OnRowSelected } from '../../../../../common/types/timeline';
|
||||
import { dataTableActions } from '../../../store/data_table';
|
||||
|
||||
type Props = EuiDataGridCellValueElementProps & {
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
|
@ -89,7 +88,7 @@ const RowActionComponent = ({
|
|||
);
|
||||
|
||||
const handleOnEventDetailPanelOpened = useCallback(() => {
|
||||
const updatedExpandedDetail: DataExpandedDetailType = {
|
||||
const updatedExpandedDetail: ExpandedDetailType = {
|
||||
panelView: 'eventDetail',
|
||||
params: {
|
||||
eventId: eventId ?? '',
|
||||
|
@ -98,7 +97,7 @@ const RowActionComponent = ({
|
|||
};
|
||||
|
||||
dispatch(
|
||||
tGridActions.toggleDetailPanel({
|
||||
dataTableActions.toggleDetailPanel({
|
||||
...updatedExpandedDetail,
|
||||
tabType,
|
||||
id: tableId,
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 type { TransformColumnsProps } from './transform_control_columns';
|
||||
import { transformControlColumns } from './transform_control_columns';
|
||||
|
||||
describe('transformControlColumns', () => {
|
||||
const defaultProps: TransformColumnsProps = {
|
||||
onRowSelected: jest.fn(),
|
||||
loadingEventIds: [],
|
||||
showCheckboxes: true,
|
||||
data: [],
|
||||
timelineId: 'test-timelineId',
|
||||
setEventsLoading: jest.fn(),
|
||||
setEventsDeleted: jest.fn(),
|
||||
columnHeaders: [],
|
||||
controlColumns: [],
|
||||
disabledCellActions: [],
|
||||
selectedEventIds: {},
|
||||
tabType: '',
|
||||
isSelectAllChecked: false,
|
||||
browserFields: {},
|
||||
onSelectPage: jest.fn(),
|
||||
pageSize: 0,
|
||||
sort: [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
theme: {} as any,
|
||||
};
|
||||
test('displays loader when id is included on loadingEventIds', () => {
|
||||
const res = transformControlColumns(defaultProps);
|
||||
expect(res.find).not.toBeNull();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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 type { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { EuiDataGridCellValueElementProps, EuiDataGridControlColumn } from '@elastic/eui';
|
||||
import type { ComponentType } from 'react';
|
||||
import React from 'react';
|
||||
import type { EuiTheme } from '@kbn/kibana-react-plugin/common';
|
||||
import type {
|
||||
BrowserFields,
|
||||
TimelineItem,
|
||||
TimelineNonEcsData,
|
||||
} from '../../../../common/search_strategy';
|
||||
import type {
|
||||
SetEventsDeleted,
|
||||
SetEventsLoading,
|
||||
ColumnHeaderOptions,
|
||||
ControlColumnProps,
|
||||
OnRowSelected,
|
||||
OnSelectAll,
|
||||
SortColumnTable,
|
||||
} from '../../../../common/types';
|
||||
import { addBuildingBlockStyle } from '../data_table/helpers';
|
||||
import { getPageRowIndex } from '../data_table/pagination';
|
||||
import { RowAction } from './row_action';
|
||||
|
||||
const EmptyHeaderCellRender: ComponentType = () => null;
|
||||
|
||||
export interface TransformColumnsProps {
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
controlColumns: ControlColumnProps[];
|
||||
data: TimelineItem[];
|
||||
disabledCellActions: string[];
|
||||
fieldBrowserOptions?: FieldBrowserOptions;
|
||||
loadingEventIds: string[];
|
||||
onRowSelected: OnRowSelected;
|
||||
onRuleChange?: () => void;
|
||||
selectedEventIds: Record<string, TimelineNonEcsData[]>;
|
||||
showCheckboxes: boolean;
|
||||
tabType: string;
|
||||
timelineId: string;
|
||||
isSelectAllChecked: boolean;
|
||||
browserFields: BrowserFields;
|
||||
onSelectPage: OnSelectAll;
|
||||
pageSize: number;
|
||||
sort: SortColumnTable[];
|
||||
theme: EuiTheme;
|
||||
setEventsLoading: SetEventsLoading;
|
||||
setEventsDeleted: SetEventsDeleted;
|
||||
}
|
||||
|
||||
export const transformControlColumns = ({
|
||||
columnHeaders,
|
||||
controlColumns,
|
||||
data,
|
||||
fieldBrowserOptions,
|
||||
loadingEventIds,
|
||||
onRowSelected,
|
||||
onRuleChange,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
tabType,
|
||||
timelineId,
|
||||
isSelectAllChecked,
|
||||
onSelectPage,
|
||||
browserFields,
|
||||
pageSize,
|
||||
sort,
|
||||
theme,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
}: TransformColumnsProps): EuiDataGridControlColumn[] =>
|
||||
controlColumns.map(
|
||||
({ id: columnId, headerCellRender = EmptyHeaderCellRender, rowCellRender, width }, i) => ({
|
||||
id: `${columnId}`,
|
||||
headerCellRender: () => {
|
||||
const HeaderActions = headerCellRender;
|
||||
return (
|
||||
<>
|
||||
{HeaderActions && (
|
||||
<HeaderActions
|
||||
width={width}
|
||||
browserFields={browserFields}
|
||||
fieldBrowserOptions={fieldBrowserOptions}
|
||||
columnHeaders={columnHeaders}
|
||||
isEventViewer={false}
|
||||
isSelectAllChecked={isSelectAllChecked}
|
||||
onSelectAll={onSelectPage}
|
||||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={showCheckboxes}
|
||||
sort={sort}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
rowCellRender: ({
|
||||
isDetails,
|
||||
isExpandable,
|
||||
isExpanded,
|
||||
rowIndex,
|
||||
colIndex,
|
||||
setCellProps,
|
||||
}: EuiDataGridCellValueElementProps) => {
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
const rowData = data[pageRowIndex];
|
||||
|
||||
if (rowData) {
|
||||
addBuildingBlockStyle(rowData.ecs, theme, setCellProps);
|
||||
} else {
|
||||
// disable the cell when it has no data
|
||||
setCellProps({ style: { display: 'none' } });
|
||||
}
|
||||
|
||||
return (
|
||||
<RowAction
|
||||
columnId={columnId ?? ''}
|
||||
columnHeaders={columnHeaders}
|
||||
controlColumn={controlColumns[i]}
|
||||
data={data}
|
||||
disabled={false}
|
||||
index={i}
|
||||
isDetails={isDetails}
|
||||
isExpanded={isExpanded}
|
||||
isEventViewer={false}
|
||||
isExpandable={isExpandable}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onRowSelected={onRowSelected}
|
||||
onRuleChange={onRuleChange}
|
||||
rowIndex={rowIndex}
|
||||
colIndex={colIndex}
|
||||
pageRowIndex={pageRowIndex}
|
||||
selectedEventIds={selectedEventIds}
|
||||
setCellProps={setCellProps}
|
||||
showCheckboxes={showCheckboxes}
|
||||
tabType={tabType}
|
||||
tableId={timelineId}
|
||||
width={width}
|
||||
setEventsLoading={setEventsLoading}
|
||||
setEventsDeleted={setEventsDeleted}
|
||||
/>
|
||||
);
|
||||
},
|
||||
width,
|
||||
})
|
||||
);
|
|
@ -15,7 +15,7 @@ export const CHECKBOX_FOR_ROW = ({
|
|||
columnValues: string;
|
||||
checked: boolean;
|
||||
}) =>
|
||||
i18n.translate('xpack.timelines.timeline.body.actions.checkboxForRowAriaLabel', {
|
||||
i18n.translate('xpack.securitySolution.controlColumns.checkboxForRowAriaLabel', {
|
||||
values: { ariaRowindex, checked, columnValues },
|
||||
defaultMessage:
|
||||
'{checked, select, false {unchecked} true {checked}} checkbox for the alert or event in row {ariaRowindex}, with columns {columnValues}',
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ColumnHeaderOptions, ColumnHeaderType } from '../../../../../common/types/timeline';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
|
||||
import type { ColumnHeaderType } from '../../../../../common/types';
|
||||
import type { ColumnHeaderOptions } from '../../../../../common/types/timeline';
|
||||
import { DEFAULT_TABLE_COLUMN_MIN_WIDTH, DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH } from '../constants';
|
||||
|
||||
export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered';
|
||||
|
||||
|
@ -14,43 +15,43 @@ export const defaultHeaders: ColumnHeaderOptions[] = [
|
|||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: '@timestamp',
|
||||
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH,
|
||||
esTypes: ['date'],
|
||||
type: 'date',
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'message',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'event.category',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'event.action',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'host.name',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'source.ip',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'destination.ip',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'user.name',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
];
|
|
@ -4,27 +4,22 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { mount } from 'enzyme';
|
||||
import { omit, set } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import { defaultHeaders } from './default_headers';
|
||||
import type { BUILT_IN_SCHEMA } from './helpers';
|
||||
import {
|
||||
BUILT_IN_SCHEMA,
|
||||
getActionsColumnWidth,
|
||||
getColumnWidthFromType,
|
||||
getColumnHeaders,
|
||||
getSchema,
|
||||
getColumnHeader,
|
||||
allowSorting,
|
||||
} from './helpers';
|
||||
import {
|
||||
DEFAULT_ACTION_BUTTON_WIDTH,
|
||||
DEFAULT_COLUMN_MIN_WIDTH,
|
||||
DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
} from '../constants';
|
||||
import { mockBrowserFields } from '../../../../mock/browser_fields';
|
||||
import { ColumnHeaderOptions } from '../../../../../common/types';
|
||||
import { DEFAULT_TABLE_COLUMN_MIN_WIDTH, DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH } from '../constants';
|
||||
import type { ColumnHeaderOptions } from '../../../../../common/types';
|
||||
import { mockBrowserFields } from '../../../containers/source/mock';
|
||||
import { defaultHeaders } from '../../../store/data_table/defaults';
|
||||
|
||||
window.matchMedia = jest.fn().mockImplementation((query) => {
|
||||
return {
|
||||
|
@ -39,11 +34,11 @@ window.matchMedia = jest.fn().mockImplementation((query) => {
|
|||
describe('helpers', () => {
|
||||
describe('getColumnWidthFromType', () => {
|
||||
test('it returns the expected width for a non-date column', () => {
|
||||
expect(getColumnWidthFromType('keyword')).toEqual(DEFAULT_COLUMN_MIN_WIDTH);
|
||||
expect(getColumnWidthFromType('keyword')).toEqual(DEFAULT_TABLE_COLUMN_MIN_WIDTH);
|
||||
});
|
||||
|
||||
test('it returns the expected width for a date column', () => {
|
||||
expect(getColumnWidthFromType('date')).toEqual(DEFAULT_DATE_COLUMN_MIN_WIDTH);
|
||||
expect(getColumnWidthFromType('date')).toEqual(DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -80,7 +75,7 @@ describe('helpers', () => {
|
|||
expect(getColumnHeader(field, [])).toEqual({
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: field,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -92,7 +87,7 @@ describe('helpers', () => {
|
|||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: field,
|
||||
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH,
|
||||
esTypes: ['date'],
|
||||
type: 'date',
|
||||
},
|
||||
|
@ -100,7 +95,7 @@ describe('helpers', () => {
|
|||
).toEqual({
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: field,
|
||||
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH,
|
||||
esTypes: ['date'],
|
||||
type: 'date',
|
||||
});
|
||||
|
@ -251,6 +246,7 @@ describe('helpers', () => {
|
|||
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
|
||||
isSortable,
|
||||
name: '@timestamp',
|
||||
readFromDocValues: true,
|
||||
schema: 'datetime',
|
||||
searchable: true,
|
||||
type: 'date',
|
||||
|
@ -263,12 +259,14 @@ describe('helpers', () => {
|
|||
columnHeaderType: 'not-filtered',
|
||||
defaultSortDirection,
|
||||
description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.',
|
||||
esTypes: ['ip'],
|
||||
example: '',
|
||||
format: '',
|
||||
id: 'source.ip',
|
||||
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
|
||||
isSortable,
|
||||
name: 'source.ip',
|
||||
schema: undefined,
|
||||
searchable: true,
|
||||
type: 'ip',
|
||||
initialWidth: 180,
|
||||
|
@ -281,12 +279,14 @@ describe('helpers', () => {
|
|||
defaultSortDirection,
|
||||
description:
|
||||
'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.',
|
||||
esTypes: ['ip'],
|
||||
example: '',
|
||||
format: '',
|
||||
id: 'destination.ip',
|
||||
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
|
||||
isSortable,
|
||||
name: 'destination.ip',
|
||||
schema: undefined,
|
||||
searchable: true,
|
||||
type: 'ip',
|
||||
initialWidth: 180,
|
||||
|
@ -309,12 +309,14 @@ describe('helpers', () => {
|
|||
defaultSortDirection,
|
||||
description:
|
||||
'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.',
|
||||
esTypes: ['date'],
|
||||
example: '2016-05-23T08:05:34.853Z',
|
||||
format: '',
|
||||
id: '@timestamp',
|
||||
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
|
||||
isSortable,
|
||||
name: '@timestamp',
|
||||
readFromDocValues: true,
|
||||
schema: 'custom', // <-- we expect our custom schema will NOT be overridden by a built-in schema
|
||||
searchable: true,
|
||||
type: 'date', // <-- the built-in schema for `type: 'date'` is 'datetime', but the custom schema overrides it
|
||||
|
@ -459,34 +461,70 @@ describe('helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getActionsColumnWidth', () => {
|
||||
// ideally the following implementation detail wouldn't be part of these tests,
|
||||
// but without it, the test would be brittle when `euiDataGridCellPaddingM` changes:
|
||||
const expectedPadding = parseInt(euiThemeVars.euiDataGridCellPaddingM, 10) * 2;
|
||||
describe('allowSorting', () => {
|
||||
const aggregatableField = {
|
||||
category: 'cloud',
|
||||
description:
|
||||
'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.',
|
||||
example: '666777888999',
|
||||
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
|
||||
name: 'cloud.account.id',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
aggregatable: true, // <-- allow sorting when this is true
|
||||
format: '',
|
||||
};
|
||||
|
||||
test('it returns the expected width', () => {
|
||||
const ACTION_BUTTON_COUNT = 5;
|
||||
const expectedContentWidth = ACTION_BUTTON_COUNT * DEFAULT_ACTION_BUTTON_WIDTH;
|
||||
|
||||
expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual(
|
||||
expectedContentWidth + expectedPadding
|
||||
);
|
||||
test('it returns true for an aggregatable field', () => {
|
||||
expect(
|
||||
allowSorting({
|
||||
browserField: aggregatableField,
|
||||
fieldName: aggregatableField.name,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('it returns the minimum width when the button count is zero', () => {
|
||||
const ACTION_BUTTON_COUNT = 0;
|
||||
|
||||
expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual(
|
||||
DEFAULT_ACTION_BUTTON_WIDTH + expectedPadding
|
||||
);
|
||||
test('it returns true for a allow-listed non-BrowserField', () => {
|
||||
expect(
|
||||
allowSorting({
|
||||
browserField: undefined, // no BrowserField metadata for this field
|
||||
fieldName: 'kibana.alert.rule.name', // an allow-listed field name
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('it returns the minimum width when the button count is negative', () => {
|
||||
const ACTION_BUTTON_COUNT = -1;
|
||||
test('it returns false for a NON-aggregatable field (aggregatable is false)', () => {
|
||||
const nonaggregatableField = {
|
||||
...aggregatableField,
|
||||
aggregatable: false, // <-- NON-aggregatable
|
||||
};
|
||||
|
||||
expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual(
|
||||
DEFAULT_ACTION_BUTTON_WIDTH + expectedPadding
|
||||
);
|
||||
expect(
|
||||
allowSorting({
|
||||
browserField: nonaggregatableField,
|
||||
fieldName: nonaggregatableField.name,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('it returns false if the BrowserField is missing the aggregatable property', () => {
|
||||
const missingAggregatable = omit('aggregatable', aggregatableField);
|
||||
|
||||
expect(
|
||||
allowSorting({
|
||||
browserField: missingAggregatable,
|
||||
fieldName: missingAggregatable.name,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("it returns false for a non-allowlisted field we don't have `BrowserField` metadata for it", () => {
|
||||
expect(
|
||||
allowSorting({
|
||||
browserField: undefined, // <-- no metadata for this field
|
||||
fieldName: 'non-allowlisted',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,8 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { EuiDataGridColumnActions } from '@elastic/eui';
|
||||
import type { EuiDataGridColumnActions } from '@elastic/eui';
|
||||
import { keyBy } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
|
@ -15,13 +14,8 @@ import type {
|
|||
BrowserFields,
|
||||
} from '../../../../../common/search_strategy/index_fields';
|
||||
import type { ColumnHeaderOptions } from '../../../../../common/types/timeline';
|
||||
import {
|
||||
DEFAULT_ACTION_BUTTON_WIDTH,
|
||||
DEFAULT_COLUMN_MIN_WIDTH,
|
||||
DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
} from '../constants';
|
||||
import { allowSorting } from '../helpers';
|
||||
import { defaultColumnHeaderType } from './default_headers';
|
||||
import { DEFAULT_TABLE_COLUMN_MIN_WIDTH, DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH } from '../constants';
|
||||
import { defaultColumnHeaderType } from '../../../store/data_table/defaults';
|
||||
|
||||
const defaultActions: EuiDataGridColumnActions = {
|
||||
showSortAsc: true,
|
||||
|
@ -29,6 +23,79 @@ const defaultActions: EuiDataGridColumnActions = {
|
|||
showHide: false,
|
||||
};
|
||||
|
||||
export const allowSorting = ({
|
||||
browserField,
|
||||
fieldName,
|
||||
}: {
|
||||
browserField: Partial<BrowserField> | undefined;
|
||||
fieldName: string;
|
||||
}): boolean => {
|
||||
const isAggregatable = browserField?.aggregatable ?? false;
|
||||
|
||||
const isAllowlistedNonBrowserField = [
|
||||
'kibana.alert.ancestors.depth',
|
||||
'kibana.alert.ancestors.id',
|
||||
'kibana.alert.ancestors.rule',
|
||||
'kibana.alert.ancestors.type',
|
||||
'kibana.alert.original_event.action',
|
||||
'kibana.alert.original_event.category',
|
||||
'kibana.alert.original_event.code',
|
||||
'kibana.alert.original_event.created',
|
||||
'kibana.alert.original_event.dataset',
|
||||
'kibana.alert.original_event.duration',
|
||||
'kibana.alert.original_event.end',
|
||||
'kibana.alert.original_event.hash',
|
||||
'kibana.alert.original_event.id',
|
||||
'kibana.alert.original_event.kind',
|
||||
'kibana.alert.original_event.module',
|
||||
'kibana.alert.original_event.original',
|
||||
'kibana.alert.original_event.outcome',
|
||||
'kibana.alert.original_event.provider',
|
||||
'kibana.alert.original_event.risk_score',
|
||||
'kibana.alert.original_event.risk_score_norm',
|
||||
'kibana.alert.original_event.sequence',
|
||||
'kibana.alert.original_event.severity',
|
||||
'kibana.alert.original_event.start',
|
||||
'kibana.alert.original_event.timezone',
|
||||
'kibana.alert.original_event.type',
|
||||
'kibana.alert.original_time',
|
||||
'kibana.alert.reason',
|
||||
'kibana.alert.rule.created_by',
|
||||
'kibana.alert.rule.description',
|
||||
'kibana.alert.rule.enabled',
|
||||
'kibana.alert.rule.false_positives',
|
||||
'kibana.alert.rule.from',
|
||||
'kibana.alert.rule.uuid',
|
||||
'kibana.alert.rule.immutable',
|
||||
'kibana.alert.rule.interval',
|
||||
'kibana.alert.rule.max_signals',
|
||||
'kibana.alert.rule.name',
|
||||
'kibana.alert.rule.note',
|
||||
'kibana.alert.rule.references',
|
||||
'kibana.alert.risk_score',
|
||||
'kibana.alert.rule.rule_id',
|
||||
'kibana.alert.severity',
|
||||
'kibana.alert.rule.size',
|
||||
'kibana.alert.rule.tags',
|
||||
'kibana.alert.rule.threat',
|
||||
'kibana.alert.rule.threat.tactic.id',
|
||||
'kibana.alert.rule.threat.tactic.name',
|
||||
'kibana.alert.rule.threat.tactic.reference',
|
||||
'kibana.alert.rule.threat.technique.id',
|
||||
'kibana.alert.rule.threat.technique.name',
|
||||
'kibana.alert.rule.threat.technique.reference',
|
||||
'kibana.alert.rule.timeline_id',
|
||||
'kibana.alert.rule.timeline_title',
|
||||
'kibana.alert.rule.to',
|
||||
'kibana.alert.rule.type',
|
||||
'kibana.alert.rule.updated_by',
|
||||
'kibana.alert.rule.version',
|
||||
'kibana.alert.workflow_status',
|
||||
].includes(fieldName);
|
||||
|
||||
return isAllowlistedNonBrowserField || isAggregatable;
|
||||
};
|
||||
|
||||
const getAllBrowserFields = (browserFields: BrowserFields): Array<Partial<BrowserField>> =>
|
||||
Object.values(browserFields).reduce<Array<Partial<BrowserField>>>(
|
||||
(acc, namespace) => [
|
||||
|
@ -127,32 +194,9 @@ export const getColumnHeader = (
|
|||
): ColumnHeaderOptions => ({
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: fieldName,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_TABLE_COLUMN_MIN_WIDTH,
|
||||
...(defaultHeaders.find((c) => c.id === fieldName) ?? {}),
|
||||
});
|
||||
|
||||
export const getColumnWidthFromType = (type: string): number =>
|
||||
type !== 'date' ? DEFAULT_COLUMN_MIN_WIDTH : DEFAULT_DATE_COLUMN_MIN_WIDTH;
|
||||
|
||||
/**
|
||||
* Returns the width of the Actions column based on the number of buttons being
|
||||
* displayed
|
||||
*
|
||||
* NOTE: This function is necessary because `width` is a required property of
|
||||
* the `EuiDataGridControlColumn` interface, so it must be calculated before
|
||||
* content is rendered. (The width of a `EuiDataGridControlColumn` does not
|
||||
* automatically size itself to fit all the content.)
|
||||
*/
|
||||
export const getActionsColumnWidth = (actionButtonCount: number): number => {
|
||||
const contentWidth =
|
||||
actionButtonCount > 0
|
||||
? actionButtonCount * DEFAULT_ACTION_BUTTON_WIDTH
|
||||
: DEFAULT_ACTION_BUTTON_WIDTH;
|
||||
|
||||
// `EuiDataGridRowCell` applies additional `padding-left` and
|
||||
// `padding-right`, which must be added to the content width to prevent the
|
||||
// content from being partially hidden due to the space occupied by padding:
|
||||
const leftRightCellPadding = parseInt(euiThemeVars.euiDataGridCellPaddingM, 10) * 2; // parseInt ignores the trailing `px`, e.g. `6px`
|
||||
|
||||
return contentWidth + leftRightCellPadding;
|
||||
};
|
||||
type !== 'date' ? DEFAULT_TABLE_COLUMN_MIN_WIDTH : DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH;
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ERROR_TIMELINE_EVENTS = i18n.translate(
|
||||
'xpack.timelines.timelineEvents.errorSearchDescription',
|
||||
export const REMOVE_COLUMN = i18n.translate(
|
||||
'xpack.securitySolution.columnHeaders.flyout.pane.removeColumnButtonLabel',
|
||||
{
|
||||
defaultMessage: `An error has occurred on timeline events search`,
|
||||
defaultMessage: 'Remove column',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/** The default minimum width of a column (when a width for the column type is not specified) */
|
||||
export const DEFAULT_TABLE_COLUMN_MIN_WIDTH = 180; // px
|
||||
|
||||
/** The default minimum width of a column of type `date` */
|
||||
export const DEFAULT_TABLE_DATE_COLUMN_MIN_WIDTH = 190; // px
|
|
@ -5,11 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { omit } from 'lodash/fp';
|
||||
|
||||
import { ColumnHeaderOptions } from '../../../../common/types';
|
||||
import type { ColumnHeaderOptions } from '../../../../common/types';
|
||||
import {
|
||||
allowSorting,
|
||||
hasCellActions,
|
||||
mapSortDirectionToDirection,
|
||||
mapSortingColumns,
|
||||
|
@ -17,7 +14,7 @@ import {
|
|||
} from './helpers';
|
||||
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { mockDnsEvent } from '../../../mock';
|
||||
import { mockDnsEvent } from '../../mock';
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('mapSortDirectionToDirection', () => {
|
||||
|
@ -180,73 +177,6 @@ describe('helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('allowSorting', () => {
|
||||
const aggregatableField = {
|
||||
category: 'cloud',
|
||||
description:
|
||||
'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.',
|
||||
example: '666777888999',
|
||||
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
|
||||
name: 'cloud.account.id',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
aggregatable: true, // <-- allow sorting when this is true
|
||||
format: '',
|
||||
};
|
||||
|
||||
test('it returns true for an aggregatable field', () => {
|
||||
expect(
|
||||
allowSorting({
|
||||
browserField: aggregatableField,
|
||||
fieldName: aggregatableField.name,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('it returns true for a allow-listed non-BrowserField', () => {
|
||||
expect(
|
||||
allowSorting({
|
||||
browserField: undefined, // no BrowserField metadata for this field
|
||||
fieldName: 'kibana.alert.rule.name', // an allow-listed field name
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('it returns false for a NON-aggregatable field (aggregatable is false)', () => {
|
||||
const nonaggregatableField = {
|
||||
...aggregatableField,
|
||||
aggregatable: false, // <-- NON-aggregatable
|
||||
};
|
||||
|
||||
expect(
|
||||
allowSorting({
|
||||
browserField: nonaggregatableField,
|
||||
fieldName: nonaggregatableField.name,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('it returns false if the BrowserField is missing the aggregatable property', () => {
|
||||
const missingAggregatable = omit('aggregatable', aggregatableField);
|
||||
|
||||
expect(
|
||||
allowSorting({
|
||||
browserField: missingAggregatable,
|
||||
fieldName: missingAggregatable.name,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("it returns false for a non-allowlisted field we don't have `BrowserField` metadata for it", () => {
|
||||
expect(
|
||||
allowSorting({
|
||||
browserField: undefined, // <-- no metadata for this field
|
||||
fieldName: 'non-allowlisted',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addBuildingBlockStyle', () => {
|
||||
const THEME = { eui: euiThemeVars, darkMode: false };
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 { isEmpty } from 'lodash/fp';
|
||||
|
||||
import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
|
||||
import type { EuiTheme } from '@kbn/kibana-react-plugin/common';
|
||||
import type { SortColumnTable } from '../../../../common/types';
|
||||
import type { Ecs } from '../../../../common/ecs';
|
||||
import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy';
|
||||
import type { ColumnHeaderOptions, SortDirection } from '../../../../common/types/timeline';
|
||||
|
||||
/**
|
||||
* Creates mapping of eventID -> fieldData for given fieldsToKeep. Used to store additional field
|
||||
* data necessary for custom timeline actions in conjunction with selection state
|
||||
* @param data
|
||||
* @param eventIds
|
||||
* @param fieldsToKeep
|
||||
*/
|
||||
export const getEventIdToDataMapping = (
|
||||
timelineData: TimelineItem[],
|
||||
eventIds: string[],
|
||||
fieldsToKeep: string[],
|
||||
hasAlertsCrud: boolean
|
||||
): Record<string, TimelineNonEcsData[]> =>
|
||||
timelineData.reduce((acc, v) => {
|
||||
const fvm =
|
||||
hasAlertsCrud && eventIds.includes(v._id)
|
||||
? { [v._id]: v.data.filter((ti) => fieldsToKeep.includes(ti.field)) }
|
||||
: {};
|
||||
return {
|
||||
...acc,
|
||||
...fvm,
|
||||
};
|
||||
}, {});
|
||||
|
||||
export const isEventBuildingBlockType = (event: Ecs): boolean =>
|
||||
!isEmpty(event.kibana?.alert?.building_block_type);
|
||||
|
||||
/** Maps (Redux) `SortDirection` to the `direction` values used by `EuiDataGrid` */
|
||||
export const mapSortDirectionToDirection = (sortDirection: SortDirection): 'asc' | 'desc' => {
|
||||
switch (sortDirection) {
|
||||
case 'asc': // fall through
|
||||
case 'desc':
|
||||
return sortDirection;
|
||||
default:
|
||||
return 'desc';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps `EuiDataGrid` columns to their Redux representation by combining the
|
||||
* `columns` with metadata from `columnHeaders`
|
||||
*/
|
||||
export const mapSortingColumns = ({
|
||||
columns,
|
||||
columnHeaders,
|
||||
}: {
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
columns: Array<{
|
||||
id: string;
|
||||
direction: 'asc' | 'desc';
|
||||
}>;
|
||||
}): SortColumnTable[] =>
|
||||
columns.map(({ id, direction }) => {
|
||||
const columnHeader = columnHeaders.find((ch) => ch.id === id);
|
||||
const columnType = columnHeader?.type ?? '';
|
||||
const esTypes = columnHeader?.esTypes ?? [];
|
||||
|
||||
return {
|
||||
columnId: id,
|
||||
columnType,
|
||||
esTypes,
|
||||
sortDirection: direction,
|
||||
};
|
||||
});
|
||||
|
||||
export const addBuildingBlockStyle = (
|
||||
ecs: Ecs,
|
||||
theme: EuiTheme,
|
||||
setCellProps: EuiDataGridCellValueElementProps['setCellProps'],
|
||||
defaultStyles?: React.CSSProperties
|
||||
) => {
|
||||
const currentStyles = defaultStyles ?? {};
|
||||
if (isEventBuildingBlockType(ecs)) {
|
||||
setCellProps({
|
||||
style: {
|
||||
...currentStyles,
|
||||
backgroundColor: `${theme.eui.euiColorHighlight}`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// reset cell style
|
||||
setCellProps({
|
||||
style: {
|
||||
...currentStyles,
|
||||
backgroundColor: 'inherit',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** Returns true when the specified column has cell actions */
|
||||
export const hasCellActions = ({
|
||||
columnId,
|
||||
disabledCellActions,
|
||||
}: {
|
||||
columnId: string;
|
||||
disabledCellActions: string[];
|
||||
}) => !disabledCellActions.includes(columnId);
|
|
@ -8,25 +8,17 @@
|
|||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { BodyComponent, StatefulBodyProps } from '.';
|
||||
import { Sort } from './sort';
|
||||
import type { DataTableProps } from '.';
|
||||
import { DataTableComponent } from '.';
|
||||
import { REMOVE_COLUMN } from './column_headers/translations';
|
||||
import { Direction } from '../../../../common/search_strategy';
|
||||
import { useMountAppended } from '../../utils/use_mount_appended';
|
||||
import { defaultHeaders, mockBrowserFields, mockTimelineData, TestProviders } from '../../../mock';
|
||||
import { TestCellRenderer } from '../../../mock/cell_renderer';
|
||||
import { mockGlobalState } from '../../../mock/global_state';
|
||||
import { EuiDataGridColumn } from '@elastic/eui';
|
||||
import { defaultColumnHeaderType } from '../../../store/t_grid/defaults';
|
||||
|
||||
const mockSort: Sort[] = [
|
||||
{
|
||||
columnId: '@timestamp',
|
||||
columnType: 'date',
|
||||
esTypes: ['date'],
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
];
|
||||
import type { EuiDataGridColumn } from '@elastic/eui';
|
||||
import { defaultHeaders, mockGlobalState, mockTimelineData, TestProviders } from '../../mock';
|
||||
import { defaultColumnHeaderType } from '../../store/data_table/defaults';
|
||||
import { mockBrowserFields } from '../../containers/source/mock';
|
||||
import { getMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns';
|
||||
import type { CellValueElementProps } from '../../../../common/types';
|
||||
import { TableId } from '../../../../common/types';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
|
@ -52,9 +44,9 @@ jest.mock('@kbn/kibana-react-plugin/public', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../hooks/use_selector', () => ({
|
||||
useShallowEqualSelector: () => mockGlobalState.tableById['table-test'],
|
||||
useDeepEqualSelector: () => mockGlobalState.tableById['table-test'],
|
||||
jest.mock('../../hooks/use_selector', () => ({
|
||||
useShallowEqualSelector: () => mockGlobalState.dataTable.tableById['table-test'],
|
||||
useDeepEqualSelector: () => mockGlobalState.dataTable.tableById['table-test'],
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
|
@ -74,38 +66,35 @@ window.matchMedia = jest.fn().mockImplementation((query) => {
|
|||
};
|
||||
});
|
||||
|
||||
describe('Body', () => {
|
||||
export const TestCellRenderer: React.FC<CellValueElementProps> = ({ columnId, data }) => (
|
||||
<>
|
||||
{getMappedNonEcsValue({
|
||||
data,
|
||||
fieldName: columnId,
|
||||
})?.reduce((x) => x[0]) ?? ''}
|
||||
</>
|
||||
);
|
||||
|
||||
describe('DataTable', () => {
|
||||
const mount = useMountAppended();
|
||||
const props: StatefulBodyProps = {
|
||||
activePage: 0,
|
||||
const props: DataTableProps = {
|
||||
browserFields: mockBrowserFields,
|
||||
clearSelected: jest.fn() as unknown as StatefulBodyProps['clearSelected'],
|
||||
columnHeaders: defaultHeaders,
|
||||
data: mockTimelineData,
|
||||
defaultCellActions: [],
|
||||
disabledCellActions: ['signal.rule.risk_score', 'signal.reason'],
|
||||
id: 'timeline-test',
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
itemsPerPageOptions: [],
|
||||
loadingEventIds: [],
|
||||
id: TableId.test,
|
||||
loadPage: jest.fn(),
|
||||
pageSize: 25,
|
||||
renderCellValue: TestCellRenderer,
|
||||
rowRenderers: [],
|
||||
selectedEventIds: {},
|
||||
setSelected: jest.fn() as unknown as StatefulBodyProps['setSelected'],
|
||||
sort: mockSort,
|
||||
showCheckboxes: false,
|
||||
tabType: 'query',
|
||||
tableView: 'gridView',
|
||||
totalItems: 1,
|
||||
leadingControlColumns: [],
|
||||
trailingControlColumns: [],
|
||||
filterStatus: 'open',
|
||||
filterQuery: '',
|
||||
refetch: jest.fn(),
|
||||
indexNames: [''],
|
||||
unitCountText: '10 events',
|
||||
pagination: {
|
||||
pageSize: 25,
|
||||
pageIndex: 0,
|
||||
onChangeItemsPerPage: jest.fn(),
|
||||
onChangePage: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -116,7 +105,7 @@ describe('Body', () => {
|
|||
test('it renders the body data grid', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...props} />
|
||||
<DataTableComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="body-data-grid"]').first().exists()).toEqual(true);
|
||||
|
@ -125,7 +114,7 @@ describe('Body', () => {
|
|||
test('it renders the column headers', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...props} />
|
||||
<DataTableComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -135,7 +124,7 @@ describe('Body', () => {
|
|||
test('it renders the scroll container', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...props} />
|
||||
<DataTableComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -145,7 +134,7 @@ describe('Body', () => {
|
|||
test('it renders events', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...props} />
|
||||
<DataTableComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -161,7 +150,7 @@ describe('Body', () => {
|
|||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...testProps} />
|
||||
<DataTableComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.update();
|
||||
|
@ -184,7 +173,7 @@ describe('Body', () => {
|
|||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...testProps} />
|
||||
<DataTableComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.update();
|
||||
|
@ -216,7 +205,7 @@ describe('Body', () => {
|
|||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...testProps} />
|
||||
<DataTableComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.update();
|
||||
|
@ -248,7 +237,7 @@ describe('Body', () => {
|
|||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...testProps} />
|
||||
<DataTableComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.update();
|
||||
|
@ -281,7 +270,7 @@ describe('Body', () => {
|
|||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...testProps} />
|
||||
<DataTableComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.update();
|
||||
|
@ -298,7 +287,7 @@ describe('Body', () => {
|
|||
test('it does NOT render switches for hiding columns in the `EuiDataGrid` `Columns` popover', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<BodyComponent {...props} />
|
||||
<DataTableComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -314,7 +303,7 @@ describe('Body', () => {
|
|||
test('it dispatches the `REMOVE_COLUMN` action when a user clicks `Remove column` in the column header popover', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<BodyComponent {...props} />
|
||||
<DataTableComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -325,15 +314,15 @@ describe('Body', () => {
|
|||
fireEvent.click(await screen.getByText(REMOVE_COLUMN));
|
||||
|
||||
expect(mockDispatch).toBeCalledWith({
|
||||
payload: { columnId: '@timestamp', id: 'timeline-test' },
|
||||
type: 'x-pack/timelines/t-grid/REMOVE_COLUMN',
|
||||
payload: { columnId: '@timestamp', id: 'table-test' },
|
||||
type: 'x-pack/security_solution/data-table/REMOVE_COLUMN',
|
||||
});
|
||||
});
|
||||
|
||||
test('it dispatches the `UPDATE_COLUMN_WIDTH` action when a user resizes a column', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<BodyComponent {...props} />
|
||||
<DataTableComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -343,8 +332,8 @@ describe('Body', () => {
|
|||
fireEvent.mouseUp(screen.getAllByTestId('dataGridColumnResizer')[0]);
|
||||
|
||||
expect(mockDispatch).toBeCalledWith({
|
||||
payload: { columnId: '@timestamp', id: 'timeline-test', width: NaN },
|
||||
type: 'x-pack/timelines/t-grid/UPDATE_COLUMN_WIDTH',
|
||||
payload: { columnId: '@timestamp', id: 'table-test', width: NaN },
|
||||
type: 'x-pack/security_solution/data-table/UPDATE_COLUMN_WIDTH',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,425 @@
|
|||
/*
|
||||
* 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 type {
|
||||
EuiDataGridRefProps,
|
||||
EuiDataGridColumn,
|
||||
EuiDataGridCellValueElementProps,
|
||||
EuiDataGridStyle,
|
||||
EuiDataGridToolBarVisibilityOptions,
|
||||
EuiDataGridControlColumn,
|
||||
EuiDataGridPaginationProps,
|
||||
} from '@elastic/eui';
|
||||
import { EuiDataGrid, EuiProgress } from '@elastic/eui';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import React, { useCallback, useEffect, useMemo, useContext, useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import styled, { ThemeContext } from 'styled-components';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { EuiTheme } from '@kbn/kibana-react-plugin/common';
|
||||
import type { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataTableCellAction } from '../../../../common/types';
|
||||
import type {
|
||||
CellValueElementProps,
|
||||
ColumnHeaderOptions,
|
||||
RowRenderer,
|
||||
} from '../../../../common/types/timeline';
|
||||
|
||||
import type { TimelineItem } from '../../../../common/search_strategy/timeline';
|
||||
|
||||
import { getColumnHeader, getColumnHeaders } from './column_headers/helpers';
|
||||
import {
|
||||
addBuildingBlockStyle,
|
||||
hasCellActions,
|
||||
mapSortDirectionToDirection,
|
||||
mapSortingColumns,
|
||||
} from './helpers';
|
||||
|
||||
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
|
||||
import { REMOVE_COLUMN } from './column_headers/translations';
|
||||
import { dataTableActions, dataTableSelectors } from '../../store/data_table';
|
||||
import type { BulkActionsProp } from '../toolbar/bulk_actions/types';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { getPageRowIndex } from './pagination';
|
||||
import { UnitCount } from '../toolbar/unit';
|
||||
import { useShallowEqualSelector } from '../../hooks/use_selector';
|
||||
import { tableDefaults } from '../../store/data_table/defaults';
|
||||
|
||||
const DATA_TABLE_ARIA_LABEL = i18n.translate('xpack.securitySolution.dataTable.ariaLabel', {
|
||||
defaultMessage: 'Alerts',
|
||||
});
|
||||
|
||||
export interface DataTableProps {
|
||||
additionalControls?: React.ReactNode;
|
||||
browserFields: BrowserFields;
|
||||
bulkActions?: BulkActionsProp;
|
||||
data: TimelineItem[];
|
||||
defaultCellActions?: DataTableCellAction[];
|
||||
disabledCellActions: string[];
|
||||
fieldBrowserOptions?: FieldBrowserOptions;
|
||||
filters?: Filter[];
|
||||
id: string;
|
||||
leadingControlColumns: EuiDataGridControlColumn[];
|
||||
loadPage: (newActivePage: number) => void;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
hasCrudPermissions?: boolean;
|
||||
unitCountText: string;
|
||||
pagination: EuiDataGridPaginationProps;
|
||||
totalItems: number;
|
||||
}
|
||||
|
||||
const ES_LIMIT_COUNT = 9999;
|
||||
|
||||
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' : ''}`};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DataTableComponent = React.memo<DataTableProps>(
|
||||
({
|
||||
additionalControls,
|
||||
browserFields,
|
||||
bulkActions = true,
|
||||
data,
|
||||
defaultCellActions,
|
||||
disabledCellActions,
|
||||
fieldBrowserOptions,
|
||||
filters,
|
||||
hasCrudPermissions,
|
||||
id,
|
||||
leadingControlColumns,
|
||||
loadPage,
|
||||
renderCellValue,
|
||||
rowRenderers,
|
||||
pagination,
|
||||
unitCountText,
|
||||
totalItems,
|
||||
}) => {
|
||||
const {
|
||||
triggersActionsUi: { getFieldBrowser },
|
||||
} = useKibana().services;
|
||||
const memoizedColumnHeaders: (
|
||||
headers: ColumnHeaderOptions[],
|
||||
browserFields: BrowserFields
|
||||
) => ColumnHeaderOptions[] = memoizeOne(getColumnHeaders);
|
||||
|
||||
const getDataTable = dataTableSelectors.getTableByIdSelector();
|
||||
const dataTable = useShallowEqualSelector((state) => getDataTable(state, id) ?? tableDefaults);
|
||||
const { columns, selectedEventIds, showCheckboxes, sort, isLoading, defaultColumns } =
|
||||
dataTable;
|
||||
const columnHeaders = memoizedColumnHeaders(columns, browserFields);
|
||||
|
||||
const dataGridRef = useRef<EuiDataGridRefProps>(null);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const selectedCount = useMemo(() => Object.keys(selectedEventIds).length, [selectedEventIds]);
|
||||
|
||||
const theme: EuiTheme = useContext(ThemeContext);
|
||||
|
||||
const showBulkActions = useMemo(() => {
|
||||
if (!hasCrudPermissions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedCount === 0 || !showCheckboxes) {
|
||||
return false;
|
||||
}
|
||||
if (typeof bulkActions === 'boolean') {
|
||||
return bulkActions;
|
||||
}
|
||||
return (bulkActions?.customBulkActions?.length || bulkActions?.alertStatusActions) ?? true;
|
||||
}, [hasCrudPermissions, selectedCount, showCheckboxes, bulkActions]);
|
||||
|
||||
const onResetColumns = useCallback(() => {
|
||||
dispatch(dataTableActions.updateColumns({ id, columns: defaultColumns }));
|
||||
}, [defaultColumns, dispatch, id]);
|
||||
|
||||
const onToggleColumn = useCallback(
|
||||
(columnId: string) => {
|
||||
if (columnHeaders.some(({ id: columnHeaderId }) => columnId === columnHeaderId)) {
|
||||
dispatch(
|
||||
dataTableActions.removeColumn({
|
||||
columnId,
|
||||
id,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
dataTableActions.upsertColumn({
|
||||
column: getColumnHeader(columnId, defaultColumns),
|
||||
id,
|
||||
index: 1,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[columnHeaders, dispatch, id, defaultColumns]
|
||||
);
|
||||
|
||||
const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo(
|
||||
() => ({
|
||||
additionalControls: {
|
||||
left: {
|
||||
append: (
|
||||
<>
|
||||
{isLoading && <EuiProgress size="xs" position="absolute" color="accent" />}
|
||||
<UnitCount data-test-subj="server-side-event-count">{unitCountText}</UnitCount>
|
||||
{additionalControls ?? null}
|
||||
{getFieldBrowser({
|
||||
browserFields,
|
||||
options: fieldBrowserOptions,
|
||||
columnIds: columnHeaders.map(({ id: columnId }) => columnId),
|
||||
onResetColumns,
|
||||
onToggleColumn,
|
||||
})}
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
...(showBulkActions
|
||||
? {
|
||||
showColumnSelector: false,
|
||||
showSortSelector: false,
|
||||
showFullScreenSelector: false,
|
||||
}
|
||||
: {
|
||||
showColumnSelector: { allowHide: false, allowReorder: true },
|
||||
showSortSelector: true,
|
||||
showFullScreenSelector: true,
|
||||
}),
|
||||
showDisplaySelector: false,
|
||||
}),
|
||||
[
|
||||
isLoading,
|
||||
unitCountText,
|
||||
additionalControls,
|
||||
getFieldBrowser,
|
||||
browserFields,
|
||||
fieldBrowserOptions,
|
||||
columnHeaders,
|
||||
onResetColumns,
|
||||
onToggleColumn,
|
||||
showBulkActions,
|
||||
]
|
||||
);
|
||||
|
||||
const sortingColumns: Array<{
|
||||
id: string;
|
||||
direction: 'asc' | 'desc';
|
||||
}> = useMemo(
|
||||
() =>
|
||||
sort.map((x) => ({
|
||||
id: x.columnId,
|
||||
direction: mapSortDirectionToDirection(x.sortDirection),
|
||||
})),
|
||||
[sort]
|
||||
);
|
||||
|
||||
const onSort = useCallback(
|
||||
(
|
||||
nextSortingColumns: Array<{
|
||||
id: string;
|
||||
direction: 'asc' | 'desc';
|
||||
}>
|
||||
) => {
|
||||
dispatch(
|
||||
dataTableActions.updateSort({
|
||||
id,
|
||||
sort: mapSortingColumns({ columns: nextSortingColumns, columnHeaders }),
|
||||
})
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
// schedule the query to be re-executed from page 0, (but only after the
|
||||
// store has been updated with the new sort):
|
||||
if (loadPage != null) {
|
||||
loadPage(0);
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
[columnHeaders, dispatch, id, loadPage]
|
||||
);
|
||||
|
||||
const visibleColumns = useMemo(() => columnHeaders.map(({ id: cid }) => cid), [columnHeaders]); // the full set of columns
|
||||
|
||||
const onColumnResize = useCallback(
|
||||
({ columnId, width }: { columnId: string; width: number }) => {
|
||||
dispatch(
|
||||
dataTableActions.updateColumnWidth({
|
||||
columnId,
|
||||
id,
|
||||
width,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, id]
|
||||
);
|
||||
|
||||
const onSetVisibleColumns = useCallback(
|
||||
(newVisibleColumns: string[]) => {
|
||||
dispatch(
|
||||
dataTableActions.updateColumnOrder({
|
||||
columnIds: newVisibleColumns,
|
||||
id,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, id]
|
||||
);
|
||||
|
||||
const columnsWithCellActions: EuiDataGridColumn[] = useMemo(
|
||||
() =>
|
||||
columnHeaders.map((header) => {
|
||||
const buildAction = (dataTableCellAction: DataTableCellAction) =>
|
||||
dataTableCellAction({
|
||||
browserFields,
|
||||
data: data.map((row) => row.data),
|
||||
ecsData: data.map((row) => row.ecs),
|
||||
header: columnHeaders.find((h) => h.id === header.id),
|
||||
pageSize: pagination.pageSize,
|
||||
scopeId: id,
|
||||
closeCellPopover: dataGridRef.current?.closeCellPopover,
|
||||
});
|
||||
return {
|
||||
...header,
|
||||
actions: {
|
||||
...header.actions,
|
||||
additional: [
|
||||
{
|
||||
iconType: 'cross',
|
||||
label: REMOVE_COLUMN,
|
||||
onClick: () => {
|
||||
dispatch(dataTableActions.removeColumn({ id, columnId: header.id }));
|
||||
},
|
||||
size: 'xs',
|
||||
},
|
||||
],
|
||||
},
|
||||
...(hasCellActions({
|
||||
columnId: header.id,
|
||||
disabledCellActions,
|
||||
})
|
||||
? {
|
||||
cellActions:
|
||||
header.dataTableCellActions?.map(buildAction) ??
|
||||
defaultCellActions?.map(buildAction),
|
||||
visibleCellActions: 3,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}),
|
||||
[
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
data,
|
||||
defaultCellActions,
|
||||
disabledCellActions,
|
||||
dispatch,
|
||||
id,
|
||||
pagination.pageSize,
|
||||
]
|
||||
);
|
||||
|
||||
const renderTGridCellValue = useMemo(() => {
|
||||
const Cell: React.FC<EuiDataGridCellValueElementProps> = ({
|
||||
columnId,
|
||||
rowIndex,
|
||||
colIndex,
|
||||
setCellProps,
|
||||
isDetails,
|
||||
}): React.ReactElement | null => {
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pagination.pageSize);
|
||||
const rowData = pageRowIndex < data.length ? data[pageRowIndex].data : null;
|
||||
const header = columnHeaders.find((h) => h.id === columnId);
|
||||
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 } });
|
||||
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 || ecs === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return renderCellValue({
|
||||
browserFields,
|
||||
columnId: header.id,
|
||||
data: rowData,
|
||||
ecsData: ecs,
|
||||
eventId,
|
||||
globalFilters: filters,
|
||||
header,
|
||||
isDetails,
|
||||
isDraggable: false,
|
||||
isExpandable: true,
|
||||
isExpanded: false,
|
||||
linkValues: getOr([], header.linkField ?? '', ecs),
|
||||
rowIndex,
|
||||
colIndex,
|
||||
rowRenderers,
|
||||
setCellProps,
|
||||
scopeId: id,
|
||||
truncate: isDetails ? false : true,
|
||||
}) as React.ReactElement;
|
||||
};
|
||||
return Cell;
|
||||
}, [
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
data,
|
||||
filters,
|
||||
id,
|
||||
pagination.pageSize,
|
||||
renderCellValue,
|
||||
rowRenderers,
|
||||
theme,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiDataGridContainer hideLastPage={totalItems > ES_LIMIT_COUNT}>
|
||||
<EuiDataGrid
|
||||
id={'body-data-grid'}
|
||||
data-test-subj="body-data-grid"
|
||||
aria-label={DATA_TABLE_ARIA_LABEL}
|
||||
columns={columnsWithCellActions}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns: onSetVisibleColumns }}
|
||||
gridStyle={gridStyle}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
toolbarVisibility={toolbarVisibility}
|
||||
rowCount={totalItems}
|
||||
renderCellValue={renderTGridCellValue}
|
||||
sorting={{ columns: sortingColumns, onSort }}
|
||||
onColumnResize={onColumnResize}
|
||||
pagination={pagination}
|
||||
ref={dataGridRef}
|
||||
/>
|
||||
</EuiDataGridContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DataTableComponent.displayName = 'DataTableComponent';
|
|
@ -5,18 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import {
|
||||
ColumnHeaderOptions,
|
||||
ColumnId,
|
||||
RowRendererId,
|
||||
Sort,
|
||||
DataExpandedDetail,
|
||||
TimelineTypeLiteral,
|
||||
} from '.';
|
||||
|
||||
import { Direction } from '../../search_strategy';
|
||||
import { DataProvider } from './data_provider';
|
||||
import type { ColumnHeaderOptions, ColumnId } from '../../../../common/types';
|
||||
import type { SortDirectionTable as SortDirection } from '../../../../common/types/data_table';
|
||||
|
||||
export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql';
|
||||
|
||||
|
@ -30,38 +20,6 @@ export interface SerializedFilterQuery {
|
|||
serializedQuery: string;
|
||||
}
|
||||
|
||||
export type SortDirection = 'none' | 'asc' | 'desc' | Direction;
|
||||
export interface SortColumnTable {
|
||||
columnId: string;
|
||||
columnType: string;
|
||||
esTypes?: string[];
|
||||
sortDirection: SortDirection;
|
||||
}
|
||||
|
||||
export interface TimelinePersistInput {
|
||||
id: string;
|
||||
dataProviders?: DataProvider[];
|
||||
dateRange?: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
excludedRowRendererIds?: RowRendererId[];
|
||||
expandedDetail?: DataExpandedDetail;
|
||||
filters?: Filter[];
|
||||
columns: ColumnHeaderOptions[];
|
||||
itemsPerPage?: number;
|
||||
indexNames: string[];
|
||||
kqlQuery?: {
|
||||
filterQuery: SerializedFilterQuery | null;
|
||||
};
|
||||
show?: boolean;
|
||||
sort?: Sort[];
|
||||
showCheckboxes?: boolean;
|
||||
timelineType?: TimelineTypeLiteral;
|
||||
templateTimelineId?: string | null;
|
||||
templateTimelineVersion?: number | null;
|
||||
}
|
||||
|
||||
/** Invoked when a column is sorted */
|
||||
export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void;
|
||||
|
||||
|
@ -90,9 +48,3 @@ export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void;
|
|||
|
||||
/** Invoked when columns are updated */
|
||||
export type OnUpdateColumns = (columns: ColumnHeaderOptions[]) => void;
|
||||
|
||||
/** Invoked when a user pins an event */
|
||||
export type OnPinEvent = (eventId: string) => void;
|
||||
|
||||
/** Invoked when a user unpins an event */
|
||||
export type OnUnPinEvent = (eventId: string) => void;
|
|
@ -14,10 +14,6 @@ import type { Dispatch } from 'redux';
|
|||
import deepEqual from 'fast-deep-equal';
|
||||
import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
|
||||
|
||||
import {
|
||||
addFieldToTimelineColumns,
|
||||
getTimelineIdFromColumnDroppableId,
|
||||
} from '@kbn/timelines-plugin/public';
|
||||
import type { BeforeCapture } from './drag_drop_context';
|
||||
import type { BrowserFields } from '../../containers/source';
|
||||
import { dragAndDropSelectors } from '../../store';
|
||||
|
@ -38,6 +34,8 @@ import {
|
|||
providerWasDroppedOnTimeline,
|
||||
draggableIsField,
|
||||
userIsReArrangingProviders,
|
||||
getIdFromColumnDroppableId,
|
||||
addFieldToColumns,
|
||||
} from './helpers';
|
||||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
|
@ -87,12 +85,12 @@ const onDragEndHandler = ({
|
|||
timelineId: TimelineId.active,
|
||||
});
|
||||
} else if (fieldWasDroppedOnTimelineColumns(result)) {
|
||||
addFieldToTimelineColumns({
|
||||
addFieldToColumns({
|
||||
browserFields,
|
||||
defaultsHeader: defaultAlertsHeaders,
|
||||
dispatch,
|
||||
result,
|
||||
timelineId: getTimelineIdFromColumnDroppableId(result.destination?.droppableId ?? ''),
|
||||
scopeId: getIdFromColumnDroppableId(result.destination?.droppableId ?? ''),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { draggableKeyDownHandler } from './helpers';
|
||||
|
||||
jest.mock('../../../lib/kibana');
|
||||
describe('draggableKeyDownHandler', () => {
|
||||
test('it calles the proper function cancelDragActions when Escape key was pressed', () => {
|
||||
const mockElement = document.createElement('div');
|
||||
const keyboardEvent = new KeyboardEvent('keydown', {
|
||||
ctrlKey: false,
|
||||
key: 'Escape',
|
||||
metaKey: false,
|
||||
}) as unknown as React.KeyboardEvent;
|
||||
|
||||
const cancelDragActions = jest.fn();
|
||||
draggableKeyDownHandler({
|
||||
closePopover: jest.fn(),
|
||||
openPopover: jest.fn(),
|
||||
beginDrag: jest.fn(),
|
||||
cancelDragActions,
|
||||
draggableElement: mockElement,
|
||||
dragActions: null,
|
||||
dragToLocation: jest.fn(),
|
||||
endDrag: jest.fn(),
|
||||
keyboardEvent,
|
||||
setDragActions: jest.fn(),
|
||||
});
|
||||
expect(cancelDragActions).toBeCalled();
|
||||
});
|
||||
});
|
|
@ -5,16 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { DropResult, FluidDragActions, Position } from 'react-beautiful-dnd';
|
||||
import { KEYBOARD_DRAG_OFFSET, getFieldIdFromDraggable } from '@kbn/securitysolution-t-grid';
|
||||
import { Dispatch } from 'redux';
|
||||
import { isString, keyBy } from 'lodash/fp';
|
||||
import type { FluidDragActions, Position } from 'react-beautiful-dnd';
|
||||
import { KEYBOARD_DRAG_OFFSET } from '@kbn/securitysolution-t-grid';
|
||||
|
||||
import { stopPropagationAndPreventDefault } from '../../../common/utils/accessibility';
|
||||
import type { BrowserField, BrowserFields } from '../../../common/search_strategy';
|
||||
import type { ColumnHeaderOptions } from '../../../common/types';
|
||||
import { TableId, tGridActions } from '../../store/t_grid';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH } from '../t_grid/body/constants';
|
||||
import { stopPropagationAndPreventDefault } from '@kbn/timelines-plugin/public';
|
||||
|
||||
/**
|
||||
* Temporarily disables tab focus on child links of the draggable to work
|
||||
|
@ -130,82 +124,3 @@ export const draggableKeyDownHandler = ({
|
|||
break;
|
||||
}
|
||||
};
|
||||
const getAllBrowserFields = (browserFields: BrowserFields): Array<Partial<BrowserField>> =>
|
||||
Object.values(browserFields).reduce<Array<Partial<BrowserField>>>(
|
||||
(acc, namespace) => [
|
||||
...acc,
|
||||
...Object.values(namespace.fields != null ? namespace.fields : {}),
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const getAllFieldsByName = (
|
||||
browserFields: BrowserFields
|
||||
): { [fieldName: string]: Partial<BrowserField> } =>
|
||||
keyBy('name', getAllBrowserFields(browserFields));
|
||||
|
||||
const linkFields: Record<string, string> = {
|
||||
'kibana.alert.rule.name': 'kibana.alert.rule.uuid',
|
||||
'event.module': 'rule.reference',
|
||||
};
|
||||
|
||||
interface AddFieldToTimelineColumnsParams {
|
||||
defaultsHeader: ColumnHeaderOptions[];
|
||||
browserFields: BrowserFields;
|
||||
dispatch: Dispatch;
|
||||
result: DropResult;
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
export const addFieldToTimelineColumns = ({
|
||||
browserFields,
|
||||
dispatch,
|
||||
result,
|
||||
timelineId,
|
||||
defaultsHeader,
|
||||
}: AddFieldToTimelineColumnsParams): void => {
|
||||
const fieldId = getFieldIdFromDraggable(result);
|
||||
const allColumns = getAllFieldsByName(browserFields);
|
||||
const column = allColumns[fieldId];
|
||||
const initColumnHeader =
|
||||
timelineId === TableId.alertsOnAlertsPage || timelineId === TableId.alertsOnRuleDetailsPage
|
||||
? defaultsHeader.find((c) => c.id === fieldId) ?? {}
|
||||
: {};
|
||||
|
||||
if (column != null) {
|
||||
dispatch(
|
||||
tGridActions.upsertColumn({
|
||||
column: {
|
||||
category: column.category,
|
||||
columnHeaderType: 'not-filtered',
|
||||
description: isString(column.description) ? column.description : undefined,
|
||||
example: isString(column.example) ? column.example : undefined,
|
||||
id: fieldId,
|
||||
linkField: linkFields[fieldId] ?? undefined,
|
||||
type: column.type,
|
||||
aggregatable: column.aggregatable,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
...initColumnHeader,
|
||||
},
|
||||
id: timelineId,
|
||||
index: result.destination != null ? result.destination.index : 0,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// create a column definition, because it doesn't exist in the browserFields:
|
||||
dispatch(
|
||||
tGridActions.upsertColumn({
|
||||
column: {
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: fieldId,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
id: timelineId,
|
||||
index: result.destination != null ? result.destination.index : 0,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getTimelineIdFromColumnDroppableId = (droppableId: string) =>
|
||||
droppableId.slice(droppableId.lastIndexOf('.') + 1);
|
|
@ -5,12 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import type { FluidDragActions } from 'react-beautiful-dnd';
|
||||
|
||||
import { useAddToTimeline } from '../../../hooks/use_add_to_timeline';
|
||||
|
||||
import { draggableKeyDownHandler } from '../helpers';
|
||||
import { useKibana } from '../../../lib/kibana';
|
||||
import { draggableKeyDownHandler } from './helpers';
|
||||
|
||||
export interface UseDraggableKeyboardWrapperProps {
|
||||
closePopover?: () => void;
|
||||
|
@ -32,6 +31,8 @@ export const useDraggableKeyboardWrapper = ({
|
|||
keyboardHandlerRef,
|
||||
openPopover,
|
||||
}: UseDraggableKeyboardWrapperProps): UseDraggableKeyboardWrapper => {
|
||||
const { timelines } = useKibana().services;
|
||||
const useAddToTimeline = timelines.getUseAddToTimeline();
|
||||
const { beginDrag, cancelDrag, dragToLocation, endDrag, hasDraggableLock } = useAddToTimeline({
|
||||
draggableId,
|
||||
fieldName,
|
|
@ -30,8 +30,8 @@ import { getDraggableId, getDroppableId } from './helpers';
|
|||
import { ProviderContainer } from './provider_container';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { useHoverActions } from '../hover_actions/use_hover_actions';
|
||||
import { useDraggableKeyboardWrapper } from './draggable_keyboard_wrapper_hook';
|
||||
|
||||
// As right now, we do not know what we want there, we will keep it as a placeholder
|
||||
export const DragEffects = styled.div``;
|
||||
|
@ -144,7 +144,6 @@ const DraggableOnWrapperComponent: React.FC<Props> = ({
|
|||
const [providerRegistered, setProviderRegistered] = useState(false);
|
||||
const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`);
|
||||
const dispatch = useDispatch();
|
||||
const { timelines } = useKibana().services;
|
||||
const {
|
||||
closePopOverTrigger,
|
||||
handleClosePopOverTrigger,
|
||||
|
@ -248,7 +247,7 @@ const DraggableOnWrapperComponent: React.FC<Props> = ({
|
|||
[dataProvider, registerProvider, render, setContainerRef, truncate]
|
||||
);
|
||||
|
||||
const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({
|
||||
const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
|
||||
closePopover: handleClosePopOverTrigger,
|
||||
draggableId: getDraggableId(dataProvider.id),
|
||||
fieldName: dataProvider.queryMatch.field,
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { omit } from 'lodash/fp';
|
||||
import type { DropResult } from 'react-beautiful-dnd';
|
||||
import { getTimelineIdFromColumnDroppableId } from '@kbn/timelines-plugin/public';
|
||||
|
||||
import type { IdToDataProvider } from '../../store/drag_and_drop/model';
|
||||
|
||||
|
@ -998,16 +997,4 @@ describe('helpers', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimelineIdFromColumnDroppableId', () => {
|
||||
test('it returns the expected timelineId from a column droppableId', () => {
|
||||
expect(getTimelineIdFromColumnDroppableId(DROPPABLE_ID_TIMELINE_COLUMNS)).toEqual(
|
||||
'timeline-1'
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns an empty string when the droppableId is an empty string', () => {
|
||||
expect(getTimelineIdFromColumnDroppableId('')).toEqual('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,11 +4,17 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { isString, keyBy } from 'lodash/fp';
|
||||
import type { DropResult } from 'react-beautiful-dnd';
|
||||
import type { Dispatch } from 'redux';
|
||||
import type { ActionCreator } from 'typescript-fsa';
|
||||
import { getProviderIdFromDraggable } from '@kbn/securitysolution-t-grid';
|
||||
|
||||
import { getFieldIdFromDraggable, getProviderIdFromDraggable } from '@kbn/securitysolution-t-grid';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
|
||||
import { getScopedActions } from '../../../helpers';
|
||||
import type { ColumnHeaderOptions } from '../../../../common/types';
|
||||
import { TableId } from '../../../../common/types';
|
||||
import type { BrowserField, BrowserFields } from '../../../../common/search_strategy';
|
||||
import { dragAndDropActions } from '../../store/actions';
|
||||
import type { IdToDataProvider } from '../../store/drag_and_drop/model';
|
||||
import { addContentToTimeline } from '../../../timelines/components/timeline/data_providers/helpers';
|
||||
|
@ -177,3 +183,84 @@ export const allowTopN = ({
|
|||
|
||||
return isAllowlistedNonBrowserField || (isAggregatable && isAllowedType);
|
||||
};
|
||||
|
||||
const getAllBrowserFields = (browserFields: BrowserFields): Array<Partial<BrowserField>> =>
|
||||
Object.values(browserFields).reduce<Array<Partial<BrowserField>>>(
|
||||
(acc, namespace) => [
|
||||
...acc,
|
||||
...Object.values(namespace.fields != null ? namespace.fields : {}),
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const getAllFieldsByName = (
|
||||
browserFields: BrowserFields
|
||||
): { [fieldName: string]: Partial<BrowserField> } =>
|
||||
keyBy('name', getAllBrowserFields(browserFields));
|
||||
|
||||
const linkFields: Record<string, string> = {
|
||||
'kibana.alert.rule.name': 'kibana.alert.rule.uuid',
|
||||
'event.module': 'rule.reference',
|
||||
};
|
||||
|
||||
interface AddFieldToTimelineColumnsParams {
|
||||
defaultsHeader: ColumnHeaderOptions[];
|
||||
browserFields: BrowserFields;
|
||||
dispatch: Dispatch;
|
||||
result: DropResult;
|
||||
scopeId: string;
|
||||
}
|
||||
|
||||
export const addFieldToColumns = ({
|
||||
browserFields,
|
||||
dispatch,
|
||||
result,
|
||||
scopeId,
|
||||
defaultsHeader,
|
||||
}: AddFieldToTimelineColumnsParams): void => {
|
||||
const fieldId = getFieldIdFromDraggable(result);
|
||||
const allColumns = getAllFieldsByName(browserFields);
|
||||
const column = allColumns[fieldId];
|
||||
const initColumnHeader =
|
||||
scopeId === TableId.alertsOnAlertsPage || scopeId === TableId.alertsOnRuleDetailsPage
|
||||
? defaultsHeader.find((c) => c.id === fieldId) ?? {}
|
||||
: {};
|
||||
|
||||
const scopedActions = getScopedActions(scopeId);
|
||||
if (column != null && scopedActions) {
|
||||
dispatch(
|
||||
scopedActions.upsertColumn({
|
||||
column: {
|
||||
category: column.category,
|
||||
columnHeaderType: 'not-filtered',
|
||||
description: isString(column.description) ? column.description : undefined,
|
||||
example: isString(column.example) ? column.example : undefined,
|
||||
id: fieldId,
|
||||
linkField: linkFields[fieldId] ?? undefined,
|
||||
type: column.type,
|
||||
aggregatable: column.aggregatable,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
...initColumnHeader,
|
||||
},
|
||||
id: scopeId,
|
||||
index: result.destination != null ? result.destination.index : 0,
|
||||
})
|
||||
);
|
||||
} else if (scopedActions) {
|
||||
// create a column definition, because it doesn't exist in the browserFields:
|
||||
dispatch(
|
||||
scopedActions.upsertColumn({
|
||||
column: {
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: fieldId,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
id: scopeId,
|
||||
index: result.destination != null ? result.destination.index : 0,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getIdFromColumnDroppableId = (droppableId: string) =>
|
||||
droppableId.slice(droppableId.lastIndexOf('.') + 1);
|
||||
|
|
|
@ -19,27 +19,14 @@ import { createStore } from '../../store/store';
|
|||
|
||||
import { ErrorToastDispatcher } from '.';
|
||||
import type { State } from '../../store/types';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
|
||||
describe('Error Toast Dispatcher', () => {
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
|
|
|
@ -19,7 +19,6 @@ import { SeverityBadge } from '../../../../detections/components/rules/severity_
|
|||
import type { State } from '../../../store';
|
||||
import { createStore } from '../../../store';
|
||||
import { TimelineId } from '../../../../../common/types';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
|
||||
const state: State = {
|
||||
...mockGlobalState,
|
||||
|
@ -35,13 +34,7 @@ const state: State = {
|
|||
};
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
const props = {
|
||||
title: 'Severity',
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { isEmpty } from 'lodash/fp';
|
||||
import type { Ecs } from '../../../../common/ecs';
|
||||
|
||||
export const isEventBuildingBlockType = (event: Ecs): boolean =>
|
||||
!isEmpty(event.kibana?.alert?.building_block_type);
|
||||
|
||||
/** This local storage key stores the `Grid / Event rendered view` selection */
|
||||
export const ALERTS_TABLE_VIEW_SELECTION_KEY = 'securitySolution.alerts.table.view-selection';
|
|
@ -7,9 +7,26 @@
|
|||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { eventRenderedProps, TestProviders } from '../../../mock';
|
||||
import type { EventRenderedViewProps } from '.';
|
||||
import { EventRenderedView } from '.';
|
||||
import { RowRendererId } from '../../../../common/types';
|
||||
import { RowRendererId, TableId } from '../../../../common/types';
|
||||
import { mockTimelineData, TestProviders } from '../../mock';
|
||||
|
||||
const eventRenderedProps: EventRenderedViewProps = {
|
||||
events: mockTimelineData,
|
||||
leadingControlColumns: [],
|
||||
onChangePage: () => null,
|
||||
onChangeItemsPerPage: () => null,
|
||||
pagination: {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 25, 50, 100],
|
||||
totalItemCount: 100,
|
||||
},
|
||||
rowRenderers: [],
|
||||
scopeId: TableId.alertsOnAlertsPage,
|
||||
unitCountText: '10 events',
|
||||
};
|
||||
|
||||
describe('event_rendered_view', () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
@ -21,7 +38,7 @@ describe('event_rendered_view', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
expect(screen.queryAllByTestId('moment-date')[0].textContent).toEqual(
|
||||
'2018-11-05T14:03:25-05:00'
|
||||
'Nov 5, 2018 @ 19:03:25.937'
|
||||
);
|
||||
});
|
||||
|
|
@ -4,29 +4,31 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import {
|
||||
import type {
|
||||
CriteriaWithPagination,
|
||||
EuiBasicTable,
|
||||
EuiBasicTableProps,
|
||||
EuiDataGridCellValueElementProps,
|
||||
EuiDataGridControlColumn,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
Pagination,
|
||||
} from '@elastic/eui';
|
||||
import { EuiBasicTable, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALERT_REASON, ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
|
||||
import { get } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React, { ComponentType, useCallback, useMemo } from 'react';
|
||||
import type { ComponentType } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { Ecs } from '../../../../common/ecs';
|
||||
import type { Ecs } from '../../../../common/ecs';
|
||||
import { APP_UI_ID } from '../../../../common/constants';
|
||||
import type { TimelineItem } from '../../../../common/search_strategy';
|
||||
import type { RowRenderer } from '../../../../common/types';
|
||||
import { RuleName } from '../../rule_name';
|
||||
import { isEventBuildingBlockType } from '../body/helpers';
|
||||
import { RuleName } from '../rule_name';
|
||||
import { isEventBuildingBlockType } from './helpers';
|
||||
import { UnitCount } from '../toolbar/unit';
|
||||
|
||||
const EventRenderedFlexItem = styled(EuiFlexItem)`
|
||||
div:first-child {
|
||||
|
@ -68,9 +70,15 @@ const StyledEuiBasicTable = styled(EuiBasicTable as BasicTableType)`
|
|||
`;
|
||||
|
||||
export interface EventRenderedViewProps {
|
||||
alertToolbar: React.ReactNode;
|
||||
appId: string;
|
||||
events: TimelineItem[];
|
||||
leadingControlColumns: EuiDataGridControlColumn[];
|
||||
onChangePage: (newActivePage: number) => void;
|
||||
onChangeItemsPerPage: (newItemsPerPage: number) => void;
|
||||
rowRenderers: RowRenderer[];
|
||||
scopeId: string;
|
||||
pagination: Pagination;
|
||||
unitCountText: string;
|
||||
additionalControls?: React.ReactNode;
|
||||
getRowRenderer?: ({
|
||||
data,
|
||||
rowRenderers,
|
||||
|
@ -78,16 +86,8 @@ export interface EventRenderedViewProps {
|
|||
data: Ecs;
|
||||
rowRenderers: RowRenderer[];
|
||||
}) => RowRenderer | null;
|
||||
leadingControlColumns: EuiDataGridControlColumn[];
|
||||
onChangePage: (newActivePage: number) => void;
|
||||
onChangeItemsPerPage: (newItemsPerPage: number) => void;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
pageSizeOptions: number[];
|
||||
rowRenderers: RowRenderer[];
|
||||
timelineId: string;
|
||||
totalItemCount: number;
|
||||
}
|
||||
|
||||
const PreferenceFormattedDateComponent = ({ value }: { value: Date }) => {
|
||||
const tz = useUiSetting<string>('dateFormat:tz');
|
||||
const dateFormat = useUiSetting<string>('dateFormat');
|
||||
|
@ -98,19 +98,16 @@ const PreferenceFormattedDateComponent = ({ value }: { value: Date }) => {
|
|||
export const PreferenceFormattedDate = React.memo(PreferenceFormattedDateComponent);
|
||||
|
||||
const EventRenderedViewComponent = ({
|
||||
alertToolbar,
|
||||
appId,
|
||||
additionalControls,
|
||||
events,
|
||||
getRowRenderer,
|
||||
leadingControlColumns,
|
||||
onChangePage,
|
||||
onChangeItemsPerPage,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageSizeOptions,
|
||||
rowRenderers,
|
||||
timelineId,
|
||||
totalItemCount,
|
||||
scopeId,
|
||||
pagination,
|
||||
unitCountText,
|
||||
}: EventRenderedViewProps) => {
|
||||
const ActionTitle = useMemo(
|
||||
() => (
|
||||
|
@ -163,7 +160,7 @@ const EventRenderedViewComponent = ({
|
|||
},
|
||||
{
|
||||
field: 'ecs.timestamp',
|
||||
name: i18n.translate('xpack.timelines.alerts.EventRenderedView.timestamp.column', {
|
||||
name: i18n.translate('xpack.securitySolution.EventRenderedView.timestamp.column', {
|
||||
defaultMessage: 'Timestamp',
|
||||
}),
|
||||
truncateText: false,
|
||||
|
@ -175,7 +172,7 @@ const EventRenderedViewComponent = ({
|
|||
},
|
||||
{
|
||||
field: `ecs.${ALERT_RULE_NAME}`,
|
||||
name: i18n.translate('xpack.timelines.alerts.EventRenderedView.rule.column', {
|
||||
name: i18n.translate('xpack.securitySolution.EventRenderedView.rule.column', {
|
||||
defaultMessage: 'Rule',
|
||||
}),
|
||||
truncateText: false,
|
||||
|
@ -183,12 +180,12 @@ const EventRenderedViewComponent = ({
|
|||
render: (name: unknown, item: TimelineItem) => {
|
||||
const ruleName = get(item, `ecs.signal.rule.name`) ?? get(item, `ecs.${ALERT_RULE_NAME}`);
|
||||
const ruleId = get(item, `ecs.signal.rule.id`) ?? get(item, `ecs.${ALERT_RULE_UUID}`);
|
||||
return <RuleName name={ruleName} id={ruleId} appId={appId} />;
|
||||
return <RuleName name={ruleName} id={ruleId} appId={APP_UI_ID} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'eventSummary',
|
||||
name: i18n.translate('xpack.timelines.alerts.EventRenderedView.eventSummary.column', {
|
||||
name: i18n.translate('xpack.securitySolution.EventRenderedView.eventSummary.column', {
|
||||
defaultMessage: 'Event Summary',
|
||||
}),
|
||||
truncateText: false,
|
||||
|
@ -209,7 +206,7 @@ const EventRenderedViewComponent = ({
|
|||
{rowRenderer.renderRow({
|
||||
data: ecsData,
|
||||
isDraggable: false,
|
||||
scopeId: timelineId,
|
||||
scopeId,
|
||||
})}
|
||||
</div>
|
||||
</EventRenderedFlexItem>
|
||||
|
@ -224,35 +221,36 @@ const EventRenderedViewComponent = ({
|
|||
width: '60%',
|
||||
},
|
||||
],
|
||||
[ActionTitle, events, leadingControlColumns, appId, getRowRenderer, rowRenderers, timelineId]
|
||||
[ActionTitle, events, getRowRenderer, leadingControlColumns, rowRenderers, scopeId]
|
||||
);
|
||||
|
||||
const handleTableChange = useCallback(
|
||||
(pageChange: CriteriaWithPagination<TimelineItem>) => {
|
||||
if (pageChange.page.index !== pageIndex) {
|
||||
if (pageChange.page.index !== pagination.pageIndex) {
|
||||
onChangePage(pageChange.page.index);
|
||||
}
|
||||
if (pageChange.page.size !== pageSize) {
|
||||
if (pageChange.page.size !== pagination.pageSize) {
|
||||
onChangeItemsPerPage(pageChange.page.size);
|
||||
}
|
||||
},
|
||||
[onChangePage, pageIndex, pageSize, onChangeItemsPerPage]
|
||||
[pagination.pageIndex, pagination.pageSize, onChangePage, onChangeItemsPerPage]
|
||||
);
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount,
|
||||
pageSizeOptions,
|
||||
showPerPageOptions: true,
|
||||
}),
|
||||
[pageIndex, pageSize, pageSizeOptions, totalItemCount]
|
||||
const toolbar = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<UnitCount data-test-subj="server-side-event-count">{unitCountText}</UnitCount>
|
||||
</EuiFlexItem>
|
||||
{additionalControls}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[additionalControls, unitCountText]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{alertToolbar}
|
||||
{toolbar}
|
||||
<StyledEuiBasicTable
|
||||
compressed
|
||||
items={events}
|
|
@ -13,9 +13,9 @@ import { TestProviders } from '../../mock';
|
|||
import type { EventsQueryTabBodyComponentProps } from './events_query_tab_body';
|
||||
import { EventsQueryTabBody, ALERTS_EVENTS_HISTOGRAM_ID } from './events_query_tab_body';
|
||||
import { useGlobalFullScreen } from '../../containers/use_full_screen';
|
||||
import * as tGridActions from '@kbn/timelines-plugin/public/store/t_grid/actions';
|
||||
import { licenseService } from '../../hooks/use_license';
|
||||
import { mockHistory } from '../../mock/router';
|
||||
import { dataTableActions } from '../../store/data_table';
|
||||
|
||||
const mockGetDefaultControlColumn = jest.fn();
|
||||
jest.mock('../../../timelines/components/timeline/body/control_columns', () => ({
|
||||
|
@ -172,7 +172,7 @@ describe('EventsQueryTabBody', () => {
|
|||
});
|
||||
|
||||
it('initializes t-grid', () => {
|
||||
const spy = jest.spyOn(tGridActions, 'initializeTGridSettings');
|
||||
const spy = jest.spyOn(dataTableActions, 'initializeDataTableSettings');
|
||||
render(
|
||||
<TestProviders>
|
||||
<EventsQueryTabBody {...commonProps} />
|
||||
|
|
|
@ -10,11 +10,8 @@ import { useDispatch } from 'react-redux';
|
|||
|
||||
import { EuiCheckbox } from '@elastic/eui';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { EntityType } from '@kbn/timelines-plugin/common';
|
||||
|
||||
import type { BulkActionsProp } from '@kbn/timelines-plugin/common/types';
|
||||
import type { TableId } from '../../../../common/types';
|
||||
import { dataTableActions } from '../../store/data_table';
|
||||
import type { TableId } from '../../../../common/types/timeline';
|
||||
import { RowRendererId } from '../../../../common/types/timeline';
|
||||
import { StatefulEventsViewer } from '../events_viewer';
|
||||
import { eventsDefaultModel } from '../events_viewer/default_model';
|
||||
|
@ -48,6 +45,7 @@ import {
|
|||
useGetInitialUrlParamValue,
|
||||
useReplaceUrlParams,
|
||||
} from '../../utils/global_query_string/helpers';
|
||||
import type { BulkActionsProp } from '../toolbar/bulk_actions/types';
|
||||
|
||||
export const ALERTS_EVENTS_HISTOGRAM_ID = 'alertsOrEventsHistogramQuery';
|
||||
|
||||
|
@ -100,7 +98,7 @@ const EventsQueryTabBodyComponent: React.FC<EventsQueryTabBodyComponentProps> =
|
|||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
dataTableActions.initializeTGridSettings({
|
||||
dataTableActions.initializeDataTableSettings({
|
||||
id: tableId,
|
||||
defaultColumns: eventsDefaultModel.columns.map((c) =>
|
||||
!tGridEnabled && c.initialWidth == null
|
||||
|
@ -187,13 +185,12 @@ const EventsQueryTabBodyComponent: React.FC<EventsQueryTabBodyComponentProps> =
|
|||
defaultCellActions={defaultCellActions}
|
||||
start={startDate}
|
||||
end={endDate}
|
||||
entityType={'events' as EntityType}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
renderCellValue={DefaultCellRenderer}
|
||||
rowRenderers={defaultRowRenderers}
|
||||
scopeId={SourcererScopeName.default}
|
||||
sourcererScope={SourcererScopeName.default}
|
||||
tableId={tableId}
|
||||
unit={showExternalAlerts ? i18n.ALERTS_UNIT : i18n.EVENTS_UNIT}
|
||||
unit={showExternalAlerts ? i18n.EXTERNAL_ALERTS_UNIT : i18n.EVENTS_UNIT}
|
||||
defaultModel={defaultModel}
|
||||
pageFilters={composedPageFilters}
|
||||
bulkActions={bulkActions}
|
||||
|
|
|
@ -17,7 +17,7 @@ const DEFAULT_EVENTS_STACK_BY = 'event.action';
|
|||
export const getSubtitleFunction =
|
||||
(defaultNumberFormat: string, isAlert: boolean) => (totalCount: number) =>
|
||||
`${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${
|
||||
isAlert ? i18n.ALERTS_UNIT(totalCount) : i18n.EVENTS_UNIT(totalCount)
|
||||
isAlert ? i18n.EXTERNAL_ALERTS_UNIT(totalCount) : i18n.EVENTS_UNIT(totalCount)
|
||||
}`;
|
||||
|
||||
export const eventsStackByOptions: MatrixHistogramOption[] = [
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ALERTS_UNIT = (totalCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.eventsTab.unit', {
|
||||
export const EXTERNAL_ALERTS_UNIT = (totalCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.eventsTab.externalAlertsUnit', {
|
||||
values: { totalCount },
|
||||
defaultMessage: `external {totalCount, plural, =1 {alert} other {alerts}}`,
|
||||
});
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
*/
|
||||
|
||||
import { tableDefaults } from '../../store/data_table/defaults';
|
||||
import type { SubsetTGridModel } from '../../store/data_table/model';
|
||||
import type { SubsetDataTableModel } from '../../store/data_table/model';
|
||||
import { defaultEventHeaders } from './default_event_headers';
|
||||
|
||||
export const eventsDefaultModel: SubsetTGridModel = {
|
||||
export const eventsDefaultModel: SubsetDataTableModel = {
|
||||
...tableDefaults,
|
||||
columns: defaultEventHeaders,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { TableId } from '../../../../common/types';
|
||||
import type { CombineQueries } from '../../lib/kuery';
|
||||
import { buildTimeRangeFilter, combineQueries } from '../../lib/kuery';
|
||||
|
||||
import { EVENTS_TABLE_CLASS_NAME } from './styles';
|
||||
import type { ViewSelection } from './summary_view_select';
|
||||
|
||||
export const getCombinedFilterQuery = ({
|
||||
from,
|
||||
to,
|
||||
filters,
|
||||
...combineQueriesParams
|
||||
}: CombineQueries & { from: string; to: string }): string | undefined => {
|
||||
const combinedQueries = combineQueries({
|
||||
...combineQueriesParams,
|
||||
filters: [...filters, buildTimeRangeFilter(from, to)],
|
||||
});
|
||||
|
||||
return combinedQueries ? combinedQueries.filterQuery : undefined;
|
||||
};
|
||||
|
||||
export const resolverIsShowing = (graphEventId: string | undefined): boolean =>
|
||||
graphEventId != null && graphEventId !== '';
|
||||
|
||||
export const EVENTS_COUNT_BUTTON_CLASS_NAME = 'local-events-count-button';
|
||||
|
||||
/** Returns `true` when the element, or one of it's children has focus */
|
||||
export const elementOrChildrenHasFocus = (element: HTMLElement | null | undefined): boolean =>
|
||||
element === document.activeElement || element?.querySelector(':focus-within') != null;
|
||||
|
||||
/** Returns true if the events table has focus */
|
||||
export const tableHasFocus = (containerElement: HTMLElement | null): boolean =>
|
||||
elementOrChildrenHasFocus(
|
||||
containerElement?.querySelector<HTMLDivElement>(`.${EVENTS_TABLE_CLASS_NAME}`)
|
||||
);
|
||||
|
||||
export const isSelectableView = (tableId: string): boolean =>
|
||||
tableId === TableId.alertsOnAlertsPage || tableId === TableId.alertsOnRuleDetailsPage;
|
||||
|
||||
export const isViewSelection = (value: unknown): value is ViewSelection =>
|
||||
value === 'gridView' || value === 'eventRenderedView';
|
||||
|
||||
/** always returns a valid default `ViewSelection` */
|
||||
export const getDefaultViewSelection = ({
|
||||
tableId,
|
||||
value,
|
||||
}: {
|
||||
tableId: string;
|
||||
value: unknown;
|
||||
}): ViewSelection => {
|
||||
const defaultViewSelection = 'gridView';
|
||||
|
||||
if (!isSelectableView(tableId)) {
|
||||
return defaultViewSelection;
|
||||
} else {
|
||||
return isViewSelection(value) ? value : defaultViewSelection;
|
||||
}
|
||||
};
|
|
@ -16,7 +16,6 @@ import { mockEventViewerResponse } from './mock';
|
|||
import { StatefulEventsViewer } from '.';
|
||||
import { eventsDefaultModel } from './default_model';
|
||||
import { EntityType } from '@kbn/timelines-plugin/common';
|
||||
import { TableId } from '../../../../common/types/timeline';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
|
||||
import { useTimelineEvents } from '../../../timelines/containers';
|
||||
|
@ -25,9 +24,21 @@ import { defaultRowRenderers } from '../../../timelines/components/timeline/body
|
|||
import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions';
|
||||
import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser';
|
||||
import { useGetUserCasesPermissions } from '../../lib/kibana';
|
||||
import { TableId } from '../../../../common/types';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
jest.mock('../../lib/kibana');
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
const originalKibanaLib = jest.requireActual('../../lib/kibana');
|
||||
|
||||
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
|
||||
|
@ -46,6 +57,12 @@ jest.mock('../../../timelines/components/fields_browser', () => ({
|
|||
useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props),
|
||||
}));
|
||||
|
||||
jest.mock('./helpers', () => ({
|
||||
getDefaultViewSelection: () => 'gridView',
|
||||
resolverIsShowing: () => false,
|
||||
getCombinedFilterQuery: () => undefined,
|
||||
}));
|
||||
|
||||
const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock;
|
||||
jest.mock('use-resize-observer/polyfilled');
|
||||
mockUseResizeObserver.mockImplementation(() => ({}));
|
||||
|
@ -64,35 +81,36 @@ const testProps = {
|
|||
leadingControlColumns: getDefaultControlColumn(ACTION_BUTTON_COUNT),
|
||||
renderCellValue: DefaultCellRenderer,
|
||||
rowRenderers: defaultRowRenderers,
|
||||
scopeId: SourcererScopeName.default,
|
||||
sourcererScope: SourcererScopeName.default,
|
||||
start: from,
|
||||
bulkActions: false,
|
||||
hasCrudPermissions: true,
|
||||
};
|
||||
describe('StatefulEventsViewer', () => {
|
||||
(useTimelineEvents as jest.Mock).mockReturnValue([false, mockEventViewerResponse]);
|
||||
|
||||
test('it renders the events viewer', async () => {
|
||||
const wrapper = render(
|
||||
test('it renders the events viewer', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventsViewer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.getByText('hello grid')).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="events-viewer-panel"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
// InspectButtonContainer controls displaying InspectButton components
|
||||
test('it renders InspectButtonContainer', async () => {
|
||||
const wrapper = render(
|
||||
test('it renders InspectButtonContainer', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventsViewer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.getByTestId(`hoverVisibilityContainer`)).toBeTruthy();
|
||||
expect(wrapper.find(`[data-test-subj="hoverVisibilityContainer"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it closes field editor when unmounted', async () => {
|
||||
test('it closes field editor when unmounted', () => {
|
||||
const mockCloseEditor = jest.fn();
|
||||
mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => {
|
||||
editorActionsRef.current = { closeEditor: mockCloseEditor };
|
||||
|
|
|
@ -5,65 +5,97 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import React, { useRef, useCallback, useMemo, useEffect, useState, useContext } from 'react';
|
||||
import type { ConnectedProps } from 'react-redux';
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { EntityType, RowRenderer } from '@kbn/timelines-plugin/common';
|
||||
import type { TGridCellAction, BulkActionsProp } from '@kbn/timelines-plugin/common/types';
|
||||
import type { ControlColumnProps, TableId } from '../../../../common/types';
|
||||
import type { Direction, EntityType, RowRenderer } from '@kbn/timelines-plugin/common';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
import type { EuiTheme } from '@kbn/kibana-react-plugin/common';
|
||||
import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer';
|
||||
import type { Sort } from '../../../timelines/components/timeline/body/sort';
|
||||
import type {
|
||||
ControlColumnProps,
|
||||
DataTableCellAction,
|
||||
OnRowSelected,
|
||||
OnSelectAll,
|
||||
SetEventsDeleted,
|
||||
SetEventsLoading,
|
||||
TableId,
|
||||
} from '../../../../common/types';
|
||||
import { dataTableActions } from '../../store/data_table';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
import type { State } from '../../store';
|
||||
import { inputsActions } from '../../store/actions';
|
||||
import { APP_UI_ID } from '../../../../common/constants';
|
||||
import type { Status } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { InspectButtonContainer } from '../inspect';
|
||||
import { useGlobalFullScreen } from '../../containers/use_full_screen';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
import { eventsViewerSelector } from './selectors';
|
||||
import type { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { useSourcererDataView } from '../../containers/sourcerer';
|
||||
import type { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
|
||||
import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
|
||||
import type { FieldEditorActions } from '../../../timelines/components/fields_browser';
|
||||
import { useFieldBrowserOptions } from '../../../timelines/components/fields_browser';
|
||||
import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer';
|
||||
import {
|
||||
useSessionViewNavigation,
|
||||
useSessionView,
|
||||
} from '../../../timelines/components/timeline/session_tab_content/use_session_view';
|
||||
import type { SubsetTGridModel } from '../../store/data_table/model';
|
||||
import type { SubsetDataTableModel } from '../../store/data_table/model';
|
||||
import {
|
||||
EventsContainerLoading,
|
||||
FullScreenContainer,
|
||||
FullWidthFlexGroupTable,
|
||||
ScrollableFlexItem,
|
||||
StyledEuiPanel,
|
||||
} from './styles';
|
||||
import { getDefaultViewSelection, getCombinedFilterQuery } from './helpers';
|
||||
import { ALERTS_TABLE_VIEW_SELECTION_KEY } from '../event_rendered_view/helpers';
|
||||
import { useTimelineEvents } from './use_timelines_events';
|
||||
import { TableContext, EmptyTable, TableLoading } from './shared';
|
||||
import { DataTableComponent } from '../data_table';
|
||||
import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants';
|
||||
import type { AlertWorkflowStatus } from '../../types';
|
||||
import { EventRenderedView } from '../event_rendered_view';
|
||||
import { useQueryInspector } from '../page/manage_query';
|
||||
import type { SetQuery } from '../../containers/use_global_time/types';
|
||||
import { defaultHeaders } from '../../store/data_table/defaults';
|
||||
import { checkBoxControlColumn, transformControlColumns } from '../control_columns';
|
||||
import { getEventIdToDataMapping } from '../data_table/helpers';
|
||||
import type { ViewSelection } from './summary_view_select';
|
||||
import { RightTopMenu } from './right_top_menu';
|
||||
import { useAlertBulkActions } from './use_alert_bulk_actions';
|
||||
import type { BulkActionsProp } from '../toolbar/bulk_actions/types';
|
||||
import { StatefulEventContext } from './stateful_event_context';
|
||||
import { defaultUnit } from '../toolbar/unit';
|
||||
|
||||
const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = [];
|
||||
const storage = new Storage(localStorage);
|
||||
|
||||
const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>`
|
||||
height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : undefined)};
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
`;
|
||||
const SECURITY_ALERTS_CONSUMERS = [AlertConsumers.SIEM];
|
||||
|
||||
export interface Props {
|
||||
defaultCellActions?: TGridCellAction[];
|
||||
defaultModel: SubsetTGridModel;
|
||||
export interface EventsViewerProps {
|
||||
defaultCellActions?: DataTableCellAction[];
|
||||
defaultModel: SubsetDataTableModel;
|
||||
end: string;
|
||||
entityType: EntityType;
|
||||
entityType?: EntityType;
|
||||
tableId: TableId;
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
scopeId: SourcererScopeName;
|
||||
sourcererScope: SourcererScopeName;
|
||||
start: string;
|
||||
showTotalCount?: boolean;
|
||||
pageFilters?: Filter[];
|
||||
currentFilter?: Status;
|
||||
currentFilter?: AlertWorkflowStatus;
|
||||
onRuleChange?: () => void;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
additionalFilters?: React.ReactNode;
|
||||
hasAlertsCrud?: boolean;
|
||||
hasCrudPermissions?: boolean;
|
||||
unit?: (n: number) => string;
|
||||
indexNames?: string[];
|
||||
bulkActions: boolean | BulkActionsProp;
|
||||
}
|
||||
|
||||
|
@ -72,11 +104,11 @@ export interface Props {
|
|||
* timeline is used BESIDES the flyout. The flyout makes use of the `EventsViewer` component which is a subcomponent here
|
||||
* NOTE: As of writting, it is not used in the Case_View component
|
||||
*/
|
||||
const StatefulEventsViewerComponent: React.FC<Props> = ({
|
||||
const StatefulEventsViewerComponent: React.FC<EventsViewerProps & PropsFromRedux> = ({
|
||||
defaultCellActions,
|
||||
defaultModel,
|
||||
end,
|
||||
entityType,
|
||||
entityType = 'events',
|
||||
tableId,
|
||||
leadingControlColumns,
|
||||
pageFilters,
|
||||
|
@ -85,15 +117,21 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
renderCellValue,
|
||||
rowRenderers,
|
||||
start,
|
||||
scopeId,
|
||||
sourcererScope,
|
||||
additionalFilters,
|
||||
unit,
|
||||
hasCrudPermissions = true,
|
||||
unit = defaultUnit,
|
||||
indexNames,
|
||||
bulkActions,
|
||||
setSelected,
|
||||
clearSelected,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const theme: EuiTheme = useContext(ThemeContext);
|
||||
const tableContext = useMemo(() => ({ tableId }), [tableId]);
|
||||
|
||||
const {
|
||||
filters,
|
||||
input,
|
||||
query,
|
||||
dataTable: {
|
||||
columns,
|
||||
|
@ -105,10 +143,23 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
sessionViewConfig,
|
||||
showCheckboxes,
|
||||
sort,
|
||||
queryFields,
|
||||
selectAll,
|
||||
selectedEventIds,
|
||||
isSelectAllChecked,
|
||||
loadingEventIds,
|
||||
title,
|
||||
} = defaultModel,
|
||||
} = useSelector((state: State) => eventsViewerSelector(state, tableId));
|
||||
|
||||
const { timelines: timelinesUi } = useKibana().services;
|
||||
const { uiSettings, data } = useKibana().services;
|
||||
|
||||
const [tableView, setTableView] = useState<ViewSelection>(
|
||||
getDefaultViewSelection({
|
||||
tableId,
|
||||
value: storage.get(ALERTS_TABLE_VIEW_SELECTION_KEY),
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
browserFields,
|
||||
|
@ -118,28 +169,24 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
selectedPatterns,
|
||||
dataViewId: selectedDataViewId,
|
||||
loading: isLoadingIndexPattern,
|
||||
} = useSourcererDataView(scopeId);
|
||||
} = useSourcererDataView(sourcererScope);
|
||||
|
||||
const { globalFullScreen } = useGlobalFullScreen();
|
||||
const tGridEventRenderedViewEnabled = useIsExperimentalFeatureEnabled(
|
||||
'tGridEventRenderedViewEnabled'
|
||||
);
|
||||
const editorActionsRef = useRef<FieldEditorActions>(null);
|
||||
|
||||
const editorActionsRef = useRef<FieldEditorActions>(null);
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
dataTableActions.createTGrid({
|
||||
dataTableActions.createDataTable({
|
||||
columns,
|
||||
dataViewId: selectedDataViewId,
|
||||
defaultColumns,
|
||||
id: tableId,
|
||||
indexNames: selectedPatterns,
|
||||
indexNames: indexNames ?? selectedPatterns,
|
||||
itemsPerPage,
|
||||
showCheckboxes,
|
||||
sort,
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
dispatch(inputsActions.deleteOneQuery({ id: tableId, inputId: InputsModelId.global }));
|
||||
if (editorActionsRef.current) {
|
||||
|
@ -151,7 +198,6 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
}, []);
|
||||
|
||||
const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]);
|
||||
const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS;
|
||||
|
||||
const { Navigation } = useSessionViewNavigation({
|
||||
scopeId: tableId,
|
||||
|
@ -170,73 +216,386 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
) : null;
|
||||
}, [graphEventId, tableId, sessionViewConfig, SessionView, Navigation]);
|
||||
const setQuery = useCallback(
|
||||
(inspect, loading, refetch) => {
|
||||
({ id, inspect, loading, refetch }: SetQuery) =>
|
||||
dispatch(
|
||||
inputsActions.setQuery({
|
||||
id: tableId,
|
||||
id,
|
||||
inputId: InputsModelId.global,
|
||||
inspect,
|
||||
loading,
|
||||
refetch,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, tableId]
|
||||
),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const fieldBrowserOptions = useFieldBrowserOptions({
|
||||
sourcererScope: scopeId,
|
||||
sourcererScope,
|
||||
editorActionsRef,
|
||||
upsertColumn: (column, index) =>
|
||||
dispatch(dataTableActions.upsertColumn({ column, id: tableId, index })),
|
||||
removeColumn: (columnId) => dispatch(dataTableActions.removeColumn({ columnId, id: tableId })),
|
||||
});
|
||||
|
||||
const isLive = input.policy.kind === 'interval';
|
||||
const columnHeaders = isEmpty(columns) ? defaultHeaders : columns;
|
||||
const esQueryConfig = getEsQueryConfig(uiSettings);
|
||||
|
||||
const filterQuery = useMemo(
|
||||
() =>
|
||||
getCombinedFilterQuery({
|
||||
config: esQueryConfig,
|
||||
browserFields,
|
||||
dataProviders: [],
|
||||
filters: globalFilters,
|
||||
from: start,
|
||||
indexPattern,
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: query,
|
||||
to: end,
|
||||
}),
|
||||
[esQueryConfig, browserFields, globalFilters, start, indexPattern, query, end]
|
||||
);
|
||||
|
||||
const canQueryTimeline = useMemo(
|
||||
() =>
|
||||
filterQuery != null &&
|
||||
isLoadingIndexPattern != null &&
|
||||
!isLoadingIndexPattern &&
|
||||
!isEmpty(start) &&
|
||||
!isEmpty(end),
|
||||
[isLoadingIndexPattern, filterQuery, start, end]
|
||||
);
|
||||
|
||||
const fields = useMemo(
|
||||
() => [...columnHeaders.map((c: { id: string }) => c.id), ...(queryFields ?? [])],
|
||||
[columnHeaders, queryFields]
|
||||
);
|
||||
|
||||
const sortField = useMemo(
|
||||
() =>
|
||||
(sort as Sort[]).map(({ columnId, columnType, esTypes, sortDirection }) => ({
|
||||
field: columnId,
|
||||
type: columnType,
|
||||
direction: sortDirection as Direction,
|
||||
esTypes: esTypes ?? [],
|
||||
})),
|
||||
[sort]
|
||||
);
|
||||
|
||||
const [loading, { events, loadPage, pageInfo, refetch, totalCount = 0, inspect }] =
|
||||
useTimelineEvents({
|
||||
// We rely on entityType to determine Events vs Alerts
|
||||
alertConsumers: SECURITY_ALERTS_CONSUMERS,
|
||||
data,
|
||||
dataViewId,
|
||||
endDate: end,
|
||||
entityType,
|
||||
fields,
|
||||
filterQuery,
|
||||
id: tableId,
|
||||
indexNames: indexNames ?? selectedPatterns,
|
||||
limit: itemsPerPage,
|
||||
runtimeMappings,
|
||||
skip: !canQueryTimeline,
|
||||
sort: sortField,
|
||||
startDate: start,
|
||||
filterStatus: currentFilter,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(dataTableActions.updateIsLoading({ id: tableId, isLoading: loading }));
|
||||
}, [dispatch, tableId, loading]);
|
||||
|
||||
const deleteQuery = useCallback(
|
||||
({ id }) => dispatch(inputsActions.deleteOneQuery({ inputId: InputsModelId.global, id })),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useQueryInspector({
|
||||
queryId: tableId,
|
||||
loading,
|
||||
refetch,
|
||||
setQuery,
|
||||
deleteQuery,
|
||||
inspect,
|
||||
});
|
||||
|
||||
const totalCountMinusDeleted = useMemo(
|
||||
() => (totalCount > 0 ? totalCount - deletedEventIds.length : 0),
|
||||
[deletedEventIds.length, totalCount]
|
||||
);
|
||||
|
||||
const hasAlerts = totalCountMinusDeleted > 0;
|
||||
|
||||
// Only show the table-spanning loading indicator when the query is loading and we
|
||||
// don't have data (e.g. for the initial fetch).
|
||||
// Subsequent fetches (e.g. for pagination) will show a small loading indicator on
|
||||
// top of the table and the table will display the current page until the next page
|
||||
// is fetched. This prevents a flicker when paginating.
|
||||
const showFullLoading = loading && !hasAlerts;
|
||||
|
||||
const nonDeletedEvents = useMemo(
|
||||
() => events.filter((e) => !deletedEventIds.includes(e._id)),
|
||||
[deletedEventIds, events]
|
||||
);
|
||||
useEffect(() => {
|
||||
setQuery({ id: tableId, inspect, loading, refetch });
|
||||
}, [inspect, loading, refetch, setQuery, tableId]);
|
||||
|
||||
// Clear checkbox selection when new events are fetched
|
||||
useEffect(() => {
|
||||
dispatch(dataTableActions.clearSelected({ id: tableId }));
|
||||
dispatch(
|
||||
dataTableActions.setDataTableSelectAll({
|
||||
id: tableId,
|
||||
selectAll: false,
|
||||
})
|
||||
);
|
||||
}, [nonDeletedEvents, dispatch, tableId]);
|
||||
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
(itemsChangedPerPage) => {
|
||||
dispatch(
|
||||
dataTableActions.updateItemsPerPage({ id: tableId, itemsPerPage: itemsChangedPerPage })
|
||||
);
|
||||
},
|
||||
[tableId, dispatch]
|
||||
);
|
||||
|
||||
const onChangePage = useCallback(
|
||||
(page) => {
|
||||
loadPage(page);
|
||||
},
|
||||
[loadPage]
|
||||
);
|
||||
|
||||
const setEventsLoading = useCallback<SetEventsLoading>(
|
||||
({ eventIds, isLoading }) => {
|
||||
dispatch(dataTableActions.setEventsLoading({ id: tableId, eventIds, isLoading }));
|
||||
},
|
||||
[dispatch, tableId]
|
||||
);
|
||||
|
||||
const setEventsDeleted = useCallback<SetEventsDeleted>(
|
||||
({ eventIds, isDeleted }) => {
|
||||
dispatch(dataTableActions.setEventsDeleted({ id: tableId, eventIds, isDeleted }));
|
||||
},
|
||||
[dispatch, tableId]
|
||||
);
|
||||
|
||||
const selectedCount = useMemo(() => Object.keys(selectedEventIds).length, [selectedEventIds]);
|
||||
|
||||
const onRowSelected: OnRowSelected = useCallback(
|
||||
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
|
||||
setSelected({
|
||||
id: tableId,
|
||||
eventIds: getEventIdToDataMapping(
|
||||
nonDeletedEvents,
|
||||
eventIds,
|
||||
queryFields,
|
||||
hasCrudPermissions
|
||||
),
|
||||
isSelected,
|
||||
isSelectAllChecked: isSelected && selectedCount + 1 === nonDeletedEvents.length,
|
||||
});
|
||||
},
|
||||
[setSelected, tableId, nonDeletedEvents, queryFields, hasCrudPermissions, selectedCount]
|
||||
);
|
||||
|
||||
const onSelectPage: OnSelectAll = useCallback(
|
||||
({ isSelected }: { isSelected: boolean }) =>
|
||||
isSelected
|
||||
? setSelected({
|
||||
id: tableId,
|
||||
eventIds: getEventIdToDataMapping(
|
||||
nonDeletedEvents,
|
||||
nonDeletedEvents.map((event) => event._id),
|
||||
queryFields,
|
||||
hasCrudPermissions
|
||||
),
|
||||
isSelected,
|
||||
isSelectAllChecked: isSelected,
|
||||
})
|
||||
: clearSelected({ id: tableId }),
|
||||
[setSelected, tableId, nonDeletedEvents, queryFields, hasCrudPermissions, clearSelected]
|
||||
);
|
||||
|
||||
// Sync to selectAll so parent components can select all events
|
||||
useEffect(() => {
|
||||
if (selectAll && !isSelectAllChecked) {
|
||||
onSelectPage({ isSelected: true });
|
||||
}
|
||||
}, [isSelectAllChecked, onSelectPage, selectAll]);
|
||||
|
||||
const [transformedLeadingControlColumns] = useMemo(() => {
|
||||
return [
|
||||
showCheckboxes ? [checkBoxControlColumn, ...leadingControlColumns] : leadingControlColumns,
|
||||
].map((controlColumns) =>
|
||||
transformControlColumns({
|
||||
columnHeaders,
|
||||
controlColumns,
|
||||
data: nonDeletedEvents,
|
||||
disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS,
|
||||
fieldBrowserOptions,
|
||||
loadingEventIds,
|
||||
onRowSelected,
|
||||
onRuleChange,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
tabType: 'query',
|
||||
timelineId: tableId,
|
||||
isSelectAllChecked,
|
||||
sort,
|
||||
browserFields,
|
||||
onSelectPage,
|
||||
theme,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
pageSize: itemsPerPage,
|
||||
})
|
||||
);
|
||||
}, [
|
||||
showCheckboxes,
|
||||
leadingControlColumns,
|
||||
columnHeaders,
|
||||
nonDeletedEvents,
|
||||
fieldBrowserOptions,
|
||||
loadingEventIds,
|
||||
onRowSelected,
|
||||
onRuleChange,
|
||||
selectedEventIds,
|
||||
tableId,
|
||||
isSelectAllChecked,
|
||||
sort,
|
||||
browserFields,
|
||||
onSelectPage,
|
||||
theme,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
itemsPerPage,
|
||||
]);
|
||||
|
||||
const alertBulkActions = useAlertBulkActions({
|
||||
tableId,
|
||||
data: nonDeletedEvents,
|
||||
totalItems: totalCountMinusDeleted,
|
||||
refetch,
|
||||
indexNames: selectedPatterns,
|
||||
hasAlertsCrud: hasCrudPermissions,
|
||||
showCheckboxes,
|
||||
filterStatus: currentFilter,
|
||||
filterQuery,
|
||||
bulkActions,
|
||||
selectedCount,
|
||||
});
|
||||
|
||||
// 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({
|
||||
timelineID: tableId,
|
||||
tabType: 'query',
|
||||
enableHostDetailsFlyout: true,
|
||||
enableIpDetailsFlyout: true,
|
||||
});
|
||||
|
||||
const unitCountText = useMemo(
|
||||
() => `${totalCountMinusDeleted.toLocaleString()} ${unit(totalCountMinusDeleted)}`,
|
||||
[totalCountMinusDeleted, unit]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FullScreenContainer $isFullScreen={globalFullScreen}>
|
||||
<InspectButtonContainer>
|
||||
{timelinesUi.getTGrid<'embedded'>({
|
||||
additionalFilters,
|
||||
appId: APP_UI_ID,
|
||||
browserFields,
|
||||
bulkActions,
|
||||
columns,
|
||||
dataViewId,
|
||||
defaultCellActions,
|
||||
deletedEventIds,
|
||||
disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS,
|
||||
end,
|
||||
entityType,
|
||||
fieldBrowserOptions,
|
||||
filters: globalFilters,
|
||||
filterStatus: currentFilter,
|
||||
getRowRenderer,
|
||||
globalFullScreen,
|
||||
graphEventId,
|
||||
graphOverlay,
|
||||
id: tableId,
|
||||
indexNames: selectedPatterns,
|
||||
indexPattern,
|
||||
isLive,
|
||||
isLoadingIndexPattern,
|
||||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
leadingControlColumns,
|
||||
onRuleChange,
|
||||
query,
|
||||
renderCellValue,
|
||||
rowRenderers,
|
||||
runtimeMappings,
|
||||
setQuery,
|
||||
sort,
|
||||
start,
|
||||
tGridEventRenderedViewEnabled,
|
||||
trailingControlColumns,
|
||||
type: 'embedded',
|
||||
unit,
|
||||
})}
|
||||
<StyledEuiPanel
|
||||
hasBorder={false}
|
||||
hasShadow={false}
|
||||
paddingSize="none"
|
||||
data-test-subj="events-viewer-panel"
|
||||
$isFullScreen={globalFullScreen}
|
||||
>
|
||||
{showFullLoading && <TableLoading height="short" />}
|
||||
|
||||
{graphOverlay}
|
||||
|
||||
{canQueryTimeline && (
|
||||
<TableContext.Provider value={tableContext}>
|
||||
<EventsContainerLoading
|
||||
data-timeline-id={tableId}
|
||||
data-test-subj={`events-container-loading-${loading}`}
|
||||
>
|
||||
<RightTopMenu
|
||||
tableView={tableView}
|
||||
loading={loading}
|
||||
tableId={tableId}
|
||||
title={title}
|
||||
onViewChange={(selectedView) => setTableView(selectedView)}
|
||||
additionalFilters={additionalFilters}
|
||||
hasRightOffset={tableView === 'gridView' && nonDeletedEvents.length > 0}
|
||||
/>
|
||||
|
||||
{!hasAlerts && !loading && !graphOverlay && <EmptyTable height="short" />}
|
||||
{hasAlerts && (
|
||||
<FullWidthFlexGroupTable
|
||||
$visible={!graphEventId && graphOverlay == null}
|
||||
gutterSize="none"
|
||||
>
|
||||
<ScrollableFlexItem grow={1}>
|
||||
<StatefulEventContext.Provider value={activeStatefulEventContext}>
|
||||
{tableView === 'gridView' && (
|
||||
<DataTableComponent
|
||||
additionalControls={alertBulkActions}
|
||||
unitCountText={unitCountText}
|
||||
browserFields={browserFields}
|
||||
data={nonDeletedEvents}
|
||||
disabledCellActions={FIELDS_WITHOUT_CELL_ACTIONS}
|
||||
id={tableId}
|
||||
loadPage={loadPage}
|
||||
renderCellValue={renderCellValue}
|
||||
rowRenderers={rowRenderers}
|
||||
totalItems={totalCountMinusDeleted}
|
||||
bulkActions={bulkActions}
|
||||
fieldBrowserOptions={fieldBrowserOptions}
|
||||
defaultCellActions={defaultCellActions}
|
||||
hasCrudPermissions={hasCrudPermissions}
|
||||
filters={filters}
|
||||
leadingControlColumns={transformedLeadingControlColumns}
|
||||
pagination={{
|
||||
pageIndex: pageInfo.activePage,
|
||||
pageSize: itemsPerPage,
|
||||
pageSizeOptions: itemsPerPageOptions,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{tableView === 'eventRenderedView' && (
|
||||
<EventRenderedView
|
||||
events={nonDeletedEvents}
|
||||
getRowRenderer={getRowRenderer}
|
||||
leadingControlColumns={transformedLeadingControlColumns}
|
||||
pagination={{
|
||||
pageIndex: pageInfo.activePage,
|
||||
pageSize: itemsPerPage,
|
||||
totalItemCount: totalCountMinusDeleted,
|
||||
pageSizeOptions: itemsPerPageOptions,
|
||||
showPerPageOptions: true,
|
||||
}}
|
||||
rowRenderers={rowRenderers}
|
||||
scopeId={tableId}
|
||||
onChangePage={onChangePage}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
additionalControls={alertBulkActions}
|
||||
unitCountText={unitCountText}
|
||||
/>
|
||||
)}
|
||||
</StatefulEventContext.Provider>
|
||||
</ScrollableFlexItem>
|
||||
</FullWidthFlexGroupTable>
|
||||
)}
|
||||
</EventsContainerLoading>
|
||||
</TableContext.Provider>
|
||||
)}
|
||||
</StyledEuiPanel>
|
||||
</InspectButtonContainer>
|
||||
</FullScreenContainer>
|
||||
{DetailsPanel}
|
||||
|
@ -244,4 +603,15 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const StatefulEventsViewer = React.memo(StatefulEventsViewerComponent);
|
||||
const mapDispatchToProps = {
|
||||
clearSelected: dataTableActions.clearSelected,
|
||||
setSelected: dataTableActions.setSelected,
|
||||
};
|
||||
|
||||
const connector = connect(undefined, mapDispatchToProps);
|
||||
|
||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export const StatefulEventsViewer: React.FunctionComponent<EventsViewerProps> = connector(
|
||||
StatefulEventsViewerComponent
|
||||
);
|
||||
|
|
|
@ -12,4 +12,10 @@ export const mockEventViewerResponse = {
|
|||
fakeTotalCount: 100,
|
||||
},
|
||||
events: [],
|
||||
inspect: {
|
||||
dsl: [],
|
||||
response: [],
|
||||
},
|
||||
loadPage: jest.fn(),
|
||||
refetch: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { TableId } from '../../../../common/types';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
import { InspectButton } from '../inspect';
|
||||
import { UpdatedFlexGroup, UpdatedFlexItem } from './styles';
|
||||
import type { ViewSelection } from './summary_view_select';
|
||||
import { SummaryViewSelector } from './summary_view_select';
|
||||
|
||||
const TitleText = styled.span`
|
||||
margin-right: 12px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
tableView: ViewSelection;
|
||||
loading: boolean;
|
||||
tableId: TableId;
|
||||
title: string;
|
||||
onViewChange: (viewSelection: ViewSelection) => void;
|
||||
additionalFilters?: React.ReactNode;
|
||||
hasRightOffset?: boolean;
|
||||
}
|
||||
|
||||
export const RightTopMenu = ({
|
||||
tableView,
|
||||
loading,
|
||||
tableId,
|
||||
title,
|
||||
onViewChange,
|
||||
additionalFilters,
|
||||
hasRightOffset,
|
||||
}: Props) => {
|
||||
const alignItems = tableView === 'gridView' ? 'baseline' : 'center';
|
||||
const justTitle = useMemo(() => <TitleText data-test-subj="title">{title}</TitleText>, [title]);
|
||||
|
||||
const tGridEventRenderedViewEnabled = useIsExperimentalFeatureEnabled(
|
||||
'tGridEventRenderedViewEnabled'
|
||||
);
|
||||
return (
|
||||
<UpdatedFlexGroup
|
||||
alignItems={alignItems}
|
||||
data-test-subj="events-viewer-updated"
|
||||
gutterSize="m"
|
||||
justifyContent="flexEnd"
|
||||
$hasRightOffset={hasRightOffset}
|
||||
>
|
||||
<UpdatedFlexItem grow={false} $show={!loading}>
|
||||
<InspectButton title={justTitle} queryId={tableId} />
|
||||
</UpdatedFlexItem>
|
||||
<UpdatedFlexItem grow={false} $show={!loading}>
|
||||
{additionalFilters}
|
||||
</UpdatedFlexItem>
|
||||
{tGridEventRenderedViewEnabled &&
|
||||
[TableId.alertsOnRuleDetailsPage, TableId.alertsOnAlertsPage].includes(tableId) && (
|
||||
<UpdatedFlexItem grow={false} $show={!loading}>
|
||||
<SummaryViewSelector viewSelected={tableView} onViewChange={onViewChange} />
|
||||
</UpdatedFlexItem>
|
||||
)}
|
||||
</UpdatedFlexGroup>
|
||||
);
|
||||
};
|
|
@ -26,7 +26,7 @@ const heights = {
|
|||
|
||||
export const TableContext = createContext<{ tableId: string | null }>({ tableId: null });
|
||||
|
||||
export const TGridLoading: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => {
|
||||
export const TableLoading: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => {
|
||||
return (
|
||||
<EuiPanel color="subdued">
|
||||
<EuiFlexGroup
|
||||
|
@ -47,7 +47,7 @@ const panelStyle = {
|
|||
maxWidth: 500,
|
||||
};
|
||||
|
||||
export const TGridEmpty: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => {
|
||||
export const EmptyTable: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => {
|
||||
const { http } = useKibana<CoreStart>().services;
|
||||
|
||||
return (
|
||||
|
@ -61,14 +61,14 @@ export const TGridEmpty: React.FC<{ height?: keyof typeof heights }> = ({ height
|
|||
<EuiTitle>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.timelines.tgrid.empty.title"
|
||||
id="xpack.securitySolution.eventsViewer.empty.title"
|
||||
defaultMessage="No results match your search criteria"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.timelines.tgrid.empty.description"
|
||||
id="xpack.securitySolution.eventsViewer.empty.description"
|
||||
defaultMessage="Try searching over a longer period of time or modifying your search"
|
||||
/>
|
||||
</p>
|
|
@ -6,6 +6,11 @@
|
|||
*/
|
||||
|
||||
import { createContext } from 'react';
|
||||
import { StatefulEventContextType } from '../types';
|
||||
export interface StatefulEventContextType {
|
||||
tabType: string | undefined;
|
||||
timelineID: string;
|
||||
enableHostDetailsFlyout: boolean;
|
||||
enableIpDetailsFlyout: boolean;
|
||||
}
|
||||
|
||||
export const StatefulEventContext = createContext<StatefulEventContextType | null>(null);
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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, EuiPanel } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
export const SELECTOR_TIMELINE_GLOBAL_CONTAINER = 'securitySolutionTimeline__container';
|
||||
export const EVENTS_TABLE_CLASS_NAME = 'siemEventsTable';
|
||||
|
||||
export const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>`
|
||||
height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : undefined)};
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const FullWidthFlexGroupTable = styled(EuiFlexGroup)<{ $visible: boolean }>`
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
display: ${({ $visible }) => ($visible ? 'flex' : 'none')};
|
||||
`;
|
||||
|
||||
export const ScrollableFlexItem = styled(EuiFlexItem)`
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible?: boolean }>`
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
min-height: 490px;
|
||||
display: ${({ $visible = true }) => ($visible ? 'flex' : 'none')};
|
||||
`;
|
||||
|
||||
export const UpdatedFlexGroup = styled(EuiFlexGroup)<{
|
||||
$hasRightOffset?: boolean;
|
||||
}>`
|
||||
${({ $hasRightOffset, theme }) =>
|
||||
$hasRightOffset
|
||||
? `margin-right: ${theme.eui.euiSizeXL};`
|
||||
: `margin-right: ${theme.eui.euiSizeXS};`}
|
||||
position: absolute;
|
||||
z-index: ${({ theme }) => theme.eui.euiZLevel1 - 3};
|
||||
${({ $hasRightOffset, theme }) =>
|
||||
$hasRightOffset ? `right: ${theme.eui.euiSizeXL};` : `right: ${theme.eui.euiSizeXS};`}
|
||||
`;
|
||||
|
||||
export const UpdatedFlexItem = styled(EuiFlexItem)<{ $show: boolean }>`
|
||||
${({ $show }) => ($show ? '' : 'visibility: hidden;')}
|
||||
`;
|
||||
|
||||
export const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`,
|
||||
}))`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
${({ $isFullScreen }) =>
|
||||
$isFullScreen &&
|
||||
`
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
`}
|
||||
`;
|
|
@ -5,20 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiPopover,
|
||||
EuiSelectable,
|
||||
EuiSelectableOption,
|
||||
EuiTitle,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import type { EuiSelectableOption } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiPopover, EuiSelectable, EuiTitle, EuiTextColor } from '@elastic/eui';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ALERTS_TABLE_VIEW_SELECTION_KEY } from '../../helpers';
|
||||
import { ALERTS_TABLE_VIEW_SELECTION_KEY } from '../../event_rendered_view/helpers';
|
||||
|
||||
const storage = new Storage(localStorage);
|
||||
|
||||
|
@ -32,12 +25,12 @@ const ContainerEuiSelectable = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
const gridView = i18n.translate('xpack.timelines.alerts.summaryView.gridView.label', {
|
||||
const gridView = i18n.translate('xpack.securitySolution.selector.summaryView.gridView.label', {
|
||||
defaultMessage: 'Grid view',
|
||||
});
|
||||
|
||||
const eventRenderedView = i18n.translate(
|
||||
'xpack.timelines.alerts.summaryView.eventRendererView.label',
|
||||
'xpack.securitySolution.selector.summaryView.eventRendererView.label',
|
||||
{
|
||||
defaultMessage: 'Event rendered view',
|
||||
}
|
||||
|
@ -91,10 +84,13 @@ const SummaryViewSelectorComponent = ({ viewSelected, onViewChange }: SummaryVie
|
|||
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',
|
||||
}),
|
||||
text: i18n.translate(
|
||||
'xpack.securitySolution.selector.summaryView.options.default.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'View as tabular data with the ability to group and sort by specific fields',
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -107,7 +103,7 @@ const SummaryViewSelectorComponent = ({ viewSelected, onViewChange }: SummaryVie
|
|||
meta: [
|
||||
{
|
||||
text: i18n.translate(
|
||||
'xpack.timelines.alerts.summaryView.options.summaryView.description',
|
||||
'xpack.securitySolution.selector.summaryView.options.summaryView.description',
|
||||
{
|
||||
defaultMessage: 'View a rendering of the event flow for each alert',
|
||||
}
|
|
@ -24,3 +24,10 @@ export const UNIT = (totalCount: number) =>
|
|||
export const ACTIONS = i18n.translate('xpack.securitySolution.eventsViewer.actionsColumnLabel', {
|
||||
defaultMessage: 'Actions',
|
||||
});
|
||||
|
||||
export const ERROR_TIMELINE_EVENTS = i18n.translate(
|
||||
'xpack.securitySolution.eventsViewer.timelineEvents.errorSearchDescription',
|
||||
{
|
||||
defaultMessage: `An error has occurred on timeline events search`,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React, { lazy, Suspense, useMemo } from 'react';
|
||||
import type { TimelineItem } from '../../../../common/search_strategy';
|
||||
import type { AlertWorkflowStatus, Refetch } from '../../types';
|
||||
import type { BulkActionsProp } from '../toolbar/bulk_actions/types';
|
||||
|
||||
const StatefulAlertBulkActions = lazy(() => import('../toolbar/bulk_actions/alert_bulk_actions'));
|
||||
|
||||
interface OwnProps {
|
||||
tableId: string;
|
||||
data: TimelineItem[];
|
||||
totalItems: number;
|
||||
refetch: Refetch;
|
||||
indexNames: string[];
|
||||
hasAlertsCrud: boolean;
|
||||
showCheckboxes: boolean;
|
||||
filterStatus?: AlertWorkflowStatus;
|
||||
filterQuery?: string;
|
||||
bulkActions?: BulkActionsProp;
|
||||
selectedCount?: number;
|
||||
}
|
||||
export const useAlertBulkActions = ({
|
||||
tableId,
|
||||
data,
|
||||
totalItems,
|
||||
refetch,
|
||||
indexNames,
|
||||
hasAlertsCrud,
|
||||
showCheckboxes,
|
||||
filterStatus,
|
||||
filterQuery,
|
||||
bulkActions,
|
||||
selectedCount,
|
||||
}: OwnProps) => {
|
||||
const showBulkActions = useMemo(() => {
|
||||
if (!hasAlertsCrud) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedCount === 0 || !showCheckboxes) {
|
||||
return false;
|
||||
}
|
||||
if (typeof bulkActions === 'boolean') {
|
||||
return bulkActions;
|
||||
}
|
||||
return (bulkActions?.customBulkActions?.length || bulkActions?.alertStatusActions) ?? true;
|
||||
}, [hasAlertsCrud, selectedCount, showCheckboxes, bulkActions]);
|
||||
|
||||
const onAlertStatusActionSuccess = useMemo(() => {
|
||||
if (bulkActions && bulkActions !== true) {
|
||||
return bulkActions.onAlertStatusActionSuccess;
|
||||
}
|
||||
}, [bulkActions]);
|
||||
|
||||
const onAlertStatusActionFailure = useMemo(() => {
|
||||
if (bulkActions && bulkActions !== true) {
|
||||
return bulkActions.onAlertStatusActionFailure;
|
||||
}
|
||||
}, [bulkActions]);
|
||||
|
||||
const showAlertStatusActions = useMemo(() => {
|
||||
if (!hasAlertsCrud) {
|
||||
return false;
|
||||
}
|
||||
if (typeof bulkActions === 'boolean') {
|
||||
return bulkActions;
|
||||
}
|
||||
return (bulkActions && bulkActions.alertStatusActions) ?? true;
|
||||
}, [bulkActions, hasAlertsCrud]);
|
||||
|
||||
const additionalBulkActions = useMemo(() => {
|
||||
if (bulkActions && bulkActions !== true && bulkActions.customBulkActions !== undefined) {
|
||||
return bulkActions.customBulkActions.map((action) => {
|
||||
return {
|
||||
...action,
|
||||
onClick: (eventIds: string[]) => {
|
||||
const items = data.filter((item) => {
|
||||
return eventIds.find((event) => item._id === event);
|
||||
});
|
||||
action.onClick(items);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}, [bulkActions, data]);
|
||||
const alertBulkActions = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{showBulkActions && (
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<StatefulAlertBulkActions
|
||||
showAlertStatusActions={showAlertStatusActions}
|
||||
data-test-subj="bulk-actions"
|
||||
id={tableId}
|
||||
totalItems={totalItems}
|
||||
filterStatus={filterStatus}
|
||||
query={filterQuery}
|
||||
indexName={indexNames.join()}
|
||||
onActionSuccess={onAlertStatusActionSuccess}
|
||||
onActionFailure={onAlertStatusActionFailure}
|
||||
customBulkActions={additionalBulkActions}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[
|
||||
additionalBulkActions,
|
||||
filterQuery,
|
||||
filterStatus,
|
||||
indexNames,
|
||||
onAlertStatusActionFailure,
|
||||
onAlertStatusActionSuccess,
|
||||
refetch,
|
||||
showAlertStatusActions,
|
||||
showBulkActions,
|
||||
tableId,
|
||||
totalItems,
|
||||
]
|
||||
);
|
||||
return alertBulkActions;
|
||||
};
|
|
@ -10,40 +10,33 @@ import { isEmpty, isString, noop } from 'lodash/fp';
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common';
|
||||
import {
|
||||
clearEventsLoading,
|
||||
clearEventsDeleted,
|
||||
setTableUpdatedAt,
|
||||
updateGraphEventId,
|
||||
updateTotalCount,
|
||||
} from '../store/t_grid/actions';
|
||||
import {
|
||||
Direction,
|
||||
TimelineFactoryQueryTypes,
|
||||
TimelineEventsQueries,
|
||||
EntityType,
|
||||
} from '../../common/search_strategy';
|
||||
import type {
|
||||
Inspect,
|
||||
PaginationInputPaginated,
|
||||
TimelineStrategyResponseType,
|
||||
TimelineEdges,
|
||||
TimelineEventsAllRequestOptions,
|
||||
TimelineEventsAllStrategyResponse,
|
||||
TimelineItem,
|
||||
} from '@kbn/timelines-plugin/common';
|
||||
import type {
|
||||
EntityType,
|
||||
TimelineFactoryQueryTypes,
|
||||
TimelineRequestSortField,
|
||||
} from '../../common/search_strategy';
|
||||
import type { ESQuery } from '../../common/typed_json';
|
||||
import type { KueryFilterQueryKind, AlertStatus } from '../../common/types/timeline';
|
||||
import { useAppToasts } from '../hooks/use_app_toasts';
|
||||
import { TableId } from '../store/t_grid/types';
|
||||
import * as i18n from './translations';
|
||||
import { getSearchTransactionName, useStartTransaction } from '../lib/apm/use_start_transaction';
|
||||
|
||||
TimelineStrategyResponseType,
|
||||
} from '@kbn/timelines-plugin/common/search_strategy';
|
||||
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { TimelineEventsQueries } from '../../../../common/search_strategy';
|
||||
import type { KueryFilterQueryKind } from '../../../../common/types';
|
||||
import { Direction, TableId } from '../../../../common/types';
|
||||
import type { ESQuery } from '../../../../common/typed_json';
|
||||
import { useAppToasts } from '../../hooks/use_app_toasts';
|
||||
import { dataTableActions } from '../../store/data_table';
|
||||
import { ERROR_TIMELINE_EVENTS } from './translations';
|
||||
import type { AlertWorkflowStatus } from '../../types';
|
||||
import { getSearchTransactionName, useStartTransaction } from '../../lib/apm/use_start_transaction';
|
||||
export type InspectResponse = Inspect & { response: string[] };
|
||||
|
||||
export const detectionsTimelineIds = [TableId.alertsOnAlertsPage, TableId.alertsOnRuleDetailsPage];
|
||||
|
@ -90,7 +83,7 @@ export interface UseTimelineEventsProps {
|
|||
sort?: TimelineRequestSortField[];
|
||||
startDate: string;
|
||||
timerangeKind?: 'absolute' | 'relative';
|
||||
filterStatus?: AlertStatus;
|
||||
filterStatus?: AlertWorkflowStatus;
|
||||
}
|
||||
|
||||
const createFilter = (filterQuery: ESQuery | string | undefined) =>
|
||||
|
@ -108,7 +101,7 @@ const getInspectResponse = <T extends TimelineFactoryQueryTypes>(
|
|||
response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response,
|
||||
});
|
||||
|
||||
const ID = 'timelineEventsQuery';
|
||||
const ID = 'eventsQuery';
|
||||
export const initSortDefault = [
|
||||
{
|
||||
direction: Direction.desc,
|
||||
|
@ -118,7 +111,7 @@ export const initSortDefault = [
|
|||
},
|
||||
];
|
||||
|
||||
const useApmTracking = (timelineId: string) => {
|
||||
const useApmTracking = (tableId: string) => {
|
||||
const { startTransaction } = useStartTransaction();
|
||||
|
||||
const startTracking = useCallback(() => {
|
||||
|
@ -126,7 +119,7 @@ const useApmTracking = (timelineId: string) => {
|
|||
// The managed flag can be turned on to investigate high latency requests in APM.
|
||||
// However, note that by enabling the managed flag, the transaction trace may be distorted by other requests information.
|
||||
const transaction = startTransaction({
|
||||
name: getSearchTransactionName(timelineId),
|
||||
name: getSearchTransactionName(tableId),
|
||||
type: 'http-request',
|
||||
options: { managed: false },
|
||||
});
|
||||
|
@ -139,7 +132,7 @@ const useApmTracking = (timelineId: string) => {
|
|||
span?.end();
|
||||
},
|
||||
};
|
||||
}, [startTransaction, timelineId]);
|
||||
}, [startTransaction, tableId]);
|
||||
|
||||
return { startTracking };
|
||||
};
|
||||
|
@ -179,8 +172,8 @@ export const useTimelineEventsHandler = ({
|
|||
|
||||
const clearSignalsState = useCallback(() => {
|
||||
if (id != null && detectionsTimelineIds.some((timelineId) => timelineId === id)) {
|
||||
dispatch(clearEventsLoading({ id }));
|
||||
dispatch(clearEventsDeleted({ id }));
|
||||
dispatch(dataTableActions.clearEventsLoading({ id }));
|
||||
dispatch(dataTableActions.clearEventsDeleted({ id }));
|
||||
}
|
||||
}, [dispatch, id]);
|
||||
|
||||
|
@ -201,13 +194,13 @@ export const useTimelineEventsHandler = ({
|
|||
|
||||
const setUpdated = useCallback(
|
||||
(updatedAt: number) => {
|
||||
dispatch(setTableUpdatedAt({ id, updated: updatedAt }));
|
||||
dispatch(dataTableActions.setTableUpdatedAt({ id, updated: updatedAt }));
|
||||
},
|
||||
[dispatch, id]
|
||||
);
|
||||
|
||||
const setTotalCount = useCallback(
|
||||
(totalCount: number) => dispatch(updateTotalCount({ id, totalCount })),
|
||||
(totalCount: number) => dispatch(dataTableActions.updateTotalCount({ id, totalCount })),
|
||||
[dispatch, id]
|
||||
);
|
||||
|
||||
|
@ -243,7 +236,6 @@ export const useTimelineEventsHandler = ({
|
|||
if (data && data.search) {
|
||||
const { endTracking } = startTracking();
|
||||
const abortSignal = abortCtrl.current.signal;
|
||||
|
||||
searchSubscription$.current = data.search
|
||||
.search<TimelineRequest<typeof language>, TimelineResponse<typeof language>>(
|
||||
{ ...request, entityType },
|
||||
|
@ -260,7 +252,6 @@ export const useTimelineEventsHandler = ({
|
|||
.subscribe({
|
||||
next: (response) => {
|
||||
if (isCompleteResponse(response)) {
|
||||
endTracking('success');
|
||||
setTimelineResponse((prevResponse) => {
|
||||
const newTimelineResponse = {
|
||||
...prevResponse,
|
||||
|
@ -277,21 +268,20 @@ export const useTimelineEventsHandler = ({
|
|||
return newTimelineResponse;
|
||||
});
|
||||
if (prevFilterStatus !== request.filterStatus) {
|
||||
dispatch(updateGraphEventId({ id, graphEventId: '' }));
|
||||
dispatch(dataTableActions.updateGraphEventId({ id, graphEventId: '' }));
|
||||
}
|
||||
setFilterStatus(request.filterStatus);
|
||||
setLoading(false);
|
||||
|
||||
searchSubscription$.current.unsubscribe();
|
||||
} else if (isErrorResponse(response)) {
|
||||
endTracking('invalid');
|
||||
setLoading(false);
|
||||
addWarning(i18n.ERROR_TIMELINE_EVENTS);
|
||||
endTracking('invalid');
|
||||
addWarning(ERROR_TIMELINE_EVENTS);
|
||||
searchSubscription$.current.unsubscribe();
|
||||
}
|
||||
},
|
||||
error: (msg) => {
|
||||
endTracking(abortSignal.aborted ? 'aborted' : 'error');
|
||||
setLoading(false);
|
||||
data.search.showError(msg);
|
||||
searchSubscription$.current.unsubscribe();
|
|
@ -13,7 +13,6 @@ import { useTourContext } from './tour';
|
|||
import { mockGlobalState, SUB_PLUGINS_REDUCER, TestProviders } from '../../mock';
|
||||
import { TimelineId } from '../../../../common/types';
|
||||
import { createStore } from '../../store';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
import { kibanaObservable } from '@kbn/timelines-plugin/public/mock';
|
||||
import { createSecuritySolutionStorageMock } from '@kbn/timelines-plugin/public/mock/mock_local_storage';
|
||||
|
||||
|
@ -274,13 +273,7 @@ describe('SecurityTourStep', () => {
|
|||
},
|
||||
};
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const mockStore = createStore(
|
||||
mockstate,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const mockStore = createStore(mockstate, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
render(
|
||||
<TestProviders store={mockStore}>
|
||||
|
|
|
@ -8,9 +8,8 @@
|
|||
import type { MouseEvent } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiContextMenuItem, EuiButtonIcon, EuiToolTip, EuiText } from '@elastic/eui';
|
||||
|
||||
import { DEFAULT_ACTION_BUTTON_WIDTH } from '@kbn/timelines-plugin/public';
|
||||
import { EventsTdContent } from '../../styles';
|
||||
import { EventsTdContent } from '../../../timelines/components/timeline/styles';
|
||||
import { DEFAULT_ACTION_BUTTON_WIDTH } from '.';
|
||||
|
||||
interface ActionIconItemProps {
|
||||
ariaLabel?: string;
|
|
@ -7,33 +7,30 @@
|
|||
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { TableId, TimelineId } from '../../../../../../common/types/timeline';
|
||||
import { TestProviders, mockTimelineModel, mockTimelineData } from '../../../../../common/mock';
|
||||
import { Actions, isAlert } from '.';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
|
||||
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { licenseService } from '../../../../../common/hooks/use_license';
|
||||
import { useTourContext } from '../../../../../common/components/guided_onboarding_tour';
|
||||
import {
|
||||
GuidedOnboardingTourStep,
|
||||
SecurityTourStep,
|
||||
} from '../../../../../common/components/guided_onboarding_tour/tour_step';
|
||||
import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../../../../../common/components/user_privileges/user_privileges_context';
|
||||
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
|
||||
import { SecurityStepId } from '../../../../../common/components/guided_onboarding_tour/tour_config';
|
||||
import { mockTimelineData, mockTimelineModel, TestProviders } from '../../mock';
|
||||
import { useShallowEqualSelector } from '../../hooks/use_selector';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
import { licenseService } from '../../hooks/use_license';
|
||||
import { TableId } from '../../../../common/types';
|
||||
import { useTourContext } from '../guided_onboarding_tour';
|
||||
import { GuidedOnboardingTourStep, SecurityTourStep } from '../guided_onboarding_tour/tour_step';
|
||||
import { SecurityStepId } from '../guided_onboarding_tour/tour_config';
|
||||
import { Actions } from './actions';
|
||||
import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../user_privileges/user_privileges_context';
|
||||
import { useUserPrivileges } from '../user_privileges';
|
||||
|
||||
jest.mock('../../../../../common/components/user_privileges');
|
||||
jest.mock('../../../../../common/components/guided_onboarding_tour');
|
||||
jest.mock('../../../../../detections/components/user_info', () => ({
|
||||
jest.mock('../guided_onboarding_tour');
|
||||
jest.mock('../user_privileges');
|
||||
jest.mock('../../../detections/components/user_info', () => ({
|
||||
useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]),
|
||||
}));
|
||||
jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
|
||||
jest.mock('../../hooks/use_experimental_features', () => ({
|
||||
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
jest.mock('../../../../../common/hooks/use_selector');
|
||||
jest.mock('../../hooks/use_selector');
|
||||
jest.mock(
|
||||
'../../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline',
|
||||
'../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline',
|
||||
() => ({
|
||||
useInvestigateInTimeline: jest.fn().mockReturnValue({
|
||||
investigateInTimelineActionItems: [],
|
||||
|
@ -43,8 +40,8 @@ jest.mock(
|
|||
})
|
||||
);
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana', () => {
|
||||
const originalKibanaLib = jest.requireActual('../../../../../common/lib/kibana');
|
||||
jest.mock('../../lib/kibana', () => {
|
||||
const originalKibanaLib = jest.requireActual('../../lib/kibana');
|
||||
|
||||
return {
|
||||
useKibana: () => ({
|
||||
|
@ -69,6 +66,7 @@ jest.mock('../../../../../common/lib/kibana', () => {
|
|||
addError: jest.fn(),
|
||||
addSuccess: jest.fn(),
|
||||
addWarning: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
}),
|
||||
useNavigateTo: jest.fn().mockReturnValue({
|
||||
navigateTo: jest.fn(),
|
||||
|
@ -77,7 +75,7 @@ jest.mock('../../../../../common/lib/kibana', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_license', () => {
|
||||
jest.mock('../../hooks/use_license', () => {
|
||||
const licenseServiceInstance = {
|
||||
isPlatinumPlus: jest.fn(),
|
||||
isEnterprise: jest.fn(() => false),
|
||||
|
@ -110,7 +108,7 @@ const defaultProps = {
|
|||
setEventsLoading: () => {},
|
||||
showCheckboxes: true,
|
||||
showNotes: false,
|
||||
timelineId: TimelineId.test,
|
||||
timelineId: 'test',
|
||||
toggleShowNotes: () => {},
|
||||
};
|
||||
|
||||
|
@ -410,7 +408,7 @@ describe('Actions', () => {
|
|||
<Actions
|
||||
{...defaultProps}
|
||||
ecsData={ecsData}
|
||||
timelineId={TableId.kubernetesPageSessions} // not a bug, this needs to be fixed by providing a generic interface for actions registry
|
||||
timelineId={TableId.kubernetesPageSessions}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -439,14 +437,4 @@ describe('Actions', () => {
|
|||
expect(wrapper.find('[data-test-subj="session-view-button"]').exists()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAlert', () => {
|
||||
test('it returns true when the eventType is an alert', () => {
|
||||
expect(isAlert('signal')).toBe(true);
|
||||
});
|
||||
|
||||
test('it returns false when the eventType is NOT an alert', () => {
|
||||
expect(isAlert('raw')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -10,44 +10,35 @@ import { useDispatch } from 'react-redux';
|
|||
import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { DEFAULT_ACTION_BUTTON_WIDTH } from '@kbn/timelines-plugin/public';
|
||||
import { GuidedOnboardingTourStep } from '../../../../../common/components/guided_onboarding_tour/tour_step';
|
||||
import { isDetectionsAlertsTable } from '../../../../../common/components/top_n/helpers';
|
||||
import { useTourContext } from '../../../../../common/components/guided_onboarding_tour';
|
||||
import {
|
||||
AlertsCasesTourSteps,
|
||||
SecurityStepId,
|
||||
} from '../../../../../common/components/guided_onboarding_tour/tour_config';
|
||||
import { getScopedActions, isTimelineScope } from '../../../../../helpers';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { eventHasNotes, getEventType, getPinOnClick } from '../helpers';
|
||||
import { AlertContextMenu } from '../../../../../detections/components/alerts_table/timeline_actions/alert_context_menu';
|
||||
import { InvestigateInTimelineAction } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action';
|
||||
eventHasNotes,
|
||||
getEventType,
|
||||
getPinOnClick,
|
||||
} from '../../../timelines/components/timeline/body/helpers';
|
||||
import { getScopedActions, isTimelineScope } from '../../../helpers';
|
||||
import { isInvestigateInResolverActionEnabled } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver';
|
||||
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
|
||||
import type { ActionProps, OnPinEvent } from '../../../../common/types';
|
||||
import { TableId, TimelineId, TimelineTabs } from '../../../../common/types';
|
||||
import { AddEventNoteAction } from './add_note_icon_item';
|
||||
import { PinEventAction } from './pin_event_action';
|
||||
import { EventsTdContent } from '../../styles';
|
||||
import * as i18n from '../translations';
|
||||
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { setActiveTabTimeline } from '../../../../store/timeline/actions';
|
||||
import {
|
||||
useGlobalFullScreen,
|
||||
useTimelineFullScreen,
|
||||
} from '../../../../../common/containers/use_full_screen';
|
||||
import type {
|
||||
ActionProps,
|
||||
OnPinEvent,
|
||||
TimelineEventsType,
|
||||
} from '../../../../../../common/types/timeline';
|
||||
import { TableId, TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { timelineActions, timelineSelectors } from '../../../../store/timeline';
|
||||
import { timelineDefaults } from '../../../../store/timeline/defaults';
|
||||
import { isInvestigateInResolverActionEnabled } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver';
|
||||
import { useStartTransaction } from '../../../../../common/lib/apm/use_start_transaction';
|
||||
import { ALERTS_ACTIONS } from '../../../../../common/lib/apm/user_actions';
|
||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
||||
|
||||
export const isAlert = (eventType: TimelineEventsType | Omit<TimelineEventsType, 'all'>): boolean =>
|
||||
eventType === 'signal';
|
||||
import { useShallowEqualSelector } from '../../hooks/use_selector';
|
||||
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
||||
import { useStartTransaction } from '../../lib/apm/use_start_transaction';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { useGlobalFullScreen, useTimelineFullScreen } from '../../containers/use_full_screen';
|
||||
import { ALERTS_ACTIONS } from '../../lib/apm/user_actions';
|
||||
import { setActiveTabTimeline } from '../../../timelines/store/timeline/actions';
|
||||
import { EventsTdContent } from '../../../timelines/components/timeline/styles';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
import { AlertContextMenu } from '../../../detections/components/alerts_table/timeline_actions/alert_context_menu';
|
||||
import { InvestigateInTimelineAction } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action';
|
||||
import * as i18n from './translations';
|
||||
import { useTourContext } from '../guided_onboarding_tour';
|
||||
import { AlertsCasesTourSteps, SecurityStepId } from '../guided_onboarding_tour/tour_config';
|
||||
import { isDetectionsAlertsTable } from '../top_n/helpers';
|
||||
import { GuidedOnboardingTourStep } from '../guided_onboarding_tour/tour_step';
|
||||
import { DEFAULT_ACTION_BUTTON_WIDTH, isAlert } from './helpers';
|
||||
|
||||
const ActionsContainer = styled.div`
|
||||
align-items: center;
|
|
@ -5,16 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TimelineType } from '../../../../common/types';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TestProviders } from '../../mock';
|
||||
import { useUserPrivileges } from '../user_privileges';
|
||||
import { getEndpointPrivilegesInitialStateMock } from '../user_privileges/endpoint/mocks';
|
||||
|
||||
import { AddEventNoteAction } from './add_note_icon_item';
|
||||
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
|
||||
import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { TimelineType } from '../../../../../../common/types';
|
||||
|
||||
jest.mock('../../../../../common/components/user_privileges');
|
||||
jest.mock('../user_privileges');
|
||||
const useUserPrivilegesMock = useUserPrivileges as jest.Mock;
|
||||
|
||||
describe('AddEventNoteAction', () => {
|
|
@ -6,12 +6,11 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { TimelineType } from '../../../../../../common/types/timeline';
|
||||
import * as i18n from '../translations';
|
||||
import { NotesButton } from '../../properties/helpers';
|
||||
import { NotesButton } from '../../../timelines/components/timeline/properties/helpers';
|
||||
import { TimelineType } from '../../../../common/types';
|
||||
import { useUserPrivileges } from '../user_privileges';
|
||||
import * as i18n from './translations';
|
||||
import { ActionIconItem } from './action_icon_item';
|
||||
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
|
||||
|
||||
interface AddEventNoteActionProps {
|
||||
ariaLabel?: string;
|
|
@ -7,19 +7,15 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { TestProviders, mockTimelineModel } from '../../../../../common/mock';
|
||||
import { mockTimelineModel, TestProviders } from '../../mock';
|
||||
import { mockTriggersActionsUi } from '../../mock/mock_triggers_actions_ui_plugin';
|
||||
import type { ColumnHeaderOptions, HeaderActionProps } from '../../../../common/types';
|
||||
import { TimelineTabs } from '../../../../common/types';
|
||||
import { HeaderActions } from './header_actions';
|
||||
import { mockTriggersActionsUi } from '../../../../../common/mock/mock_triggers_actions_ui_plugin';
|
||||
import type {
|
||||
ColumnHeaderOptions,
|
||||
HeaderActionProps,
|
||||
} from '../../../../../../common/types/timeline';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { timelineActions } from '../../../../store/timeline';
|
||||
import { getColumnHeader } from '../column_headers/helpers';
|
||||
import { timelineActions } from '../../../timelines/store/timeline';
|
||||
import { getColumnHeader } from '../../../timelines/components/timeline/body/column_headers/helpers';
|
||||
|
||||
jest.mock('../../../row_renderers_browser', () => ({
|
||||
jest.mock('../../../timelines/components/row_renderers_browser', () => ({
|
||||
StatefulRowRenderersBrowser: () => null,
|
||||
}));
|
||||
|
||||
|
@ -29,13 +25,13 @@ jest.mock('react-redux', () => ({
|
|||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_selector', () => ({
|
||||
jest.mock('../../hooks/use_selector', () => ({
|
||||
useDeepEqualSelector: () => mockTimelineModel,
|
||||
useShallowEqualSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
const columnId = 'test-field';
|
||||
const timelineId = TimelineId.test;
|
||||
const timelineId = 'test-timeline';
|
||||
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
mockTriggersActionsUi.getFieldBrowser.mockImplementation(
|
||||
|
@ -53,7 +49,7 @@ mockTriggersActionsUi.getFieldBrowser.mockImplementation(
|
|||
)
|
||||
);
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana', () => ({
|
||||
jest.mock('../../lib/kibana', () => ({
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
triggersActionsUi: { ...mockTriggersActionsUi },
|
|
@ -11,25 +11,22 @@ import { EuiButtonIcon, EuiCheckbox, EuiToolTip, useDataGridColumnSorting } from
|
|||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { DEFAULT_ACTION_BUTTON_WIDTH } from '@kbn/timelines-plugin/public';
|
||||
import { isActiveTimeline } from '../../../../../helpers';
|
||||
import type { HeaderActionProps, SortDirection } from '../../../../../../common/types/timeline';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { EXIT_FULL_SCREEN } from '../../../../../common/components/exit_full_screen/translations';
|
||||
import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../../../common/constants';
|
||||
import {
|
||||
useGlobalFullScreen,
|
||||
useTimelineFullScreen,
|
||||
} from '../../../../../common/containers/use_full_screen';
|
||||
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser';
|
||||
import { EventsTh, EventsThContent } from '../../styles';
|
||||
import { EventsSelect } from '../column_headers/events_select';
|
||||
import * as i18n from '../column_headers/translations';
|
||||
import { timelineActions, timelineSelectors } from '../../../../store/timeline';
|
||||
import { isFullScreen } from '../column_headers';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { getColumnHeader } from '../column_headers/helpers';
|
||||
import type { HeaderActionProps, SortDirection } from '../../../../common/types';
|
||||
import { TimelineTabs, TimelineId } from '../../../../common/types';
|
||||
import { isFullScreen } from '../../../timelines/components/timeline/body/column_headers';
|
||||
import { isActiveTimeline } from '../../../helpers';
|
||||
import { getColumnHeader } from '../../../timelines/components/timeline/body/column_headers/helpers';
|
||||
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
|
||||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import { useGlobalFullScreen, useTimelineFullScreen } from '../../containers/use_full_screen';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { DEFAULT_ACTION_BUTTON_WIDTH } from '.';
|
||||
import { EventsTh, EventsThContent } from '../../../timelines/components/timeline/styles';
|
||||
import { StatefulRowRenderersBrowser } from '../../../timelines/components/row_renderers_browser';
|
||||
import { EXIT_FULL_SCREEN } from '../exit_full_screen/translations';
|
||||
import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants';
|
||||
import { EventsSelect } from '../../../timelines/components/timeline/body/column_headers/events_select';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const SortingColumnsContainer = styled.div`
|
||||
button {
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { DEFAULT_ACTION_BUTTON_WIDTH, getActionsColumnWidth, isAlert } from './helpers';
|
||||
|
||||
describe('isAlert', () => {
|
||||
test('it returns true when the eventType is an alert', () => {
|
||||
expect(isAlert('signal')).toBe(true);
|
||||
});
|
||||
|
||||
test('it returns false when the eventType is NOT an alert', () => {
|
||||
expect(isAlert('raw')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActionsColumnWidth', () => {
|
||||
// ideally the following implementation detail wouldn't be part of these tests,
|
||||
// but without it, the test would be brittle when `euiDataGridCellPaddingM` changes:
|
||||
const expectedPadding = parseInt(euiThemeVars.euiDataGridCellPaddingM, 10) * 2;
|
||||
|
||||
test('it returns the expected width', () => {
|
||||
const ACTION_BUTTON_COUNT = 5;
|
||||
const expectedContentWidth = ACTION_BUTTON_COUNT * DEFAULT_ACTION_BUTTON_WIDTH;
|
||||
|
||||
expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual(
|
||||
expectedContentWidth + expectedPadding
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns the minimum width when the button count is zero', () => {
|
||||
const ACTION_BUTTON_COUNT = 0;
|
||||
|
||||
expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual(
|
||||
DEFAULT_ACTION_BUTTON_WIDTH + expectedPadding
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns the minimum width when the button count is negative', () => {
|
||||
const ACTION_BUTTON_COUNT = -1;
|
||||
|
||||
expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual(
|
||||
DEFAULT_ACTION_BUTTON_WIDTH + expectedPadding
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { euiThemeVars } from '@kbn/ui-theme';
|
||||
import type { TimelineEventsType } from '../../../../common/types';
|
||||
|
||||
/**
|
||||
* This is the effective width in pixels of an action button used with
|
||||
* `EuiDataGrid` `leadingControlColumns`. (See Notes below for details)
|
||||
*
|
||||
* Notes:
|
||||
* 1) This constant is necessary because `width` is a required property of
|
||||
* the `EuiDataGridControlColumn` interface, so it must be calculated before
|
||||
* content is rendered. (The width of a `EuiDataGridControlColumn` does not
|
||||
* automatically size itself to fit all the content.)
|
||||
*
|
||||
* 2) This is the *effective* width, because at the time of this writing,
|
||||
* `EuiButtonIcon` has a `margin-left: -4px`, which is subtracted from the
|
||||
* `width`
|
||||
*/
|
||||
export const DEFAULT_ACTION_BUTTON_WIDTH =
|
||||
parseInt(euiThemeVars.euiSizeXL, 10) - parseInt(euiThemeVars.euiSizeXS, 10); // px
|
||||
|
||||
export const isAlert = (eventType: TimelineEventsType | Omit<TimelineEventsType, 'all'>): boolean =>
|
||||
eventType === 'signal';
|
||||
|
||||
/**
|
||||
* Returns the width of the Actions column based on the number of buttons being
|
||||
* displayed
|
||||
*
|
||||
* NOTE: This function is necessary because `width` is a required property of
|
||||
* the `EuiDataGridControlColumn` interface, so it must be calculated before
|
||||
* content is rendered. (The width of a `EuiDataGridControlColumn` does not
|
||||
* automatically size itself to fit all the content.)
|
||||
*/
|
||||
export const getActionsColumnWidth = (actionButtonCount: number): number => {
|
||||
const contentWidth =
|
||||
actionButtonCount > 0
|
||||
? actionButtonCount * DEFAULT_ACTION_BUTTON_WIDTH
|
||||
: DEFAULT_ACTION_BUTTON_WIDTH;
|
||||
|
||||
// `EuiDataGridRowCell` applies additional `padding-left` and
|
||||
// `padding-right`, which must be added to the content width to prevent the
|
||||
// content from being partially hidden due to the space occupied by padding:
|
||||
const leftRightCellPadding = parseInt(euiThemeVars.euiDataGridCellPaddingM, 10) * 2; // parseInt ignores the trailing `px`, e.g. `6px`
|
||||
|
||||
return contentWidth + leftRightCellPadding;
|
||||
};
|
|
@ -5,4 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const getEmptyValue = () => '—';
|
||||
export * from './helpers';
|
||||
export * from './actions';
|
||||
export * from './header_actions';
|
|
@ -9,12 +9,12 @@ import { render, screen } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
|
||||
import { PinEventAction } from './pin_event_action';
|
||||
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
|
||||
import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { TimelineType } from '../../../../../../common/types';
|
||||
import { useUserPrivileges } from '../user_privileges';
|
||||
import { getEndpointPrivilegesInitialStateMock } from '../user_privileges/endpoint/mocks';
|
||||
import { TestProviders } from '../../mock';
|
||||
import { TimelineType } from '../../../../common/types';
|
||||
|
||||
jest.mock('../../../../../common/components/user_privileges');
|
||||
jest.mock('../user_privileges');
|
||||
const useUserPrivilegesMock = useUserPrivileges as jest.Mock;
|
||||
|
||||
describe('PinEventAction', () => {
|
|
@ -7,13 +7,12 @@
|
|||
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { DEFAULT_ACTION_BUTTON_WIDTH } from '@kbn/timelines-plugin/public';
|
||||
import { EventsTdContent } from '../../styles';
|
||||
import { eventHasNotes, getPinTooltip } from '../helpers';
|
||||
import { Pin } from '../../pin';
|
||||
import type { TimelineType } from '../../../../../../common/types/timeline';
|
||||
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
|
||||
import { EventsTdContent } from '../../../timelines/components/timeline/styles';
|
||||
import { eventHasNotes, getPinTooltip } from '../../../timelines/components/timeline/body/helpers';
|
||||
import type { TimelineType } from '../../../../common/types';
|
||||
import { useUserPrivileges } from '../user_privileges';
|
||||
import { DEFAULT_ACTION_BUTTON_WIDTH } from '.';
|
||||
import { Pin } from '../../../timelines/components/timeline/pin';
|
||||
|
||||
interface PinEventActionProps {
|
||||
ariaLabel?: string;
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const OPEN_SESSION_VIEW = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.openSessionViewLabel',
|
||||
{
|
||||
defaultMessage: 'Open Session View',
|
||||
}
|
||||
);
|
||||
|
||||
export const NOTES_DISABLE_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.notes.disableEventTooltip',
|
||||
{
|
||||
defaultMessage: 'Notes may not be added here while editing a template timeline',
|
||||
}
|
||||
);
|
||||
|
||||
export const NOTES_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.notes.addNoteTooltip',
|
||||
{
|
||||
defaultMessage: 'Add note',
|
||||
}
|
||||
);
|
||||
|
||||
export const SORT_FIELDS = i18n.translate('xpack.securitySolution.timeline.sortFieldsButton', {
|
||||
defaultMessage: 'Sort fields',
|
||||
});
|
||||
|
||||
export const FULL_SCREEN = i18n.translate('xpack.securitySolution.timeline.fullScreenButton', {
|
||||
defaultMessage: 'Full screen',
|
||||
});
|
||||
|
||||
export const VIEW_DETAILS = i18n.translate(
|
||||
'xpack.securitySolution.hoverActions.viewDetailsAriaLabel',
|
||||
{
|
||||
defaultMessage: 'View details',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_DETAILS_FOR_ROW = ({
|
||||
ariaRowindex,
|
||||
columnValues,
|
||||
}: {
|
||||
ariaRowindex: number;
|
||||
columnValues: string;
|
||||
}) =>
|
||||
i18n.translate('xpack.securitySolution.hoverActions.viewDetailsForRowAriaLabel', {
|
||||
values: { ariaRowindex, columnValues },
|
||||
defaultMessage:
|
||||
'View details for the alert or event in row {ariaRowindex}, with columns {columnValues}',
|
||||
});
|
||||
|
||||
export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate(
|
||||
'xpack.securitySolution.hoverActions.investigateInResolverTooltip',
|
||||
{
|
||||
defaultMessage: 'Analyze event',
|
||||
}
|
||||
);
|
||||
|
||||
export const CHECKBOX_FOR_ROW = ({
|
||||
ariaRowindex,
|
||||
columnValues,
|
||||
checked,
|
||||
}: {
|
||||
ariaRowindex: number;
|
||||
columnValues: string;
|
||||
checked: boolean;
|
||||
}) =>
|
||||
i18n.translate('xpack.securitySolution.hoverActions.checkboxForRowAriaLabel', {
|
||||
values: { ariaRowindex, checked, columnValues },
|
||||
defaultMessage:
|
||||
'{checked, select, false {unchecked} true {checked}} checkbox for the alert or event in row {ariaRowindex}, with columns {columnValues}',
|
||||
});
|
||||
|
||||
export const ACTION_INVESTIGATE_IN_RESOLVER_FOR_ROW = ({
|
||||
ariaRowindex,
|
||||
columnValues,
|
||||
}: {
|
||||
ariaRowindex: number;
|
||||
columnValues: string;
|
||||
}) =>
|
||||
i18n.translate('xpack.securitySolution.hoverActions.investigateInResolverForRowAriaLabel', {
|
||||
values: { ariaRowindex, columnValues },
|
||||
defaultMessage: 'Analyze the alert or event in row {ariaRowindex}, with columns {columnValues}',
|
||||
});
|
||||
|
||||
export const SEND_ALERT_TO_TIMELINE_FOR_ROW = ({
|
||||
ariaRowindex,
|
||||
columnValues,
|
||||
}: {
|
||||
ariaRowindex: number;
|
||||
columnValues: string;
|
||||
}) =>
|
||||
i18n.translate('xpack.securitySolution.hoverActions.sendAlertToTimelineForRowAriaLabel', {
|
||||
values: { ariaRowindex, columnValues },
|
||||
defaultMessage: 'Send the alert in row {ariaRowindex} to timeline, with columns {columnValues}',
|
||||
});
|
||||
|
||||
export const ADD_NOTES_FOR_ROW = ({
|
||||
ariaRowindex,
|
||||
columnValues,
|
||||
}: {
|
||||
ariaRowindex: number;
|
||||
columnValues: string;
|
||||
}) =>
|
||||
i18n.translate('xpack.securitySolution.hoverActions.addNotesForRowAriaLabel', {
|
||||
values: { ariaRowindex, columnValues },
|
||||
defaultMessage:
|
||||
'Add notes for the event in row {ariaRowindex} to timeline, with columns {columnValues}',
|
||||
});
|
||||
|
||||
export const PIN_EVENT_FOR_ROW = ({
|
||||
ariaRowindex,
|
||||
columnValues,
|
||||
isEventPinned,
|
||||
}: {
|
||||
ariaRowindex: number;
|
||||
columnValues: string;
|
||||
isEventPinned: boolean;
|
||||
}) =>
|
||||
i18n.translate('xpack.securitySolution.hoverActions.pinEventForRowAriaLabel', {
|
||||
values: { ariaRowindex, columnValues, isEventPinned },
|
||||
defaultMessage:
|
||||
'{isEventPinned, select, false {Pin} true {Unpin}} the event in row {ariaRowindex} to timeline, with columns {columnValues}',
|
||||
});
|
||||
|
||||
export const MORE_ACTIONS_FOR_ROW = ({
|
||||
ariaRowindex,
|
||||
columnValues,
|
||||
}: {
|
||||
ariaRowindex: number;
|
||||
columnValues: string;
|
||||
}) =>
|
||||
i18n.translate('xpack.securitySolution.hoverActions.moreActionsForRowAriaLabel', {
|
||||
values: { ariaRowindex, columnValues },
|
||||
defaultMessage:
|
||||
'Select more actions for the alert or event in row {ariaRowindex}, with columns {columnValues}',
|
||||
});
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
import React, { useCallback, useMemo, useState, useRef, useContext } from 'react';
|
||||
import type { DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd';
|
||||
import { TableContext } from '@kbn/timelines-plugin/public';
|
||||
import { TimelineContext } from '../../../timelines/components/timeline';
|
||||
import { HoverActions } from '.';
|
||||
|
||||
import type { DataProvider } from '../../../../common/types';
|
||||
import { ProviderContentWrapper } from '../drag_and_drop/draggable_wrapper';
|
||||
import { getDraggableId } from '../drag_and_drop/helpers';
|
||||
import { TableContext } from '../events_viewer/shared';
|
||||
import { useTopNPopOver } from './utils';
|
||||
|
||||
const draggableContainsLinks = (draggableElement: HTMLDivElement | null) => {
|
||||
|
|
|
@ -23,7 +23,6 @@ import { upsertQuery } from '../../store/inputs/helpers';
|
|||
import { InspectButton } from '.';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
|
||||
jest.mock('./modal', () => ({
|
||||
ModalInspectQuery: jest.fn(() => <div data-test-subj="mocker-modal" />),
|
||||
|
@ -42,13 +41,7 @@ describe('Inspect Button', () => {
|
|||
state: state.inputs,
|
||||
};
|
||||
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
describe('Render', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -57,7 +50,7 @@ describe('Inspect Button', () => {
|
|||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
@ -166,13 +159,7 @@ describe('Inspect Button', () => {
|
|||
const myQuery = cloneDeep(newQuery);
|
||||
myQuery.inspect = null;
|
||||
myState.inputs = upsertQuery(myQuery);
|
||||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<InspectButton queryId={newQuery.id} title="My title" />
|
||||
|
@ -189,13 +176,7 @@ describe('Inspect Button', () => {
|
|||
response: ['my response'],
|
||||
};
|
||||
myState.inputs = upsertQuery(myQuery);
|
||||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<InspectButton queryId={newQuery.id} title="My title" />
|
||||
|
@ -212,13 +193,7 @@ describe('Inspect Button', () => {
|
|||
response: [],
|
||||
};
|
||||
myState.inputs = upsertQuery(myQuery);
|
||||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<InspectButton queryId={newQuery.id} title="My title" />
|
||||
|
@ -237,13 +212,7 @@ describe('Inspect Button', () => {
|
|||
response: ['my response'],
|
||||
};
|
||||
myState.inputs = upsertQuery(myQuery);
|
||||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
});
|
||||
test('Open Inspect Modal', () => {
|
||||
const wrapper = mount(
|
||||
|
@ -288,13 +257,7 @@ describe('Inspect Button', () => {
|
|||
};
|
||||
myState.inputs = upsertQuery(myQuery);
|
||||
myState.inputs.global.queries[0].isInspected = true;
|
||||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<InspectButton queryId={newQuery.id} title="My title" />
|
||||
|
@ -313,13 +276,7 @@ describe('Inspect Button', () => {
|
|||
};
|
||||
myState.inputs = upsertQuery(myQuery);
|
||||
myState.inputs.global.queries[0].isInspected = false;
|
||||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<InspectButton queryId={newQuery.id} title="My title" />
|
||||
|
@ -338,13 +295,7 @@ describe('Inspect Button', () => {
|
|||
};
|
||||
myState.inputs = upsertQuery(myQuery);
|
||||
myState.inputs.global.queries[0].isInspected = true;
|
||||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<InspectButton queryId={newQuery.id} title="My title" />
|
||||
|
@ -363,13 +314,7 @@ describe('Inspect Button', () => {
|
|||
};
|
||||
myState.inputs = upsertQuery(myQuery);
|
||||
myState.inputs.global.queries[0].isInspected = true;
|
||||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<InspectButton queryId={newQuery.id} title="My title" />
|
||||
|
|
|
@ -18,17 +18,10 @@ import { createStore } from '../../../store';
|
|||
import type { State } from '../../../store';
|
||||
|
||||
import type { SecurityJob } from '../types';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<TestProviders store={store}>{children}</TestProviders>
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
import type { RenderHookResult } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import {
|
||||
|
@ -26,13 +25,7 @@ import type { Refetch } from '../../store/inputs/model';
|
|||
const state: State = mockGlobalState;
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<TestProviders store={store}>{children}</TestProviders>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { EuiLink } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
interface RuleNameProps {
|
|
@ -21,7 +21,6 @@ import { coreMock } from '@kbn/core/public/mocks';
|
|||
import { createStore } from '../../store';
|
||||
import { inputsActions } from '../../store/inputs';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
|
||||
const mockSetAppFilters = jest.fn();
|
||||
const mockFilterManager = new FilterManager(coreMock.createStart().uiSettings);
|
||||
|
@ -138,13 +137,7 @@ describe('SearchBarComponent', () => {
|
|||
};
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
render(
|
||||
<TestProviders store={store}>
|
||||
|
@ -180,13 +173,7 @@ describe('SearchBarComponent', () => {
|
|||
};
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
render(
|
||||
<TestProviders store={store}>
|
||||
|
@ -201,13 +188,7 @@ describe('SearchBarComponent', () => {
|
|||
|
||||
it('calls useUpdateUrlParam when query query changes', async () => {
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
render(
|
||||
<TestProviders store={store}>
|
||||
|
@ -232,13 +213,7 @@ describe('SearchBarComponent', () => {
|
|||
|
||||
it('calls useUpdateUrlParam when filters change', async () => {
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
render(
|
||||
<TestProviders store={store}>
|
||||
|
@ -274,13 +249,7 @@ describe('SearchBarComponent', () => {
|
|||
|
||||
it('calls useUpdateUrlParam when savedQuery changes', async () => {
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
render(
|
||||
<TestProviders store={store}>
|
||||
|
@ -313,13 +282,7 @@ describe('SearchBarComponent', () => {
|
|||
describe('Timerange', () => {
|
||||
it('calls useUpdateUrlParam when global timerange changes', async () => {
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
render(
|
||||
<TestProviders store={store}>
|
||||
|
@ -355,13 +318,7 @@ describe('SearchBarComponent', () => {
|
|||
|
||||
it('calls useUpdateUrlParam when timeline timerange changes', async () => {
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
render(
|
||||
<TestProviders store={store}>
|
||||
|
@ -400,13 +357,7 @@ describe('SearchBarComponent', () => {
|
|||
|
||||
it('initializes timerange URL param with redux date on mount', async () => {
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
jest.clearAllMocks();
|
||||
render(
|
||||
<TestProviders store={store}>
|
||||
|
|
|
@ -2,98 +2,50 @@
|
|||
|
||||
exports[`SessionsView renders correctly against snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
.c1 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex-grow: 1;
|
||||
-ms-flex-positive: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.c1 > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.c1 .inspectButtonComponent {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 250ms ease;
|
||||
transition: opacity 250ms ease;
|
||||
}
|
||||
|
||||
.c1:hover .inspectButtonComponent {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
-webkit-flex: 1 1 auto;
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
<div
|
||||
<div
|
||||
data-test-subj="security_solution:sessions_viewer:sessions_view"
|
||||
>
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c1"
|
||||
data-test-subj="hoverVisibilityContainer"
|
||||
data-test-subj="security_solution:sessions_viewer:sessions_view:entityType"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="security_solution:sessions_viewer:sessions_view:entityType"
|
||||
>
|
||||
sessions
|
||||
</div>
|
||||
<div
|
||||
data-test-subj="security_solution:sessions_viewer:sessions_view:startDate"
|
||||
>
|
||||
2022-03-22T22:10:56.794Z
|
||||
</div>
|
||||
<div
|
||||
data-test-subj="security_solution:sessions_viewer:sessions_view:endDate"
|
||||
>
|
||||
2022-03-21T22:10:56.791Z
|
||||
</div>
|
||||
<div
|
||||
data-test-subj="security_solution:sessions_viewer:sessions_view:timelineId"
|
||||
>
|
||||
hosts-page-sessions-v2
|
||||
</div>
|
||||
<div>
|
||||
Started
|
||||
</div>
|
||||
<div>
|
||||
Executable
|
||||
</div>
|
||||
<div>
|
||||
User
|
||||
</div>
|
||||
<div>
|
||||
Interactive
|
||||
</div>
|
||||
<div>
|
||||
Hostname
|
||||
</div>
|
||||
<div>
|
||||
Type
|
||||
</div>
|
||||
<div>
|
||||
Source IP
|
||||
</div>
|
||||
</div>
|
||||
sessions
|
||||
</div>
|
||||
<div
|
||||
data-test-subj="security_solution:sessions_viewer:sessions_view:startDate"
|
||||
>
|
||||
2022-03-22T22:10:56.794Z
|
||||
</div>
|
||||
<div
|
||||
data-test-subj="security_solution:sessions_viewer:sessions_view:endDate"
|
||||
>
|
||||
2022-03-21T22:10:56.791Z
|
||||
</div>
|
||||
<div
|
||||
data-test-subj="security_solution:sessions_viewer:sessions_view:timelineId"
|
||||
>
|
||||
hosts-page-sessions-v2
|
||||
</div>
|
||||
<div>
|
||||
Started
|
||||
</div>
|
||||
<div>
|
||||
Executable
|
||||
</div>
|
||||
<div>
|
||||
User
|
||||
</div>
|
||||
<div>
|
||||
Interactive
|
||||
</div>
|
||||
<div>
|
||||
Hostname
|
||||
</div>
|
||||
<div>
|
||||
Type
|
||||
</div>
|
||||
<div>
|
||||
Source IP
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { tableDefaults } from '../../store/data_table/defaults';
|
||||
import type { SubsetTGridModel } from '../../store/data_table/model';
|
||||
import type { SubsetDataTableModel } from '../../store/data_table/model';
|
||||
import type { ColumnHeaderOptions } from '../../../../common/types/timeline';
|
||||
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
|
||||
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
|
||||
|
@ -62,7 +62,7 @@ export const sessionsHeaders: ColumnHeaderOptions[] = [
|
|||
export const getSessionsDefaultModel = (
|
||||
columns: ColumnHeaderOptions[],
|
||||
defaultColumns: ColumnHeaderOptions[]
|
||||
): SubsetTGridModel => ({
|
||||
): SubsetDataTableModel => ({
|
||||
...tableDefaults,
|
||||
columns,
|
||||
defaultColumns,
|
||||
|
|
|
@ -11,10 +11,11 @@ import { TestProviders } from '../../mock';
|
|||
import { TEST_ID, SessionsView, defaultSessionsFilter } from '.';
|
||||
import type { EntityType } from '@kbn/timelines-plugin/common';
|
||||
import type { SessionsComponentsProps } from './types';
|
||||
import type { TimelineModel } from '../../../timelines/store/timeline/model';
|
||||
import { useGetUserCasesPermissions } from '../../lib/kibana';
|
||||
import { TableId } from '../../../../common/types';
|
||||
import { licenseService } from '../../hooks/use_license';
|
||||
import { mount } from 'enzyme';
|
||||
import type { EventsViewerProps } from '../events_viewer';
|
||||
|
||||
jest.mock('../../lib/kibana');
|
||||
|
||||
|
@ -42,7 +43,7 @@ const testProps: SessionsComponentsProps = {
|
|||
filterQuery,
|
||||
};
|
||||
|
||||
type Props = Partial<TimelineModel> & {
|
||||
type Props = Partial<EventsViewerProps> & {
|
||||
start: string;
|
||||
end: string;
|
||||
entityType: EntityType;
|
||||
|
@ -70,47 +71,58 @@ jest.mock('../../hooks/use_license', () => {
|
|||
};
|
||||
});
|
||||
|
||||
// creating a dummy component for testing TGrid to avoid mocking all the implementation details
|
||||
// but still test if the TGrid will render properly
|
||||
const SessionsViewerTGrid: React.FC<Props> = ({ columns, start, end, id, filters, entityType }) => {
|
||||
// creating a dummy component for testing data table to avoid mocking all the implementation details
|
||||
// but still test if the data table will render properly
|
||||
const SessionsViewerEventsViewer: React.FC<Props> = ({
|
||||
defaultModel,
|
||||
start,
|
||||
end,
|
||||
tableId,
|
||||
pageFilters,
|
||||
entityType,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
callFilters(filters);
|
||||
}, [filters]);
|
||||
callFilters(pageFilters);
|
||||
}, [pageFilters]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-test-subj={`${TEST_PREFIX}:entityType`}>{entityType}</div>
|
||||
<div data-test-subj={`${TEST_PREFIX}:startDate`}>{start}</div>
|
||||
<div data-test-subj={`${TEST_PREFIX}:endDate`}>{end}</div>
|
||||
<div data-test-subj={`${TEST_PREFIX}:timelineId`}>{id}</div>
|
||||
{columns?.map((header) => (
|
||||
<div data-test-subj={`${TEST_PREFIX}:timelineId`}>{tableId}</div>
|
||||
{defaultModel?.columns?.map((header) => (
|
||||
<div key={header.id}>{header.display ?? header.id}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
jest.mock('@kbn/timelines-plugin/public/mock/plugin_mock', () => {
|
||||
const originalModule = jest.requireActual('@kbn/timelines-plugin/public/mock/plugin_mock');
|
||||
jest.mock('../events_viewer', () => {
|
||||
return {
|
||||
...originalModule,
|
||||
createTGridMocks: () => ({
|
||||
...originalModule.createTGridMocks,
|
||||
getTGrid: SessionsViewerTGrid,
|
||||
}),
|
||||
StatefulEventsViewer: SessionsViewerEventsViewer,
|
||||
};
|
||||
});
|
||||
|
||||
mockGetDefaultControlColumn.mockReturnValue([
|
||||
{
|
||||
headerCellRender: () => <></>,
|
||||
id: 'default-timeline-control-column',
|
||||
rowCellRender: jest.fn(),
|
||||
width: jest.fn(),
|
||||
},
|
||||
]);
|
||||
|
||||
describe('SessionsView', () => {
|
||||
it('renders the session view', async () => {
|
||||
const wrapper = render(
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<SessionsView {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.queryByTestId(TEST_ID)).toBeInTheDocument();
|
||||
expect(wrapper.find(`[data-test-subj="${TEST_ID}"]`).exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -126,7 +138,7 @@ describe('SessionsView', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('passes in the right parameters to TGrid', async () => {
|
||||
it('passes in the right parameters to EventsViewer', async () => {
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<SessionsView {...testProps} />
|
||||
|
@ -142,7 +154,7 @@ describe('SessionsView', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('passes in the right filters to TGrid', async () => {
|
||||
it('passes in the right filters to EventsViewer', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<SessionsView {...testProps} />
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
import React, { useMemo, useEffect } from 'react';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { ENTRY_SESSION_ENTITY_ID_PROPERTY, EventAction } from '@kbn/session-view-plugin/public';
|
||||
import type { BulkActionsProp } from '@kbn/timelines-plugin/common/types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { EVENT_ACTION } from '@kbn/rule-data-utils';
|
||||
import { TableId } from '../../../../common/types';
|
||||
import { useAddBulkToTimelineAction } from '../../../detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline';
|
||||
import type { SessionsComponentsProps } from './types';
|
||||
import type { ESBoolQuery } from '../../../../common/typed_json';
|
||||
|
@ -22,11 +22,11 @@ import * as i18n from './translations';
|
|||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { TableId } from '../../../../common/types/timeline';
|
||||
import { dataTableActions } from '../../store/data_table';
|
||||
import { eventsDefaultModel } from '../events_viewer/default_model';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
|
||||
import type { BulkActionsProp } from '../toolbar/bulk_actions/types';
|
||||
|
||||
export const TEST_ID = 'security_solution:sessions_viewer:sessions_view';
|
||||
|
||||
|
@ -104,7 +104,7 @@ const SessionsViewComponent: React.FC<SessionsComponentsProps> = ({
|
|||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
dataTableActions.initializeTGridSettings({
|
||||
dataTableActions.initializeDataTableSettings({
|
||||
id: tableId,
|
||||
title: i18n.SESSIONS_TITLE,
|
||||
defaultColumns: eventsDefaultModel.columns.map((c) =>
|
||||
|
@ -159,7 +159,7 @@ const SessionsViewComponent: React.FC<SessionsComponentsProps> = ({
|
|||
leadingControlColumns={leadingControlColumns}
|
||||
renderCellValue={DefaultCellRenderer}
|
||||
rowRenderers={defaultRowRenderers}
|
||||
scopeId={SourcererScopeName.default}
|
||||
sourcererScope={SourcererScopeName.default}
|
||||
start={startDate}
|
||||
unit={unit}
|
||||
/>
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
*/
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { EntityType } from '@kbn/timelines-plugin/common';
|
||||
import type { TableIdLiteral } from '../../../../common/types';
|
||||
import type { QueryTabBodyProps } from '../../../hosts/pages/navigation/types';
|
||||
import type { ColumnHeaderOptions, TableIdLiteral } from '../../../../common/types/timeline';
|
||||
import type { ColumnHeaderOptions } from '../../../../common/types/timeline';
|
||||
|
||||
export interface SessionsComponentsProps extends Pick<QueryTabBodyProps, 'endDate' | 'startDate'> {
|
||||
tableId: TableIdLiteral;
|
||||
|
|
|
@ -28,7 +28,6 @@ import { useSignalHelpers } from '../../containers/sourcerer/use_signal_helpers'
|
|||
import { TimelineId, TimelineType } from '../../../../common/types';
|
||||
import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants';
|
||||
import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
|
@ -95,13 +94,7 @@ describe('Sourcerer component', () => {
|
|||
const pollForSignalIndexMock = jest.fn();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
(useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView);
|
||||
(useSignalHelpers as jest.Mock).mockReturnValue({ signalIndexNeedsInit: false });
|
||||
});
|
||||
|
@ -213,7 +206,6 @@ describe('Sourcerer component', () => {
|
|||
},
|
||||
},
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
@ -264,7 +256,6 @@ describe('Sourcerer component', () => {
|
|||
},
|
||||
},
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
@ -316,13 +307,7 @@ describe('Sourcerer component', () => {
|
|||
},
|
||||
};
|
||||
|
||||
store = createStore(
|
||||
state2,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer {...defaultProps} />
|
||||
|
@ -367,13 +352,7 @@ describe('Sourcerer component', () => {
|
|||
},
|
||||
};
|
||||
|
||||
store = createStore(
|
||||
state2,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer scope={sourcererModel.SourcererScopeName.timeline} />
|
||||
|
@ -413,7 +392,6 @@ describe('Sourcerer component', () => {
|
|||
},
|
||||
},
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
@ -471,7 +449,6 @@ describe('Sourcerer component', () => {
|
|||
},
|
||||
},
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
@ -540,7 +517,6 @@ describe('Sourcerer component', () => {
|
|||
},
|
||||
},
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
@ -585,13 +561,7 @@ describe('Sourcerer component', () => {
|
|||
},
|
||||
};
|
||||
|
||||
store = createStore(
|
||||
state2,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer scope={sourcererModel.SourcererScopeName.timeline} />
|
||||
|
@ -635,13 +605,7 @@ describe('Sourcerer component', () => {
|
|||
},
|
||||
};
|
||||
|
||||
store = createStore(
|
||||
state2,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer {...defaultProps} />
|
||||
|
@ -720,13 +684,7 @@ describe('Sourcerer component', () => {
|
|||
describe('sourcerer on alerts page or rules details page', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const testProps = {
|
||||
scope: sourcererModel.SourcererScopeName.detections,
|
||||
};
|
||||
|
@ -795,13 +753,7 @@ describe('sourcerer on alerts page or rules details page', () => {
|
|||
describe('timeline sourcerer', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const testProps = {
|
||||
scope: sourcererModel.SourcererScopeName.timeline,
|
||||
};
|
||||
|
@ -894,7 +846,7 @@ describe('timeline sourcerer', () => {
|
|||
store = createStore(
|
||||
state2,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
@ -941,13 +893,7 @@ describe('Sourcerer integration tests', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
(useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
@ -992,13 +938,7 @@ describe('No data', () => {
|
|||
...sourcererDataView,
|
||||
indicesExist: false,
|
||||
});
|
||||
store = createStore(
|
||||
mockNoIndicesState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(mockNoIndicesState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
@ -1073,13 +1013,7 @@ describe('Update available', () => {
|
|||
...sourcererDataView,
|
||||
activePatterns: ['myFakebeat-*'],
|
||||
});
|
||||
store = createStore(
|
||||
state2,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
|
@ -1208,13 +1142,7 @@ describe('Update available for timeline template', () => {
|
|||
...sourcererDataView,
|
||||
activePatterns: ['myFakebeat-*'],
|
||||
});
|
||||
store = createStore(
|
||||
state2,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
|
@ -1299,13 +1227,7 @@ describe('Missing index patterns', () => {
|
|||
});
|
||||
const state3 = cloneDeep(state2);
|
||||
state3.timeline.timelineById[TimelineId.active].timelineType = TimelineType.default;
|
||||
store = createStore(
|
||||
state3,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(state3, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
|
@ -1341,13 +1263,7 @@ describe('Missing index patterns', () => {
|
|||
...sourcererDataView,
|
||||
activePatterns: ['myFakebeat-*'],
|
||||
});
|
||||
store = createStore(
|
||||
state2,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
|
|
|
@ -27,7 +27,6 @@ import { createStore } from '../../store';
|
|||
import { Provider as ReduxStoreProvider } from 'react-redux';
|
||||
import { getMockTheme } from '../../lib/kibana/kibana_react.mock';
|
||||
import * as module from '../../containers/query_toggle';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
import type { LensAttributes } from '../visualization_actions/types';
|
||||
|
||||
const from = '2019-06-15T06:00:00.000Z';
|
||||
|
@ -58,13 +57,7 @@ describe('Stat Items Component', () => {
|
|||
const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } });
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
const testProps = {
|
||||
description: 'HOSTS',
|
||||
fields: [{ key: 'hosts', value: null, color: '#6092C0', icon: 'cross' }],
|
||||
|
|
|
@ -24,7 +24,6 @@ import { createStore } from '../../store';
|
|||
import { SuperDatePicker, makeMapStateToProps } from '.';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
import { tGridReducer } from '@kbn/timelines-plugin/public';
|
||||
|
||||
jest.mock('../../lib/kibana');
|
||||
const mockUseUiSetting$ = useUiSetting$ as jest.Mock;
|
||||
|
@ -85,23 +84,11 @@ describe('SIEM Super Date Picker', () => {
|
|||
describe('#SuperDatePicker', () => {
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
{ dataTable: tGridReducer },
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
mockUseUiSetting$.mockImplementation((key, defaultValue) => {
|
||||
const useUiSetting$Mock = createUseUiSetting$Mock();
|
||||
|
||||
|
|
|
@ -6,24 +6,25 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { connect, ConnectedProps, useDispatch } from 'react-redux';
|
||||
import type { ConnectedProps } from 'react-redux';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import type {
|
||||
AlertStatus,
|
||||
SetEventsLoading,
|
||||
SetEventsDeleted,
|
||||
OnUpdateAlertStatusSuccess,
|
||||
OnUpdateAlertStatusError,
|
||||
CustomBulkActionProp,
|
||||
SetEventsDeleted,
|
||||
SetEventsLoading,
|
||||
} from '../../../../../common/types';
|
||||
import type { Refetch } from '../../../../store/t_grid/inputs';
|
||||
import { TableState, tGridActions, TGridModel, tGridSelectors } from '../../../../store/t_grid';
|
||||
import { BulkActions } from '.';
|
||||
import { useBulkActionItems } from '../../../../hooks/use_bulk_action_items';
|
||||
import { useBulkActionItems } from './use_bulk_action_items';
|
||||
import { dataTableActions, dataTableSelectors } from '../../../store/data_table';
|
||||
import type { DataTableModel } from '../../../store/data_table/model';
|
||||
import type { AlertWorkflowStatus, Refetch } from '../../../types';
|
||||
import type { DataTableState } from '../../../store/data_table/types';
|
||||
import type { OnUpdateAlertStatusError, OnUpdateAlertStatusSuccess } from './types';
|
||||
|
||||
interface OwnProps {
|
||||
id: string;
|
||||
totalItems: number;
|
||||
filterStatus?: AlertStatus;
|
||||
filterStatus?: AlertWorkflowStatus;
|
||||
query?: string;
|
||||
indexName: string;
|
||||
showAlertStatusActions?: boolean;
|
||||
|
@ -61,7 +62,7 @@ export const AlertBulkActionsComponent = React.memo<StatefulAlertBulkActionsProp
|
|||
// Catches state change isSelectAllChecked->false (page checkbox) upon user selection change to reset toolbar select all
|
||||
useEffect(() => {
|
||||
if (isSelectAllChecked) {
|
||||
dispatch(tGridActions.setTGridSelectAll({ id, selectAll: false }));
|
||||
dispatch(dataTableActions.setDataTableSelectAll({ id, selectAll: false }));
|
||||
} else {
|
||||
setShowClearSelection(false);
|
||||
}
|
||||
|
@ -71,19 +72,19 @@ export const AlertBulkActionsComponent = React.memo<StatefulAlertBulkActionsProp
|
|||
// Dispatches to stateful_body's selectAll via TimelineTypeContext props
|
||||
// as scope of response data required to actually set selectedEvents
|
||||
const onSelectAll = useCallback(() => {
|
||||
dispatch(tGridActions.setTGridSelectAll({ id, selectAll: true }));
|
||||
dispatch(dataTableActions.setDataTableSelectAll({ id, selectAll: true }));
|
||||
setShowClearSelection(true);
|
||||
}, [dispatch, id]);
|
||||
|
||||
// Callback for clearing entire selection from toolbar
|
||||
const onClearSelection = useCallback(() => {
|
||||
clearSelected({ id });
|
||||
dispatch(tGridActions.setTGridSelectAll({ id, selectAll: false }));
|
||||
dispatch(dataTableActions.setDataTableSelectAll({ id, selectAll: false }));
|
||||
setShowClearSelection(false);
|
||||
}, [clearSelected, dispatch, id]);
|
||||
|
||||
const onUpdateSuccess = useCallback(
|
||||
(updated: number, conflicts: number, newStatus: AlertStatus) => {
|
||||
(updated: number, conflicts: number, newStatus: AlertWorkflowStatus) => {
|
||||
refetch();
|
||||
if (onActionSuccess) {
|
||||
onActionSuccess(updated, conflicts, newStatus);
|
||||
|
@ -93,7 +94,7 @@ export const AlertBulkActionsComponent = React.memo<StatefulAlertBulkActionsProp
|
|||
);
|
||||
|
||||
const onUpdateFailure = useCallback(
|
||||
(newStatus: AlertStatus, error: Error) => {
|
||||
(newStatus: AlertWorkflowStatus, error: Error) => {
|
||||
refetch();
|
||||
if (onActionFailure) {
|
||||
onActionFailure(newStatus, error);
|
||||
|
@ -104,14 +105,14 @@ export const AlertBulkActionsComponent = React.memo<StatefulAlertBulkActionsProp
|
|||
|
||||
const setEventsLoading = useCallback<SetEventsLoading>(
|
||||
({ eventIds, isLoading }) => {
|
||||
dispatch(tGridActions.setEventsLoading({ id, eventIds, isLoading }));
|
||||
dispatch(dataTableActions.setEventsLoading({ id, eventIds, isLoading }));
|
||||
},
|
||||
[dispatch, id]
|
||||
);
|
||||
|
||||
const setEventsDeleted = useCallback<SetEventsDeleted>(
|
||||
({ eventIds, isDeleted }) => {
|
||||
dispatch(tGridActions.setEventsDeleted({ id, eventIds, isDeleted }));
|
||||
dispatch(dataTableActions.setEventsDeleted({ id, eventIds, isDeleted }));
|
||||
},
|
||||
[dispatch, id]
|
||||
);
|
||||
|
@ -146,9 +147,9 @@ export const AlertBulkActionsComponent = React.memo<StatefulAlertBulkActionsProp
|
|||
AlertBulkActionsComponent.displayName = 'AlertBulkActionsComponent';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getTGrid = tGridSelectors.getTGridByIdSelector();
|
||||
const mapStateToProps = (state: TableState, { id }: OwnProps) => {
|
||||
const dataTable: TGridModel = getTGrid(state, id);
|
||||
const getTable = dataTableSelectors.getTableByIdSelector();
|
||||
const mapStateToProps = (state: DataTableState, { id }: OwnProps) => {
|
||||
const dataTable: DataTableModel = getTable(state, id);
|
||||
const { selectedEventIds, isSelectAllChecked } = dataTable;
|
||||
|
||||
return {
|
||||
|
@ -160,7 +161,7 @@ const makeMapStateToProps = () => {
|
|||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearSelected: tGridActions.clearSelected,
|
||||
clearSelected: dataTableActions.clearSelected,
|
||||
};
|
||||
|
||||
const connector = connect(makeMapStateToProps, mapDispatchToProps);
|
|
@ -13,7 +13,7 @@ import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
|
|||
import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface BulkActionsProps {
|
||||
interface OwnProps {
|
||||
totalItems: number;
|
||||
selectedCount: number;
|
||||
showClearSelection: boolean;
|
||||
|
@ -32,7 +32,7 @@ BulkActionsContainer.displayName = 'BulkActionsContainer';
|
|||
/**
|
||||
* Stateless component integrating the bulk actions menu and the select all button
|
||||
*/
|
||||
const BulkActionsComponent: React.FC<BulkActionsProps> = ({
|
||||
const BulkActionsComponent: React.FC<OwnProps> = ({
|
||||
selectedCount,
|
||||
totalItems,
|
||||
showClearSelection,
|
|
@ -7,112 +7,107 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const EVENTS_TABLE_ARIA_LABEL = ({
|
||||
activePage,
|
||||
totalPages,
|
||||
}: {
|
||||
activePage: number;
|
||||
totalPages: number;
|
||||
}) =>
|
||||
i18n.translate('xpack.timelines.timeline.eventsTableAriaLabel', {
|
||||
values: { activePage, totalPages },
|
||||
defaultMessage: 'events; Page {activePage} of {totalPages}',
|
||||
});
|
||||
|
||||
export const BULK_ACTION_OPEN_SELECTED = i18n.translate(
|
||||
'xpack.timelines.timeline.openSelectedTitle',
|
||||
{
|
||||
defaultMessage: 'Mark as open',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_CLOSE_SELECTED = i18n.translate(
|
||||
'xpack.timelines.timeline.closeSelectedTitle',
|
||||
{
|
||||
defaultMessage: 'Mark as closed',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_ACKNOWLEDGED_SELECTED = i18n.translate(
|
||||
'xpack.timelines.timeline.acknowledgedSelectedTitle',
|
||||
{
|
||||
defaultMessage: 'Mark as acknowledged',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_FAILED_SINGLE_ALERT = i18n.translate(
|
||||
'xpack.timelines.timeline.updateAlertStatusFailedSingleAlert',
|
||||
{
|
||||
defaultMessage: 'Failed to update alert because it was already being modified.',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_ATTACH_NEW_CASE = i18n.translate(
|
||||
'xpack.timelines.timeline.attachNewCase',
|
||||
{
|
||||
defaultMessage: 'Attach to new case',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_ATTACH_EXISTING_CASE = i18n.translate(
|
||||
'xpack.timelines.timeline.attachExistingCase',
|
||||
{
|
||||
defaultMessage: 'Attach to existing case',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLOSED_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||
i18n.translate('xpack.timelines.timeline.closedAlertSuccessToastMessage', {
|
||||
values: { totalAlerts },
|
||||
export const SELECTED_ALERTS = (selectedAlertsFormatted: string, selectedAlerts: number) =>
|
||||
i18n.translate('xpack.securitySolution.toolbar.bulkActions.selectedAlertsTitle', {
|
||||
values: { selectedAlertsFormatted, selectedAlerts },
|
||||
defaultMessage:
|
||||
'Successfully closed {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.',
|
||||
'Selected {selectedAlertsFormatted} {selectedAlerts, plural, =1 {alert} other {alerts}}',
|
||||
});
|
||||
|
||||
export const OPENED_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||
i18n.translate('xpack.timelines.timeline.openedAlertSuccessToastMessage', {
|
||||
values: { totalAlerts },
|
||||
export const SELECT_ALL_ALERTS = (totalAlertsFormatted: string, totalAlerts: number) =>
|
||||
i18n.translate('xpack.securitySolution.toolbar.bulkActions.selectAllAlertsTitle', {
|
||||
values: { totalAlertsFormatted, totalAlerts },
|
||||
defaultMessage:
|
||||
'Successfully opened {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.',
|
||||
'Select all {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}',
|
||||
});
|
||||
|
||||
export const ACKNOWLEDGED_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||
i18n.translate('xpack.timelines.timeline.acknowledgedAlertSuccessToastMessage', {
|
||||
values: { totalAlerts },
|
||||
defaultMessage:
|
||||
'Successfully marked {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}} as acknowledged.',
|
||||
});
|
||||
|
||||
export const CLOSED_ALERT_FAILED_TOAST = i18n.translate(
|
||||
'xpack.timelines.timeline.closedAlertFailedToastMessage',
|
||||
export const CLEAR_SELECTION = i18n.translate(
|
||||
'xpack.securitySolution.toolbar.bulkActions.clearSelectionTitle',
|
||||
{
|
||||
defaultMessage: 'Failed to close alert(s).',
|
||||
}
|
||||
);
|
||||
|
||||
export const OPENED_ALERT_FAILED_TOAST = i18n.translate(
|
||||
'xpack.timelines.timeline.openedAlertFailedToastMessage',
|
||||
{
|
||||
defaultMessage: 'Failed to open alert(s)',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACKNOWLEDGED_ALERT_FAILED_TOAST = i18n.translate(
|
||||
'xpack.timelines.timeline.acknowledgedAlertFailedToastMessage',
|
||||
{
|
||||
defaultMessage: 'Failed to mark alert(s) as acknowledged',
|
||||
defaultMessage: 'Clear selection',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_ALERT_STATUS_FAILED = (conflicts: number) =>
|
||||
i18n.translate('xpack.timelines.timeline.updateAlertStatusFailed', {
|
||||
i18n.translate('xpack.securitySolution.bulkActions.updateAlertStatusFailed', {
|
||||
values: { conflicts },
|
||||
defaultMessage:
|
||||
'Failed to update { conflicts } {conflicts, plural, =1 {alert} other {alerts}}.',
|
||||
});
|
||||
|
||||
export const UPDATE_ALERT_STATUS_FAILED_DETAILED = (updated: number, conflicts: number) =>
|
||||
i18n.translate('xpack.timelines.timeline.updateAlertStatusFailedDetailed', {
|
||||
i18n.translate('xpack.securitySolution.bulkActions.updateAlertStatusFailedDetailed', {
|
||||
values: { updated, conflicts },
|
||||
defaultMessage: `{ updated } {updated, plural, =1 {alert was} other {alerts were}} updated successfully, but { conflicts } failed to update
|
||||
because { conflicts, plural, =1 {it was} other {they were}} already being modified.`,
|
||||
});
|
||||
|
||||
export const CLOSED_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||
i18n.translate('xpack.securitySolution.bulkActions.closedAlertSuccessToastMessage', {
|
||||
values: { totalAlerts },
|
||||
defaultMessage:
|
||||
'Successfully closed {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.',
|
||||
});
|
||||
|
||||
export const OPENED_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||
i18n.translate('xpack.securitySolution.bulkActions.openedAlertSuccessToastMessage', {
|
||||
values: { totalAlerts },
|
||||
defaultMessage:
|
||||
'Successfully opened {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.',
|
||||
});
|
||||
|
||||
export const ACKNOWLEDGED_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||
i18n.translate('xpack.securitySolution.bulkActions.acknowledgedAlertSuccessToastMessage', {
|
||||
values: { totalAlerts },
|
||||
defaultMessage:
|
||||
'Successfully marked {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}} as acknowledged.',
|
||||
});
|
||||
|
||||
export const CLOSED_ALERT_FAILED_TOAST = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.closedAlertFailedToastMessage',
|
||||
{
|
||||
defaultMessage: 'Failed to close alert(s).',
|
||||
}
|
||||
);
|
||||
|
||||
export const OPENED_ALERT_FAILED_TOAST = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.openedAlertFailedToastMessage',
|
||||
{
|
||||
defaultMessage: 'Failed to open alert(s)',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACKNOWLEDGED_ALERT_FAILED_TOAST = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.acknowledgedAlertFailedToastMessage',
|
||||
{
|
||||
defaultMessage: 'Failed to mark alert(s) as acknowledged',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_FAILED_SINGLE_ALERT = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.updateAlertStatusFailedSingleAlert',
|
||||
{
|
||||
defaultMessage: 'Failed to update alert because it was already being modified.',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_OPEN_SELECTED = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.openSelectedTitle',
|
||||
{
|
||||
defaultMessage: 'Mark as open',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_ACKNOWLEDGED_SELECTED = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.acknowledgedSelectedTitle',
|
||||
{
|
||||
defaultMessage: 'Mark as acknowledged',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_ACTION_CLOSE_SELECTED = i18n.translate(
|
||||
'xpack.securitySolution.bulkActions.closeSelectedTitle',
|
||||
{
|
||||
defaultMessage: 'Mark as closed',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 type { CustomBulkAction } from '../../../../../common/types';
|
||||
import type { AlertWorkflowStatus } from '../../../types';
|
||||
|
||||
export interface BulkActionsObjectProp {
|
||||
alertStatusActions?: boolean;
|
||||
onAlertStatusActionSuccess?: OnUpdateAlertStatusSuccess;
|
||||
onAlertStatusActionFailure?: OnUpdateAlertStatusError;
|
||||
customBulkActions?: CustomBulkAction[];
|
||||
}
|
||||
|
||||
export type OnUpdateAlertStatusSuccess = (
|
||||
updated: number,
|
||||
conflicts: number,
|
||||
status: AlertWorkflowStatus
|
||||
) => void;
|
||||
export type OnUpdateAlertStatusError = (status: AlertWorkflowStatus, error: Error) => void;
|
||||
|
||||
export type BulkActionsProp = boolean | BulkActionsObjectProp;
|
|
@ -7,18 +7,37 @@
|
|||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import { FILTER_CLOSED, FILTER_ACKNOWLEDGED, FILTER_OPEN } from '../../common/constants';
|
||||
import * as i18n from '../components/t_grid/translations';
|
||||
import type { AlertStatus, BulkActionsProps } from '../../common/types/timeline';
|
||||
import { useUpdateAlertsStatus } from '../container/use_update_alerts';
|
||||
import { useAppToasts } from './use_app_toasts';
|
||||
import { useStartTransaction } from '../lib/apm/use_start_transaction';
|
||||
import { APM_USER_INTERACTIONS } from '../lib/apm/constants';
|
||||
import { FILTER_ACKNOWLEDGED, FILTER_CLOSED, FILTER_OPEN } from '../../../../../common/types';
|
||||
import type {
|
||||
CustomBulkActionProp,
|
||||
SetEventsDeleted,
|
||||
SetEventsLoading,
|
||||
} from '../../../../../common/types';
|
||||
import * as i18n from './translations';
|
||||
import { useUpdateAlertsStatus } from './use_update_alerts';
|
||||
import { useAppToasts } from '../../../hooks/use_app_toasts';
|
||||
import { useStartTransaction } from '../../../lib/apm/use_start_transaction';
|
||||
import { APM_USER_INTERACTIONS } from '../../../lib/apm/constants';
|
||||
import type { AlertWorkflowStatus } from '../../../types';
|
||||
import type { OnUpdateAlertStatusError, OnUpdateAlertStatusSuccess } from './types';
|
||||
|
||||
export const getUpdateAlertsQuery = (eventIds: Readonly<string[]>) => {
|
||||
return { bool: { filter: { terms: { _id: eventIds } } } };
|
||||
};
|
||||
|
||||
export interface BulkActionsProps {
|
||||
eventIds: string[];
|
||||
currentStatus?: AlertWorkflowStatus;
|
||||
query?: string;
|
||||
indexName: string;
|
||||
setEventsLoading: SetEventsLoading;
|
||||
setEventsDeleted: SetEventsDeleted;
|
||||
showAlertStatusActions?: boolean;
|
||||
onUpdateSuccess?: OnUpdateAlertStatusSuccess;
|
||||
onUpdateFailure?: OnUpdateAlertStatusError;
|
||||
customBulkActions?: CustomBulkActionProp[];
|
||||
}
|
||||
|
||||
export const useBulkActionItems = ({
|
||||
eventIds,
|
||||
currentStatus,
|
||||
|
@ -31,12 +50,12 @@ export const useBulkActionItems = ({
|
|||
onUpdateFailure,
|
||||
customBulkActions,
|
||||
}: BulkActionsProps) => {
|
||||
const { updateAlertStatus } = useUpdateAlertsStatus(true);
|
||||
const { updateAlertStatus } = useUpdateAlertsStatus();
|
||||
const { addSuccess, addError, addWarning } = useAppToasts();
|
||||
const { startTransaction } = useStartTransaction();
|
||||
|
||||
const onAlertStatusUpdateSuccess = useCallback(
|
||||
(updated: number, conflicts: number, newStatus: AlertStatus) => {
|
||||
(updated: number, conflicts: number, newStatus: AlertWorkflowStatus) => {
|
||||
if (conflicts > 0) {
|
||||
// Partial failure
|
||||
addWarning({
|
||||
|
@ -52,7 +71,6 @@ export const useBulkActionItems = ({
|
|||
case 'open':
|
||||
title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated);
|
||||
break;
|
||||
case 'in-progress':
|
||||
case 'acknowledged':
|
||||
title = i18n.ACKNOWLEDGED_ALERT_SUCCESS_TOAST(updated);
|
||||
}
|
||||
|
@ -66,7 +84,7 @@ export const useBulkActionItems = ({
|
|||
);
|
||||
|
||||
const onAlertStatusUpdateFailure = useCallback(
|
||||
(newStatus: AlertStatus, error: Error) => {
|
||||
(newStatus: AlertWorkflowStatus, error: Error) => {
|
||||
let title: string;
|
||||
switch (newStatus) {
|
||||
case 'closed':
|
||||
|
@ -75,7 +93,6 @@ export const useBulkActionItems = ({
|
|||
case 'open':
|
||||
title = i18n.OPENED_ALERT_FAILED_TOAST;
|
||||
break;
|
||||
case 'in-progress':
|
||||
case 'acknowledged':
|
||||
title = i18n.ACKNOWLEDGED_ALERT_FAILED_TOAST;
|
||||
}
|
||||
|
@ -88,7 +105,7 @@ export const useBulkActionItems = ({
|
|||
);
|
||||
|
||||
const onClickUpdate = useCallback(
|
||||
async (status: AlertStatus) => {
|
||||
async (status: AlertWorkflowStatus) => {
|
||||
if (query) {
|
||||
startTransaction({ name: APM_USER_INTERACTIONS.BULK_QUERY_STATUS_UPDATE });
|
||||
} else if (eventIds.length > 1) {
|
||||
|
@ -141,7 +158,7 @@ export const useBulkActionItems = ({
|
|||
<EuiContextMenuItem
|
||||
key="open"
|
||||
data-test-subj="open-alert-status"
|
||||
onClick={() => onClickUpdate(FILTER_OPEN)}
|
||||
onClick={() => onClickUpdate(FILTER_OPEN as AlertWorkflowStatus)}
|
||||
>
|
||||
{i18n.BULK_ACTION_OPEN_SELECTED}
|
||||
</EuiContextMenuItem>
|
||||
|
@ -152,7 +169,7 @@ export const useBulkActionItems = ({
|
|||
<EuiContextMenuItem
|
||||
key="acknowledge"
|
||||
data-test-subj="acknowledged-alert-status"
|
||||
onClick={() => onClickUpdate(FILTER_ACKNOWLEDGED)}
|
||||
onClick={() => onClickUpdate(FILTER_ACKNOWLEDGED as AlertWorkflowStatus)}
|
||||
>
|
||||
{i18n.BULK_ACTION_ACKNOWLEDGED_SELECTED}
|
||||
</EuiContextMenuItem>
|
||||
|
@ -163,7 +180,7 @@ export const useBulkActionItems = ({
|
|||
<EuiContextMenuItem
|
||||
key="close"
|
||||
data-test-subj="close-alert-status"
|
||||
onClick={() => onClickUpdate(FILTER_CLOSED)}
|
||||
onClick={() => onClickUpdate(FILTER_CLOSED as AlertWorkflowStatus)}
|
||||
>
|
||||
{i18n.BULK_ACTION_CLOSE_SELECTED}
|
||||
</EuiContextMenuItem>
|
|
@ -6,14 +6,11 @@
|
|||
*/
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { AlertStatus } from '../../common/types';
|
||||
import {
|
||||
DETECTION_ENGINE_SIGNALS_STATUS_URL,
|
||||
RAC_ALERTS_BULK_UPDATE_URL,
|
||||
} from '../../common/constants';
|
||||
import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants';
|
||||
import type { AlertWorkflowStatus } from '../../../types';
|
||||
|
||||
/**
|
||||
* Update alert status by query
|
||||
|
@ -27,11 +24,9 @@ import {
|
|||
*
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const useUpdateAlertsStatus = (
|
||||
useDetectionEngine: boolean = false
|
||||
): {
|
||||
export const useUpdateAlertsStatus = (): {
|
||||
updateAlertStatus: (params: {
|
||||
status: AlertStatus;
|
||||
status: AlertWorkflowStatus;
|
||||
index: string;
|
||||
query: object;
|
||||
}) => Promise<estypes.UpdateByQueryResponse>;
|
||||
|
@ -39,18 +34,10 @@ export const useUpdateAlertsStatus = (
|
|||
const { http } = useKibana<CoreStart>().services;
|
||||
return {
|
||||
updateAlertStatus: async ({ status, index, query }) => {
|
||||
if (useDetectionEngine) {
|
||||
return http.fetch<estypes.UpdateByQueryResponse>(DETECTION_ENGINE_SIGNALS_STATUS_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ status, query }),
|
||||
});
|
||||
} else {
|
||||
const response = await http.post<estypes.UpdateByQueryResponse>(
|
||||
RAC_ALERTS_BULK_UPDATE_URL,
|
||||
{ body: JSON.stringify({ index, status, query }) }
|
||||
);
|
||||
return response;
|
||||
}
|
||||
return http.fetch<estypes.UpdateByQueryResponse>(DETECTION_ENGINE_SIGNALS_STATUS_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ status, query }),
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue