mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
## Summary This is `Part II` of `II` for adding the `Signals Table` to the main Detection Engine landing page ([meta issue](https://github.com/elastic/kibana/issues/50405)). `Part II` includes: * Adding `selection`, `selectAll` & `selectAllGlobal` (i.e. query select) functionality to the EventsViewer * Includes ability to specify a fieldset when storing selection state so it can be used by custom actions * Introduces following new Timeline state: * `deletedEventIds: string[]` * `loadingEventIds: string[]` * `selectedEventIds: Record<string, TimelineNonEcsData[]>` * `showCheckboxes: boolean` * `showRowRenderers: boolean` * Adds Send to Timeline overflow/batch action (detailed [here](https://github.com/elastic/kibana/issues/50405#issuecomment-565470830)) * Adds Update Signal Status overflow/batch action Resolves https://github.com/elastic/kibana/issues/51785 ##### Selection / Update Signal Status  ##### Send Signal to Timeline Action ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios - [ ] ~This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~ ### For maintainers - [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ - [ ] ~This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~
This commit is contained in:
parent
1b6a87b8d7
commit
3b0e778c79
48 changed files with 1869 additions and 330 deletions
|
@ -11,6 +11,7 @@ export const DEFAULT_DATE_FORMAT = 'dateFormat';
|
|||
export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz';
|
||||
export const DEFAULT_DARK_MODE = 'theme:darkMode';
|
||||
export const DEFAULT_INDEX_KEY = 'siem:defaultIndex';
|
||||
export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern';
|
||||
export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults';
|
||||
export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults';
|
||||
export const DEFAULT_SIEM_TIME_RANGE = 'siem:timeDefaults';
|
||||
|
|
|
@ -64,7 +64,7 @@ export const AlertsTable = React.memo(
|
|||
const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]);
|
||||
return (
|
||||
<StatefulEventsViewer
|
||||
defaultFilters={alertsFilter}
|
||||
pageFilters={alertsFilter}
|
||||
defaultModel={alertsDefaultModel}
|
||||
end={endDate}
|
||||
id={ALERTS_TABLE_ID}
|
||||
|
@ -73,8 +73,6 @@ export const AlertsTable = React.memo(
|
|||
() => ({
|
||||
documentType: i18n.ALERTS_DOCUMENT_TYPE,
|
||||
footerText: i18n.TOTAL_COUNT_OF_ALERTS,
|
||||
showCheckboxes: false,
|
||||
showRowRenderers: false,
|
||||
title: i18n.ALERTS_TABLE_TITLE,
|
||||
}),
|
||||
[]
|
||||
|
|
|
@ -65,4 +65,5 @@ export const alertsHeaders: ColumnHeader[] = [
|
|||
export const alertsDefaultModel: SubsetTimelineModel = {
|
||||
...timelineDefaults,
|
||||
columns: alertsHeaders,
|
||||
showRowRenderers: false,
|
||||
};
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { getOr, isEmpty, isEqual } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { getOr, isEmpty, isEqual, union } from 'lodash/fp';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { BrowserFields } from '../../containers/source';
|
||||
|
@ -46,6 +46,7 @@ interface Props {
|
|||
browserFields: BrowserFields;
|
||||
columns: ColumnHeader[];
|
||||
dataProviders: DataProvider[];
|
||||
deletedEventIds: Readonly<string[]>;
|
||||
end: number;
|
||||
filters: esFilters.Filter[];
|
||||
headerFilterGroup?: React.ReactNode;
|
||||
|
@ -71,6 +72,7 @@ export const EventsViewer = React.memo<Props>(
|
|||
browserFields,
|
||||
columns,
|
||||
dataProviders,
|
||||
deletedEventIds,
|
||||
end,
|
||||
filters,
|
||||
headerFilterGroup,
|
||||
|
@ -104,6 +106,14 @@ export const EventsViewer = React.memo<Props>(
|
|||
end,
|
||||
isEventViewer: true,
|
||||
});
|
||||
const queryFields = useMemo(
|
||||
() =>
|
||||
union(
|
||||
columnsHeader.map(c => c.id),
|
||||
timelineTypeContext.queryFields ?? []
|
||||
),
|
||||
[columnsHeader, timelineTypeContext.queryFields]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel data-test-subj="events-viewer-panel" grow={false}>
|
||||
|
@ -119,7 +129,7 @@ export const EventsViewer = React.memo<Props>(
|
|||
|
||||
{combinedQueries != null ? (
|
||||
<TimelineQuery
|
||||
fields={columnsHeader.map(c => c.id)}
|
||||
fields={queryFields}
|
||||
filterQuery={combinedQueries.filterQuery}
|
||||
id={id}
|
||||
indexPattern={indexPattern}
|
||||
|
@ -139,73 +149,81 @@ export const EventsViewer = React.memo<Props>(
|
|||
pageInfo,
|
||||
refetch,
|
||||
totalCount = 0,
|
||||
}) => (
|
||||
<>
|
||||
<HeaderSection
|
||||
id={id}
|
||||
showInspect={showInspect}
|
||||
subtitle={
|
||||
utilityBar
|
||||
? undefined
|
||||
: `${i18n.SHOWING}: ${totalCount.toLocaleString()} ${i18n.UNIT(
|
||||
totalCount
|
||||
)}`
|
||||
}
|
||||
title={timelineTypeContext?.title ?? i18n.EVENTS}
|
||||
>
|
||||
{headerFilterGroup}
|
||||
</HeaderSection>
|
||||
}) => {
|
||||
const totalCountMinusDeleted =
|
||||
totalCount > 0 ? totalCount - deletedEventIds.length : 0;
|
||||
|
||||
{utilityBar?.(totalCount)}
|
||||
|
||||
<div
|
||||
data-test-subj={`events-container-loading-${loading}`}
|
||||
style={{ width: `${width}px` }}
|
||||
>
|
||||
<ManageTimelineContext
|
||||
loading={loading}
|
||||
width={width}
|
||||
type={timelineTypeContext}
|
||||
// TODO: Reset eventDeletedIds/eventLoadingIds on refresh/loadmore (getUpdatedAt)
|
||||
return (
|
||||
<>
|
||||
<HeaderSection
|
||||
id={id}
|
||||
showInspect={showInspect}
|
||||
subtitle={
|
||||
utilityBar
|
||||
? undefined
|
||||
: `${
|
||||
i18n.SHOWING
|
||||
}: ${totalCountMinusDeleted.toLocaleString()} ${i18n.UNIT(
|
||||
totalCountMinusDeleted
|
||||
)}`
|
||||
}
|
||||
title={timelineTypeContext?.title ?? i18n.EVENTS}
|
||||
>
|
||||
<TimelineRefetch
|
||||
id={id}
|
||||
inputId="global"
|
||||
inspect={inspect}
|
||||
{headerFilterGroup}
|
||||
</HeaderSection>
|
||||
|
||||
{utilityBar?.(totalCountMinusDeleted)}
|
||||
|
||||
<div
|
||||
data-test-subj={`events-container-loading-${loading}`}
|
||||
style={{ width: `${width}px` }}
|
||||
>
|
||||
<ManageTimelineContext
|
||||
loading={loading}
|
||||
refetch={refetch}
|
||||
/>
|
||||
width={width}
|
||||
type={timelineTypeContext}
|
||||
>
|
||||
<TimelineRefetch
|
||||
id={id}
|
||||
inputId="global"
|
||||
inspect={inspect}
|
||||
loading={loading}
|
||||
refetch={refetch}
|
||||
/>
|
||||
|
||||
<StatefulBody
|
||||
browserFields={browserFields}
|
||||
data={events}
|
||||
id={id}
|
||||
isEventViewer={true}
|
||||
height={height}
|
||||
sort={sort}
|
||||
toggleColumn={toggleColumn}
|
||||
/>
|
||||
<StatefulBody
|
||||
browserFields={browserFields}
|
||||
data={events.filter(e => !deletedEventIds.includes(e._id))}
|
||||
id={id}
|
||||
isEventViewer={true}
|
||||
height={height}
|
||||
sort={sort}
|
||||
toggleColumn={toggleColumn}
|
||||
/>
|
||||
|
||||
<Footer
|
||||
compact={isCompactFooter(width)}
|
||||
getUpdatedAt={getUpdatedAt}
|
||||
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
|
||||
height={footerHeight}
|
||||
isEventViewer={true}
|
||||
isLive={isLive}
|
||||
isLoading={loading}
|
||||
itemsCount={events.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
itemsPerPageOptions={itemsPerPageOptions}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onLoadMore={loadMore}
|
||||
nextCursor={getOr(null, 'endCursor.value', pageInfo)!}
|
||||
serverSideEventCount={totalCount}
|
||||
tieBreaker={getOr(null, 'endCursor.tiebreaker', pageInfo)}
|
||||
/>
|
||||
</ManageTimelineContext>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Footer
|
||||
compact={isCompactFooter(width)}
|
||||
getUpdatedAt={getUpdatedAt}
|
||||
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
|
||||
height={footerHeight}
|
||||
isEventViewer={true}
|
||||
isLive={isLive}
|
||||
isLoading={loading}
|
||||
itemsCount={events.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
itemsPerPageOptions={itemsPerPageOptions}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onLoadMore={loadMore}
|
||||
nextCursor={getOr(null, 'endCursor.value', pageInfo)!}
|
||||
serverSideEventCount={totalCountMinusDeleted}
|
||||
tieBreaker={getOr(null, 'endCursor.tiebreaker', pageInfo)}
|
||||
/>
|
||||
</ManageTimelineContext>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</TimelineQuery>
|
||||
) : null}
|
||||
</>
|
||||
|
@ -218,6 +236,7 @@ export const EventsViewer = React.memo<Props>(
|
|||
prevProps.browserFields === nextProps.browserFields &&
|
||||
prevProps.columns === nextProps.columns &&
|
||||
prevProps.dataProviders === nextProps.dataProviders &&
|
||||
prevProps.deletedEventIds === nextProps.deletedEventIds &&
|
||||
prevProps.end === nextProps.end &&
|
||||
isEqual(prevProps.filters, nextProps.filters) &&
|
||||
prevProps.height === nextProps.height &&
|
||||
|
@ -230,6 +249,8 @@ export const EventsViewer = React.memo<Props>(
|
|||
isEqual(prevProps.query, nextProps.query) &&
|
||||
prevProps.showInspect === nextProps.showInspect &&
|
||||
prevProps.start === nextProps.start &&
|
||||
prevProps.sort === nextProps.sort
|
||||
prevProps.sort === nextProps.sort &&
|
||||
isEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) &&
|
||||
prevProps.utilityBar === nextProps.utilityBar
|
||||
);
|
||||
EventsViewer.displayName = 'EventsViewer';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
import chrome from 'ui/chrome';
|
||||
|
@ -23,10 +23,10 @@ import { InputsModelId } from '../../store/inputs/constants';
|
|||
import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns';
|
||||
import { TimelineTypeContextProps } from '../timeline/timeline_context';
|
||||
import { DEFAULT_INDEX_KEY } from '../../../common/constants';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface OwnProps {
|
||||
defaultIndices?: string[];
|
||||
defaultFilters?: esFilters.Filter[];
|
||||
defaultModel: SubsetTimelineModel;
|
||||
end: number;
|
||||
id: string;
|
||||
|
@ -38,7 +38,6 @@ export interface OwnProps {
|
|||
}
|
||||
|
||||
interface StateReduxProps {
|
||||
activePage?: number;
|
||||
columns: ColumnHeader[];
|
||||
dataProviders?: DataProvider[];
|
||||
filters: esFilters.Filter[];
|
||||
|
@ -46,9 +45,12 @@ interface StateReduxProps {
|
|||
itemsPerPage?: number;
|
||||
itemsPerPageOptions?: number[];
|
||||
kqlMode: KqlMode;
|
||||
deletedEventIds: Readonly<string[]>;
|
||||
query: Query;
|
||||
pageCount?: number;
|
||||
sort?: Sort;
|
||||
showCheckboxes: boolean;
|
||||
showRowRenderers: boolean;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
|
@ -57,6 +59,8 @@ interface DispatchProps {
|
|||
columns: ColumnHeader[];
|
||||
itemsPerPage?: number;
|
||||
sort?: Sort;
|
||||
showCheckboxes?: boolean;
|
||||
showRowRenderers?: boolean;
|
||||
}>;
|
||||
deleteEventQuery: ActionCreator<{
|
||||
id: string;
|
||||
|
@ -84,8 +88,8 @@ const StatefulEventsViewerComponent = React.memo<Props>(
|
|||
createTimeline,
|
||||
columns,
|
||||
dataProviders,
|
||||
defaultFilters = [],
|
||||
defaultModel,
|
||||
deletedEventIds,
|
||||
defaultIndices,
|
||||
deleteEventQuery,
|
||||
end,
|
||||
|
@ -96,13 +100,15 @@ const StatefulEventsViewerComponent = React.memo<Props>(
|
|||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
kqlMode,
|
||||
pageFilters = [],
|
||||
query,
|
||||
removeColumn,
|
||||
start,
|
||||
showCheckboxes,
|
||||
showRowRenderers,
|
||||
sort,
|
||||
timelineTypeContext = {
|
||||
showCheckboxes: false,
|
||||
showRowRenderers: true,
|
||||
loadingText: i18n.LOADING_EVENTS,
|
||||
},
|
||||
updateItemsPerPage,
|
||||
upsertColumn,
|
||||
|
@ -115,7 +121,7 @@ const StatefulEventsViewerComponent = React.memo<Props>(
|
|||
|
||||
useEffect(() => {
|
||||
if (createTimeline != null) {
|
||||
createTimeline({ id, columns, sort, itemsPerPage });
|
||||
createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers });
|
||||
}
|
||||
return () => {
|
||||
deleteEventQuery({ id, inputId: 'global' });
|
||||
|
@ -151,7 +157,7 @@ const StatefulEventsViewerComponent = React.memo<Props>(
|
|||
|
||||
const handleOnMouseEnter = useCallback(() => setShowInspect(true), []);
|
||||
const handleOnMouseLeave = useCallback(() => setShowInspect(false), []);
|
||||
const eventsFilter = useMemo(() => [...filters], [defaultFilters]);
|
||||
|
||||
return (
|
||||
<div onMouseEnter={handleOnMouseEnter} onMouseLeave={handleOnMouseLeave}>
|
||||
<EventsViewer
|
||||
|
@ -159,8 +165,9 @@ const StatefulEventsViewerComponent = React.memo<Props>(
|
|||
columns={columns}
|
||||
id={id}
|
||||
dataProviders={dataProviders!}
|
||||
deletedEventIds={deletedEventIds}
|
||||
end={end}
|
||||
filters={eventsFilter}
|
||||
filters={filters}
|
||||
headerFilterGroup={headerFilterGroup}
|
||||
indexPattern={indexPatterns ?? { fields: [], title: '' }}
|
||||
isLive={isLive}
|
||||
|
@ -181,9 +188,9 @@ const StatefulEventsViewerComponent = React.memo<Props>(
|
|||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.id === nextProps.id &&
|
||||
prevProps.activePage === nextProps.activePage &&
|
||||
isEqual(prevProps.columns, nextProps.columns) &&
|
||||
isEqual(prevProps.dataProviders, nextProps.dataProviders) &&
|
||||
prevProps.deletedEventIds === nextProps.deletedEventIds &&
|
||||
prevProps.end === nextProps.end &&
|
||||
isEqual(prevProps.filters, nextProps.filters) &&
|
||||
prevProps.isLive === nextProps.isLive &&
|
||||
|
@ -194,7 +201,12 @@ const StatefulEventsViewerComponent = React.memo<Props>(
|
|||
prevProps.pageCount === nextProps.pageCount &&
|
||||
isEqual(prevProps.sort, nextProps.sort) &&
|
||||
prevProps.start === nextProps.start &&
|
||||
isEqual(prevProps.defaultFilters, nextProps.defaultFilters)
|
||||
isEqual(prevProps.pageFilters, nextProps.pageFilters) &&
|
||||
prevProps.showCheckboxes === nextProps.showCheckboxes &&
|
||||
prevProps.showRowRenderers === nextProps.showRowRenderers &&
|
||||
prevProps.start === nextProps.start &&
|
||||
isEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) &&
|
||||
prevProps.utilityBar === nextProps.utilityBar
|
||||
);
|
||||
|
||||
StatefulEventsViewerComponent.displayName = 'StatefulEventsViewerComponent';
|
||||
|
@ -204,15 +216,26 @@ const makeMapStateToProps = () => {
|
|||
const getGlobalQuerySelector = inputsSelectors.globalQuerySelector();
|
||||
const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector();
|
||||
const getEvents = timelineSelectors.getEventsByIdSelector();
|
||||
const mapStateToProps = (state: State, { id, defaultFilters = [], defaultModel }: OwnProps) => {
|
||||
const mapStateToProps = (state: State, { id, pageFilters = [], defaultModel }: OwnProps) => {
|
||||
const input: inputsModel.InputsRange = getInputsTimeline(state);
|
||||
const events: TimelineModel = getEvents(state, id) ?? defaultModel;
|
||||
const { columns, dataProviders, itemsPerPage, itemsPerPageOptions, kqlMode, sort } = events;
|
||||
const {
|
||||
columns,
|
||||
dataProviders,
|
||||
deletedEventIds,
|
||||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
kqlMode,
|
||||
sort,
|
||||
showCheckboxes,
|
||||
showRowRenderers,
|
||||
} = events;
|
||||
|
||||
return {
|
||||
columns,
|
||||
dataProviders,
|
||||
filters: [...getGlobalFiltersQuerySelector(state), ...defaultFilters],
|
||||
deletedEventIds,
|
||||
filters: [...getGlobalFiltersQuerySelector(state), ...pageFilters],
|
||||
id,
|
||||
isLive: input.policy.kind === 'interval',
|
||||
itemsPerPage,
|
||||
|
@ -220,6 +243,8 @@ const makeMapStateToProps = () => {
|
|||
kqlMode,
|
||||
query: getGlobalQuerySelector(state),
|
||||
sort,
|
||||
showCheckboxes,
|
||||
showRowRenderers,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
@ -231,5 +256,4 @@ export const StatefulEventsViewer = connect(makeMapStateToProps, {
|
|||
updateItemsPerPage: timelineActions.updateItemsPerPage,
|
||||
removeColumn: timelineActions.removeColumn,
|
||||
upsertColumn: timelineActions.upsertColumn,
|
||||
setSearchBarFilter: inputsActions.setSearchBarFilter,
|
||||
})(StatefulEventsViewerComponent);
|
||||
|
|
|
@ -14,6 +14,13 @@ export const EVENTS = i18n.translate('xpack.siem.eventsViewer.eventsLabel', {
|
|||
defaultMessage: 'Events',
|
||||
});
|
||||
|
||||
export const LOADING_EVENTS = i18n.translate(
|
||||
'xpack.siem.eventsViewer.footer.loadingEventsDataLabel',
|
||||
{
|
||||
defaultMessage: 'Loading Events',
|
||||
}
|
||||
);
|
||||
|
||||
export const UNIT = (totalCount: number) =>
|
||||
i18n.translate('xpack.siem.eventsViewer.unit', {
|
||||
values: { totalCount },
|
||||
|
|
|
@ -234,6 +234,7 @@ describe('helpers', () => {
|
|||
start: 0,
|
||||
},
|
||||
description: '',
|
||||
deletedEventIds: [],
|
||||
eventIdToNoteIds: {},
|
||||
filters: [],
|
||||
highlightedDropAndProviderId: '',
|
||||
|
@ -241,6 +242,7 @@ describe('helpers', () => {
|
|||
id: 'savedObject-1',
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
itemsPerPage: 25,
|
||||
|
@ -250,11 +252,15 @@ describe('helpers', () => {
|
|||
filterQuery: null,
|
||||
filterQueryDraft: null,
|
||||
},
|
||||
loadingEventIds: [],
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
pinnedEventsSaveObject: {},
|
||||
savedObjectId: 'savedObject-1',
|
||||
selectedEventIds: {},
|
||||
show: false,
|
||||
showCheckboxes: false,
|
||||
showRowRenderers: true,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: 'desc',
|
||||
|
@ -321,6 +327,7 @@ describe('helpers', () => {
|
|||
start: 0,
|
||||
},
|
||||
description: '',
|
||||
deletedEventIds: [],
|
||||
eventIdToNoteIds: {},
|
||||
filters: [],
|
||||
highlightedDropAndProviderId: '',
|
||||
|
@ -328,6 +335,7 @@ describe('helpers', () => {
|
|||
id: 'savedObject-1',
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
itemsPerPage: 25,
|
||||
|
@ -337,11 +345,15 @@ describe('helpers', () => {
|
|||
filterQuery: null,
|
||||
filterQueryDraft: null,
|
||||
},
|
||||
loadingEventIds: [],
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
pinnedEventsSaveObject: {},
|
||||
savedObjectId: 'savedObject-1',
|
||||
selectedEventIds: {},
|
||||
show: false,
|
||||
showCheckboxes: false,
|
||||
showRowRenderers: true,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: 'desc',
|
||||
|
@ -401,12 +413,14 @@ describe('helpers', () => {
|
|||
version: '1',
|
||||
dataProviders: [],
|
||||
description: '',
|
||||
deletedEventIds: [],
|
||||
eventIdToNoteIds: {},
|
||||
filters: [],
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
itemsPerPage: 25,
|
||||
|
@ -416,6 +430,7 @@ describe('helpers', () => {
|
|||
filterQuery: null,
|
||||
filterQueryDraft: null,
|
||||
},
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
|
@ -424,7 +439,10 @@ describe('helpers', () => {
|
|||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
selectedEventIds: {},
|
||||
show: false,
|
||||
showCheckboxes: false,
|
||||
showRowRenderers: true,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: 'desc',
|
||||
|
@ -516,6 +534,7 @@ describe('helpers', () => {
|
|||
version: '1',
|
||||
dataProviders: [],
|
||||
description: '',
|
||||
deletedEventIds: [],
|
||||
eventIdToNoteIds: {},
|
||||
filters: [
|
||||
{
|
||||
|
@ -565,6 +584,7 @@ describe('helpers', () => {
|
|||
historyIds: [],
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
itemsPerPage: 25,
|
||||
|
@ -574,6 +594,7 @@ describe('helpers', () => {
|
|||
filterQuery: null,
|
||||
filterQueryDraft: null,
|
||||
},
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
|
@ -582,7 +603,10 @@ describe('helpers', () => {
|
|||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
selectedEventIds: {},
|
||||
show: false,
|
||||
showCheckboxes: false,
|
||||
showRowRenderers: true,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: 'desc',
|
||||
|
|
|
@ -24,9 +24,11 @@ describe('Actions', () => {
|
|||
eventIsPinned={false}
|
||||
getNotesByIds={jest.fn()}
|
||||
loading={false}
|
||||
loadingEventIds={[]}
|
||||
noteIds={[]}
|
||||
onEventToggled={jest.fn()}
|
||||
onPinClicked={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
showCheckboxes={true}
|
||||
showNotes={false}
|
||||
toggleShowNotes={jest.fn()}
|
||||
|
@ -50,9 +52,11 @@ describe('Actions', () => {
|
|||
eventIsPinned={false}
|
||||
getNotesByIds={jest.fn()}
|
||||
loading={false}
|
||||
loadingEventIds={[]}
|
||||
noteIds={[]}
|
||||
onEventToggled={jest.fn()}
|
||||
onPinClicked={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
showCheckboxes={false}
|
||||
showNotes={false}
|
||||
toggleShowNotes={jest.fn()}
|
||||
|
@ -76,9 +80,11 @@ describe('Actions', () => {
|
|||
eventIsPinned={false}
|
||||
getNotesByIds={jest.fn()}
|
||||
loading={false}
|
||||
loadingEventIds={[]}
|
||||
noteIds={[]}
|
||||
onEventToggled={jest.fn()}
|
||||
onPinClicked={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
showCheckboxes={false}
|
||||
showNotes={false}
|
||||
toggleShowNotes={jest.fn()}
|
||||
|
@ -104,9 +110,11 @@ describe('Actions', () => {
|
|||
eventIsPinned={false}
|
||||
getNotesByIds={jest.fn()}
|
||||
loading={false}
|
||||
loadingEventIds={[]}
|
||||
noteIds={[]}
|
||||
onEventToggled={onEventToggled}
|
||||
onPinClicked={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
showCheckboxes={false}
|
||||
showNotes={false}
|
||||
toggleShowNotes={jest.fn()}
|
||||
|
@ -138,9 +146,11 @@ describe('Actions', () => {
|
|||
getNotesByIds={jest.fn()}
|
||||
isEventViewer={true}
|
||||
loading={false}
|
||||
loadingEventIds={[]}
|
||||
noteIds={[]}
|
||||
onEventToggled={jest.fn()}
|
||||
onPinClicked={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
showCheckboxes={false}
|
||||
showNotes={false}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
|
@ -166,9 +176,11 @@ describe('Actions', () => {
|
|||
eventIsPinned={false}
|
||||
getNotesByIds={jest.fn()}
|
||||
loading={false}
|
||||
loadingEventIds={[]}
|
||||
noteIds={[]}
|
||||
onEventToggled={jest.fn()}
|
||||
onPinClicked={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
showCheckboxes={false}
|
||||
showNotes={false}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
|
@ -200,9 +212,11 @@ describe('Actions', () => {
|
|||
getNotesByIds={jest.fn()}
|
||||
isEventViewer={true}
|
||||
loading={false}
|
||||
loadingEventIds={[]}
|
||||
noteIds={[]}
|
||||
onEventToggled={jest.fn()}
|
||||
onPinClicked={onPinClicked}
|
||||
onRowSelected={jest.fn()}
|
||||
showCheckboxes={false}
|
||||
showNotes={false}
|
||||
toggleShowNotes={jest.fn()}
|
||||
|
@ -228,9 +242,11 @@ describe('Actions', () => {
|
|||
eventIsPinned={false}
|
||||
getNotesByIds={jest.fn()}
|
||||
loading={false}
|
||||
loadingEventIds={[]}
|
||||
noteIds={[]}
|
||||
onEventToggled={jest.fn()}
|
||||
onPinClicked={onPinClicked}
|
||||
onRowSelected={jest.fn()}
|
||||
showCheckboxes={false}
|
||||
showNotes={false}
|
||||
toggleShowNotes={jest.fn()}
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiButtonIcon, EuiCheckbox, EuiToolTip } from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Note } from '../../../../lib/note';
|
||||
|
@ -14,17 +13,33 @@ import { NotesButton } from '../../properties/helpers';
|
|||
import { EventsLoading, EventsTd, EventsTdContent, EventsTdGroupActions } from '../../styles';
|
||||
import { eventHasNotes, getPinTooltip } from '../helpers';
|
||||
import * as i18n from '../translations';
|
||||
import { OnRowSelected } from '../../events';
|
||||
import { TimelineNonEcsData } from '../../../../graphql/types';
|
||||
|
||||
export interface TimelineActionProps {
|
||||
eventId: string;
|
||||
data: TimelineNonEcsData[];
|
||||
}
|
||||
|
||||
export interface TimelineAction {
|
||||
getAction: ({ eventId, data }: TimelineActionProps) => JSX.Element;
|
||||
width: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
actionsColumnWidth: number;
|
||||
additionalActions?: JSX.Element[];
|
||||
associateNote: AssociateNote;
|
||||
checked: boolean;
|
||||
onRowSelected: OnRowSelected;
|
||||
expanded: boolean;
|
||||
eventId: string;
|
||||
eventIsPinned: boolean;
|
||||
getNotesByIds: (noteIds: string[]) => Note[];
|
||||
isEventViewer?: boolean;
|
||||
loading: boolean;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
noteIds: string[];
|
||||
onEventToggled: () => void;
|
||||
onPinClicked: () => void;
|
||||
|
@ -39,6 +54,7 @@ const emptyNotes: string[] = [];
|
|||
export const Actions = React.memo<Props>(
|
||||
({
|
||||
actionsColumnWidth,
|
||||
additionalActions,
|
||||
associateNote,
|
||||
checked,
|
||||
expanded,
|
||||
|
@ -47,9 +63,11 @@ export const Actions = React.memo<Props>(
|
|||
getNotesByIds,
|
||||
isEventViewer = false,
|
||||
loading = false,
|
||||
loadingEventIds,
|
||||
noteIds,
|
||||
onEventToggled,
|
||||
onPinClicked,
|
||||
onRowSelected,
|
||||
showCheckboxes,
|
||||
showNotes,
|
||||
toggleShowNotes,
|
||||
|
@ -62,16 +80,27 @@ export const Actions = React.memo<Props>(
|
|||
{showCheckboxes && (
|
||||
<EventsTd data-test-subj="select-event-container">
|
||||
<EventsTdContent textAlign="center">
|
||||
<EuiCheckbox
|
||||
data-test-subj="select-event"
|
||||
id={eventId}
|
||||
checked={checked}
|
||||
onChange={noop}
|
||||
/>
|
||||
{loadingEventIds.includes(eventId) ? (
|
||||
<EuiLoadingSpinner size="m" data-test-subj="event-loader" />
|
||||
) : (
|
||||
<EuiCheckbox
|
||||
data-test-subj="select-event"
|
||||
id={eventId}
|
||||
checked={checked}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onRowSelected({
|
||||
eventIds: [eventId],
|
||||
isSelected: event.currentTarget.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EventsTdContent>
|
||||
</EventsTd>
|
||||
)}
|
||||
|
||||
<>{additionalActions}</>
|
||||
|
||||
<EventsTd>
|
||||
<EventsTdContent textAlign="center">
|
||||
{loading && <EventsLoading />}
|
||||
|
@ -137,7 +166,9 @@ export const Actions = React.memo<Props>(
|
|||
prevProps.eventId === nextProps.eventId &&
|
||||
prevProps.eventIsPinned === nextProps.eventIsPinned &&
|
||||
prevProps.loading === nextProps.loading &&
|
||||
prevProps.loadingEventIds === nextProps.loadingEventIds &&
|
||||
prevProps.noteIds === nextProps.noteIds &&
|
||||
prevProps.onRowSelected === nextProps.onRowSelected &&
|
||||
prevProps.showCheckboxes === nextProps.showCheckboxes &&
|
||||
prevProps.showNotes === nextProps.showNotes
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
|
|||
<EventsThGroupActions
|
||||
actionsColumnWidth={115}
|
||||
data-test-subj="actions-container"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<EventsTh>
|
||||
<EventsThContent
|
||||
|
|
|
@ -41,11 +41,14 @@ describe('ColumnHeaders', () => {
|
|||
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={defaultHeaders}
|
||||
isSelectAllChecked={false}
|
||||
onColumnSorted={jest.fn()}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onSelectAll={jest.fn}
|
||||
onUpdateColumns={jest.fn()}
|
||||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={sort}
|
||||
timelineId={'test'}
|
||||
toggleColumn={jest.fn()}
|
||||
|
@ -61,11 +64,14 @@ describe('ColumnHeaders', () => {
|
|||
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={defaultHeaders}
|
||||
isSelectAllChecked={false}
|
||||
onColumnSorted={jest.fn()}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onSelectAll={jest.fn}
|
||||
onUpdateColumns={jest.fn()}
|
||||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={sort}
|
||||
timelineId={'test'}
|
||||
toggleColumn={jest.fn()}
|
||||
|
@ -88,11 +94,14 @@ describe('ColumnHeaders', () => {
|
|||
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={defaultHeaders}
|
||||
isSelectAllChecked={false}
|
||||
onColumnSorted={jest.fn()}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onSelectAll={jest.fn}
|
||||
onUpdateColumns={jest.fn()}
|
||||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={sort}
|
||||
timelineId={'test'}
|
||||
toggleColumn={jest.fn()}
|
||||
|
@ -117,11 +126,14 @@ describe('ColumnHeaders', () => {
|
|||
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={defaultHeaders}
|
||||
isSelectAllChecked={false}
|
||||
onColumnSorted={jest.fn()}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onSelectAll={jest.fn}
|
||||
onUpdateColumns={jest.fn()}
|
||||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={sort}
|
||||
timelineId={'test'}
|
||||
toggleColumn={jest.fn()}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiCheckbox } from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import * as React from 'react';
|
||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
|
@ -11,9 +12,9 @@ import { Draggable, Droppable } from 'react-beautiful-dnd';
|
|||
import { BrowserFields } from '../../../../containers/source';
|
||||
import { DragEffects } from '../../../drag_and_drop/draggable_wrapper';
|
||||
import {
|
||||
DRAG_TYPE_FIELD,
|
||||
droppableTimelineColumnsPrefix,
|
||||
getDraggableFieldId,
|
||||
DRAG_TYPE_FIELD,
|
||||
} from '../../../drag_and_drop/helpers';
|
||||
import { DraggableFieldBadge } from '../../../draggables/field_badge';
|
||||
import { StatefulFieldsBrowser } from '../../../fields_browser';
|
||||
|
@ -24,6 +25,7 @@ import {
|
|||
OnColumnResized,
|
||||
OnColumnSorted,
|
||||
OnFilterChange,
|
||||
OnSelectAll,
|
||||
OnUpdateColumns,
|
||||
} from '../../events';
|
||||
import {
|
||||
|
@ -44,12 +46,15 @@ interface Props {
|
|||
browserFields: BrowserFields;
|
||||
columnHeaders: ColumnHeader[];
|
||||
isEventViewer?: boolean;
|
||||
isSelectAllChecked: boolean;
|
||||
onColumnRemoved: OnColumnRemoved;
|
||||
onColumnResized: OnColumnResized;
|
||||
onColumnSorted: OnColumnSorted;
|
||||
onFilterChange?: OnFilterChange;
|
||||
onSelectAll: OnSelectAll;
|
||||
onUpdateColumns: OnUpdateColumns;
|
||||
showEventsSelect: boolean;
|
||||
showSelectAllCheckbox: boolean;
|
||||
sort: Sort;
|
||||
timelineId: string;
|
||||
toggleColumn: (column: ColumnHeader) => void;
|
||||
|
@ -61,12 +66,15 @@ export const ColumnHeadersComponent = ({
|
|||
browserFields,
|
||||
columnHeaders,
|
||||
isEventViewer = false,
|
||||
isSelectAllChecked,
|
||||
onColumnRemoved,
|
||||
onColumnResized,
|
||||
onColumnSorted,
|
||||
onSelectAll,
|
||||
onUpdateColumns,
|
||||
onFilterChange = noop,
|
||||
showEventsSelect,
|
||||
showSelectAllCheckbox,
|
||||
sort,
|
||||
timelineId,
|
||||
toggleColumn,
|
||||
|
@ -78,6 +86,7 @@ export const ColumnHeadersComponent = ({
|
|||
<EventsTrHeader>
|
||||
<EventsThGroupActions
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
justifyContent={showSelectAllCheckbox ? 'flexStart' : 'space-between'}
|
||||
data-test-subj="actions-container"
|
||||
>
|
||||
{showEventsSelect && (
|
||||
|
@ -88,8 +97,23 @@ export const ColumnHeadersComponent = ({
|
|||
</EventsTh>
|
||||
)}
|
||||
|
||||
{showSelectAllCheckbox && (
|
||||
<EventsTh>
|
||||
<EventsThContent textAlign="center">
|
||||
<EuiCheckbox
|
||||
data-test-subj="select-all-events"
|
||||
id={'select-all-events'}
|
||||
checked={isSelectAllChecked}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onSelectAll({ isSelected: event.currentTarget.checked });
|
||||
}}
|
||||
/>
|
||||
</EventsThContent>
|
||||
</EventsTh>
|
||||
)}
|
||||
|
||||
<EventsTh>
|
||||
<EventsThContent textAlign="center">
|
||||
<EventsThContent textAlign={showSelectAllCheckbox ? 'left' : 'center'}>
|
||||
<StatefulFieldsBrowser
|
||||
browserFields={browserFields}
|
||||
columnHeaders={columnHeaders}
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { TimelineNonEcsData } from '../../../../graphql/types';
|
||||
import { Note } from '../../../../lib/note';
|
||||
import { AssociateNote, UpdateNote } from '../../../notes/helpers';
|
||||
import { OnColumnResized, OnPinEvent, OnUnPinEvent } from '../../events';
|
||||
import { EventsTrData } from '../../styles';
|
||||
import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events';
|
||||
import { EventsTdContent, EventsTrData } from '../../styles';
|
||||
import { Actions } from '../actions';
|
||||
import { ColumnHeader } from '../column_headers/column_header';
|
||||
import { DataDrivenColumns } from '../data_driven_columns';
|
||||
|
@ -32,10 +32,14 @@ interface Props {
|
|||
isEventPinned: boolean;
|
||||
isEventViewer?: boolean;
|
||||
loading: boolean;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onColumnResized: OnColumnResized;
|
||||
onEventToggled: () => void;
|
||||
onPinEvent: OnPinEvent;
|
||||
onRowSelected: OnRowSelected;
|
||||
onUnPinEvent: OnUnPinEvent;
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
showNotes: boolean;
|
||||
timelineId: string;
|
||||
toggleShowNotes: () => void;
|
||||
|
@ -60,10 +64,14 @@ export const EventColumnView = React.memo<Props>(
|
|||
isEventPinned = false,
|
||||
isEventViewer = false,
|
||||
loading,
|
||||
loadingEventIds,
|
||||
onColumnResized,
|
||||
onEventToggled,
|
||||
onPinEvent,
|
||||
onRowSelected,
|
||||
onUnPinEvent,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
showNotes,
|
||||
timelineId,
|
||||
toggleShowNotes,
|
||||
|
@ -71,12 +79,24 @@ export const EventColumnView = React.memo<Props>(
|
|||
}) => {
|
||||
const timelineTypeContext = useTimelineTypeContext();
|
||||
|
||||
const additionalActions = useMemo<JSX.Element[]>(() => {
|
||||
return (
|
||||
timelineTypeContext.timelineActions?.map(action => (
|
||||
<EventsTdContent key={action.id} textAlign="center">
|
||||
{action.getAction({ eventId: id, data })}
|
||||
</EventsTdContent>
|
||||
)) ?? []
|
||||
);
|
||||
}, [data, timelineTypeContext.timelineActions]);
|
||||
|
||||
return (
|
||||
<EventsTrData data-test-subj="event-column-view">
|
||||
<Actions
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
additionalActions={additionalActions}
|
||||
associateNote={associateNote}
|
||||
checked={false}
|
||||
checked={Object.keys(selectedEventIds).includes(id)}
|
||||
onRowSelected={onRowSelected}
|
||||
expanded={expanded}
|
||||
data-test-subj="actions"
|
||||
eventId={id}
|
||||
|
@ -84,6 +104,7 @@ export const EventColumnView = React.memo<Props>(
|
|||
getNotesByIds={getNotesByIds}
|
||||
isEventViewer={isEventViewer}
|
||||
loading={loading}
|
||||
loadingEventIds={loadingEventIds}
|
||||
noteIds={eventIdToNoteIds[id] || emptyNotes}
|
||||
onEventToggled={onEventToggled}
|
||||
onPinClicked={getPinOnClick({
|
||||
|
@ -93,7 +114,7 @@ export const EventColumnView = React.memo<Props>(
|
|||
onUnPinEvent,
|
||||
isEventPinned,
|
||||
})}
|
||||
showCheckboxes={timelineTypeContext.showCheckboxes}
|
||||
showCheckboxes={showCheckboxes}
|
||||
showNotes={showNotes}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
updateNote={updateNote}
|
||||
|
@ -120,7 +141,11 @@ export const EventColumnView = React.memo<Props>(
|
|||
prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds &&
|
||||
prevProps.expanded === nextProps.expanded &&
|
||||
prevProps.loading === nextProps.loading &&
|
||||
prevProps.loadingEventIds === nextProps.loadingEventIds &&
|
||||
prevProps.isEventPinned === nextProps.isEventPinned &&
|
||||
prevProps.onRowSelected === nextProps.onRowSelected &&
|
||||
prevProps.selectedEventIds === nextProps.selectedEventIds &&
|
||||
prevProps.showCheckboxes === nextProps.showCheckboxes &&
|
||||
prevProps.showNotes === nextProps.showNotes &&
|
||||
prevProps.timelineId === nextProps.timelineId
|
||||
);
|
||||
|
|
|
@ -7,11 +7,17 @@
|
|||
import React from 'react';
|
||||
|
||||
import { BrowserFields } from '../../../../containers/source';
|
||||
import { TimelineItem } from '../../../../graphql/types';
|
||||
import { TimelineItem, TimelineNonEcsData } from '../../../../graphql/types';
|
||||
import { maxDelay } from '../../../../lib/helpers/scheduler';
|
||||
import { Note } from '../../../../lib/note';
|
||||
import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers';
|
||||
import { OnColumnResized, OnPinEvent, OnUnPinEvent, OnUpdateColumns } from '../../events';
|
||||
import {
|
||||
OnColumnResized,
|
||||
OnPinEvent,
|
||||
OnRowSelected,
|
||||
OnUnPinEvent,
|
||||
OnUpdateColumns,
|
||||
} from '../../events';
|
||||
import { EventsTbody } from '../../styles';
|
||||
import { ColumnHeader } from '../column_headers/column_header';
|
||||
import { ColumnRenderer } from '../renderers/column_renderer';
|
||||
|
@ -30,12 +36,16 @@ interface Props {
|
|||
getNotesByIds: (noteIds: string[]) => Note[];
|
||||
id: string;
|
||||
isEventViewer?: boolean;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onColumnResized: OnColumnResized;
|
||||
onPinEvent: OnPinEvent;
|
||||
onRowSelected: OnRowSelected;
|
||||
onUpdateColumns: OnUpdateColumns;
|
||||
onUnPinEvent: OnUnPinEvent;
|
||||
pinnedEventIds: Readonly<Record<string, boolean>>;
|
||||
rowRenderers: RowRenderer[];
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
toggleColumn: (column: ColumnHeader) => void;
|
||||
updateNote: UpdateNote;
|
||||
}
|
||||
|
@ -55,12 +65,16 @@ export const Events = React.memo<Props>(
|
|||
getNotesByIds,
|
||||
id,
|
||||
isEventViewer = false,
|
||||
loadingEventIds,
|
||||
onColumnResized,
|
||||
onPinEvent,
|
||||
onRowSelected,
|
||||
onUpdateColumns,
|
||||
onUnPinEvent,
|
||||
pinnedEventIds,
|
||||
rowRenderers,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
toggleColumn,
|
||||
updateNote,
|
||||
}) => (
|
||||
|
@ -78,12 +92,16 @@ export const Events = React.memo<Props>(
|
|||
isEventPinned={eventIsPinned({ eventId: event._id, pinnedEventIds })}
|
||||
isEventViewer={isEventViewer}
|
||||
key={event._id}
|
||||
loadingEventIds={loadingEventIds}
|
||||
maxDelay={maxDelay(i)}
|
||||
onColumnResized={onColumnResized}
|
||||
onPinEvent={onPinEvent}
|
||||
onRowSelected={onRowSelected}
|
||||
onUnPinEvent={onUnPinEvent}
|
||||
onUpdateColumns={onUpdateColumns}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={selectedEventIds}
|
||||
showCheckboxes={showCheckboxes}
|
||||
timelineId={id}
|
||||
toggleColumn={toggleColumn}
|
||||
updateNote={updateNote}
|
||||
|
|
|
@ -10,12 +10,18 @@ import VisibilitySensor from 'react-visibility-sensor';
|
|||
|
||||
import { BrowserFields } from '../../../../containers/source';
|
||||
import { TimelineDetailsComponentQuery } from '../../../../containers/timeline/details';
|
||||
import { TimelineItem, DetailItem } from '../../../../graphql/types';
|
||||
import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../graphql/types';
|
||||
import { requestIdleCallbackViaScheduler } from '../../../../lib/helpers/scheduler';
|
||||
import { Note } from '../../../../lib/note';
|
||||
import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers';
|
||||
import { SkeletonRow } from '../../../skeleton_row';
|
||||
import { OnColumnResized, OnPinEvent, OnUnPinEvent, OnUpdateColumns } from '../../events';
|
||||
import {
|
||||
OnColumnResized,
|
||||
OnPinEvent,
|
||||
OnRowSelected,
|
||||
OnUnPinEvent,
|
||||
OnUpdateColumns,
|
||||
} from '../../events';
|
||||
import { ExpandableEvent } from '../../expandable_event';
|
||||
import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers';
|
||||
import { EventsTrGroup, EventsTrSupplement, OFFSET_SCROLLBAR } from '../../styles';
|
||||
|
@ -36,13 +42,17 @@ interface Props {
|
|||
eventIdToNoteIds: Readonly<Record<string, string[]>>;
|
||||
getNotesByIds: (noteIds: string[]) => Note[];
|
||||
isEventViewer?: boolean;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
maxDelay?: number;
|
||||
onColumnResized: OnColumnResized;
|
||||
onPinEvent: OnPinEvent;
|
||||
onRowSelected: OnRowSelected;
|
||||
onUnPinEvent: OnUnPinEvent;
|
||||
onUpdateColumns: OnUpdateColumns;
|
||||
isEventPinned: boolean;
|
||||
rowRenderers: RowRenderer[];
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
timelineId: string;
|
||||
toggleColumn: (column: ColumnHeader) => void;
|
||||
updateNote: UpdateNote;
|
||||
|
@ -110,12 +120,16 @@ export const StatefulEvent = React.memo<Props>(
|
|||
getNotesByIds,
|
||||
isEventViewer = false,
|
||||
isEventPinned = false,
|
||||
loadingEventIds,
|
||||
maxDelay = 0,
|
||||
onColumnResized,
|
||||
onPinEvent,
|
||||
onRowSelected,
|
||||
onUnPinEvent,
|
||||
onUpdateColumns,
|
||||
rowRenderers,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
timelineId,
|
||||
toggleColumn,
|
||||
updateNote,
|
||||
|
@ -224,11 +238,15 @@ export const StatefulEvent = React.memo<Props>(
|
|||
isEventPinned={isEventPinned}
|
||||
isEventViewer={isEventViewer}
|
||||
loading={loading}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onColumnResized={onColumnResized}
|
||||
onPinEvent={onPinEvent}
|
||||
onRowSelected={onRowSelected}
|
||||
onToggleExpanded={onToggleExpanded}
|
||||
onToggleShowNotes={onToggleShowNotes}
|
||||
onUnPinEvent={onUnPinEvent}
|
||||
selectedEventIds={selectedEventIds}
|
||||
showCheckboxes={showCheckboxes}
|
||||
showNotes={!!showNotes[event._id]}
|
||||
timelineId={timelineId}
|
||||
updateNote={updateNote}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { TimelineNonEcsData } from '../../../../graphql/types';
|
|||
import { Note } from '../../../../lib/note';
|
||||
import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers';
|
||||
import { NoteCards } from '../../../notes/note_cards';
|
||||
import { OnPinEvent, OnColumnResized, OnUnPinEvent } from '../../events';
|
||||
import { OnPinEvent, OnColumnResized, OnUnPinEvent, OnRowSelected } from '../../events';
|
||||
import { EventsTrSupplement, OFFSET_SCROLLBAR } from '../../styles';
|
||||
import { useTimelineWidthContext } from '../../timeline_context';
|
||||
import { ColumnHeader } from '../column_headers/column_header';
|
||||
|
@ -31,8 +31,12 @@ interface Props {
|
|||
isEventViewer?: boolean;
|
||||
isEventPinned: boolean;
|
||||
loading: boolean;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onColumnResized: OnColumnResized;
|
||||
onRowSelected: OnRowSelected;
|
||||
onUnPinEvent: OnUnPinEvent;
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
showNotes: boolean;
|
||||
timelineId: string;
|
||||
updateNote: UpdateNote;
|
||||
|
@ -62,9 +66,13 @@ export const StatefulEventChild = React.memo<Props>(
|
|||
isEventViewer = false,
|
||||
isEventPinned = false,
|
||||
loading,
|
||||
loadingEventIds,
|
||||
onColumnResized,
|
||||
onRowSelected,
|
||||
onToggleExpanded,
|
||||
onUnPinEvent,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
showNotes,
|
||||
timelineId,
|
||||
onToggleShowNotes,
|
||||
|
@ -90,10 +98,14 @@ export const StatefulEventChild = React.memo<Props>(
|
|||
isEventPinned={isEventPinned}
|
||||
isEventViewer={isEventViewer}
|
||||
loading={loading}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onColumnResized={onColumnResized}
|
||||
onEventToggled={onToggleExpanded}
|
||||
onPinEvent={onPinEvent}
|
||||
onRowSelected={onRowSelected}
|
||||
onUnPinEvent={onUnPinEvent}
|
||||
selectedEventIds={selectedEventIds}
|
||||
showCheckboxes={showCheckboxes}
|
||||
showNotes={showNotes}
|
||||
timelineId={timelineId}
|
||||
toggleShowNotes={onToggleShowNotes}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { get, isEmpty, noop } from 'lodash/fp';
|
||||
|
||||
import { BrowserFields } from '../../../containers/source';
|
||||
import { Ecs } from '../../../graphql/types';
|
||||
import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../graphql/types';
|
||||
import { OnPinEvent, OnUnPinEvent } from '../events';
|
||||
import { ColumnHeader } from './column_headers/column_header';
|
||||
import * as i18n from './translations';
|
||||
|
@ -95,6 +95,34 @@ export const getColumnHeaders = (
|
|||
};
|
||||
|
||||
/** Returns the (fixed) width of the Actions column */
|
||||
export const getActionsColumnWidth = (isEventViewer: boolean, showCheckboxes = false): number =>
|
||||
export const getActionsColumnWidth = (
|
||||
isEventViewer: boolean,
|
||||
showCheckboxes = false,
|
||||
additionalActionWidth = 0
|
||||
): number =>
|
||||
(showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0) +
|
||||
(isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH);
|
||||
(isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) +
|
||||
additionalActionWidth;
|
||||
|
||||
/**
|
||||
* 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 timelineData
|
||||
* @param eventIds
|
||||
* @param fieldsToKeep
|
||||
*/
|
||||
export const getEventIdToDataMapping = (
|
||||
timelineData: TimelineItem[],
|
||||
eventIds: string[],
|
||||
fieldsToKeep: string[]
|
||||
): Record<string, TimelineNonEcsData[]> => {
|
||||
return timelineData.reduce((acc, v) => {
|
||||
const fvm = eventIds.includes(v._id)
|
||||
? { [v._id]: v.data.filter(ti => fieldsToKeep.includes(ti.field)) }
|
||||
: {};
|
||||
return {
|
||||
...acc,
|
||||
...fvm,
|
||||
};
|
||||
}, {});
|
||||
};
|
||||
|
|
|
@ -55,18 +55,24 @@ describe('Body', () => {
|
|||
eventIdToNoteIds={{}}
|
||||
height={testBodyHeight}
|
||||
id={'timeline-test'}
|
||||
isSelectAllChecked={false}
|
||||
getNotesByIds={mockGetNotesByIds}
|
||||
loadingEventIds={[]}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
onFilterChange={jest.fn()}
|
||||
onPinEvent={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
onSelectAll={jest.fn()}
|
||||
onUnPinEvent={jest.fn()}
|
||||
onUpdateColumns={jest.fn()}
|
||||
pinnedEventIds={{}}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={{}}
|
||||
sort={mockSort}
|
||||
showCheckboxes={false}
|
||||
toggleColumn={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
|
@ -93,18 +99,24 @@ describe('Body', () => {
|
|||
eventIdToNoteIds={{}}
|
||||
height={testBodyHeight}
|
||||
id={'timeline-test'}
|
||||
isSelectAllChecked={false}
|
||||
getNotesByIds={mockGetNotesByIds}
|
||||
loadingEventIds={[]}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
onFilterChange={jest.fn()}
|
||||
onPinEvent={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
onSelectAll={jest.fn()}
|
||||
onUnPinEvent={jest.fn()}
|
||||
onUpdateColumns={jest.fn()}
|
||||
pinnedEventIds={{}}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={{}}
|
||||
sort={mockSort}
|
||||
showCheckboxes={false}
|
||||
toggleColumn={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
|
@ -131,18 +143,24 @@ describe('Body', () => {
|
|||
eventIdToNoteIds={{}}
|
||||
height={testBodyHeight}
|
||||
id={'timeline-test'}
|
||||
isSelectAllChecked={false}
|
||||
getNotesByIds={mockGetNotesByIds}
|
||||
loadingEventIds={[]}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
onFilterChange={jest.fn()}
|
||||
onPinEvent={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
onSelectAll={jest.fn()}
|
||||
onUnPinEvent={jest.fn()}
|
||||
onUpdateColumns={jest.fn()}
|
||||
pinnedEventIds={{}}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={{}}
|
||||
sort={mockSort}
|
||||
showCheckboxes={false}
|
||||
toggleColumn={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
|
@ -171,18 +189,24 @@ describe('Body', () => {
|
|||
eventIdToNoteIds={{}}
|
||||
height={testBodyHeight}
|
||||
id={'timeline-test'}
|
||||
isSelectAllChecked={false}
|
||||
getNotesByIds={mockGetNotesByIds}
|
||||
loadingEventIds={[]}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
onFilterChange={jest.fn()}
|
||||
onPinEvent={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
onSelectAll={jest.fn()}
|
||||
onUnPinEvent={jest.fn()}
|
||||
onUpdateColumns={jest.fn()}
|
||||
pinnedEventIds={{}}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={{}}
|
||||
sort={mockSort}
|
||||
showCheckboxes={false}
|
||||
toggleColumn={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
|
@ -258,18 +282,24 @@ describe('Body', () => {
|
|||
eventIdToNoteIds={{}}
|
||||
height={testBodyHeight}
|
||||
id={'timeline-test'}
|
||||
isSelectAllChecked={false}
|
||||
getNotesByIds={mockGetNotesByIds}
|
||||
loadingEventIds={[]}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
onFilterChange={jest.fn()}
|
||||
onPinEvent={dispatchOnPinEvent}
|
||||
onRowSelected={jest.fn()}
|
||||
onSelectAll={jest.fn()}
|
||||
onUnPinEvent={jest.fn()}
|
||||
onUpdateColumns={jest.fn()}
|
||||
pinnedEventIds={{}}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={{}}
|
||||
sort={mockSort}
|
||||
showCheckboxes={false}
|
||||
toggleColumn={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
|
@ -298,18 +328,24 @@ describe('Body', () => {
|
|||
eventIdToNoteIds={{}}
|
||||
height={testBodyHeight}
|
||||
id={'timeline-test'}
|
||||
isSelectAllChecked={false}
|
||||
getNotesByIds={mockGetNotesByIds}
|
||||
loadingEventIds={[]}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
onFilterChange={jest.fn()}
|
||||
onPinEvent={dispatchOnPinEvent}
|
||||
onRowSelected={jest.fn()}
|
||||
onSelectAll={jest.fn()}
|
||||
onUnPinEvent={jest.fn()}
|
||||
onUpdateColumns={jest.fn()}
|
||||
pinnedEventIds={{}}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={{}}
|
||||
sort={mockSort}
|
||||
showCheckboxes={false}
|
||||
toggleColumn={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { BrowserFields } from '../../../containers/source';
|
||||
import { TimelineItem } from '../../../graphql/types';
|
||||
import { TimelineItem, TimelineNonEcsData } from '../../../graphql/types';
|
||||
import { Note } from '../../../lib/note';
|
||||
import { AddNoteToEvent, UpdateNote } from '../../notes/helpers';
|
||||
import {
|
||||
|
@ -16,6 +16,8 @@ import {
|
|||
OnColumnSorted,
|
||||
OnFilterChange,
|
||||
OnPinEvent,
|
||||
OnRowSelected,
|
||||
OnSelectAll,
|
||||
OnUnPinEvent,
|
||||
OnUpdateColumns,
|
||||
} from '../events';
|
||||
|
@ -39,10 +41,14 @@ export interface BodyProps {
|
|||
height: number;
|
||||
id: string;
|
||||
isEventViewer?: boolean;
|
||||
isSelectAllChecked: boolean;
|
||||
eventIdToNoteIds: Readonly<Record<string, string[]>>;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onColumnRemoved: OnColumnRemoved;
|
||||
onColumnResized: OnColumnResized;
|
||||
onColumnSorted: OnColumnSorted;
|
||||
onRowSelected: OnRowSelected;
|
||||
onSelectAll: OnSelectAll;
|
||||
onFilterChange: OnFilterChange;
|
||||
onPinEvent: OnPinEvent;
|
||||
onUpdateColumns: OnUpdateColumns;
|
||||
|
@ -50,6 +56,8 @@ export interface BodyProps {
|
|||
pinnedEventIds: Readonly<Record<string, boolean>>;
|
||||
range: string;
|
||||
rowRenderers: RowRenderer[];
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
sort: Sort;
|
||||
toggleColumn: (column: ColumnHeader) => void;
|
||||
updateNote: UpdateNote;
|
||||
|
@ -68,24 +76,38 @@ export const Body = React.memo<BodyProps>(
|
|||
height,
|
||||
id,
|
||||
isEventViewer = false,
|
||||
isSelectAllChecked,
|
||||
loadingEventIds,
|
||||
onColumnRemoved,
|
||||
onColumnResized,
|
||||
onColumnSorted,
|
||||
onRowSelected,
|
||||
onSelectAll,
|
||||
onFilterChange,
|
||||
onPinEvent,
|
||||
onUpdateColumns,
|
||||
onUnPinEvent,
|
||||
pinnedEventIds,
|
||||
rowRenderers,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
sort,
|
||||
toggleColumn,
|
||||
updateNote,
|
||||
}) => {
|
||||
const timelineTypeContext = useTimelineTypeContext();
|
||||
const additionalActionWidth =
|
||||
timelineTypeContext.timelineActions?.reduce((acc, v) => acc + v.width, 0) ?? 0;
|
||||
|
||||
const columnWidths = columnHeaders.reduce(
|
||||
(totalWidth, header) => totalWidth + header.width,
|
||||
getActionsColumnWidth(isEventViewer, timelineTypeContext.showCheckboxes)
|
||||
const actionsColumnWidth = useMemo(
|
||||
() => getActionsColumnWidth(isEventViewer, showCheckboxes, additionalActionWidth),
|
||||
[isEventViewer, showCheckboxes, additionalActionWidth]
|
||||
);
|
||||
|
||||
const columnWidths = useMemo(
|
||||
() =>
|
||||
columnHeaders.reduce((totalWidth, header) => totalWidth + header.width, actionsColumnWidth),
|
||||
[actionsColumnWidth, columnHeaders]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -97,29 +119,26 @@ export const Body = React.memo<BodyProps>(
|
|||
style={{ minWidth: columnWidths + 'px' }}
|
||||
>
|
||||
<ColumnHeaders
|
||||
actionsColumnWidth={getActionsColumnWidth(
|
||||
isEventViewer,
|
||||
timelineTypeContext.showCheckboxes
|
||||
)}
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
browserFields={browserFields}
|
||||
columnHeaders={columnHeaders}
|
||||
isEventViewer={isEventViewer}
|
||||
isSelectAllChecked={isSelectAllChecked}
|
||||
onColumnRemoved={onColumnRemoved}
|
||||
onColumnResized={onColumnResized}
|
||||
onColumnSorted={onColumnSorted}
|
||||
onFilterChange={onFilterChange}
|
||||
onSelectAll={onSelectAll}
|
||||
onUpdateColumns={onUpdateColumns}
|
||||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={showCheckboxes}
|
||||
sort={sort}
|
||||
timelineId={id}
|
||||
toggleColumn={toggleColumn}
|
||||
/>
|
||||
|
||||
<Events
|
||||
actionsColumnWidth={getActionsColumnWidth(
|
||||
isEventViewer,
|
||||
timelineTypeContext.showCheckboxes
|
||||
)}
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
addNoteToEvent={addNoteToEvent}
|
||||
browserFields={browserFields}
|
||||
columnHeaders={columnHeaders}
|
||||
|
@ -129,12 +148,16 @@ export const Body = React.memo<BodyProps>(
|
|||
getNotesByIds={getNotesByIds}
|
||||
id={id}
|
||||
isEventViewer={isEventViewer}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onColumnResized={onColumnResized}
|
||||
onPinEvent={onPinEvent}
|
||||
onRowSelected={onRowSelected}
|
||||
onUpdateColumns={onUpdateColumns}
|
||||
onUnPinEvent={onUnPinEvent}
|
||||
pinnedEventIds={pinnedEventIds}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={selectedEventIds}
|
||||
showCheckboxes={showCheckboxes}
|
||||
toggleColumn={toggleColumn}
|
||||
updateNote={updateNote}
|
||||
/>
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
|
||||
import { noop } from 'lodash/fp';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
|
||||
import { BrowserFields } from '../../../containers/source';
|
||||
import { TimelineItem } from '../../../graphql/types';
|
||||
import { TimelineItem, TimelineNonEcsData } from '../../../graphql/types';
|
||||
import { Note } from '../../../lib/note';
|
||||
import { appModel, appSelectors, State, timelineSelectors } from '../../../store';
|
||||
import { AddNoteToEvent, UpdateNote } from '../../notes/helpers';
|
||||
|
@ -20,12 +20,14 @@ import {
|
|||
OnColumnResized,
|
||||
OnColumnSorted,
|
||||
OnPinEvent,
|
||||
OnRowSelected,
|
||||
OnSelectAll,
|
||||
OnUnPinEvent,
|
||||
OnUpdateColumns,
|
||||
} from '../events';
|
||||
|
||||
import { ColumnHeader } from './column_headers/column_header';
|
||||
import { getColumnHeaders } from './helpers';
|
||||
import { getColumnHeaders, getEventIdToDataMapping } from './helpers';
|
||||
import { Body } from './index';
|
||||
import { columnRenderers, rowRenderers } from './renderers';
|
||||
import { Sort } from './sort';
|
||||
|
@ -47,9 +49,14 @@ interface OwnProps {
|
|||
interface ReduxProps {
|
||||
columnHeaders: ColumnHeader[];
|
||||
eventIdToNoteIds: Readonly<Record<string, string[]>>;
|
||||
isSelectAllChecked: boolean;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
notesById: appModel.NotesById;
|
||||
pinnedEventIds: Readonly<Record<string, boolean>>;
|
||||
range?: string;
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
showRowRenderers: boolean;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
|
@ -59,6 +66,9 @@ interface DispatchProps {
|
|||
columnId: string;
|
||||
delta: number;
|
||||
}>;
|
||||
clearSelected?: ActionCreator<{
|
||||
id: string;
|
||||
}>;
|
||||
pinEvent?: ActionCreator<{
|
||||
id: string;
|
||||
eventId: string;
|
||||
|
@ -67,6 +77,12 @@ interface DispatchProps {
|
|||
id: string;
|
||||
columnId: string;
|
||||
}>;
|
||||
setSelected?: ActionCreator<{
|
||||
id: string;
|
||||
eventIds: Record<string, TimelineNonEcsData[]>;
|
||||
isSelected: boolean;
|
||||
isSelectAllChecked: boolean;
|
||||
}>;
|
||||
unPinEvent?: ActionCreator<{
|
||||
id: string;
|
||||
eventId: string;
|
||||
|
@ -97,11 +113,18 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
|
|||
height,
|
||||
id,
|
||||
isEventViewer = false,
|
||||
isSelectAllChecked,
|
||||
loadingEventIds,
|
||||
notesById,
|
||||
pinEvent,
|
||||
pinnedEventIds,
|
||||
range,
|
||||
removeColumn,
|
||||
selectedEventIds,
|
||||
setSelected,
|
||||
clearSelected,
|
||||
showCheckboxes,
|
||||
showRowRenderers,
|
||||
sort,
|
||||
toggleColumn,
|
||||
unPinEvent,
|
||||
|
@ -122,6 +145,36 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
|
|||
[id]
|
||||
);
|
||||
|
||||
const onRowSelected: OnRowSelected = useCallback(
|
||||
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
|
||||
setSelected!({
|
||||
id,
|
||||
eventIds: getEventIdToDataMapping(data, eventIds, timelineTypeContext.queryFields ?? []),
|
||||
isSelected,
|
||||
isSelectAllChecked:
|
||||
isSelected && Object.keys(selectedEventIds).length + 1 === data.length,
|
||||
});
|
||||
},
|
||||
[id, data, selectedEventIds, timelineTypeContext.queryFields]
|
||||
);
|
||||
|
||||
const onSelectAll: OnSelectAll = useCallback(
|
||||
({ isSelected }: { isSelected: boolean }) =>
|
||||
isSelected
|
||||
? setSelected!({
|
||||
id,
|
||||
eventIds: getEventIdToDataMapping(
|
||||
data,
|
||||
data.map(event => event._id),
|
||||
timelineTypeContext.queryFields ?? []
|
||||
),
|
||||
isSelected,
|
||||
isSelectAllChecked: isSelected,
|
||||
})
|
||||
: clearSelected!({ id }),
|
||||
[id, data, timelineTypeContext.queryFields]
|
||||
);
|
||||
|
||||
const onColumnSorted: OnColumnSorted = useCallback(
|
||||
sorted => {
|
||||
updateSort!({ id, sort: sorted });
|
||||
|
@ -150,6 +203,13 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
|
|||
[id]
|
||||
);
|
||||
|
||||
// Sync to timelineTypeContext.selectAll so parent components can select all events
|
||||
useEffect(() => {
|
||||
if (timelineTypeContext.selectAll) {
|
||||
onSelectAll({ isSelected: true });
|
||||
}
|
||||
}, [timelineTypeContext.selectAll]); // onSelectAll dependency not necessary
|
||||
|
||||
return (
|
||||
<Body
|
||||
addNoteToEvent={onAddNoteToEvent}
|
||||
|
@ -162,16 +222,22 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
|
|||
height={height}
|
||||
id={id}
|
||||
isEventViewer={isEventViewer}
|
||||
isSelectAllChecked={isSelectAllChecked}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onColumnRemoved={onColumnRemoved}
|
||||
onColumnResized={onColumnResized}
|
||||
onColumnSorted={onColumnSorted}
|
||||
onRowSelected={onRowSelected}
|
||||
onSelectAll={onSelectAll}
|
||||
onFilterChange={noop} // TODO: this is the callback for column filters, which is out scope for this phase of delivery
|
||||
onPinEvent={onPinEvent}
|
||||
onUnPinEvent={onUnPinEvent}
|
||||
onUpdateColumns={onUpdateColumns}
|
||||
pinnedEventIds={pinnedEventIds}
|
||||
range={range!}
|
||||
rowRenderers={timelineTypeContext.showRowRenderers ? rowRenderers : [plainRowRenderer]}
|
||||
rowRenderers={showRowRenderers ? rowRenderers : [plainRowRenderer]}
|
||||
selectedEventIds={selectedEventIds}
|
||||
showCheckboxes={showCheckboxes}
|
||||
sort={sort}
|
||||
toggleColumn={toggleColumn}
|
||||
updateNote={onUpdateNote}
|
||||
|
@ -188,7 +254,12 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
|
|||
prevProps.height === nextProps.height &&
|
||||
prevProps.id === nextProps.id &&
|
||||
prevProps.isEventViewer === nextProps.isEventViewer &&
|
||||
prevProps.isSelectAllChecked === nextProps.isSelectAllChecked &&
|
||||
prevProps.loadingEventIds === nextProps.loadingEventIds &&
|
||||
prevProps.pinnedEventIds === nextProps.pinnedEventIds &&
|
||||
prevProps.selectedEventIds === nextProps.selectedEventIds &&
|
||||
prevProps.showCheckboxes === nextProps.showCheckboxes &&
|
||||
prevProps.showRowRenderers === nextProps.showRowRenderers &&
|
||||
prevProps.range === nextProps.range &&
|
||||
prevProps.sort === nextProps.sort
|
||||
);
|
||||
|
@ -207,14 +278,28 @@ const makeMapStateToProps = () => {
|
|||
const getNotesByIds = appSelectors.notesByIdsSelector();
|
||||
const mapStateToProps = (state: State, { browserFields, id }: OwnProps) => {
|
||||
const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults;
|
||||
const { columns, eventIdToNoteIds, pinnedEventIds } = timeline;
|
||||
const {
|
||||
columns,
|
||||
eventIdToNoteIds,
|
||||
isSelectAllChecked,
|
||||
loadingEventIds,
|
||||
pinnedEventIds,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
showRowRenderers,
|
||||
} = timeline;
|
||||
|
||||
return {
|
||||
columnHeaders: memoizedColumnHeaders(columns, browserFields),
|
||||
eventIdToNoteIds,
|
||||
isSelectAllChecked,
|
||||
loadingEventIds,
|
||||
notesById: getNotesByIds(state),
|
||||
id,
|
||||
pinnedEventIds,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
showRowRenderers,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
@ -223,9 +308,11 @@ const makeMapStateToProps = () => {
|
|||
export const StatefulBody = connect(makeMapStateToProps, {
|
||||
addNoteToEvent: timelineActions.addNoteToEvent,
|
||||
applyDeltaToColumnWidth: timelineActions.applyDeltaToColumnWidth,
|
||||
clearSelected: timelineActions.clearSelected,
|
||||
pinEvent: timelineActions.pinEvent,
|
||||
removeColumn: timelineActions.removeColumn,
|
||||
removeProvider: timelineActions.removeProvider,
|
||||
setSelected: timelineActions.setSelected,
|
||||
unPinEvent: timelineActions.unPinEvent,
|
||||
updateColumns: timelineActions.updateColumns,
|
||||
updateNote: appActions.updateNote,
|
||||
|
|
|
@ -72,6 +72,18 @@ export type OnChangeDroppableAndProvider = (providerId: string) => void;
|
|||
/** Invoked when a user pins an event */
|
||||
export type OnPinEvent = (eventId: string) => void;
|
||||
|
||||
/** Invoked when a user checks/un-checks a row */
|
||||
export type OnRowSelected = ({
|
||||
eventIds,
|
||||
isSelected,
|
||||
}: {
|
||||
eventIds: string[];
|
||||
isSelected: boolean;
|
||||
}) => void;
|
||||
|
||||
/** Invoked when a user checks/un-checks the select all checkbox */
|
||||
export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void;
|
||||
|
||||
/** Invoked when columns are updated */
|
||||
export type OnUpdateColumns = (columns: ColumnHeader[]) => void;
|
||||
|
||||
|
|
|
@ -211,6 +211,7 @@ export const FooterComponent = ({
|
|||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [paginationLoading, setPaginationLoading] = useState(false);
|
||||
const [updatedAt, setUpdatedAt] = useState<number | null>(null);
|
||||
const timelineTypeContext = useTimelineTypeContext();
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
setPaginationLoading(true);
|
||||
|
@ -239,7 +240,7 @@ export const FooterComponent = ({
|
|||
data-test-subj="LoadingPanelTimeline"
|
||||
height="35px"
|
||||
showBorder={false}
|
||||
text={isEventViewer ? `${i18n.LOADING_EVENTS}...` : `${i18n.LOADING_TIMELINE_DATA}...`}
|
||||
text={`${timelineTypeContext.loadingText ?? i18n.LOADING_TIMELINE_DATA}...`}
|
||||
width="100%"
|
||||
/>
|
||||
</LoadingPanelContainer>
|
||||
|
|
|
@ -6,10 +6,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const LOADING_EVENTS = i18n.translate('xpack.siem.footer.loadingEventsData', {
|
||||
defaultMessage: 'Loading Events',
|
||||
});
|
||||
|
||||
export const LOADING_TIMELINE_DATA = i18n.translate('xpack.siem.footer.loadingTimelineData', {
|
||||
defaultMessage: 'Loading Timeline data',
|
||||
});
|
||||
|
|
|
@ -87,10 +87,10 @@ EventsTrHeader.displayName = 'EventsTrHeader';
|
|||
|
||||
export const EventsThGroupActions = styled.div.attrs(({ className }) => ({
|
||||
className: `siemEventsTable__thGroupActions ${className}`,
|
||||
}))<{ actionsColumnWidth: number }>`
|
||||
}))<{ actionsColumnWidth: number; justifyContent: string }>`
|
||||
display: flex;
|
||||
flex: 0 0 ${({ actionsColumnWidth }) => actionsColumnWidth + 'px'};
|
||||
justify-content: space-between;
|
||||
justify-content: ${({ justifyContent }) => justifyContent};
|
||||
min-width: 0;
|
||||
`;
|
||||
EventsThGroupActions.displayName = 'EventsThGroupActions';
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { createContext, memo, useContext, useEffect, useState } from 'react';
|
||||
import { TimelineAction } from './body/actions';
|
||||
|
||||
const initTimelineContext = false;
|
||||
export const TimelineContext = createContext<boolean>(initTimelineContext);
|
||||
|
@ -17,15 +18,19 @@ export const useTimelineWidthContext = () => useContext(TimelineWidthContext);
|
|||
export interface TimelineTypeContextProps {
|
||||
documentType?: string;
|
||||
footerText?: string;
|
||||
showCheckboxes: boolean;
|
||||
showRowRenderers: boolean;
|
||||
loadingText?: string;
|
||||
queryFields?: string[];
|
||||
selectAll?: boolean;
|
||||
timelineActions?: TimelineAction[];
|
||||
title?: string;
|
||||
}
|
||||
const initTimelineType: TimelineTypeContextProps = {
|
||||
documentType: undefined,
|
||||
footerText: undefined,
|
||||
showCheckboxes: false,
|
||||
showRowRenderers: true,
|
||||
loadingText: undefined,
|
||||
queryFields: [],
|
||||
selectAll: false,
|
||||
timelineActions: [],
|
||||
title: undefined,
|
||||
};
|
||||
export const TimelineTypeContext = createContext<TimelineTypeContextProps>(initTimelineType);
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { UpdateSignalStatusProps } from './types';
|
||||
import { throwIfNotOk } from '../../../hooks/api/api';
|
||||
import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../common/constants';
|
||||
|
||||
/**
|
||||
* Update signal status by query
|
||||
*
|
||||
* @param query of signals to update
|
||||
* @param status to update to ('open' / 'closed')
|
||||
* @param kbnVersion current Kibana Version to use for headers
|
||||
* @param signal to cancel request
|
||||
*/
|
||||
export const updateSignalStatus = async ({
|
||||
query,
|
||||
status,
|
||||
kbnVersion,
|
||||
signal,
|
||||
}: UpdateSignalStatusProps): Promise<unknown> => {
|
||||
const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_SIGNALS_STATUS_URL}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'kbn-version': kbnVersion,
|
||||
'kbn-xsrf': kbnVersion,
|
||||
},
|
||||
body: JSON.stringify({ status, ...query }),
|
||||
signal,
|
||||
});
|
||||
|
||||
await throwIfNotOk(response);
|
||||
return response.json();
|
||||
};
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface UpdateSignalStatusProps {
|
||||
query: object;
|
||||
status: 'open' | 'closed';
|
||||
kbnVersion: string;
|
||||
signal?: AbortSignal; // TODO: implement cancelling
|
||||
}
|
|
@ -183,6 +183,7 @@ export const mockGlobalState: State = {
|
|||
},
|
||||
timelineById: {
|
||||
test: {
|
||||
deletedEventIds: [],
|
||||
id: 'test',
|
||||
savedObjectId: null,
|
||||
columns: defaultHeaders,
|
||||
|
@ -194,16 +195,21 @@ export const mockGlobalState: State = {
|
|||
historyIds: [],
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
selectedEventIds: {},
|
||||
show: false,
|
||||
showRowRenderers: true,
|
||||
showCheckboxes: false,
|
||||
pinnedEventIds: {},
|
||||
pinnedEventsSaveObject: {},
|
||||
itemsPerPageOptions: [5, 10, 20],
|
||||
|
|
|
@ -19,6 +19,7 @@ import { SpyRoute } from '../../utils/route/spy_routes';
|
|||
import { DetectionEngineEmptyPage } from './detection_engine_empty_page';
|
||||
import * as i18n from './translations';
|
||||
import { SignalsTable } from './signals';
|
||||
import { GlobalTime } from '../../containers/global_time';
|
||||
|
||||
export const DetectionEngineComponent = React.memo(() => {
|
||||
const sampleChartOptions = [
|
||||
|
@ -65,8 +66,7 @@ export const DetectionEngineComponent = React.memo(() => {
|
|||
</EuiPanel>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<SignalsTable />
|
||||
<GlobalTime>{({ to, from }) => <SignalsTable from={from} to={to} />}</GlobalTime>
|
||||
</WrapperPage>
|
||||
</StickyContainer>
|
||||
) : (
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import { updateSignalStatus } from '../../../containers/detection_engine/signals/api';
|
||||
import { SendSignalsToTimelineActionProps, UpdateSignalStatusActionProps } from './types';
|
||||
import { TimelineNonEcsData } from '../../../graphql/types';
|
||||
|
||||
export const getUpdateSignalsQuery = (eventIds: Readonly<string[]>) => {
|
||||
return {
|
||||
query: {
|
||||
bool: {
|
||||
filter: {
|
||||
terms: {
|
||||
_id: [...eventIds],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getFilterAndRuleBounds = (
|
||||
data: TimelineNonEcsData[][]
|
||||
): [string[], number, number] => {
|
||||
const stringFilter = data?.[0].filter(d => d.field === 'signal.rule.filters')?.[0]?.value ?? [];
|
||||
|
||||
const eventTimes = data
|
||||
.flatMap(signal => signal.filter(d => d.field === 'signal.original_time')?.[0]?.value ?? [])
|
||||
.map(d => moment(d));
|
||||
|
||||
return [stringFilter, moment.min(eventTimes).valueOf(), moment.max(eventTimes).valueOf()];
|
||||
};
|
||||
|
||||
export const updateSignalStatusAction = async ({
|
||||
query,
|
||||
signalIds,
|
||||
status,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
kbnVersion,
|
||||
}: UpdateSignalStatusActionProps) => {
|
||||
try {
|
||||
setEventsLoading({ eventIds: signalIds, isLoading: true });
|
||||
|
||||
const queryObject = query ? { query: JSON.parse(query) } : getUpdateSignalsQuery(signalIds);
|
||||
|
||||
await updateSignalStatus({
|
||||
query: queryObject,
|
||||
status,
|
||||
kbnVersion,
|
||||
});
|
||||
// TODO: Only delete those that were successfully updated from updatedRules
|
||||
setEventsDeleted({ eventIds: signalIds, isDeleted: true });
|
||||
} catch (e) {
|
||||
// TODO: Show error toasts
|
||||
} finally {
|
||||
setEventsLoading({ eventIds: signalIds, isLoading: false });
|
||||
}
|
||||
};
|
||||
|
||||
export const sendSignalsToTimelineAction = async ({
|
||||
createTimeline,
|
||||
data,
|
||||
}: SendSignalsToTimelineActionProps) => {
|
||||
const stringFilter = data[0].filter(d => d.field === 'signal.rule.filters')?.[0]?.value ?? [];
|
||||
|
||||
// TODO: Switch to using from/to when adding dateRange
|
||||
// const [stringFilters, from, to] = getFilterAndRuleBounds(data);
|
||||
const parsedFilter = stringFilter.map(sf => JSON.parse(sf));
|
||||
createTimeline({
|
||||
id: 'timeline-1',
|
||||
filters: parsedFilter,
|
||||
dateRange: undefined, // TODO
|
||||
kqlQuery: undefined, // TODO
|
||||
});
|
||||
};
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
UtilityBar,
|
||||
UtilityBarAction,
|
||||
UtilityBarGroup,
|
||||
UtilityBarSection,
|
||||
UtilityBarText,
|
||||
} from '../../../../../components/detection_engine/utility_bar';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
export const ClosedSignals = React.memo<{ totalCount: number }>(({ totalCount }) => {
|
||||
return (
|
||||
<>
|
||||
<UtilityBar>
|
||||
<UtilityBarSection>
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarText>{`${i18n.PANEL_SUBTITLE_SHOWING}: ${totalCount} signals`}</UtilityBarText>
|
||||
</UtilityBarGroup>
|
||||
</UtilityBarSection>
|
||||
|
||||
<UtilityBarSection>
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarAction
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
popoverContent={() => <p>{'Customize columns context menu here.'}</p>}
|
||||
>
|
||||
{'Customize columns'}
|
||||
</UtilityBarAction>
|
||||
|
||||
<UtilityBarAction iconType="indexMapping">{'Aggregate data'}</UtilityBarAction>
|
||||
</UtilityBarGroup>
|
||||
</UtilityBarSection>
|
||||
</UtilityBar>
|
||||
|
||||
{/* Michael: Closed signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */}
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
UtilityBar,
|
||||
UtilityBarAction,
|
||||
UtilityBarGroup,
|
||||
UtilityBarSection,
|
||||
UtilityBarText,
|
||||
} from '../../../../../components/detection_engine/utility_bar';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
export const OpenSignals = React.memo<{ totalCount: number }>(({ totalCount }) => {
|
||||
return (
|
||||
<>
|
||||
<UtilityBar>
|
||||
<UtilityBarSection>
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarText>{`${i18n.PANEL_SUBTITLE_SHOWING}: ${totalCount} signals`}</UtilityBarText>
|
||||
</UtilityBarGroup>
|
||||
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarText>{'Selected: 20 signals'}</UtilityBarText>
|
||||
|
||||
<UtilityBarAction
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
popoverContent={() => <p>{'Batch actions context menu here.'}</p>}
|
||||
>
|
||||
{'Batch actions'}
|
||||
</UtilityBarAction>
|
||||
|
||||
<UtilityBarAction iconType="listAdd">
|
||||
{'Select all signals on all pages'}
|
||||
</UtilityBarAction>
|
||||
</UtilityBarGroup>
|
||||
</UtilityBarSection>
|
||||
</UtilityBar>
|
||||
|
||||
{/* Michael: Open signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */}
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
export const FILTER_OPEN = 'open';
|
||||
export const FILTER_CLOSED = 'closed';
|
||||
export type SignalFilterOption = typeof FILTER_OPEN | typeof FILTER_CLOSED;
|
||||
|
||||
export const SignalsTableFilterGroup = React.memo(
|
||||
({
|
||||
onFilterGroupChanged,
|
||||
}: {
|
||||
onFilterGroupChanged: (filterGroup: SignalFilterOption) => void;
|
||||
}) => {
|
||||
const [filterGroup, setFilterGroup] = useState(FILTER_OPEN);
|
||||
|
||||
const onClickOpenFilterCallback = useCallback(() => {
|
||||
setFilterGroup(FILTER_OPEN);
|
||||
onFilterGroupChanged(FILTER_OPEN);
|
||||
}, [setFilterGroup, onFilterGroupChanged]);
|
||||
|
||||
const onClickCloseFilterCallback = useCallback(() => {
|
||||
setFilterGroup(FILTER_CLOSED);
|
||||
onFilterGroupChanged(FILTER_CLOSED);
|
||||
}, [setFilterGroup, onFilterGroupChanged]);
|
||||
|
||||
return (
|
||||
<EuiFilterGroup>
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={filterGroup === FILTER_OPEN}
|
||||
onClick={onClickOpenFilterCallback}
|
||||
withNext
|
||||
>
|
||||
{i18n.OPEN_SIGNALS}
|
||||
</EuiFilterButton>
|
||||
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={filterGroup === FILTER_CLOSED}
|
||||
onClick={onClickCloseFilterCallback}
|
||||
>
|
||||
{i18n.CLOSED_SIGNALS}
|
||||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import * as i18n from './translations';
|
||||
import { TimelineNonEcsData } from '../../../../../graphql/types';
|
||||
import { SendSignalsToTimeline, UpdateSignalsStatus } from '../../types';
|
||||
import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group/signals_filter_group';
|
||||
|
||||
/**
|
||||
* Returns ViewInTimeline / UpdateSignalStatus actions to be display within an EuiContextMenuPanel
|
||||
*
|
||||
* @param areEventsLoading are any events loading
|
||||
* @param allEventsSelected are all events on all pages selected
|
||||
* @param selectedEventIds
|
||||
* @param updateSignalsStatus function for updating signal status
|
||||
* @param sendSignalsToTimeline function for sending signals to timeline
|
||||
* @param closePopover
|
||||
* @param isFilteredToOpen currently selected filter options
|
||||
*/
|
||||
export const getBatchItems = (
|
||||
areEventsLoading: boolean,
|
||||
allEventsSelected: boolean,
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>,
|
||||
updateSignalsStatus: UpdateSignalsStatus,
|
||||
sendSignalsToTimeline: SendSignalsToTimeline,
|
||||
closePopover: () => void,
|
||||
isFilteredToOpen: boolean
|
||||
) => {
|
||||
const allDisabled = areEventsLoading || Object.keys(selectedEventIds).length === 0;
|
||||
const sendToTimelineDisabled = allEventsSelected || uniqueRuleCount(selectedEventIds) > 1;
|
||||
const filterString = isFilteredToOpen
|
||||
? i18n.BATCH_ACTION_CLOSE_SELECTED
|
||||
: i18n.BATCH_ACTION_OPEN_SELECTED;
|
||||
|
||||
return [
|
||||
<EuiContextMenuItem
|
||||
key={i18n.BATCH_ACTION_VIEW_SELECTED_IN_TIMELINE}
|
||||
icon="editorUnorderedList"
|
||||
disabled={allDisabled || sendToTimelineDisabled}
|
||||
onClick={async () => {
|
||||
closePopover();
|
||||
sendSignalsToTimeline();
|
||||
}}
|
||||
>
|
||||
{i18n.BATCH_ACTION_VIEW_SELECTED_IN_TIMELINE}
|
||||
</EuiContextMenuItem>,
|
||||
|
||||
<EuiContextMenuItem
|
||||
key={filterString}
|
||||
icon={isFilteredToOpen ? 'indexClose' : 'indexOpen'}
|
||||
disabled={allDisabled}
|
||||
onClick={async () => {
|
||||
closePopover();
|
||||
await updateSignalsStatus({
|
||||
signalIds: Object.keys(selectedEventIds),
|
||||
status: isFilteredToOpen ? FILTER_CLOSED : FILTER_OPEN,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{filterString}
|
||||
</EuiContextMenuItem>,
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the number of unique rules for a given list of signals
|
||||
*
|
||||
* @param signals
|
||||
*/
|
||||
export const uniqueRuleCount = (
|
||||
signals: Readonly<Record<string, TimelineNonEcsData[]>>
|
||||
): number => {
|
||||
const ruleIds = Object.values(signals).flatMap(
|
||||
data => data.find(d => d.field === 'signal.rule.id')?.value
|
||||
);
|
||||
|
||||
return Array.from(new Set(ruleIds)).length;
|
||||
};
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiContextMenuPanel } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import {
|
||||
UtilityBar,
|
||||
UtilityBarAction,
|
||||
UtilityBarGroup,
|
||||
UtilityBarSection,
|
||||
UtilityBarText,
|
||||
} from '../../../../../components/detection_engine/utility_bar';
|
||||
import * as i18n from './translations';
|
||||
import { getBatchItems } from './batch_actions';
|
||||
import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting';
|
||||
import { DEFAULT_NUMBER_FORMAT } from '../../../../../../common/constants';
|
||||
import { TimelineNonEcsData } from '../../../../../graphql/types';
|
||||
import { SendSignalsToTimeline, UpdateSignalsStatus } from '../../types';
|
||||
|
||||
interface SignalsUtilityBarProps {
|
||||
areEventsLoading: boolean;
|
||||
clearSelection: () => void;
|
||||
isFilteredToOpen: boolean;
|
||||
selectAll: () => void;
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
sendSignalsToTimeline: SendSignalsToTimeline;
|
||||
showClearSelection: boolean;
|
||||
totalCount: number;
|
||||
updateSignalsStatus: UpdateSignalsStatus;
|
||||
}
|
||||
|
||||
export const SignalsUtilityBar = React.memo<SignalsUtilityBarProps>(
|
||||
({
|
||||
areEventsLoading,
|
||||
clearSelection,
|
||||
totalCount,
|
||||
selectedEventIds,
|
||||
isFilteredToOpen,
|
||||
selectAll,
|
||||
showClearSelection,
|
||||
updateSignalsStatus,
|
||||
sendSignalsToTimeline,
|
||||
}) => {
|
||||
const [defaultNumberFormat] = useKibanaUiSetting(DEFAULT_NUMBER_FORMAT);
|
||||
|
||||
const getBatchItemsPopoverContent = useCallback(
|
||||
(closePopover: () => void) => (
|
||||
<EuiContextMenuPanel
|
||||
items={getBatchItems(
|
||||
areEventsLoading,
|
||||
showClearSelection,
|
||||
selectedEventIds,
|
||||
updateSignalsStatus,
|
||||
sendSignalsToTimeline,
|
||||
closePopover,
|
||||
isFilteredToOpen
|
||||
)}
|
||||
/>
|
||||
),
|
||||
[
|
||||
areEventsLoading,
|
||||
selectedEventIds,
|
||||
updateSignalsStatus,
|
||||
sendSignalsToTimeline,
|
||||
isFilteredToOpen,
|
||||
]
|
||||
);
|
||||
|
||||
const formattedTotalCount = numeral(totalCount).format(defaultNumberFormat);
|
||||
const formattedSelectedEventsCount = numeral(Object.keys(selectedEventIds).length).format(
|
||||
defaultNumberFormat
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UtilityBar>
|
||||
<UtilityBarSection>
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarText>
|
||||
{i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)}
|
||||
</UtilityBarText>
|
||||
</UtilityBarGroup>
|
||||
|
||||
<UtilityBarGroup>
|
||||
{totalCount > 0 && (
|
||||
<>
|
||||
<UtilityBarText>
|
||||
{i18n.SELECTED_SIGNALS(
|
||||
showClearSelection ? formattedTotalCount : formattedSelectedEventsCount,
|
||||
showClearSelection ? totalCount : Object.keys(selectedEventIds).length
|
||||
)}
|
||||
</UtilityBarText>
|
||||
|
||||
<UtilityBarAction
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
popoverContent={getBatchItemsPopoverContent}
|
||||
>
|
||||
{i18n.BATCH_ACTIONS}
|
||||
</UtilityBarAction>
|
||||
|
||||
<UtilityBarAction
|
||||
iconType="listAdd"
|
||||
onClick={() => {
|
||||
if (!showClearSelection) {
|
||||
selectAll();
|
||||
} else {
|
||||
clearSelection();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{showClearSelection
|
||||
? i18n.CLEAR_SELECTION
|
||||
: i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)}
|
||||
</UtilityBarAction>
|
||||
</>
|
||||
)}
|
||||
</UtilityBarGroup>
|
||||
</UtilityBarSection>
|
||||
</UtilityBar>
|
||||
</>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.selectedEventIds === nextProps.selectedEventIds &&
|
||||
prevProps.totalCount === nextProps.totalCount &&
|
||||
prevProps.showClearSelection === nextProps.showClearSelection
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SHOWING_SIGNALS = (totalSignalsFormatted: string, totalSignals: number) =>
|
||||
i18n.translate('xpack.siem.detectionEngine.signals.utilityBar.showingSignalsTitle', {
|
||||
values: { totalSignalsFormatted, totalSignals },
|
||||
defaultMessage:
|
||||
'Showing {totalSignalsFormatted} {totalSignals, plural, =1 {signal} other {signals}}',
|
||||
});
|
||||
|
||||
export const SELECTED_SIGNALS = (selectedSignalsFormatted: string, selectedSignals: number) =>
|
||||
i18n.translate('xpack.siem.detectionEngine.signals.utilityBar.selectedSignalsTitle', {
|
||||
values: { selectedSignalsFormatted, selectedSignals },
|
||||
defaultMessage:
|
||||
'Selected {selectedSignalsFormatted} {selectedSignals, plural, =1 {signal} other {signals}}',
|
||||
});
|
||||
|
||||
export const SELECT_ALL_SIGNALS = (totalSignalsFormatted: string, totalSignals: number) =>
|
||||
i18n.translate('xpack.siem.detectionEngine.signals.utilityBar.selectAllSignalsTitle', {
|
||||
values: { totalSignalsFormatted, totalSignals },
|
||||
defaultMessage:
|
||||
'Select all {totalSignalsFormatted} {totalSignals, plural, =1 {signal} other {signals}}',
|
||||
});
|
||||
|
||||
export const CLEAR_SELECTION = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.utilityBar.clearSelectionTitle',
|
||||
{
|
||||
defaultMessage: 'Clear selection',
|
||||
}
|
||||
);
|
||||
|
||||
export const BATCH_ACTIONS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.utilityBar.batchActionsTitle',
|
||||
{
|
||||
defaultMessage: 'Batch actions',
|
||||
}
|
||||
);
|
||||
|
||||
export const BATCH_ACTION_VIEW_SELECTED_IN_HOSTS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.utilityBar.batchActions.viewSelectedInHostsTitle',
|
||||
{
|
||||
defaultMessage: 'View selected in hosts',
|
||||
}
|
||||
);
|
||||
|
||||
export const BATCH_ACTION_VIEW_SELECTED_IN_NETWORK = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.utilityBar.batchActions.viewSelectedInNetworkTitle',
|
||||
{
|
||||
defaultMessage: 'View selected in network',
|
||||
}
|
||||
);
|
||||
|
||||
export const BATCH_ACTION_VIEW_SELECTED_IN_TIMELINE = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.utilityBar.batchActions.viewSelectedInTimelineTitle',
|
||||
{
|
||||
defaultMessage: 'View selected in timeline',
|
||||
}
|
||||
);
|
||||
|
||||
export const BATCH_ACTION_OPEN_SELECTED = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.utilityBar.batchActions.openSelectedTitle',
|
||||
{
|
||||
defaultMessage: 'Open selected',
|
||||
}
|
||||
);
|
||||
|
||||
export const BATCH_ACTION_CLOSE_SELECTED = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.utilityBar.batchActions.closeSelectedTitle',
|
||||
{
|
||||
defaultMessage: 'Close selected',
|
||||
}
|
||||
);
|
|
@ -4,6 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { ColumnHeader } from '../../../components/timeline/body/column_headers/column_header';
|
||||
import { defaultColumnHeaderType } from '../../../components/timeline/body/column_headers/default_headers';
|
||||
import {
|
||||
|
@ -14,6 +16,11 @@ import {
|
|||
import * as i18n from './translations';
|
||||
import { SubsetTimelineModel, timelineDefaults } from '../../../store/timeline/model';
|
||||
import { esFilters } from '../../../../../../../../src/plugins/data/common/es_query';
|
||||
import { sendSignalsToTimelineAction, updateSignalStatusAction } from './actions';
|
||||
import { FILTER_OPEN } from './components/signals_filter_group/signals_filter_group';
|
||||
|
||||
import { TimelineAction, TimelineActionProps } from '../../../components/timeline/body/actions';
|
||||
import { CreateTimeline, SetEventsDeletedProps, SetEventsLoadingProps } from './types';
|
||||
|
||||
export const signalsOpenFilters: esFilters.Filter[] = [
|
||||
{
|
||||
|
@ -123,4 +130,74 @@ export const signalsHeaders: ColumnHeader[] = [
|
|||
export const signalsDefaultModel: SubsetTimelineModel = {
|
||||
...timelineDefaults,
|
||||
columns: signalsHeaders,
|
||||
showCheckboxes: true,
|
||||
showRowRenderers: false,
|
||||
};
|
||||
|
||||
export const requiredFieldsForActions = [
|
||||
'@timestamp',
|
||||
'signal.original_time',
|
||||
'signal.rule.filters',
|
||||
'signal.rule.from',
|
||||
'signal.rule.language',
|
||||
'signal.rule.query',
|
||||
'signal.rule.to',
|
||||
'signal.rule.id',
|
||||
];
|
||||
|
||||
export const getSignalsActions = ({
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
createTimeline,
|
||||
status,
|
||||
kbnVersion,
|
||||
}: {
|
||||
setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void;
|
||||
setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void;
|
||||
createTimeline: CreateTimeline;
|
||||
status: 'open' | 'closed';
|
||||
kbnVersion: string;
|
||||
}): TimelineAction[] => [
|
||||
{
|
||||
getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => (
|
||||
<EuiToolTip
|
||||
data-test-subj="send-signal-to-timeline-tool-tip"
|
||||
content={i18n.ACTION_VIEW_IN_TIMELINE}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={'send-signal-to-timeline-tool-tip'}
|
||||
onClick={() => sendSignalsToTimelineAction({ createTimeline, data: [data] })}
|
||||
iconType="tableDensityNormal"
|
||||
aria-label="Next"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
),
|
||||
id: 'sendSignalToTimeline',
|
||||
width: 26,
|
||||
},
|
||||
{
|
||||
getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => (
|
||||
<EuiToolTip
|
||||
data-test-subj="update-signal-status-tool-tip"
|
||||
content={status === FILTER_OPEN ? i18n.ACTION_OPEN_SIGNAL : i18n.ACTION_CLOSE_SIGNAL}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={'update-signal-status-button'}
|
||||
onClick={() =>
|
||||
updateSignalStatusAction({
|
||||
signalIds: [eventId],
|
||||
status,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
kbnVersion,
|
||||
})
|
||||
}
|
||||
iconType={status === FILTER_OPEN ? 'indexOpen' : 'indexClose'}
|
||||
aria-label="Next"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
),
|
||||
id: 'updateSignalStatus',
|
||||
width: 26,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -4,94 +4,338 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { OpenSignals } from './components/open_signals';
|
||||
import { ClosedSignals } from './components/closed_signals';
|
||||
import { GlobalTime } from '../../../containers/global_time';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
import { SignalsUtilityBar } from './components/signals_utility_bar';
|
||||
import { StatefulEventsViewer } from '../../../components/events_viewer';
|
||||
import * as i18n from './translations';
|
||||
import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants';
|
||||
import { signalsClosedFilters, signalsDefaultModel, signalsOpenFilters } from './default_config';
|
||||
import {
|
||||
getSignalsActions,
|
||||
requiredFieldsForActions,
|
||||
signalsClosedFilters,
|
||||
signalsDefaultModel,
|
||||
signalsOpenFilters,
|
||||
} from './default_config';
|
||||
import { timelineActions, timelineSelectors } from '../../../store/timeline';
|
||||
import { timelineDefaults, TimelineModel } from '../../../store/timeline/model';
|
||||
import {
|
||||
FILTER_CLOSED,
|
||||
FILTER_OPEN,
|
||||
SignalFilterOption,
|
||||
SignalsTableFilterGroup,
|
||||
} from './components/signals_filter_group/signals_filter_group';
|
||||
import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting';
|
||||
import { DEFAULT_KBN_VERSION, DEFAULT_SIGNALS_INDEX } from '../../../../common/constants';
|
||||
import { defaultHeaders } from '../../../components/timeline/body/column_headers/default_headers';
|
||||
import { ColumnHeader } from '../../../components/timeline/body/column_headers/column_header';
|
||||
import { esFilters, esQuery } from '../../../../../../../../src/plugins/data/common/es_query';
|
||||
import { TimelineNonEcsData } from '../../../graphql/types';
|
||||
import { inputsSelectors, SerializedFilterQuery, State } from '../../../store';
|
||||
import { sendSignalsToTimelineAction, updateSignalStatusAction } from './actions';
|
||||
import {
|
||||
CreateTimelineProps,
|
||||
SendSignalsToTimeline,
|
||||
SetEventsDeletedProps,
|
||||
SetEventsLoadingProps,
|
||||
UpdateSignalsStatus,
|
||||
UpdateSignalsStatusProps,
|
||||
} from './types';
|
||||
import { inputsActions } from '../../../store/inputs';
|
||||
import { combineQueries } from '../../../components/timeline/helpers';
|
||||
import { useKibanaCore } from '../../../lib/compose/kibana_core';
|
||||
import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules/fetch_index_patterns';
|
||||
import { InputsRange } from '../../../store/inputs/model';
|
||||
import { Query } from '../../../../../../../../src/plugins/data/common/query';
|
||||
|
||||
const SIGNALS_PAGE_TIMELINE_ID = 'signals-page';
|
||||
const FILTER_OPEN = 'open';
|
||||
const FILTER_CLOSED = 'closed';
|
||||
|
||||
export const SignalsTableFilterGroup = React.memo(
|
||||
({ onFilterGroupChanged }: { onFilterGroupChanged: (filterGroup: string) => void }) => {
|
||||
const [filterGroup, setFilterGroup] = useState(FILTER_OPEN);
|
||||
interface ReduxProps {
|
||||
globalQuery: Query;
|
||||
globalFilters: esFilters.Filter[];
|
||||
deletedEventIds: string[];
|
||||
isSelectAllChecked: boolean;
|
||||
loadingEventIds: string[];
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
createTimeline: ActionCreator<{
|
||||
dateRange?: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
filters?: esFilters.Filter[];
|
||||
id: string;
|
||||
kqlQuery?: {
|
||||
filterQuery: SerializedFilterQuery | null;
|
||||
};
|
||||
columns: ColumnHeader[];
|
||||
show?: boolean;
|
||||
}>;
|
||||
clearEventsDeleted?: ActionCreator<{ id: string }>;
|
||||
clearEventsLoading?: ActionCreator<{ id: string }>;
|
||||
clearSelected?: ActionCreator<{ id: string }>;
|
||||
removeTimelineLinkTo: ActionCreator<{}>;
|
||||
setEventsDeleted?: ActionCreator<{
|
||||
id: string;
|
||||
eventIds: string[];
|
||||
isDeleted: boolean;
|
||||
}>;
|
||||
setEventsLoading?: ActionCreator<{
|
||||
id: string;
|
||||
eventIds: string[];
|
||||
isLoading: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
defaultFilters?: esFilters.Filter[];
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
type SignalsTableComponentProps = OwnProps & ReduxProps & DispatchProps;
|
||||
|
||||
export const SignalsTableComponent = React.memo<SignalsTableComponentProps>(
|
||||
({
|
||||
createTimeline,
|
||||
clearEventsDeleted,
|
||||
clearEventsLoading,
|
||||
clearSelected,
|
||||
defaultFilters = [],
|
||||
from,
|
||||
globalFilters,
|
||||
globalQuery,
|
||||
isSelectAllChecked,
|
||||
loadingEventIds,
|
||||
removeTimelineLinkTo,
|
||||
selectedEventIds,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
to,
|
||||
}) => {
|
||||
const [selectAll, setSelectAll] = useState(false);
|
||||
|
||||
const [showClearSelectionAction, setShowClearSelectionAction] = useState(false);
|
||||
const [filterGroup, setFilterGroup] = useState<SignalFilterOption>(FILTER_OPEN);
|
||||
const [{ browserFields, indexPatterns }] = useFetchIndexPatterns([DEFAULT_SIGNALS_INDEX]); // TODO Get from new FrankInspired XavierHook
|
||||
const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
|
||||
const core = useKibanaCore();
|
||||
|
||||
const getGlobalQuery = useCallback(() => {
|
||||
if (browserFields != null && indexPatterns != null) {
|
||||
return combineQueries({
|
||||
config: esQuery.getEsQueryConfig(core.uiSettings),
|
||||
dataProviders: [],
|
||||
indexPattern: indexPatterns,
|
||||
browserFields,
|
||||
filters: globalFilters,
|
||||
kqlQuery: globalQuery,
|
||||
kqlMode: globalQuery.language,
|
||||
start: from,
|
||||
end: to,
|
||||
isEventViewer: true,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}, [browserFields, globalFilters, globalQuery, indexPatterns, to, from]);
|
||||
|
||||
// Callback for creating a new timeline -- utilized by row/batch actions
|
||||
const createTimelineCallback = useCallback(
|
||||
({ id, kqlQuery, filters, dateRange }: CreateTimelineProps) => {
|
||||
removeTimelineLinkTo({});
|
||||
createTimeline({ id, columns: defaultHeaders, show: true, filters, dateRange, kqlQuery });
|
||||
},
|
||||
[createTimeline, removeTimelineLinkTo]
|
||||
);
|
||||
|
||||
const setEventsLoadingCallback = useCallback(
|
||||
({ eventIds, isLoading }: SetEventsLoadingProps) => {
|
||||
setEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isLoading });
|
||||
},
|
||||
[setEventsLoading, SIGNALS_PAGE_TIMELINE_ID]
|
||||
);
|
||||
|
||||
const setEventsDeletedCallback = useCallback(
|
||||
({ eventIds, isDeleted }: SetEventsDeletedProps) => {
|
||||
setEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isDeleted });
|
||||
},
|
||||
[setEventsDeleted, SIGNALS_PAGE_TIMELINE_ID]
|
||||
);
|
||||
|
||||
// Catches state change isSelectAllChecked->false upon user selection change to reset utility bar
|
||||
useEffect(() => {
|
||||
if (!isSelectAllChecked) {
|
||||
setShowClearSelectionAction(false);
|
||||
} else {
|
||||
setSelectAll(false);
|
||||
}
|
||||
}, [isSelectAllChecked]);
|
||||
|
||||
// Callback for when open/closed filter changes
|
||||
const onFilterGroupChangedCallback = useCallback(
|
||||
(newFilterGroup: SignalFilterOption) => {
|
||||
clearEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID });
|
||||
clearEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID });
|
||||
clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID });
|
||||
setFilterGroup(newFilterGroup);
|
||||
},
|
||||
[setFilterGroup]
|
||||
);
|
||||
|
||||
// Callback for clearing entire selection from utility bar
|
||||
const clearSelectionCallback = useCallback(() => {
|
||||
clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID });
|
||||
setSelectAll(false);
|
||||
setShowClearSelectionAction(false);
|
||||
}, [clearSelected, setShowClearSelectionAction]);
|
||||
|
||||
// Callback for selecting all events on all pages from utility bar
|
||||
// Dispatches to stateful_body's selectAll via TimelineTypeContext props
|
||||
// as scope of response data required to actually set selectedEvents
|
||||
const selectAllCallback = useCallback(() => {
|
||||
setSelectAll(true);
|
||||
setShowClearSelectionAction(true);
|
||||
}, [setShowClearSelectionAction]);
|
||||
|
||||
const updateSignalsStatusCallback: UpdateSignalsStatus = useCallback(
|
||||
async ({ signalIds, status }: UpdateSignalsStatusProps) => {
|
||||
await updateSignalStatusAction({
|
||||
query: showClearSelectionAction ? getGlobalQuery()?.filterQuery : undefined,
|
||||
signalIds: Object.keys(selectedEventIds),
|
||||
status,
|
||||
setEventsDeleted: setEventsDeletedCallback,
|
||||
setEventsLoading: setEventsLoadingCallback,
|
||||
kbnVersion,
|
||||
});
|
||||
},
|
||||
[
|
||||
getGlobalQuery,
|
||||
selectedEventIds,
|
||||
setEventsDeletedCallback,
|
||||
setEventsLoadingCallback,
|
||||
showClearSelectionAction,
|
||||
]
|
||||
);
|
||||
const sendSignalsToTimelineCallback: SendSignalsToTimeline = useCallback(async () => {
|
||||
await sendSignalsToTimelineAction({
|
||||
createTimeline: createTimelineCallback,
|
||||
data: Object.values(selectedEventIds),
|
||||
});
|
||||
}, [selectedEventIds, setEventsDeletedCallback, setEventsLoadingCallback]);
|
||||
|
||||
// Callback for creating the SignalUtilityBar which receives totalCount from EventsViewer component
|
||||
const utilityBarCallback = useCallback(
|
||||
(totalCount: number) => {
|
||||
return (
|
||||
<SignalsUtilityBar
|
||||
areEventsLoading={loadingEventIds.length > 0}
|
||||
clearSelection={clearSelectionCallback}
|
||||
isFilteredToOpen={filterGroup === FILTER_OPEN}
|
||||
selectAll={selectAllCallback}
|
||||
selectedEventIds={selectedEventIds}
|
||||
sendSignalsToTimeline={sendSignalsToTimelineCallback}
|
||||
showClearSelection={showClearSelectionAction}
|
||||
totalCount={totalCount}
|
||||
updateSignalsStatus={updateSignalsStatusCallback}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[
|
||||
clearSelectionCallback,
|
||||
filterGroup,
|
||||
loadingEventIds.length,
|
||||
selectAllCallback,
|
||||
selectedEventIds,
|
||||
showClearSelectionAction,
|
||||
]
|
||||
);
|
||||
|
||||
// Send to Timeline / Update Signal Status Actions for each table row
|
||||
const additionalActions = useMemo(
|
||||
() =>
|
||||
getSignalsActions({
|
||||
createTimeline: createTimelineCallback,
|
||||
setEventsLoading: setEventsLoadingCallback,
|
||||
setEventsDeleted: setEventsDeletedCallback,
|
||||
status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN,
|
||||
kbnVersion,
|
||||
}),
|
||||
[createTimelineCallback, filterGroup, kbnVersion]
|
||||
);
|
||||
|
||||
const defaultIndices = useMemo(() => [`${DEFAULT_SIGNALS_INDEX}`], [DEFAULT_SIGNALS_INDEX]);
|
||||
const defaultFiltersMemo = useMemo(
|
||||
() => [
|
||||
...defaultFilters,
|
||||
...(filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters),
|
||||
],
|
||||
[defaultFilters, filterGroup]
|
||||
);
|
||||
|
||||
const timelineTypeContext = useMemo(
|
||||
() => ({
|
||||
documentType: i18n.SIGNALS_DOCUMENT_TYPE,
|
||||
footerText: i18n.TOTAL_COUNT_OF_SIGNALS,
|
||||
loadingText: i18n.LOADING_SIGNALS,
|
||||
queryFields: requiredFieldsForActions,
|
||||
timelineActions: additionalActions,
|
||||
title: i18n.SIGNALS_TABLE_TITLE,
|
||||
selectAll,
|
||||
}),
|
||||
[additionalActions, selectAll]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFilterGroup>
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={filterGroup === FILTER_OPEN}
|
||||
onClick={() => {
|
||||
setFilterGroup(FILTER_OPEN);
|
||||
onFilterGroupChanged(FILTER_OPEN);
|
||||
}}
|
||||
withNext
|
||||
>
|
||||
{'Open signals'}
|
||||
</EuiFilterButton>
|
||||
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={filterGroup === FILTER_CLOSED}
|
||||
onClick={() => {
|
||||
setFilterGroup(FILTER_CLOSED);
|
||||
onFilterGroupChanged(FILTER_CLOSED);
|
||||
}}
|
||||
>
|
||||
{'Closed signals'}
|
||||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
<StatefulEventsViewer
|
||||
defaultIndices={defaultIndices} // TODO Get from new FrankInspired XavierHook
|
||||
pageFilters={defaultFiltersMemo}
|
||||
defaultModel={signalsDefaultModel}
|
||||
end={to}
|
||||
headerFilterGroup={
|
||||
<SignalsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />
|
||||
}
|
||||
id={SIGNALS_PAGE_TIMELINE_ID}
|
||||
start={from}
|
||||
timelineTypeContext={timelineTypeContext}
|
||||
utilityBar={utilityBarCallback}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const SignalsTable = React.memo(() => {
|
||||
const [filterGroup, setFilterGroup] = useState(FILTER_OPEN);
|
||||
SignalsTableComponent.displayName = 'SignalsTableComponent';
|
||||
|
||||
const onFilterGroupChangedCallback = useCallback(
|
||||
(newFilterGroup: string) => {
|
||||
setFilterGroup(newFilterGroup);
|
||||
},
|
||||
[setFilterGroup]
|
||||
);
|
||||
const makeMapStateToProps = () => {
|
||||
const getTimeline = timelineSelectors.getTimelineByIdSelector();
|
||||
const getGlobalInputs = inputsSelectors.globalSelector();
|
||||
const mapStateToProps = (state: State) => {
|
||||
const timeline: TimelineModel =
|
||||
getTimeline(state, SIGNALS_PAGE_TIMELINE_ID) ?? timelineDefaults;
|
||||
const { deletedEventIds, isSelectAllChecked, loadingEventIds, selectedEventIds } = timeline;
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalTime>
|
||||
{({ to, from, setQuery, deleteQuery, isInitializing }) => (
|
||||
<StatefulEventsViewer
|
||||
defaultIndices={[`${DEFAULT_SIGNALS_INDEX}-default`]}
|
||||
defaultFilters={filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters}
|
||||
defaultModel={signalsDefaultModel}
|
||||
end={to}
|
||||
headerFilterGroup={
|
||||
<SignalsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />
|
||||
}
|
||||
id={SIGNALS_PAGE_TIMELINE_ID}
|
||||
start={from}
|
||||
timelineTypeContext={{
|
||||
documentType: i18n.SIGNALS_DOCUMENT_TYPE,
|
||||
footerText: i18n.TOTAL_COUNT_OF_SIGNALS,
|
||||
showCheckboxes: true,
|
||||
showRowRenderers: false,
|
||||
title: i18n.SIGNALS_TABLE_TITLE,
|
||||
}}
|
||||
utilityBar={(totalCount: number) =>
|
||||
filterGroup === FILTER_OPEN ? (
|
||||
<OpenSignals totalCount={totalCount} />
|
||||
) : (
|
||||
<ClosedSignals totalCount={totalCount} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</GlobalTime>
|
||||
</>
|
||||
);
|
||||
});
|
||||
const globalInputs: InputsRange = getGlobalInputs(state);
|
||||
const { query, filters } = globalInputs;
|
||||
|
||||
SignalsTable.displayName = 'SignalsTable';
|
||||
return {
|
||||
globalQuery: query,
|
||||
globalFilters: filters,
|
||||
deletedEventIds,
|
||||
isSelectAllChecked,
|
||||
loadingEventIds,
|
||||
selectedEventIds,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export const SignalsTable = connect(makeMapStateToProps, {
|
||||
removeTimelineLinkTo: inputsActions.removeTimelineLinkTo,
|
||||
clearSelected: timelineActions.clearSelected,
|
||||
setEventsLoading: timelineActions.setEventsLoading,
|
||||
clearEventsLoading: timelineActions.clearEventsLoading,
|
||||
setEventsDeleted: timelineActions.setEventsDeleted,
|
||||
clearEventsDeleted: timelineActions.clearEventsDeleted,
|
||||
createTimeline: timelineActions.createTimeline,
|
||||
})(SignalsTableComponent);
|
||||
|
|
|
@ -10,13 +10,6 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle',
|
|||
defaultMessage: 'Detection engine',
|
||||
});
|
||||
|
||||
export const PANEL_SUBTITLE_SHOWING = i18n.translate(
|
||||
'xpack.siem.detectionEngine.panelSubtitleShowing',
|
||||
{
|
||||
defaultMessage: 'Showing',
|
||||
}
|
||||
);
|
||||
|
||||
export const SIGNALS_TABLE_TITLE = i18n.translate('xpack.siem.detectionEngine.signals.tableTitle', {
|
||||
defaultMessage: 'All signals',
|
||||
});
|
||||
|
@ -28,6 +21,24 @@ export const SIGNALS_DOCUMENT_TYPE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const OPEN_SIGNALS = i18n.translate('xpack.siem.detectionEngine.signals.openSignalsTitle', {
|
||||
defaultMessage: 'Open signals',
|
||||
});
|
||||
|
||||
export const CLOSED_SIGNALS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.closedSignalsTitle',
|
||||
{
|
||||
defaultMessage: 'Closed signals',
|
||||
}
|
||||
);
|
||||
|
||||
export const LOADING_SIGNALS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.loadingSignalsTitle',
|
||||
{
|
||||
defaultMessage: 'Loading Signals',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOTAL_COUNT_OF_SIGNALS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.totalCountOfSignalsTitle',
|
||||
{
|
||||
|
@ -62,3 +73,24 @@ export const SIGNALS_HEADERS_RISK_SCORE = i18n.translate(
|
|||
defaultMessage: 'Risk Score',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_OPEN_SIGNAL = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.actions.openSignalTitle',
|
||||
{
|
||||
defaultMessage: 'Open signal',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_CLOSE_SIGNAL = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.actions.closeSignalTitle',
|
||||
{
|
||||
defaultMessage: 'Close signal',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_VIEW_IN_TIMELINE = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.actions.viewInTimelineTitle',
|
||||
{
|
||||
defaultMessage: 'View in timeline',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { esFilters } from '../../../../../../../../src/plugins/data/common/es_query';
|
||||
import { TimelineNonEcsData } from '../../../graphql/types';
|
||||
import { KueryFilterQuery, SerializedFilterQuery } from '../../../store';
|
||||
|
||||
export interface SetEventsLoadingProps {
|
||||
eventIds: string[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export interface SetEventsDeletedProps {
|
||||
eventIds: string[];
|
||||
isDeleted: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateSignalsStatusProps {
|
||||
signalIds: string[];
|
||||
status: 'open' | 'closed';
|
||||
}
|
||||
|
||||
export type UpdateSignalsStatus = ({ signalIds, status }: UpdateSignalsStatusProps) => void;
|
||||
|
||||
export interface UpdateSignalStatusActionProps {
|
||||
query?: string;
|
||||
signalIds: string[];
|
||||
status: 'open' | 'closed';
|
||||
setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void;
|
||||
setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void;
|
||||
kbnVersion: string;
|
||||
}
|
||||
|
||||
export type SendSignalsToTimeline = () => void;
|
||||
|
||||
export interface SendSignalsToTimelineActionProps {
|
||||
createTimeline: CreateTimeline;
|
||||
data: TimelineNonEcsData[][];
|
||||
}
|
||||
|
||||
export interface CreateTimelineProps {
|
||||
id: string;
|
||||
kqlQuery?: {
|
||||
filterQuery: SerializedFilterQuery | null;
|
||||
filterQueryDraft: KueryFilterQuery | null;
|
||||
};
|
||||
filters?: esFilters.Filter[];
|
||||
dateRange?: { start: number; end: number };
|
||||
}
|
||||
|
||||
export type CreateTimeline = ({ id, kqlQuery, filters, dateRange }: CreateTimelineProps) => void;
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { KueryFilterQuery, SerializedFilterQuery } from '../model';
|
||||
|
||||
import { KqlMode, TimelineModel } from './model';
|
||||
import { TimelineNonEcsData } from '../../graphql/types';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/siem/local/timeline');
|
||||
|
||||
|
@ -49,10 +50,21 @@ export const applyDeltaToColumnWidth = actionCreator<{
|
|||
|
||||
export const createTimeline = actionCreator<{
|
||||
id: string;
|
||||
dateRange?: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
filters?: esFilters.Filter[];
|
||||
columns: ColumnHeader[];
|
||||
itemsPerPage?: number;
|
||||
kqlQuery?: {
|
||||
filterQuery: SerializedFilterQuery | null;
|
||||
filterQueryDraft: KueryFilterQuery | null;
|
||||
};
|
||||
show?: boolean;
|
||||
sort?: Sort;
|
||||
showCheckboxes?: boolean;
|
||||
showRowRenderers?: boolean;
|
||||
}>('CREATE_TIMELINE');
|
||||
|
||||
export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT');
|
||||
|
@ -198,3 +210,34 @@ export const setFilters = actionCreator<{
|
|||
id: string;
|
||||
filters: esFilters.Filter[];
|
||||
}>('SET_TIMELINE_FILTERS');
|
||||
|
||||
export const setSelected = actionCreator<{
|
||||
id: string;
|
||||
eventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
isSelected: boolean;
|
||||
isSelectAllChecked: boolean;
|
||||
}>('SET_TIMELINE_SELECTED');
|
||||
|
||||
export const clearSelected = actionCreator<{
|
||||
id: string;
|
||||
}>('CLEAR_TIMELINE_SELECTED');
|
||||
|
||||
export const setEventsLoading = actionCreator<{
|
||||
id: string;
|
||||
eventIds: string[];
|
||||
isLoading: boolean;
|
||||
}>('SET_TIMELINE_EVENTS_LOADING');
|
||||
|
||||
export const clearEventsLoading = actionCreator<{
|
||||
id: string;
|
||||
}>('CLEAR_TIMELINE_EVENTS_LOADING');
|
||||
|
||||
export const setEventsDeleted = actionCreator<{
|
||||
id: string;
|
||||
eventIds: string[];
|
||||
isDeleted: boolean;
|
||||
}>('SET_TIMELINE_EVENTS_DELETED');
|
||||
|
||||
export const clearEventsDeleted = actionCreator<{
|
||||
id: string;
|
||||
}>('CLEAR_TIMELINE_EVENTS_DELETED');
|
||||
|
|
|
@ -85,6 +85,7 @@ describe('Epic Timeline', () => {
|
|||
],
|
||||
},
|
||||
],
|
||||
deletedEventIds: [],
|
||||
description: '',
|
||||
eventIdToNoteIds: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
|
@ -117,6 +118,7 @@ describe('Epic Timeline', () => {
|
|||
],
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
itemsPerPage: 25,
|
||||
|
@ -130,13 +132,17 @@ describe('Epic Timeline', () => {
|
|||
},
|
||||
filterQueryDraft: { kind: 'kuery', expression: 'endgame.user_name : "zeus" ' },
|
||||
},
|
||||
loadingEventIds: [],
|
||||
title: 'saved',
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
pinnedEventsSaveObject: {},
|
||||
dateRange: { start: 1572469587644, end: 1572555987644 },
|
||||
savedObjectId: '11169110-fc22-11e9-8ca9-072f15ce2685',
|
||||
selectedEventIds: {},
|
||||
show: true,
|
||||
showCheckboxes: false,
|
||||
showRowRenderers: true,
|
||||
sort: { columnId: '@timestamp', sortDirection: Direction.desc },
|
||||
width: 1100,
|
||||
version: 'WzM4LDFd',
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getOr, omit, uniq, isEmpty, isEqualWith } from 'lodash/fp';
|
||||
import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp';
|
||||
|
||||
import { esFilters } from '../../../../../../../src/plugins/data/public';
|
||||
import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header';
|
||||
|
@ -19,6 +19,7 @@ import { KueryFilterQuery, SerializedFilterQuery } from '../model';
|
|||
|
||||
import { KqlMode, timelineDefaults, TimelineModel } from './model';
|
||||
import { TimelineById, TimelineState } from './types';
|
||||
import { TimelineNonEcsData } from '../../graphql/types';
|
||||
|
||||
const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference
|
||||
|
||||
|
@ -129,20 +130,36 @@ export const addTimelineToStore = ({
|
|||
|
||||
interface AddNewTimelineParams {
|
||||
columns: ColumnHeader[];
|
||||
dateRange?: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
filters?: esFilters.Filter[];
|
||||
id: string;
|
||||
itemsPerPage?: number;
|
||||
kqlQuery?: {
|
||||
filterQuery: SerializedFilterQuery | null;
|
||||
filterQueryDraft: KueryFilterQuery | null;
|
||||
};
|
||||
show?: boolean;
|
||||
sort?: Sort;
|
||||
showCheckboxes?: boolean;
|
||||
showRowRenderers?: boolean;
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
|
||||
/** Adds a new `Timeline` to the provided collection of `TimelineById` */
|
||||
export const addNewTimeline = ({
|
||||
columns,
|
||||
dateRange = { start: 0, end: 0 },
|
||||
filters = timelineDefaults.filters,
|
||||
id,
|
||||
itemsPerPage = timelineDefaults.itemsPerPage,
|
||||
kqlQuery = { filterQuery: null, filterQueryDraft: null },
|
||||
sort = timelineDefaults.sort,
|
||||
show = false,
|
||||
showCheckboxes = false,
|
||||
showRowRenderers = true,
|
||||
timelineById,
|
||||
}: AddNewTimelineParams): TimelineById => ({
|
||||
...timelineById,
|
||||
|
@ -150,13 +167,18 @@ export const addNewTimeline = ({
|
|||
id,
|
||||
...timelineDefaults,
|
||||
columns,
|
||||
dateRange,
|
||||
filters,
|
||||
itemsPerPage,
|
||||
kqlQuery,
|
||||
sort,
|
||||
show,
|
||||
savedObjectId: null,
|
||||
version: null,
|
||||
isSaving: false,
|
||||
isLoading: false,
|
||||
showCheckboxes,
|
||||
showRowRenderers,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1095,6 +1117,93 @@ export const removeTimelineProvider = ({
|
|||
};
|
||||
};
|
||||
|
||||
interface SetDeletedTimelineEventsParams {
|
||||
id: string;
|
||||
eventIds: string[];
|
||||
isDeleted: boolean;
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
|
||||
export const setDeletedTimelineEvents = ({
|
||||
id,
|
||||
eventIds,
|
||||
isDeleted,
|
||||
timelineById,
|
||||
}: SetDeletedTimelineEventsParams): TimelineById => {
|
||||
const timeline = timelineById[id];
|
||||
|
||||
const deletedEventIds = isDeleted
|
||||
? union(timeline.deletedEventIds, eventIds)
|
||||
: timeline.deletedEventIds.filter(currentEventId => !eventIds.includes(currentEventId));
|
||||
|
||||
return {
|
||||
...timelineById,
|
||||
[id]: {
|
||||
...timeline,
|
||||
deletedEventIds,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface SetLoadingTimelineEventsParams {
|
||||
id: string;
|
||||
eventIds: string[];
|
||||
isLoading: boolean;
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
|
||||
export const setLoadingTimelineEvents = ({
|
||||
id,
|
||||
eventIds,
|
||||
isLoading,
|
||||
timelineById,
|
||||
}: SetLoadingTimelineEventsParams): TimelineById => {
|
||||
const timeline = timelineById[id];
|
||||
|
||||
const loadingEventIds = isLoading
|
||||
? union(timeline.loadingEventIds, eventIds)
|
||||
: timeline.loadingEventIds.filter(currentEventId => !eventIds.includes(currentEventId));
|
||||
|
||||
return {
|
||||
...timelineById,
|
||||
[id]: {
|
||||
...timeline,
|
||||
loadingEventIds,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface SetSelectedTimelineEventsParams {
|
||||
id: string;
|
||||
eventIds: Record<string, TimelineNonEcsData[]>;
|
||||
isSelectAllChecked: boolean;
|
||||
isSelected: boolean;
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
|
||||
export const setSelectedTimelineEvents = ({
|
||||
id,
|
||||
eventIds,
|
||||
isSelectAllChecked = false,
|
||||
isSelected,
|
||||
timelineById,
|
||||
}: SetSelectedTimelineEventsParams): TimelineById => {
|
||||
const timeline = timelineById[id];
|
||||
|
||||
const selectedEventIds = isSelected
|
||||
? { ...timeline.selectedEventIds, ...eventIds }
|
||||
: omit(Object.keys(eventIds), timeline.selectedEventIds);
|
||||
|
||||
return {
|
||||
...timelineById,
|
||||
[id]: {
|
||||
...timeline,
|
||||
selectedEventIds,
|
||||
isSelectAllChecked,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface UnPinTimelineEventParams {
|
||||
id: string;
|
||||
eventId: string;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { DataProvider } from '../../components/timeline/data_providers/data_prov
|
|||
import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/helpers';
|
||||
import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers';
|
||||
import { Sort } from '../../components/timeline/body/sort';
|
||||
import { Direction, PinnedEvent } from '../../graphql/types';
|
||||
import { Direction, PinnedEvent, TimelineNonEcsData } from '../../graphql/types';
|
||||
import { KueryFilterQuery, SerializedFilterQuery } from '../model';
|
||||
|
||||
export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages
|
||||
|
@ -21,6 +21,8 @@ export interface TimelineModel {
|
|||
columns: ColumnHeader[];
|
||||
/** The sources of the event data shown in the timeline */
|
||||
dataProviders: DataProvider[];
|
||||
/** Events to not be rendered **/
|
||||
deletedEventIds: string[];
|
||||
/** A summary of the events and notes in this timeline */
|
||||
description: string;
|
||||
/** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */
|
||||
|
@ -32,6 +34,10 @@ export interface TimelineModel {
|
|||
highlightedDropAndProviderId: string;
|
||||
/** Uniquely identifies the timeline */
|
||||
id: string;
|
||||
/** If selectAll checkbox in header is checked **/
|
||||
isSelectAllChecked: boolean;
|
||||
/** Events to be rendered as loading **/
|
||||
loadingEventIds: string[];
|
||||
savedObjectId: string | null;
|
||||
/** When true, this timeline was marked as "favorite" by the user */
|
||||
isFavorite: boolean;
|
||||
|
@ -61,8 +67,14 @@ export interface TimelineModel {
|
|||
end: number;
|
||||
};
|
||||
savedQueryId?: string | null;
|
||||
/** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/
|
||||
selectedEventIds: Record<string, TimelineNonEcsData[]>;
|
||||
/** When true, show the timeline flyover */
|
||||
show: boolean;
|
||||
/** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/
|
||||
showCheckboxes: boolean;
|
||||
/** When true, shows additional rowRenderers below the PlainRowRenderer **/
|
||||
showRowRenderers: boolean;
|
||||
/** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */
|
||||
sort: Sort;
|
||||
/** Persists the UI state (width) of the timeline flyover */
|
||||
|
@ -78,22 +90,28 @@ export type SubsetTimelineModel = Readonly<
|
|||
TimelineModel,
|
||||
| 'columns'
|
||||
| 'dataProviders'
|
||||
| 'deletedEventIds'
|
||||
| 'description'
|
||||
| 'eventIdToNoteIds'
|
||||
| 'highlightedDropAndProviderId'
|
||||
| 'historyIds'
|
||||
| 'isFavorite'
|
||||
| 'isLive'
|
||||
| 'isSelectAllChecked'
|
||||
| 'itemsPerPage'
|
||||
| 'itemsPerPageOptions'
|
||||
| 'kqlMode'
|
||||
| 'kqlQuery'
|
||||
| 'title'
|
||||
| 'loadingEventIds'
|
||||
| 'noteIds'
|
||||
| 'pinnedEventIds'
|
||||
| 'pinnedEventsSaveObject'
|
||||
| 'dateRange'
|
||||
| 'selectedEventIds'
|
||||
| 'show'
|
||||
| 'showCheckboxes'
|
||||
| 'showRowRenderers'
|
||||
| 'sort'
|
||||
| 'width'
|
||||
| 'isSaving'
|
||||
|
@ -106,6 +124,7 @@ export type SubsetTimelineModel = Readonly<
|
|||
export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filters'> = {
|
||||
columns: defaultHeaders,
|
||||
dataProviders: [],
|
||||
deletedEventIds: [],
|
||||
description: '',
|
||||
eventIdToNoteIds: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
|
@ -113,6 +132,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter
|
|||
filters: [],
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
itemsPerPage: 25,
|
||||
|
@ -122,6 +142,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter
|
|||
filterQuery: null,
|
||||
filterQueryDraft: null,
|
||||
},
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
|
@ -131,7 +152,10 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter
|
|||
end: 0,
|
||||
},
|
||||
savedObjectId: null,
|
||||
selectedEventIds: {},
|
||||
show: false,
|
||||
showCheckboxes: false,
|
||||
showRowRenderers: true,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: Direction.desc,
|
||||
|
|
|
@ -64,6 +64,7 @@ const timelineByIdMock: TimelineById = {
|
|||
],
|
||||
columns: [],
|
||||
description: '',
|
||||
deletedEventIds: [],
|
||||
eventIdToNoteIds: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
|
@ -71,11 +72,13 @@ const timelineByIdMock: TimelineById = {
|
|||
savedObjectId: null,
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
itemsPerPage: 25,
|
||||
itemsPerPageOptions: [10, 25, 50],
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
|
@ -84,7 +87,10 @@ const timelineByIdMock: TimelineById = {
|
|||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
selectedEventIds: {},
|
||||
show: true,
|
||||
showCheckboxes: false,
|
||||
showRowRenderers: true,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: Direction.desc,
|
||||
|
@ -1087,22 +1093,28 @@ describe('Timeline', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
deletedEventIds: [],
|
||||
description: '',
|
||||
eventIdToNoteIds: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
selectedEventIds: {},
|
||||
show: true,
|
||||
showRowRenderers: true,
|
||||
showCheckboxes: false,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: Direction.desc,
|
||||
|
@ -1174,21 +1186,27 @@ describe('Timeline', () => {
|
|||
},
|
||||
],
|
||||
description: '',
|
||||
deletedEventIds: [],
|
||||
eventIdToNoteIds: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
selectedEventIds: {},
|
||||
show: true,
|
||||
showRowRenderers: true,
|
||||
showCheckboxes: false,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: Direction.desc,
|
||||
|
@ -1366,21 +1384,27 @@ describe('Timeline', () => {
|
|||
},
|
||||
],
|
||||
description: '',
|
||||
deletedEventIds: [],
|
||||
eventIdToNoteIds: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
selectedEventIds: {},
|
||||
show: true,
|
||||
showRowRenderers: true,
|
||||
showCheckboxes: false,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: Direction.desc,
|
||||
|
@ -1452,21 +1476,27 @@ describe('Timeline', () => {
|
|||
},
|
||||
],
|
||||
description: '',
|
||||
deletedEventIds: [],
|
||||
eventIdToNoteIds: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
selectedEventIds: {},
|
||||
show: true,
|
||||
showRowRenderers: true,
|
||||
showCheckboxes: false,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: Direction.desc,
|
||||
|
@ -1633,21 +1663,27 @@ describe('Timeline', () => {
|
|||
},
|
||||
],
|
||||
description: '',
|
||||
deletedEventIds: [],
|
||||
eventIdToNoteIds: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
selectedEventIds: {},
|
||||
show: true,
|
||||
showRowRenderers: true,
|
||||
showCheckboxes: false,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: Direction.desc,
|
||||
|
@ -1701,23 +1737,29 @@ describe('Timeline', () => {
|
|||
},
|
||||
],
|
||||
description: '',
|
||||
deletedEventIds: [],
|
||||
eventIdToNoteIds: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
id: 'foo',
|
||||
savedObjectId: null,
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
selectedEventIds: {},
|
||||
show: true,
|
||||
showRowRenderers: true,
|
||||
showCheckboxes: false,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: Direction.desc,
|
||||
|
@ -1795,6 +1837,7 @@ describe('Timeline', () => {
|
|||
},
|
||||
],
|
||||
description: '',
|
||||
deletedEventIds: [],
|
||||
eventIdToNoteIds: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
|
@ -1802,16 +1845,21 @@ describe('Timeline', () => {
|
|||
savedObjectId: null,
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isSelectAllChecked: false,
|
||||
isLoading: false,
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
selectedEventIds: {},
|
||||
show: true,
|
||||
showRowRenderers: true,
|
||||
showCheckboxes: false,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: Direction.desc,
|
||||
|
|
|
@ -20,7 +20,13 @@ import {
|
|||
pinEvent,
|
||||
removeColumn,
|
||||
removeProvider,
|
||||
setEventsDeleted,
|
||||
clearEventsDeleted,
|
||||
setEventsLoading,
|
||||
clearEventsLoading,
|
||||
setKqlFilterQueryDraft,
|
||||
setSelected,
|
||||
clearSelected,
|
||||
showCallOutUnauthorizedMsg,
|
||||
showTimeline,
|
||||
startTimelineSaving,
|
||||
|
@ -61,6 +67,9 @@ import {
|
|||
pinTimelineEvent,
|
||||
removeTimelineColumn,
|
||||
removeTimelineProvider,
|
||||
setDeletedTimelineEvents,
|
||||
setLoadingTimelineEvents,
|
||||
setSelectedTimelineEvents,
|
||||
unPinTimelineEvent,
|
||||
updateHighlightedDropAndProvider,
|
||||
updateKqlFilterQueryDraft,
|
||||
|
@ -103,17 +112,39 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
|
|||
...state,
|
||||
timelineById: addTimelineToStore({ id, timeline, timelineById: state.timelineById }),
|
||||
}))
|
||||
.case(createTimeline, (state, { id, show, columns, itemsPerPage, sort }) => ({
|
||||
...state,
|
||||
timelineById: addNewTimeline({
|
||||
columns,
|
||||
id,
|
||||
itemsPerPage,
|
||||
sort,
|
||||
show,
|
||||
timelineById: state.timelineById,
|
||||
}),
|
||||
}))
|
||||
.case(
|
||||
createTimeline,
|
||||
(
|
||||
state,
|
||||
{
|
||||
id,
|
||||
dateRange,
|
||||
show,
|
||||
columns,
|
||||
itemsPerPage,
|
||||
kqlQuery,
|
||||
sort,
|
||||
showCheckboxes,
|
||||
showRowRenderers,
|
||||
filters,
|
||||
}
|
||||
) => ({
|
||||
...state,
|
||||
timelineById: addNewTimeline({
|
||||
columns,
|
||||
dateRange,
|
||||
filters,
|
||||
id,
|
||||
itemsPerPage,
|
||||
kqlQuery,
|
||||
sort,
|
||||
show,
|
||||
showCheckboxes,
|
||||
showRowRenderers,
|
||||
timelineById: state.timelineById,
|
||||
}),
|
||||
})
|
||||
)
|
||||
.case(upsertColumn, (state, { column, id, index }) => ({
|
||||
...state,
|
||||
timelineById: upsertTimelineColumn({ column, id, index, timelineById: state.timelineById }),
|
||||
|
@ -218,6 +249,65 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
|
|||
},
|
||||
},
|
||||
}))
|
||||
.case(setEventsDeleted, (state, { id, eventIds, isDeleted }) => ({
|
||||
...state,
|
||||
timelineById: setDeletedTimelineEvents({
|
||||
id,
|
||||
eventIds,
|
||||
timelineById: state.timelineById,
|
||||
isDeleted,
|
||||
}),
|
||||
}))
|
||||
.case(clearEventsDeleted, (state, { id }) => ({
|
||||
...state,
|
||||
timelineById: {
|
||||
...state.timelineById,
|
||||
[id]: {
|
||||
...state.timelineById[id],
|
||||
deletedEventIds: [],
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(setEventsLoading, (state, { id, eventIds, isLoading }) => ({
|
||||
...state,
|
||||
timelineById: setLoadingTimelineEvents({
|
||||
id,
|
||||
eventIds,
|
||||
timelineById: state.timelineById,
|
||||
isLoading,
|
||||
}),
|
||||
}))
|
||||
.case(clearEventsLoading, (state, { id }) => ({
|
||||
...state,
|
||||
timelineById: {
|
||||
...state.timelineById,
|
||||
[id]: {
|
||||
...state.timelineById[id],
|
||||
loadingEventIds: [],
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(setSelected, (state, { id, eventIds, isSelected, isSelectAllChecked }) => ({
|
||||
...state,
|
||||
timelineById: setSelectedTimelineEvents({
|
||||
id,
|
||||
eventIds,
|
||||
timelineById: state.timelineById,
|
||||
isSelected,
|
||||
isSelectAllChecked,
|
||||
}),
|
||||
}))
|
||||
.case(clearSelected, (state, { id }) => ({
|
||||
...state,
|
||||
timelineById: {
|
||||
...state.timelineById,
|
||||
[id]: {
|
||||
...state.timelineById[id],
|
||||
selectedEventIds: {},
|
||||
isSelectAllChecked: false,
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(updateIsLoading, (state, { id, isLoading }) => ({
|
||||
...state,
|
||||
timelineById: {
|
||||
|
|
|
@ -10834,7 +10834,6 @@
|
|||
"xpack.siem.eventsOverTime.showing": "表示中",
|
||||
"xpack.siem.eventsOverTime.unit": "{totalCount, plural, =1 {event} other {events}}",
|
||||
"xpack.siem.flyout.button.text": "タイムライン",
|
||||
"xpack.siem.footer.loadingEventsData": "イベントを読み込み中",
|
||||
"xpack.siem.network.navigation.anomaliesTitle": "異常",
|
||||
"xpack.siem.network.navigation.dnsTitle": "DNS",
|
||||
"xpack.siem.network.navigation.flowsTitle": "Flow",
|
||||
|
@ -12717,4 +12716,4 @@
|
|||
"xpack.licensing.welcomeBanner.licenseIsExpiredDescription.updateYourLicenseLinkText": "ライセンスを更新",
|
||||
"xpack.licensing.welcomeBanner.licenseIsExpiredTitle": "ご使用の {licenseType} ライセンスは期限切れです"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10862,7 +10862,6 @@
|
|||
"xpack.siem.eventsOverTime.showing": "显示",
|
||||
"xpack.siem.eventsOverTime.unit": "{totalCount, plural, =1 {event} other {events}}",
|
||||
"xpack.siem.flyout.button.text": "时间线",
|
||||
"xpack.siem.footer.loadingEventsData": "正在加载事件",
|
||||
"xpack.siem.network.navigation.anomaliesTitle": "异常",
|
||||
"xpack.siem.network.navigation.dnsTitle": "DNS",
|
||||
"xpack.siem.network.navigation.flowsTitle": "Flows",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue