mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[SIEM] Adds Signals Table and additional configuration options to StatefulEventsViewer (#52044)
## Summary This is `Part I` of `II` for adding the `Signals Table` to the main Detection Engine landing page ([meta issue](https://github.com/elastic/kibana/issues/50405)). Breaking into two parts as this contains additional configuration options to the `StatefulEventsViewer` which will be used as part of https://github.com/elastic/kibana/issues/51016. `Part I` includes: * `SignalsTable` component that displays signals from the default signals index `.siem-signals` * Refactors `StatefulEventsViewer` to use `useFetchIndexPatterns` hook instead of `WithSource` * Adds ability to specify `alias` to `ColumnHeader` when providing column names * Adds the following new props to `StatefulEventsViewer` * `defaultIndices?: string[]` -- for specifying a different index than `siemDefaultIndex` * `headerFilterGroup?: React.ReactNode` -- for providing a component to display in the top right of the table (e.g. filter buttons, select, etc.) * `timelineTypeContext?: TimelineTypeContextProps` -- config for when creating a new table * `documentType?: string` -- user string for type of records displayed (e.g. Signals) * `footerText?: string` -- custom footer text for given document type * `showCheckboxes: boolean` -- whether or not to show selection checkboxes * `showRowRenderers: boolean` -- whether or not to show row renderers * `timelineType: TimelineType` -- type of Timeline for setting default columns * `title?: string` -- optional custom title * `utilityBar?: (totalCount: number) => React.ReactNode` -- optional param for providing your own custom `UtilityBar` instead of using the default `Showing xxx events`. `Part II` will add support for selection and overflow/batch actions. <img width="1548" alt="Screen Shot 2019-12-02 at 19 59 34" src="https://user-images.githubusercontent.com/2946766/70016801-89aa0c80-153e-11ea-9dbf-b7b8648fb260.png"> ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [x] 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) * Note: some placeholders were moved to their own files, and so some raw strings will still exist - [ ] ~[Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~ - [x] [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
e8f3fa91d9
commit
f21d5ada5a
34 changed files with 765 additions and 348 deletions
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { defaultHeaders } from './default_headers';
|
||||
import { SubsetTimelineModel, timelineDefaults } from '../../store/timeline/model';
|
||||
|
||||
export const eventsDefaultModel: SubsetTimelineModel = {
|
||||
...timelineDefaults,
|
||||
columns: defaultHeaders,
|
||||
};
|
|
@ -8,7 +8,7 @@ import { mount } from 'enzyme';
|
|||
import React from 'react';
|
||||
import { MockedProvider } from 'react-apollo/test-utils';
|
||||
|
||||
import { TestProviders } from '../../mock';
|
||||
import { mockIndexPattern, TestProviders } from '../../mock';
|
||||
import { mockUiSettings } from '../../mock/ui_settings';
|
||||
import { wait } from '../../lib/helpers';
|
||||
|
||||
|
@ -16,6 +16,9 @@ import { mockEventViewerResponse } from './mock';
|
|||
import { StatefulEventsViewer } from '.';
|
||||
import { defaultHeaders } from './default_headers';
|
||||
import { useKibanaCore } from '../../lib/compose/kibana_core';
|
||||
import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns';
|
||||
import { mockBrowserFields } from '../../containers/source/mock';
|
||||
import { eventsDefaultModel } from './default_model';
|
||||
|
||||
jest.mock('../../lib/settings/use_kibana_ui_setting');
|
||||
|
||||
|
@ -25,6 +28,15 @@ mockUseKibanaCore.mockImplementation(() => ({
|
|||
uiSettings: mockUiSettings,
|
||||
}));
|
||||
|
||||
const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock;
|
||||
jest.mock('../../containers/detection_engine/rules/fetch_index_patterns');
|
||||
mockUseFetchIndexPatterns.mockImplementation(() => [
|
||||
{
|
||||
browserFields: mockBrowserFields,
|
||||
indexPatterns: mockIndexPattern,
|
||||
},
|
||||
]);
|
||||
|
||||
const from = 1566943856794;
|
||||
const to = 1566857456791;
|
||||
|
||||
|
@ -33,7 +45,12 @@ describe('EventsViewer', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<MockedProvider mocks={mockEventViewerResponse} addTypename={false}>
|
||||
<StatefulEventsViewer end={to} id={'test-stateful-events-viewer'} start={from} />
|
||||
<StatefulEventsViewer
|
||||
defaultModel={eventsDefaultModel}
|
||||
end={to}
|
||||
id={'test-stateful-events-viewer'}
|
||||
start={from}
|
||||
/>
|
||||
</MockedProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -53,7 +70,12 @@ describe('EventsViewer', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<MockedProvider mocks={mockEventViewerResponse} addTypename={false}>
|
||||
<StatefulEventsViewer end={to} id={'test-stateful-events-viewer'} start={from} />
|
||||
<StatefulEventsViewer
|
||||
defaultModel={eventsDefaultModel}
|
||||
end={to}
|
||||
id={'test-stateful-events-viewer'}
|
||||
start={from}
|
||||
/>
|
||||
</MockedProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -73,7 +95,12 @@ describe('EventsViewer', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<MockedProvider mocks={mockEventViewerResponse} addTypename={false}>
|
||||
<StatefulEventsViewer end={to} id={'test-stateful-events-viewer'} start={from} />
|
||||
<StatefulEventsViewer
|
||||
defaultModel={eventsDefaultModel}
|
||||
end={to}
|
||||
id={'test-stateful-events-viewer'}
|
||||
start={from}
|
||||
/>
|
||||
</MockedProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -94,7 +121,12 @@ describe('EventsViewer', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<MockedProvider mocks={mockEventViewerResponse} addTypename={false}>
|
||||
<StatefulEventsViewer end={to} id={'test-stateful-events-viewer'} start={from} />
|
||||
<StatefulEventsViewer
|
||||
defaultModel={eventsDefaultModel}
|
||||
end={to}
|
||||
id={'test-stateful-events-viewer'}
|
||||
start={from}
|
||||
/>
|
||||
</MockedProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
|
|
@ -26,13 +26,13 @@ import { Footer, footerHeight } from '../timeline/footer';
|
|||
import { combineQueries } from '../timeline/helpers';
|
||||
import { TimelineRefetch } from '../timeline/refetch_timeline';
|
||||
import { isCompactFooter } from '../timeline/timeline';
|
||||
import { ManageTimelineContext } from '../timeline/timeline_context';
|
||||
import { ManageTimelineContext, TimelineTypeContextProps } from '../timeline/timeline_context';
|
||||
import * as i18n from './translations';
|
||||
import {
|
||||
IIndexPattern,
|
||||
Query,
|
||||
esFilters,
|
||||
esQuery,
|
||||
IIndexPattern,
|
||||
Query,
|
||||
} from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
const DEFAULT_EVENTS_VIEWER_HEIGHT = 500;
|
||||
|
@ -48,6 +48,7 @@ interface Props {
|
|||
dataProviders: DataProvider[];
|
||||
end: number;
|
||||
filters: esFilters.Filter[];
|
||||
headerFilterGroup?: React.ReactNode;
|
||||
height?: number;
|
||||
id: string;
|
||||
indexPattern: IIndexPattern;
|
||||
|
@ -60,7 +61,9 @@ interface Props {
|
|||
showInspect: boolean;
|
||||
start: number;
|
||||
sort: Sort;
|
||||
timelineTypeContext: TimelineTypeContextProps;
|
||||
toggleColumn: (column: ColumnHeader) => void;
|
||||
utilityBar?: (totalCount: number) => React.ReactNode;
|
||||
}
|
||||
|
||||
export const EventsViewer = React.memo<Props>(
|
||||
|
@ -70,6 +73,7 @@ export const EventsViewer = React.memo<Props>(
|
|||
dataProviders,
|
||||
end,
|
||||
filters,
|
||||
headerFilterGroup,
|
||||
height = DEFAULT_EVENTS_VIEWER_HEIGHT,
|
||||
id,
|
||||
indexPattern,
|
||||
|
@ -82,7 +86,9 @@ export const EventsViewer = React.memo<Props>(
|
|||
showInspect,
|
||||
start,
|
||||
sort,
|
||||
timelineTypeContext,
|
||||
toggleColumn,
|
||||
utilityBar,
|
||||
}) => {
|
||||
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
|
||||
const core = useKibanaCore();
|
||||
|
@ -116,6 +122,7 @@ export const EventsViewer = React.memo<Props>(
|
|||
fields={columnsHeader.map(c => c.id)}
|
||||
filterQuery={combinedQueries.filterQuery}
|
||||
id={id}
|
||||
indexPattern={indexPattern}
|
||||
limit={itemsPerPage}
|
||||
sortField={{
|
||||
sortFieldId: sort.columnId,
|
||||
|
@ -137,17 +144,29 @@ export const EventsViewer = React.memo<Props>(
|
|||
<HeaderSection
|
||||
id={id}
|
||||
showInspect={showInspect}
|
||||
subtitle={`${i18n.SHOWING}: ${totalCount.toLocaleString()} ${i18n.UNIT(
|
||||
totalCount
|
||||
)}`}
|
||||
title={i18n.EVENTS}
|
||||
/>
|
||||
subtitle={
|
||||
utilityBar
|
||||
? undefined
|
||||
: `${i18n.SHOWING}: ${totalCount.toLocaleString()} ${i18n.UNIT(
|
||||
totalCount
|
||||
)}`
|
||||
}
|
||||
title={timelineTypeContext?.title ?? i18n.EVENTS}
|
||||
>
|
||||
{headerFilterGroup}
|
||||
</HeaderSection>
|
||||
|
||||
{utilityBar?.(totalCount)}
|
||||
|
||||
<div
|
||||
data-test-subj={`events-container-loading-${loading}`}
|
||||
style={{ width: `${width}px` }}
|
||||
>
|
||||
<ManageTimelineContext loading={loading} width={width}>
|
||||
<ManageTimelineContext
|
||||
loading={loading}
|
||||
width={width}
|
||||
type={timelineTypeContext}
|
||||
>
|
||||
<TimelineRefetch
|
||||
id={id}
|
||||
inputId="global"
|
||||
|
|
|
@ -10,11 +10,14 @@ import { MockedProvider } from 'react-apollo/test-utils';
|
|||
|
||||
import { useKibanaCore } from '../../lib/compose/kibana_core';
|
||||
import { wait } from '../../lib/helpers';
|
||||
import { TestProviders } from '../../mock';
|
||||
import { mockIndexPattern, TestProviders } from '../../mock';
|
||||
import { mockUiSettings } from '../../mock/ui_settings';
|
||||
|
||||
import { mockEventViewerResponse } from './mock';
|
||||
import { StatefulEventsViewer } from '.';
|
||||
import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns';
|
||||
import { mockBrowserFields } from '../../containers/source/mock';
|
||||
import { eventsDefaultModel } from './default_model';
|
||||
|
||||
jest.mock('../../lib/settings/use_kibana_ui_setting');
|
||||
|
||||
|
@ -24,6 +27,15 @@ mockUseKibanaCore.mockImplementation(() => ({
|
|||
uiSettings: mockUiSettings,
|
||||
}));
|
||||
|
||||
const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock;
|
||||
jest.mock('../../containers/detection_engine/rules/fetch_index_patterns');
|
||||
mockUseFetchIndexPatterns.mockImplementation(() => [
|
||||
{
|
||||
browserFields: mockBrowserFields,
|
||||
indexPatterns: mockIndexPattern,
|
||||
},
|
||||
]);
|
||||
|
||||
const from = 1566943856794;
|
||||
const to = 1566857456791;
|
||||
|
||||
|
@ -32,7 +44,12 @@ describe('StatefulEventsViewer', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<MockedProvider mocks={mockEventViewerResponse} addTypename={false}>
|
||||
<StatefulEventsViewer end={to} id={'test-stateful-events-viewer'} start={from} />
|
||||
<StatefulEventsViewer
|
||||
defaultModel={eventsDefaultModel}
|
||||
end={to}
|
||||
id={'test-stateful-events-viewer'}
|
||||
start={from}
|
||||
/>
|
||||
</MockedProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -52,7 +69,12 @@ describe('StatefulEventsViewer', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<MockedProvider mocks={mockEventViewerResponse} addTypename={false}>
|
||||
<StatefulEventsViewer end={to} id={'test-stateful-events-viewer'} start={from} />
|
||||
<StatefulEventsViewer
|
||||
defaultModel={eventsDefaultModel}
|
||||
end={to}
|
||||
id={'test-stateful-events-viewer'}
|
||||
start={from}
|
||||
/>
|
||||
</MockedProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -72,7 +94,12 @@ describe('StatefulEventsViewer', () => {
|
|||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<MockedProvider mocks={mockEventViewerResponse} addTypename={false}>
|
||||
<StatefulEventsViewer end={to} id={'test-stateful-events-viewer'} start={from} />
|
||||
<StatefulEventsViewer
|
||||
defaultModel={eventsDefaultModel}
|
||||
end={to}
|
||||
id={'test-stateful-events-viewer'}
|
||||
start={from}
|
||||
/>
|
||||
</MockedProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
|
|
@ -5,26 +5,35 @@
|
|||
*/
|
||||
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
import { WithSource } from '../../containers/source';
|
||||
import chrome from 'ui/chrome';
|
||||
import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store';
|
||||
import { timelineActions, inputsActions } from '../../store/actions';
|
||||
import { KqlMode, TimelineModel } from '../../store/timeline/model';
|
||||
import { inputsActions, timelineActions } from '../../store/actions';
|
||||
import { KqlMode, SubsetTimelineModel, TimelineModel } from '../../store/timeline/model';
|
||||
import { ColumnHeader } from '../timeline/body/column_headers/column_header';
|
||||
import { DataProvider } from '../timeline/data_providers/data_provider';
|
||||
import { Sort } from '../timeline/body/sort';
|
||||
import { OnChangeItemsPerPage } from '../timeline/events';
|
||||
import { Query, esFilters } from '../../../../../../../src/plugins/data/public';
|
||||
import { esFilters, Query } from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
import { EventsViewer } from './events_viewer';
|
||||
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';
|
||||
|
||||
export interface OwnProps {
|
||||
defaultIndices?: string[];
|
||||
defaultFilters?: esFilters.Filter[];
|
||||
defaultModel: SubsetTimelineModel;
|
||||
end: number;
|
||||
id: string;
|
||||
start: number;
|
||||
headerFilterGroup?: React.ReactNode;
|
||||
timelineTypeContext?: TimelineTypeContextProps;
|
||||
utilityBar?: (totalCount: number) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface StateReduxProps {
|
||||
|
@ -74,9 +83,12 @@ const StatefulEventsViewerComponent = React.memo<Props>(
|
|||
createTimeline,
|
||||
columns,
|
||||
dataProviders,
|
||||
defaultModel,
|
||||
defaultIndices,
|
||||
deleteEventQuery,
|
||||
end,
|
||||
filters,
|
||||
headerFilterGroup,
|
||||
id,
|
||||
isLive,
|
||||
itemsPerPage,
|
||||
|
@ -86,10 +98,18 @@ const StatefulEventsViewerComponent = React.memo<Props>(
|
|||
removeColumn,
|
||||
start,
|
||||
sort,
|
||||
timelineTypeContext = {
|
||||
showCheckboxes: false,
|
||||
showRowRenderers: true,
|
||||
},
|
||||
updateItemsPerPage,
|
||||
upsertColumn,
|
||||
utilityBar,
|
||||
}) => {
|
||||
const [showInspect, setShowInspect] = useState(false);
|
||||
const [{ browserFields, indexPatterns }] = useFetchIndexPatterns(
|
||||
defaultIndices ?? chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (createTimeline != null) {
|
||||
|
@ -131,31 +151,30 @@ const StatefulEventsViewerComponent = React.memo<Props>(
|
|||
const handleOnMouseLeave = useCallback(() => setShowInspect(false), []);
|
||||
|
||||
return (
|
||||
<WithSource sourceId="default">
|
||||
{({ indexPattern, browserFields }) => (
|
||||
<div onMouseEnter={handleOnMouseEnter} onMouseLeave={handleOnMouseLeave}>
|
||||
<EventsViewer
|
||||
browserFields={browserFields}
|
||||
columns={columns}
|
||||
id={id}
|
||||
dataProviders={dataProviders!}
|
||||
end={end}
|
||||
filters={filters}
|
||||
indexPattern={indexPattern}
|
||||
isLive={isLive}
|
||||
itemsPerPage={itemsPerPage!}
|
||||
itemsPerPageOptions={itemsPerPageOptions!}
|
||||
kqlMode={kqlMode}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
query={query}
|
||||
showInspect={showInspect}
|
||||
start={start}
|
||||
sort={sort!}
|
||||
toggleColumn={toggleColumn}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</WithSource>
|
||||
<div onMouseEnter={handleOnMouseEnter} onMouseLeave={handleOnMouseLeave}>
|
||||
<EventsViewer
|
||||
browserFields={browserFields ?? {}}
|
||||
columns={columns}
|
||||
id={id}
|
||||
dataProviders={dataProviders!}
|
||||
end={end}
|
||||
filters={filters}
|
||||
headerFilterGroup={headerFilterGroup}
|
||||
indexPattern={indexPatterns ?? { fields: [], title: '' }}
|
||||
isLive={isLive}
|
||||
itemsPerPage={itemsPerPage!}
|
||||
itemsPerPageOptions={itemsPerPageOptions!}
|
||||
kqlMode={kqlMode}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
query={query}
|
||||
showInspect={showInspect}
|
||||
start={start}
|
||||
sort={sort!}
|
||||
timelineTypeContext={timelineTypeContext}
|
||||
toggleColumn={toggleColumn}
|
||||
utilityBar={utilityBar}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
|
@ -182,9 +201,9 @@ const makeMapStateToProps = () => {
|
|||
const getGlobalQuerySelector = inputsSelectors.globalQuerySelector();
|
||||
const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector();
|
||||
const getEvents = timelineSelectors.getEventsByIdSelector();
|
||||
const mapStateToProps = (state: State, { id }: OwnProps) => {
|
||||
const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => {
|
||||
const input: inputsModel.InputsRange = getInputsTimeline(state);
|
||||
const events: TimelineModel = getEvents(state, id);
|
||||
const events: TimelineModel = getEvents(state, id) ?? defaultModel;
|
||||
const { columns, dataProviders, itemsPerPage, itemsPerPageOptions, kqlMode, sort } = events;
|
||||
|
||||
return {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import { noop } from 'lodash/fp';
|
||||
import { defaultIndexPattern } from '../../../default_index_pattern';
|
||||
import { timelineQuery } from '../../containers/timeline/index.gql_query';
|
||||
|
||||
export const mockEventViewerResponse = [
|
||||
|
@ -31,7 +30,7 @@ export const mockEventViewerResponse = [
|
|||
sourceId: 'default',
|
||||
pagination: { limit: 25, cursor: null, tiebreaker: null },
|
||||
sortField: { sortFieldId: '@timestamp', direction: 'desc' },
|
||||
defaultIndex: defaultIndexPattern,
|
||||
defaultIndex: ['filebeat-*', 'auditbeat-*', 'packetbeat-*'],
|
||||
inspect: false,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -25,7 +25,7 @@ import { Properties } from '../../timeline/properties';
|
|||
import { appActions, appModel } from '../../../store/app';
|
||||
import { inputsActions } from '../../../store/inputs';
|
||||
import { timelineActions } from '../../../store/actions';
|
||||
import { TimelineModel } from '../../../store/timeline/model';
|
||||
import { timelineDefaults, TimelineModel } from '../../../store/timeline/model';
|
||||
import { DEFAULT_TIMELINE_WIDTH } from '../../timeline/body/helpers';
|
||||
import { InputsModelId } from '../../../store/inputs/constants';
|
||||
|
||||
|
@ -129,7 +129,7 @@ const makeMapStateToProps = () => {
|
|||
const getNotesByIds = appSelectors.notesByIdsSelector();
|
||||
const getGlobalInput = inputsSelectors.globalSelector();
|
||||
const mapStateToProps = (state: State, { timelineId }: OwnProps) => {
|
||||
const timeline: TimelineModel = getTimeline(state, timelineId);
|
||||
const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults;
|
||||
const globalInput: inputsModel.InputsRange = getGlobalInput(state);
|
||||
const {
|
||||
dataProviders,
|
||||
|
|
|
@ -42,6 +42,7 @@ import {
|
|||
} from './types';
|
||||
import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants';
|
||||
import { ColumnHeader } from '../timeline/body/column_headers/column_header';
|
||||
import { timelineDefaults } from '../../store/timeline/model';
|
||||
|
||||
interface OwnProps<TCache = object> {
|
||||
apolloClient: ApolloClient<TCache>;
|
||||
|
@ -315,7 +316,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
const makeMapStateToProps = () => {
|
||||
const getTimeline = timelineSelectors.getTimelineByIdSelector();
|
||||
const mapStateToProps = (state: State) => {
|
||||
const timeline = getTimeline(state, 'timeline-1');
|
||||
const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults;
|
||||
|
||||
return {
|
||||
timeline,
|
||||
|
|
|
@ -17,6 +17,7 @@ export interface ColumnHeader {
|
|||
example?: string;
|
||||
format?: string;
|
||||
id: ColumnId;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
width: number;
|
||||
|
|
|
@ -67,6 +67,31 @@ describe('Header', () => {
|
|||
).toEqual(columnHeader.id);
|
||||
});
|
||||
|
||||
test('it renders the header text alias when label is provided', () => {
|
||||
const label = 'Timestamp';
|
||||
const headerWithLabel = { ...columnHeader, label };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent
|
||||
header={headerWithLabel}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
setIsResizing={jest.fn()}
|
||||
sort={sort}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="header-text-${columnHeader.id}"]`)
|
||||
.first()
|
||||
.text()
|
||||
).toEqual(label);
|
||||
});
|
||||
|
||||
test('it renders a sort indicator', () => {
|
||||
const headerSortable = { ...columnHeader, aggregatable: true };
|
||||
const wrapper = mount(
|
||||
|
|
|
@ -50,7 +50,7 @@ const HeaderComp = React.memo<HeaderCompProps>(
|
|||
data-test-subj="header-tooltip"
|
||||
content={<HeaderToolTipContent header={header} />}
|
||||
>
|
||||
<>{header.id}</>
|
||||
<>{header.label ?? header.id}</>
|
||||
</EuiToolTip>
|
||||
</TruncatableText>
|
||||
|
||||
|
@ -66,7 +66,7 @@ const HeaderComp = React.memo<HeaderCompProps>(
|
|||
data-test-subj="header-tooltip"
|
||||
content={<HeaderToolTipContent header={header} />}
|
||||
>
|
||||
<>{header.id}</>
|
||||
<>{header.label ?? header.id}</>
|
||||
</EuiToolTip>
|
||||
</TruncatableText>
|
||||
</EventsHeadingTitleSpan>
|
||||
|
|
|
@ -17,6 +17,7 @@ import { ColumnHeader } from '../column_headers/column_header';
|
|||
import { DataDrivenColumns } from '../data_driven_columns';
|
||||
import { eventHasNotes, getPinOnClick } from '../helpers';
|
||||
import { ColumnRenderer } from '../renderers/column_renderer';
|
||||
import { useTimelineTypeContext } from '../../timeline_context';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
|
@ -67,44 +68,48 @@ export const EventColumnView = React.memo<Props>(
|
|||
timelineId,
|
||||
toggleShowNotes,
|
||||
updateNote,
|
||||
}) => (
|
||||
<EventsTrData data-test-subj="event-column-view">
|
||||
<Actions
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
associateNote={associateNote}
|
||||
checked={false}
|
||||
expanded={expanded}
|
||||
data-test-subj="actions"
|
||||
eventId={id}
|
||||
eventIsPinned={isEventPinned}
|
||||
getNotesByIds={getNotesByIds}
|
||||
isEventViewer={isEventViewer}
|
||||
loading={loading}
|
||||
noteIds={eventIdToNoteIds[id] || emptyNotes}
|
||||
onEventToggled={onEventToggled}
|
||||
onPinClicked={getPinOnClick({
|
||||
allowUnpinning: !eventHasNotes(eventIdToNoteIds[id]),
|
||||
eventId: id,
|
||||
onPinEvent,
|
||||
onUnPinEvent,
|
||||
isEventPinned,
|
||||
})}
|
||||
showCheckboxes={false}
|
||||
showNotes={showNotes}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
updateNote={updateNote}
|
||||
/>
|
||||
}) => {
|
||||
const timelineTypeContext = useTimelineTypeContext();
|
||||
|
||||
<DataDrivenColumns
|
||||
_id={id}
|
||||
columnHeaders={columnHeaders}
|
||||
columnRenderers={columnRenderers}
|
||||
data={data}
|
||||
onColumnResized={onColumnResized}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</EventsTrData>
|
||||
),
|
||||
return (
|
||||
<EventsTrData data-test-subj="event-column-view">
|
||||
<Actions
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
associateNote={associateNote}
|
||||
checked={false}
|
||||
expanded={expanded}
|
||||
data-test-subj="actions"
|
||||
eventId={id}
|
||||
eventIsPinned={isEventPinned}
|
||||
getNotesByIds={getNotesByIds}
|
||||
isEventViewer={isEventViewer}
|
||||
loading={loading}
|
||||
noteIds={eventIdToNoteIds[id] || emptyNotes}
|
||||
onEventToggled={onEventToggled}
|
||||
onPinClicked={getPinOnClick({
|
||||
allowUnpinning: !eventHasNotes(eventIdToNoteIds[id]),
|
||||
eventId: id,
|
||||
onPinEvent,
|
||||
onUnPinEvent,
|
||||
isEventPinned,
|
||||
})}
|
||||
showCheckboxes={timelineTypeContext.showCheckboxes}
|
||||
showNotes={showNotes}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
updateNote={updateNote}
|
||||
/>
|
||||
|
||||
<DataDrivenColumns
|
||||
_id={id}
|
||||
columnHeaders={columnHeaders}
|
||||
columnRenderers={columnRenderers}
|
||||
data={data}
|
||||
onColumnResized={onColumnResized}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</EventsTrData>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.id === nextProps.id &&
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
getColumnWidthFromType,
|
||||
getPinTooltip,
|
||||
stringifyEvent,
|
||||
SHOW_CHECK_BOXES_COLUMN_WIDTH,
|
||||
} from './helpers';
|
||||
|
||||
describe('helpers', () => {
|
||||
|
@ -258,8 +259,20 @@ describe('helpers', () => {
|
|||
expect(getActionsColumnWidth(false)).toEqual(DEFAULT_ACTIONS_COLUMN_WIDTH);
|
||||
});
|
||||
|
||||
test('returns the default actions column width + checkbox width when isEventViewer is false and showCheckboxes is true', () => {
|
||||
expect(getActionsColumnWidth(false, true)).toEqual(
|
||||
DEFAULT_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH
|
||||
);
|
||||
});
|
||||
|
||||
test('returns the events viewer actions column width when isEventViewer is true', () => {
|
||||
expect(getActionsColumnWidth(true)).toEqual(EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH);
|
||||
});
|
||||
|
||||
test('returns the events viewer actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => {
|
||||
expect(getActionsColumnWidth(true, true)).toEqual(
|
||||
EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,8 @@ export const DEFAULT_ACTIONS_COLUMN_WIDTH = 115; // px;
|
|||
* an events viewer, which has fewer actions than a regular events viewer
|
||||
*/
|
||||
export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = 32; // px;
|
||||
/** Additional column width to include when checkboxes are shown **/
|
||||
export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 32; // px;
|
||||
/** The default minimum width of a column (when a width for the column type is not specified) */
|
||||
export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px
|
||||
/** The default minimum width of a column of type `date` */
|
||||
|
@ -93,5 +95,6 @@ export const getColumnHeaders = (
|
|||
};
|
||||
|
||||
/** Returns the (fixed) width of the Actions column */
|
||||
export const getActionsColumnWidth = (isEventViewer: boolean): number =>
|
||||
isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH;
|
||||
export const getActionsColumnWidth = (isEventViewer: boolean, showCheckboxes = false): number =>
|
||||
(showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0) +
|
||||
(isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH);
|
||||
|
|
|
@ -27,6 +27,7 @@ import { getActionsColumnWidth } from './helpers';
|
|||
import { ColumnRenderer } from './renderers/column_renderer';
|
||||
import { RowRenderer } from './renderers/row_renderer';
|
||||
import { Sort } from './sort';
|
||||
import { useTimelineTypeContext } from '../timeline_context';
|
||||
|
||||
export interface BodyProps {
|
||||
addNoteToEvent: AddNoteToEvent;
|
||||
|
@ -80,9 +81,11 @@ export const Body = React.memo<BodyProps>(
|
|||
toggleColumn,
|
||||
updateNote,
|
||||
}) => {
|
||||
const timelineTypeContext = useTimelineTypeContext();
|
||||
|
||||
const columnWidths = columnHeaders.reduce(
|
||||
(totalWidth, header) => totalWidth + header.width,
|
||||
getActionsColumnWidth(isEventViewer)
|
||||
getActionsColumnWidth(isEventViewer, timelineTypeContext.showCheckboxes)
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -94,7 +97,10 @@ export const Body = React.memo<BodyProps>(
|
|||
style={{ minWidth: columnWidths + 'px' }}
|
||||
>
|
||||
<ColumnHeaders
|
||||
actionsColumnWidth={getActionsColumnWidth(isEventViewer)}
|
||||
actionsColumnWidth={getActionsColumnWidth(
|
||||
isEventViewer,
|
||||
timelineTypeContext.showCheckboxes
|
||||
)}
|
||||
browserFields={browserFields}
|
||||
columnHeaders={columnHeaders}
|
||||
isEventViewer={isEventViewer}
|
||||
|
@ -110,7 +116,10 @@ export const Body = React.memo<BodyProps>(
|
|||
/>
|
||||
|
||||
<Events
|
||||
actionsColumnWidth={getActionsColumnWidth(isEventViewer)}
|
||||
actionsColumnWidth={getActionsColumnWidth(
|
||||
isEventViewer,
|
||||
timelineTypeContext.showCheckboxes
|
||||
)}
|
||||
addNoteToEvent={addNoteToEvent}
|
||||
browserFields={browserFields}
|
||||
columnHeaders={columnHeaders}
|
||||
|
|
|
@ -30,7 +30,9 @@ import { Body } from './index';
|
|||
import { columnRenderers, rowRenderers } from './renderers';
|
||||
import { Sort } from './sort';
|
||||
import { timelineActions, appActions } from '../../../store/actions';
|
||||
import { TimelineModel } from '../../../store/timeline/model';
|
||||
import { timelineDefaults, TimelineModel } from '../../../store/timeline/model';
|
||||
import { plainRowRenderer } from './renderers/plain_row_renderer';
|
||||
import { useTimelineTypeContext } from '../timeline_context';
|
||||
|
||||
interface OwnProps {
|
||||
browserFields: BrowserFields;
|
||||
|
@ -107,6 +109,8 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
|
|||
updateNote,
|
||||
updateSort,
|
||||
}) => {
|
||||
const timelineTypeContext = useTimelineTypeContext();
|
||||
|
||||
const getNotesByIds = useCallback(
|
||||
(noteIds: string[]): Note[] => appSelectors.getNotes(notesById, noteIds),
|
||||
[notesById]
|
||||
|
@ -167,7 +171,7 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
|
|||
onUpdateColumns={onUpdateColumns}
|
||||
pinnedEventIds={pinnedEventIds}
|
||||
range={range!}
|
||||
rowRenderers={rowRenderers}
|
||||
rowRenderers={timelineTypeContext.showRowRenderers ? rowRenderers : [plainRowRenderer]}
|
||||
sort={sort}
|
||||
toggleColumn={toggleColumn}
|
||||
updateNote={onUpdateNote}
|
||||
|
@ -202,7 +206,7 @@ const makeMapStateToProps = () => {
|
|||
const getTimeline = timelineSelectors.getTimelineByIdSelector();
|
||||
const getNotesByIds = appSelectors.notesByIdsSelector();
|
||||
const mapStateToProps = (state: State, { browserFields, id }: OwnProps) => {
|
||||
const timeline: TimelineModel = getTimeline(state, id);
|
||||
const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults;
|
||||
const { columns, eventIdToNoteIds, pinnedEventIds } = timeline;
|
||||
|
||||
return {
|
||||
|
|
|
@ -27,6 +27,7 @@ import { OnChangeItemsPerPage, OnLoadMore } from '../events';
|
|||
|
||||
import { LastUpdatedAt } from './last_updated';
|
||||
import * as i18n from './translations';
|
||||
import { useTimelineTypeContext } from '../timeline_context';
|
||||
|
||||
const FixedWidthLastUpdated = styled.div<{ compact: boolean }>`
|
||||
width: ${({ compact }) => (!compact ? 200 : 25)}px;
|
||||
|
@ -110,43 +111,49 @@ export const EventsCountComponent = ({
|
|||
itemsCount: number;
|
||||
onClick: () => void;
|
||||
serverSideEventCount: number;
|
||||
}) => (
|
||||
<h5>
|
||||
<PopoverRowItems
|
||||
className="footer-popover"
|
||||
id="customizablePagination"
|
||||
data-test-subj="timelineSizeRowPopover"
|
||||
button={
|
||||
<>
|
||||
<EuiBadge data-test-subj="local-events-count" color="hollow">
|
||||
{itemsCount}
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
color="text"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={onClick}
|
||||
/>
|
||||
</EuiBadge>
|
||||
{` ${i18n.OF} `}
|
||||
</>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel items={items} data-test-subj="timelinePickSizeRow" />
|
||||
</PopoverRowItems>
|
||||
<EuiToolTip content={`${serverSideEventCount} ${i18n.TOTAL_COUNT_OF_EVENTS}`}>
|
||||
<ServerSideEventCount>
|
||||
<EuiBadge color="hollow" data-test-subj="server-side-event-count">
|
||||
{serverSideEventCount}
|
||||
</EuiBadge>{' '}
|
||||
{i18n.EVENTS}
|
||||
</ServerSideEventCount>
|
||||
</EuiToolTip>
|
||||
</h5>
|
||||
);
|
||||
}) => {
|
||||
const timelineTypeContext = useTimelineTypeContext();
|
||||
return (
|
||||
<h5>
|
||||
<PopoverRowItems
|
||||
className="footer-popover"
|
||||
id="customizablePagination"
|
||||
data-test-subj="timelineSizeRowPopover"
|
||||
button={
|
||||
<>
|
||||
<EuiBadge data-test-subj="local-events-count" color="hollow">
|
||||
{itemsCount}
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
color="text"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={onClick}
|
||||
/>
|
||||
</EuiBadge>
|
||||
{` ${i18n.OF} `}
|
||||
</>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel items={items} data-test-subj="timelinePickSizeRow" />
|
||||
</PopoverRowItems>
|
||||
<EuiToolTip
|
||||
content={`${serverSideEventCount} ${timelineTypeContext.footerText ??
|
||||
i18n.TOTAL_COUNT_OF_EVENTS}`}
|
||||
>
|
||||
<ServerSideEventCount>
|
||||
<EuiBadge color="hollow" data-test-subj="server-side-event-count">
|
||||
{serverSideEventCount}
|
||||
</EuiBadge>{' '}
|
||||
{timelineTypeContext.documentType ?? i18n.EVENTS}
|
||||
</ServerSideEventCount>
|
||||
</EuiToolTip>
|
||||
</h5>
|
||||
);
|
||||
};
|
||||
|
||||
EventsCountComponent.displayName = 'EventsCountComponent';
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { esFilters } from '../../../../../../../src/plugins/data/public';
|
|||
import { WithSource } from '../../containers/source';
|
||||
import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store';
|
||||
import { timelineActions } from '../../store/actions';
|
||||
import { KqlMode, TimelineModel } from '../../store/timeline/model';
|
||||
import { KqlMode, timelineDefaults, TimelineModel } from '../../store/timeline/model';
|
||||
|
||||
import { ColumnHeader } from './body/column_headers/column_header';
|
||||
import { DataProvider, QueryOperator } from './data_providers/data_provider';
|
||||
|
@ -315,7 +315,7 @@ const makeMapStateToProps = () => {
|
|||
const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector();
|
||||
const getInputsTimeline = inputsSelectors.getTimelineSelector();
|
||||
const mapStateToProps = (state: State, { id }: OwnProps) => {
|
||||
const timeline: TimelineModel = getTimeline(state, id);
|
||||
const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults;
|
||||
const input: inputsModel.InputsRange = getInputsTimeline(state);
|
||||
const {
|
||||
columns,
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
inputsSelectors,
|
||||
} from '../../../store';
|
||||
import { timelineActions } from '../../../store/actions';
|
||||
import { KqlMode, TimelineModel } from '../../../store/timeline/model';
|
||||
import { KqlMode, timelineDefaults, TimelineModel } from '../../../store/timeline/model';
|
||||
import { DispatchUpdateReduxTime, dispatchUpdateReduxTime } from '../../super_date_picker';
|
||||
import { DataProvider } from '../data_providers/data_provider';
|
||||
import { SearchOrFilter } from './search_or_filter';
|
||||
|
@ -195,7 +195,7 @@ const makeMapStateToProps = () => {
|
|||
const getInputsTimeline = inputsSelectors.getTimelineSelector();
|
||||
const getInputsPolicy = inputsSelectors.getTimelinePolicySelector();
|
||||
const mapStateToProps = (state: State, { timelineId }: OwnProps) => {
|
||||
const timeline: TimelineModel = getTimeline(state, timelineId);
|
||||
const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults;
|
||||
const input: inputsModel.InputsRange = getInputsTimeline(state);
|
||||
const policy: inputsModel.Policy = getInputsPolicy(state);
|
||||
return {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, useEffect, memo, useState } from 'react';
|
||||
import React, { createContext, memo, useContext, useEffect, useState } from 'react';
|
||||
|
||||
const initTimelineContext = false;
|
||||
export const TimelineContext = createContext<boolean>(initTimelineContext);
|
||||
|
@ -14,30 +14,55 @@ const initTimelineWidth = 0;
|
|||
export const TimelineWidthContext = createContext<number>(initTimelineWidth);
|
||||
export const useTimelineWidthContext = () => useContext(TimelineWidthContext);
|
||||
|
||||
export interface TimelineTypeContextProps {
|
||||
documentType?: string;
|
||||
footerText?: string;
|
||||
showCheckboxes: boolean;
|
||||
showRowRenderers: boolean;
|
||||
title?: string;
|
||||
}
|
||||
const initTimelineType: TimelineTypeContextProps = {
|
||||
documentType: undefined,
|
||||
footerText: undefined,
|
||||
showCheckboxes: false,
|
||||
showRowRenderers: true,
|
||||
title: undefined,
|
||||
};
|
||||
export const TimelineTypeContext = createContext<TimelineTypeContextProps>(initTimelineType);
|
||||
export const useTimelineTypeContext = () => useContext(TimelineTypeContext);
|
||||
|
||||
interface ManageTimelineContextProps {
|
||||
children: React.ReactNode;
|
||||
loading: boolean;
|
||||
width: number;
|
||||
type?: TimelineTypeContextProps;
|
||||
}
|
||||
|
||||
// todo we need to refactor this as more complex context/reducer with useReducer
|
||||
// to avoid so many Context, at least the separation of code is there now
|
||||
export const ManageTimelineContext = memo<ManageTimelineContextProps>(
|
||||
({ children, loading, width }) => {
|
||||
({ children, loading, width, type = initTimelineType }) => {
|
||||
const [myLoading, setLoading] = useState(initTimelineContext);
|
||||
const [myWidth, setWidth] = useState(initTimelineWidth);
|
||||
const [myType, setType] = useState(initTimelineType);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(loading);
|
||||
}, [loading]);
|
||||
|
||||
useEffect(() => {
|
||||
setType(type);
|
||||
}, [type]);
|
||||
|
||||
useEffect(() => {
|
||||
setWidth(width);
|
||||
}, [width]);
|
||||
|
||||
return (
|
||||
<TimelineContext.Provider value={myLoading}>
|
||||
<TimelineWidthContext.Provider value={myWidth}>{children}</TimelineWidthContext.Provider>
|
||||
<TimelineWidthContext.Provider value={myWidth}>
|
||||
<TimelineTypeContext.Provider value={myType}>{children}</TimelineTypeContext.Provider>
|
||||
</TimelineWidthContext.Provider>
|
||||
</TimelineContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,15 +16,17 @@ import {
|
|||
Rule,
|
||||
} from './types';
|
||||
import { throwIfNotOk } from '../../../hooks/api/api';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants';
|
||||
|
||||
/**
|
||||
* Add provided Rule
|
||||
*
|
||||
* @param rule to add
|
||||
* @param kbnVersion current Kibana Version to use for headers
|
||||
* @param signal to cancel request
|
||||
*/
|
||||
export const addRule = async ({ rule, kbnVersion, signal }: AddRulesProps): Promise<NewRule> => {
|
||||
const response = await fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, {
|
||||
const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
|
@ -47,6 +49,7 @@ export const addRule = async ({ rule, kbnVersion, signal }: AddRulesProps): Prom
|
|||
* @param pagination desired pagination options (e.g. page/perPage)
|
||||
* @param id if specified, will return specific rule if exists
|
||||
* @param kbnVersion current Kibana Version to use for headers
|
||||
* @param signal to cancel request
|
||||
*/
|
||||
export const fetchRules = async ({
|
||||
filterOptions = {
|
||||
|
@ -75,8 +78,8 @@ export const fetchRules = async ({
|
|||
|
||||
const endpoint =
|
||||
id != null
|
||||
? `${chrome.getBasePath()}/api/detection_engine/rules?id="${id}"`
|
||||
: `${chrome.getBasePath()}/api/detection_engine/rules/_find?${queryParams.join('&')}`;
|
||||
? `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id="${id}"`
|
||||
: `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_find?${queryParams.join('&')}`;
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
|
@ -106,7 +109,7 @@ export const enableRules = async ({
|
|||
kbnVersion,
|
||||
}: EnableRulesProps): Promise<Rule[]> => {
|
||||
const requests = ids.map(id =>
|
||||
fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, {
|
||||
fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, {
|
||||
method: 'PUT',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
|
@ -134,7 +137,7 @@ export const enableRules = async ({
|
|||
export const deleteRules = async ({ ids, kbnVersion }: DeleteRulesProps): Promise<Rule[]> => {
|
||||
// TODO: Don't delete if immutable!
|
||||
const requests = ids.map(id =>
|
||||
fetch(`${chrome.getBasePath()}/api/detection_engine/rules?id=${id}`, {
|
||||
fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id=${id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
|
@ -163,7 +166,7 @@ export const duplicateRules = async ({
|
|||
kbnVersion,
|
||||
}: DuplicateRulesProps): Promise<Rule[]> => {
|
||||
const requests = rules.map(rule =>
|
||||
fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, {
|
||||
fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
|
|
|
@ -8,7 +8,12 @@ import { isEmpty, get } from 'lodash/fp';
|
|||
import { useEffect, useState, Dispatch, SetStateAction } from 'react';
|
||||
import { IIndexPattern } from 'src/plugins/data/public';
|
||||
|
||||
import { getIndexFields, sourceQuery } from '../../../containers/source';
|
||||
import {
|
||||
BrowserFields,
|
||||
getBrowserFields,
|
||||
getIndexFields,
|
||||
sourceQuery,
|
||||
} from '../../../containers/source';
|
||||
import { useStateToaster } from '../../../components/toasters';
|
||||
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
|
||||
import { SourceQuery } from '../../../graphql/types';
|
||||
|
@ -16,20 +21,22 @@ import { useApolloClient } from '../../../utils/apollo_context';
|
|||
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface FetchIndexPattern {
|
||||
interface FetchIndexPatternReturn {
|
||||
browserFields: BrowserFields | null;
|
||||
isLoading: boolean;
|
||||
indices: string[];
|
||||
indicesExists: boolean;
|
||||
indexPatterns: IIndexPattern | null;
|
||||
}
|
||||
|
||||
type Return = [FetchIndexPattern, Dispatch<SetStateAction<string[]>>];
|
||||
type Return = [FetchIndexPatternReturn, Dispatch<SetStateAction<string[]>>];
|
||||
|
||||
export const useFetchIndexPatterns = (): Return => {
|
||||
export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => {
|
||||
const apolloClient = useApolloClient();
|
||||
const [indices, setIndices] = useState<string[]>([]);
|
||||
const [indices, setIndices] = useState<string[]>(defaultIndices);
|
||||
const [indicesExists, setIndicesExists] = useState(false);
|
||||
const [indexPatterns, setIndexPatterns] = useState<IIndexPattern | null>(null);
|
||||
const [browserFields, setBrowserFields] = useState<BrowserFields | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
|
@ -62,6 +69,7 @@ export const useFetchIndexPatterns = (): Return => {
|
|||
setIndexPatterns(
|
||||
getIndexFields(indices.join(), get('data.source.status.indexFields', result))
|
||||
);
|
||||
setBrowserFields(getBrowserFields(get('data.source.status.indexFields', result)));
|
||||
}
|
||||
},
|
||||
error => {
|
||||
|
@ -80,5 +88,5 @@ export const useFetchIndexPatterns = (): Return => {
|
|||
};
|
||||
}, [indices]);
|
||||
|
||||
return [{ isLoading, indices, indicesExists, indexPatterns }, setIndices];
|
||||
return [{ browserFields, isLoading, indices, indicesExists, indexPatterns }, setIndices];
|
||||
};
|
||||
|
|
|
@ -70,7 +70,7 @@ export const getIndexFields = memoizeOne(
|
|||
: { fields: [], title }
|
||||
);
|
||||
|
||||
const getBrowserFields = memoizeOne(
|
||||
export const getBrowserFields = memoizeOne(
|
||||
(fields: IndexField[]): BrowserFields =>
|
||||
fields && fields.length > 0
|
||||
? fields.reduce<BrowserFields>(
|
||||
|
|
|
@ -19,11 +19,12 @@ import {
|
|||
TimelineEdges,
|
||||
TimelineItem,
|
||||
} from '../../graphql/types';
|
||||
import { inputsModel, State, inputsSelectors } from '../../store';
|
||||
import { inputsModel, inputsSelectors, State } from '../../store';
|
||||
import { createFilter } from '../helpers';
|
||||
import { QueryTemplate, QueryTemplateProps } from '../query_template';
|
||||
|
||||
import { timelineQuery } from './index.gql_query';
|
||||
import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns';
|
||||
|
||||
export interface TimelineArgs {
|
||||
events: TimelineItem[];
|
||||
|
@ -44,6 +45,7 @@ export interface TimelineQueryReduxProps {
|
|||
export interface OwnProps extends QueryTemplateProps {
|
||||
children?: (args: TimelineArgs) => React.ReactNode;
|
||||
id: string;
|
||||
indexPattern?: IIndexPattern;
|
||||
limit: number;
|
||||
sortField: SortField;
|
||||
fields: string[];
|
||||
|
@ -67,6 +69,7 @@ class TimelineQueryComponent extends QueryTemplate<
|
|||
const {
|
||||
children,
|
||||
id,
|
||||
indexPattern,
|
||||
isInspected,
|
||||
limit,
|
||||
fields,
|
||||
|
@ -80,7 +83,8 @@ class TimelineQueryComponent extends QueryTemplate<
|
|||
sourceId,
|
||||
pagination: { limit, cursor: null, tiebreaker: null },
|
||||
sortField,
|
||||
defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY),
|
||||
defaultIndex:
|
||||
indexPattern?.title.split(',') ?? chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY),
|
||||
inspect: isInspected,
|
||||
};
|
||||
return (
|
||||
|
|
|
@ -4,15 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiPanel,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import { EuiButton, EuiPanel, EuiSelect, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { StickyContainer } from 'react-sticky';
|
||||
|
||||
import { FiltersGlobal } from '../../components/filters_global';
|
||||
|
@ -20,100 +13,12 @@ import { HeaderPage } from '../../components/header_page';
|
|||
import { HeaderSection } from '../../components/header_section';
|
||||
import { HistogramSignals } from '../../components/page/detection_engine/histogram_signals';
|
||||
import { SiemSearchBar } from '../../components/search_bar';
|
||||
import {
|
||||
UtilityBar,
|
||||
UtilityBarAction,
|
||||
UtilityBarGroup,
|
||||
UtilityBarSection,
|
||||
UtilityBarText,
|
||||
} from '../../components/detection_engine/utility_bar';
|
||||
import { WrapperPage } from '../../components/wrapper_page';
|
||||
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
|
||||
import { SpyRoute } from '../../utils/route/spy_routes';
|
||||
import { DetectionEngineEmptyPage } from './detection_engine_empty_page';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const OpenSignals = React.memo(() => {
|
||||
return (
|
||||
<>
|
||||
<UtilityBar>
|
||||
<UtilityBarSection>
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarText>{`${i18n.PANEL_SUBTITLE_SHOWING}: 7,712 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>
|
||||
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarAction iconType="cross">{'Clear 7 filters'}</UtilityBarAction>
|
||||
|
||||
<UtilityBarAction iconType="cross">{'Clear aggregation'}</UtilityBarAction>
|
||||
</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: Open signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const ClosedSignals = React.memo(() => {
|
||||
return (
|
||||
<>
|
||||
<UtilityBar>
|
||||
<UtilityBarSection>
|
||||
<UtilityBarGroup>
|
||||
<UtilityBarText>{`${i18n.PANEL_SUBTITLE_SHOWING}: 7,712 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. */}
|
||||
</>
|
||||
);
|
||||
});
|
||||
import { SignalsTable } from './signals';
|
||||
|
||||
export const DetectionEngineComponent = React.memo(() => {
|
||||
const sampleChartOptions = [
|
||||
|
@ -129,9 +34,6 @@ export const DetectionEngineComponent = React.memo(() => {
|
|||
{ text: 'Top users', value: 'users' },
|
||||
];
|
||||
|
||||
const filterGroupOptions = ['open', 'closed'];
|
||||
const [filterGroupState, setFilterGroupState] = useState(filterGroupOptions[0]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WithSource sourceId="default">
|
||||
|
@ -164,28 +66,7 @@ export const DetectionEngineComponent = React.memo(() => {
|
|||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiPanel>
|
||||
<HeaderSection title="All signals">
|
||||
<EuiFilterGroup>
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={filterGroupState === filterGroupOptions[0]}
|
||||
onClick={() => setFilterGroupState(filterGroupOptions[0])}
|
||||
withNext
|
||||
>
|
||||
{'Open signals'}
|
||||
</EuiFilterButton>
|
||||
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={filterGroupState === filterGroupOptions[1]}
|
||||
onClick={() => setFilterGroupState(filterGroupOptions[1])}
|
||||
>
|
||||
{'Closed signals'}
|
||||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
</HeaderSection>
|
||||
|
||||
{filterGroupState === filterGroupOptions[0] ? <OpenSignals /> : <ClosedSignals />}
|
||||
</EuiPanel>
|
||||
<SignalsTable />
|
||||
</WrapperPage>
|
||||
</StickyContainer>
|
||||
) : (
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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. */}
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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,79 @@
|
|||
/*
|
||||
* 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 { ColumnHeader } from '../../../components/timeline/body/column_headers/column_header';
|
||||
import { defaultColumnHeaderType } from '../../../components/timeline/body/column_headers/default_headers';
|
||||
import {
|
||||
DEFAULT_COLUMN_MIN_WIDTH,
|
||||
DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
} from '../../../components/timeline/body/helpers';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const signalsHeaders: ColumnHeader[] = [
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'signal.rule.name',
|
||||
label: i18n.SIGNALS_HEADERS_RULE,
|
||||
width: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'signal.rule.type',
|
||||
label: i18n.SIGNALS_HEADERS_METHOD,
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'signal.rule.severity',
|
||||
label: i18n.SIGNALS_HEADERS_SEVERITY,
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'signal.rule.risk_score',
|
||||
label: i18n.SIGNALS_HEADERS_RISK_SCORE,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
category: 'event',
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'event.action',
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'event.category',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'host.name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'user.name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'source.ip',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'destination.ip',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: '@timestamp',
|
||||
width: DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { signalsHeaders } from './default_headers';
|
||||
import { SubsetTimelineModel, timelineDefaults } from '../../../store/timeline/model';
|
||||
|
||||
export const signalsDefaultModel: SubsetTimelineModel = {
|
||||
...timelineDefaults,
|
||||
columns: signalsHeaders,
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { OpenSignals } from './components/open_signals';
|
||||
import { ClosedSignals } from './components/closed_signals';
|
||||
import { GlobalTime } from '../../../containers/global_time';
|
||||
import { StatefulEventsViewer } from '../../../components/events_viewer';
|
||||
import * as i18n from './translations';
|
||||
import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants';
|
||||
import { signalsDefaultModel } from './default_model';
|
||||
|
||||
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);
|
||||
|
||||
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)}
|
||||
>
|
||||
{'Closed signals'}
|
||||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const SignalsTable = React.memo(() => {
|
||||
const [filterGroup, setFilterGroup] = useState(FILTER_OPEN);
|
||||
|
||||
const onFilterGroupChangedCallback = useCallback(
|
||||
(newFilterGroup: string) => {
|
||||
setFilterGroup(newFilterGroup);
|
||||
},
|
||||
[setFilterGroup]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalTime>
|
||||
{({ to, from, setQuery, deleteQuery, isInitializing }) => (
|
||||
<StatefulEventsViewer
|
||||
defaultIndices={[DEFAULT_SIGNALS_INDEX]}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
SignalsTable.displayName = 'SignalsTable';
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 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',
|
||||
});
|
||||
|
||||
export const SIGNALS_DOCUMENT_TYPE = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.documentTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Signals',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOTAL_COUNT_OF_SIGNALS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.signals.totalCountOfSignalsTitle',
|
||||
{
|
||||
defaultMessage: 'signals match the search criteria',
|
||||
}
|
||||
);
|
||||
|
||||
export const SIGNALS_HEADERS_RULE = i18n.translate(
|
||||
'xpack.siem.eventsViewer.signals.defaultHeaders.ruleTitle',
|
||||
{
|
||||
defaultMessage: 'Rule',
|
||||
}
|
||||
);
|
||||
|
||||
export const SIGNALS_HEADERS_METHOD = i18n.translate(
|
||||
'xpack.siem.eventsViewer.signals.defaultHeaders.methodTitle',
|
||||
{
|
||||
defaultMessage: 'Method',
|
||||
}
|
||||
);
|
||||
|
||||
export const SIGNALS_HEADERS_SEVERITY = i18n.translate(
|
||||
'xpack.siem.eventsViewer.signals.defaultHeaders.severityTitle',
|
||||
{
|
||||
defaultMessage: 'Severity',
|
||||
}
|
||||
);
|
||||
|
||||
export const SIGNALS_HEADERS_RISK_SCORE = i18n.translate(
|
||||
'xpack.siem.eventsViewer.signals.defaultHeaders.riskScoreTitle',
|
||||
{
|
||||
defaultMessage: 'Risk Score',
|
||||
}
|
||||
);
|
|
@ -12,6 +12,7 @@ import { manageQuery } from '../../../components/page/manage_query';
|
|||
import { EventsOverTimeHistogram } from '../../../components/page/hosts/events_over_time';
|
||||
import { EventsOverTimeQuery } from '../../../containers/events/events_over_time';
|
||||
import { hostsModel } from '../../../store/hosts';
|
||||
import { eventsDefaultModel } from '../../../components/events_viewer/default_model';
|
||||
|
||||
const HOSTS_PAGE_TIMELINE_ID = 'hosts-page';
|
||||
const EventsOverTimeManage = manageQuery(EventsOverTimeHistogram);
|
||||
|
@ -48,7 +49,12 @@ export const EventsQueryTabBody = ({
|
|||
)}
|
||||
</EventsOverTimeQuery>
|
||||
<EuiSpacer size="l" />
|
||||
<StatefulEventsViewer end={endDate} id={HOSTS_PAGE_TIMELINE_ID} start={startDate} />
|
||||
<StatefulEventsViewer
|
||||
defaultModel={eventsDefaultModel}
|
||||
end={endDate}
|
||||
id={HOSTS_PAGE_TIMELINE_ID}
|
||||
start={startDate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,7 +9,6 @@ import { ColumnHeader } from '../../components/timeline/body/column_headers/colu
|
|||
import { DataProvider } from '../../components/timeline/data_providers/data_provider';
|
||||
import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/helpers';
|
||||
import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers';
|
||||
import { defaultHeaders as eventsDefaultHeaders } from '../../components/events_viewer/default_headers';
|
||||
import { Sort } from '../../components/timeline/body/sort';
|
||||
import { Direction, PinnedEvent } from '../../graphql/types';
|
||||
import { KueryFilterQuery, SerializedFilterQuery } from '../model';
|
||||
|
@ -74,34 +73,37 @@ export interface TimelineModel {
|
|||
version: string | null;
|
||||
}
|
||||
|
||||
export const timelineDefaults: Readonly<Pick<
|
||||
TimelineModel,
|
||||
| 'columns'
|
||||
| 'dataProviders'
|
||||
| 'description'
|
||||
| 'eventIdToNoteIds'
|
||||
| 'filters'
|
||||
| 'highlightedDropAndProviderId'
|
||||
| 'historyIds'
|
||||
| 'isFavorite'
|
||||
| 'isLive'
|
||||
| 'itemsPerPage'
|
||||
| 'itemsPerPageOptions'
|
||||
| 'kqlMode'
|
||||
| 'kqlQuery'
|
||||
| 'title'
|
||||
| 'noteIds'
|
||||
| 'pinnedEventIds'
|
||||
| 'pinnedEventsSaveObject'
|
||||
| 'dateRange'
|
||||
| 'show'
|
||||
| 'sort'
|
||||
| 'width'
|
||||
| 'isSaving'
|
||||
| 'isLoading'
|
||||
| 'savedObjectId'
|
||||
| 'version'
|
||||
>> = {
|
||||
export type SubsetTimelineModel = Readonly<
|
||||
Pick<
|
||||
TimelineModel,
|
||||
| 'columns'
|
||||
| 'dataProviders'
|
||||
| 'description'
|
||||
| 'eventIdToNoteIds'
|
||||
| 'highlightedDropAndProviderId'
|
||||
| 'historyIds'
|
||||
| 'isFavorite'
|
||||
| 'isLive'
|
||||
| 'itemsPerPage'
|
||||
| 'itemsPerPageOptions'
|
||||
| 'kqlMode'
|
||||
| 'kqlQuery'
|
||||
| 'title'
|
||||
| 'noteIds'
|
||||
| 'pinnedEventIds'
|
||||
| 'pinnedEventsSaveObject'
|
||||
| 'dateRange'
|
||||
| 'show'
|
||||
| 'sort'
|
||||
| 'width'
|
||||
| 'isSaving'
|
||||
| 'isLoading'
|
||||
| 'savedObjectId'
|
||||
| 'version'
|
||||
>
|
||||
>;
|
||||
|
||||
export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filters'> = {
|
||||
columns: defaultHeaders,
|
||||
dataProviders: [],
|
||||
description: '',
|
||||
|
@ -137,31 +139,3 @@ export const timelineDefaults: Readonly<Pick<
|
|||
width: DEFAULT_TIMELINE_WIDTH,
|
||||
version: null,
|
||||
};
|
||||
|
||||
export const eventsDefaults: Readonly<Pick<
|
||||
TimelineModel,
|
||||
| 'columns'
|
||||
| 'dataProviders'
|
||||
| 'description'
|
||||
| 'eventIdToNoteIds'
|
||||
| 'highlightedDropAndProviderId'
|
||||
| 'historyIds'
|
||||
| 'isFavorite'
|
||||
| 'isLive'
|
||||
| 'itemsPerPage'
|
||||
| 'itemsPerPageOptions'
|
||||
| 'kqlMode'
|
||||
| 'kqlQuery'
|
||||
| 'title'
|
||||
| 'noteIds'
|
||||
| 'pinnedEventIds'
|
||||
| 'pinnedEventsSaveObject'
|
||||
| 'dateRange'
|
||||
| 'show'
|
||||
| 'sort'
|
||||
| 'width'
|
||||
| 'isSaving'
|
||||
| 'isLoading'
|
||||
| 'savedObjectId'
|
||||
| 'version'
|
||||
>> = { ...timelineDefaults, columns: eventsDefaultHeaders };
|
||||
|
|
|
@ -9,8 +9,8 @@ import { createSelector } from 'reselect';
|
|||
import { isFromKueryExpressionValid } from '../../lib/keury';
|
||||
import { State } from '../reducer';
|
||||
|
||||
import { eventsDefaults, timelineDefaults, TimelineModel } from './model';
|
||||
import { TimelineById, AutoSavedWarningMsg } from './types';
|
||||
import { TimelineModel } from './model';
|
||||
import { AutoSavedWarningMsg, TimelineById } from './types';
|
||||
|
||||
const selectTimelineById = (state: State): TimelineById => state.timeline.timelineById;
|
||||
|
||||
|
@ -37,11 +37,9 @@ export const getShowCallOutUnauthorizedMsg = () =>
|
|||
|
||||
export const getTimelines = () => timelineByIdSelector;
|
||||
|
||||
export const getTimelineByIdSelector = () =>
|
||||
createSelector(selectTimeline, timeline => timeline || timelineDefaults);
|
||||
export const getTimelineByIdSelector = () => createSelector(selectTimeline, timeline => timeline);
|
||||
|
||||
export const getEventsByIdSelector = () =>
|
||||
createSelector(selectTimeline, timeline => timeline || eventsDefaults);
|
||||
export const getEventsByIdSelector = () => createSelector(selectTimeline, timeline => timeline);
|
||||
|
||||
export const getKqlFilterQuerySelector = () =>
|
||||
createSelector(selectTimeline, timeline =>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue