[Security Solution] remove analyzer as overlay (#220597)

## Summary

This PR is a follow up of [this previous
one](https://github.com/elastic/kibana/pull/220590) that removed the
advanced setting allowing users to switch between the Session View
component rendered in the expandable flyout and as an overlay to the
alerts table.

After removing related to the [Session View displayed as
overlay](https://github.com/elastic/kibana/pull/220596), this PR removes
all code related to the Analyzer Graph component when displayed as an
overlay. This is applied to the Alerts page alerts table and to
Timeline.

**_As [this previous PR](https://github.com/elastic/kibana/pull/220590)
had removed the ability to switch back to this overlay mode, this PR
does not introduces any changes visible in the UI. If anything looks
different or behaves differently, then there is an issue and this PR
should not be merged._**

Analyzer Graph in expandable flyout remains unchanged:


https://github.com/user-attachments/assets/eb110ecc-d857-475a-a9e3-1bd0ff4fa5ef

### Notes

The timeline adds a `graphEventId` parameter to the url. This was used
to reopen the timeline to the Analyzer tab. This functionality was
broken a few months back when we enabled the advanced settings by
default. This functionality is completely removed in this PR, as we now
do not have the `graphEvenId` url parameter anymore. This was run by the
team or product and we're ok with it.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Philippe Oberti 2025-06-05 09:46:59 -05:00 committed by GitHub
parent b974589671
commit 77fe98dcef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 173 additions and 1509 deletions

View file

@ -666,7 +666,7 @@
"string | undefined"
],
"path": "x-pack/solutions/security/packages/data-table/store/data_table/model.ts",
"deprecated": false,
"deprecated": true,
"trackAdoption": false
},
{
@ -1245,7 +1245,7 @@
},
" | undefined; type?: string | undefined; })[]; readonly additionalFilters: Record<",
"AlertPageFilterType",
", boolean>; readonly itemsPerPage: number; readonly queryFields: string[]; readonly selectAll: boolean; readonly showCheckboxes: boolean; readonly deletedEventIds: string[]; readonly graphEventId?: string | undefined; readonly indexNames: string[]; readonly isSelectAllChecked: boolean; readonly itemsPerPageOptions: number[]; readonly loadingEventIds: string[]; readonly selectedEventIds: Record<string, ",
", boolean>; readonly itemsPerPage: number; readonly queryFields: string[]; readonly selectAll: boolean; readonly showCheckboxes: boolean; readonly deletedEventIds: string[]; readonly indexNames: string[]; readonly isSelectAllChecked: boolean; readonly itemsPerPageOptions: number[]; readonly loadingEventIds: string[]; readonly selectedEventIds: Record<string, ",
{
"pluginId": "timelines",
"scope": "common",
@ -1526,7 +1526,7 @@
"label": "graphEventId",
"description": [],
"path": "x-pack/solutions/security/packages/data-table/store/data_table/defaults.ts",
"deprecated": false,
"deprecated": true,
"trackAdoption": false
},
{

View file

@ -1527,7 +1527,7 @@
"string | undefined"
],
"path": "x-pack/solutions/security/plugins/security_solution/public/timelines/store/model.ts",
"deprecated": false,
"deprecated": true,
"trackAdoption": false
},
{

View file

@ -186,7 +186,6 @@ module.exports = {
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]common[\/\\]components[\/\\]events_viewer[\/\\]index.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]common[\/\\]components[\/\\]events_viewer[\/\\]summary_view_select[\/\\]index.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]common[\/\\]components[\/\\]field_selection[\/\\]index.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]common[\/\\]components[\/\\]filters_global[\/\\]filters_global.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]common[\/\\]components[\/\\]header_actions[\/\\]actions.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]common[\/\\]components[\/\\]health_truncate_text[\/\\]index.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]common[\/\\]components[\/\\]hover_visibility_container[\/\\]index.tsx/,
@ -386,7 +385,6 @@ module.exports = {
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]timelines[\/\\]components[\/\\]fields_browser[\/\\]create_field_button[\/\\]index.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]timelines[\/\\]components[\/\\]fields_browser[\/\\]field_table_columns[\/\\]index.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]timelines[\/\\]components[\/\\]formatted_duration[\/\\]tooltip[\/\\]index.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]timelines[\/\\]components[\/\\]graph_overlay[\/\\]index.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]timelines[\/\\]components[\/\\]ja3_fingerprint[\/\\]index.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]timelines[\/\\]components[\/\\]loading[\/\\]index.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]timelines[\/\\]components[\/\\]modal[\/\\]header[\/\\]index.tsx/,

View file

@ -41129,9 +41129,6 @@
"xpack.securitySolution.timeline.footer.loadingLabel": "Chargement",
"xpack.securitySolution.timeline.footer.of": "de",
"xpack.securitySolution.timeline.footer.rows": "lignes",
"xpack.securitySolution.timeline.graphOverlay.closeAnalyzerButton": "Fermer l'analyseur",
"xpack.securitySolution.timeline.graphOverlay.closeSessionButton": "Fermer le visualiseur de session",
"xpack.securitySolution.timeline.graphOverlay.fullScreenButton": "Plein écran",
"xpack.securitySolution.timeline.kpiFailDescription": "Une erreur s'est produite",
"xpack.securitySolution.timeline.kpiFailSearchDescription": "Échec de chargement des KPI",
"xpack.securitySolution.timeline.kpis.destinationKpiTitle": "IP de destination",

View file

@ -41093,9 +41093,6 @@
"xpack.securitySolution.timeline.footer.loadingLabel": "読み込み中",
"xpack.securitySolution.timeline.footer.of": "/",
"xpack.securitySolution.timeline.footer.rows": "行",
"xpack.securitySolution.timeline.graphOverlay.closeAnalyzerButton": "アナライザーを閉じる",
"xpack.securitySolution.timeline.graphOverlay.closeSessionButton": "セッションビューアーの終了",
"xpack.securitySolution.timeline.graphOverlay.fullScreenButton": "全画面",
"xpack.securitySolution.timeline.kpiFailDescription": "エラーが発生しました",
"xpack.securitySolution.timeline.kpiFailSearchDescription": "KPIの読み込みに失敗",
"xpack.securitySolution.timeline.kpis.destinationKpiTitle": "デスティネーション IP",

View file

@ -41170,9 +41170,6 @@
"xpack.securitySolution.timeline.footer.loadingLabel": "正在加载",
"xpack.securitySolution.timeline.footer.of": "/",
"xpack.securitySolution.timeline.footer.rows": "行",
"xpack.securitySolution.timeline.graphOverlay.closeAnalyzerButton": "关闭分析器",
"xpack.securitySolution.timeline.graphOverlay.closeSessionButton": "关闭会话查看器",
"xpack.securitySolution.timeline.graphOverlay.fullScreenButton": "全屏",
"xpack.securitySolution.timeline.kpiFailDescription": "发生错误",
"xpack.securitySolution.timeline.kpiFailSearchDescription": "无法加载 KPI",
"xpack.securitySolution.timeline.kpis.destinationKpiTitle": "目标 IP",

View file

@ -7,8 +7,9 @@
import type { FC, PropsWithChildren } from 'react';
import React, { useState } from 'react';
import type { EuiMarkdownEditorUiPlugin, EuiMarkdownAstNodePosition } from '@elastic/eui';
import type { EuiMarkdownAstNodePosition, EuiMarkdownEditorUiPlugin } from '@elastic/eui';
import type { Plugin } from 'unified';
/**
* @description - manage the plugins, hooks, and ui components needed to enable timeline functionality within the cases plugin
* @TODO - To better encapsulate the timeline logic needed by cases, we are managing it in this top level context.
@ -17,14 +18,14 @@ import type { Plugin } from 'unified';
// TODO: copied from 'use_insert_timeline' in security_solution till timeline moved into it's own plugin.
interface UseInsertTimelineReturn {
handleOnTimelineChange: (title: string, id: string | null, graphEventId?: string) => void;
handleOnTimelineChange: (title: string, id: string | null) => void;
}
interface TimelineProcessingPluginRendererProps {
id: string | null;
title: string;
graphEventId?: string;
type: 'timeline';
[key: string]: string | null | undefined;
}

View file

@ -1,19 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export enum FlowTargetSourceDest {
destination = 'destination',
source = 'source',
}
export enum TimelineTabs {
query = 'query',
graph = 'graph',
notes = 'notes',
pinned = 'pinned',
eql = 'eql',
}

View file

@ -10,9 +10,6 @@ export * from './header_actions';
export const FILTER_OPEN = 'open' as const;
export const FILTER_CLOSED = 'closed' as const;
export const FILTER_ACKNOWLEDGED = 'acknowledged' as const;
export type SetEventsLoading = (params: { eventIds: string[]; isLoading: boolean }) => void;
export type SetEventsDeleted = (params: { eventIds: string[]; isDeleted: boolean }) => void;
export { TimelineTabs } from './detail_panel';

View file

@ -19,14 +19,7 @@ export {
export type { TableState, DataTableState, TableById } from './store/data_table/types';
export type { DataTableModel, SubsetDataTableModel } from './store/data_table/model';
export {
Direction,
tableEntity,
FILTER_OPEN,
TimelineTabs,
TableId,
TableEntityType,
} from './common/types';
export { Direction, tableEntity, FILTER_OPEN, TableId, TableEntityType } from './common/types';
export type {
TableIdLiteral,
ViewSelection,

View file

@ -40,7 +40,6 @@ export const mockGlobalState = {
sortDirection: 'desc',
},
],
graphEventId: '',
selectAll: false,
id: TableId.test,
title: '',

View file

@ -101,10 +101,6 @@ export const setDataTableSelectAll = actionCreator<{ id: string; selectAll: bool
'SET_DATA_TABLE_SELECT_ALL'
);
export const updateGraphEventId = actionCreator<{ id: string; graphEventId: string }>(
'UPDATE_DATA_TABLE_GRAPH_EVENT_ID'
);
export const setTableUpdatedAt = actionCreator<{ id: string; updated: number }>(
'SET_TABLE_UPDATED_AT'
);

View file

@ -82,7 +82,6 @@ export const tableDefaults: SubsetDataTableModel = {
},
],
selectAll: false,
graphEventId: '',
columns: defaultHeaders,
queryFields: [],
title: '',
@ -104,5 +103,4 @@ export const getDataTableManageDefaults = (id: string) => ({
queryFields: [],
title: '',
unit: (n: number) => i18n.UNIT(n),
graphEventId: '',
});

View file

@ -434,23 +434,3 @@ export const setSelectedTableEvents = ({
},
};
};
export const updateTableGraphEventId = ({
id,
graphEventId,
tableById,
}: {
id: string;
graphEventId: string;
tableById: TableById;
}): TableById => {
const table = tableById[id];
return {
...tableById,
[id]: {
...table,
graphEventId,
},
};
};

View file

@ -39,8 +39,6 @@ export interface DataTableModel extends DataTableModelSettings {
/** Events to not be rendered **/
deletedEventIds: string[];
filters?: Filter[];
/** When non-empty, display a graph view for this event */
graphEventId?: string;
/** Uniquely identifies the data table */
id: string;
indexNames: string[];
@ -83,7 +81,6 @@ export type SubsetDataTableModel = Readonly<
| 'showCheckboxes'
| 'sort'
| 'selectedEventIds'
| 'graphEventId'
| 'queryFields'
| 'title'
| 'initialized'

View file

@ -24,7 +24,6 @@ import {
updateColumnOrder,
updateColumns,
updateColumnWidth,
updateGraphEventId,
updateIsLoading,
updateItemsPerPage,
updateItemsPerPageOptions,
@ -46,7 +45,6 @@ import {
updateDataTableColumnOrder,
updateDataTableColumnWidth,
updateTableColumns,
updateTableGraphEventId,
updateTableItemsPerPage,
updateTablePerPageOptions,
updateTableSort,
@ -224,10 +222,6 @@ export const dataTableReducer = reducerWithInitialState(initialDataTableState)
},
},
}))
.case(updateGraphEventId, (state, { id, graphEventId }) => ({
...state,
tableById: updateTableGraphEventId({ id, graphEventId, tableById: state.tableById }),
}))
.case(setTableUpdatedAt, (state, { id, updated }) => ({
...state,
tableById: {

View file

@ -7,8 +7,8 @@
import { getOr } from 'lodash/fp';
import { createSelector } from 'reselect';
import { tableDefaults, getDataTableManageDefaults } from './defaults';
import type { DataTableState, TableById, DataTableModel } from './types';
import { getDataTableManageDefaults, tableDefaults } from './defaults';
import type { DataTableModel, DataTableState, TableById } from './types';
const selectTableById = (state: DataTableState): TableById => state.dataTable.tableById;
@ -35,7 +35,7 @@ const selectTGridById = (state: unknown, tableId: string): DataTableModel => {
export const getManageDataTableById = () =>
createSelector(
selectTGridById,
({
({ dataViewId, defaultColumns, isLoading, loadingText, queryFields, title, selectAll }) => ({
dataViewId,
defaultColumns,
isLoading,
@ -43,15 +43,5 @@ export const getManageDataTableById = () =>
queryFields,
title,
selectAll,
graphEventId,
}) => ({
dataViewId,
defaultColumns,
isLoading,
loadingText,
queryFields,
title,
selectAll,
graphEventId,
})
);

View file

@ -24,11 +24,9 @@ export interface ScrollToTopEvent {
export enum TimelineTabs {
query = 'query',
graph = 'graph',
notes = 'notes',
pinned = 'pinned',
eql = 'eql',
securityAssistant = 'securityAssistant',
esql = 'esql',
}

View file

@ -14,9 +14,9 @@ import { FilterManager } from '@kbn/data-plugin/public';
import { createMockStore, mockGlobalState, TestProviders } from '../../common/mock';
import { inputsActions } from '../../common/store/inputs';
import {
setSearchBarFilter,
setAbsoluteRangeDatePicker,
setRelativeRangeDatePicker,
setSearchBarFilter,
} from '../../common/store/inputs/actions';
import { coreMock } from '@kbn/core/public/mocks';
import type { Filter } from '@kbn/es-query';
@ -641,7 +641,6 @@ describe('HomePage', () => {
expect(mockUpdateUrlParam).toHaveBeenCalledWith({
activeTab: 'query',
graphEventId: '',
isOpen: false,
});
});

View file

@ -38,7 +38,6 @@ jest.mock('../../../common/hooks/use_selector', () => ({
useShallowEqualSelector: jest.fn().mockReturnValue({
timelineTitle: mockTimelineModel.title,
timelineSavedObjectId: mockTimelineModel.savedObjectId,
graphEventId: mockTimelineModel.graphEventId,
timelineId: mockTimelineModel.id,
}),
}));

View file

@ -11,12 +11,12 @@ import { isEmpty } from 'lodash/fp';
import { getTimelineUrl, useFormatUrl } from '../../../common/components/link_to';
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { timelineSelectors, timelineActions } from '../../../timelines/store';
import { timelineActions, timelineSelectors } from '../../../timelines/store';
import { SecurityPageName } from '../../../app/types';
import { setInsertTimeline } from '../../../timelines/store/actions';
export interface UseInsertTimelineReturn {
handleOnTimelineChange: (title: string, id: string | null, graphEventId?: string) => void;
handleOnTimelineChange: (title: string, id: string | null) => void;
}
export const useInsertTimeline = (
@ -29,8 +29,8 @@ export const useInsertTimeline = (
const insertTimeline = useShallowEqualSelector(timelineSelectors.selectInsertTimeline);
const handleOnTimelineChange = useCallback(
(title: string, id: string | null, graphEventId?: string) => {
const url = formatUrl(getTimelineUrl(id ?? '', graphEventId), {
(title: string, id: string | null) => {
const url = formatUrl(getTimelineUrl(id ?? ''), {
absolute: true,
skipSearch: true,
});
@ -49,11 +49,7 @@ export const useInsertTimeline = (
useEffect(() => {
if (insertTimeline != null && value != null) {
dispatch(timelineActions.showTimeline({ id: insertTimeline.timelineId, show: false }));
handleOnTimelineChange(
insertTimeline.timelineTitle,
insertTimeline.timelineSavedObjectId,
insertTimeline.graphEventId
);
handleOnTimelineChange(insertTimeline.timelineTitle, insertTimeline.timelineSavedObjectId);
dispatch(setInsertTimeline(null));
}
}, [insertTimeline, dispatch, handleOnTimelineChange, value]);

View file

@ -6,7 +6,6 @@
*/
/* TODO: (new data view picker) remove this after new picker is enabled */
/* eslint-disable complexity */
import { css } from '@emotion/react';
import type { SubsetDataTableModel, TableId } from '@kbn/securitysolution-data-table';
@ -52,10 +51,8 @@ import type { SourcererScopeName } from '../../../sourcerer/store/model';
import { useSourcererDataView } from '../../../sourcerer/containers';
import type { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { useKibana } from '../../lib/kibana';
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
import type { FieldEditorActions } from '../../../timelines/components/fields_browser';
import { useFieldBrowserOptions } from '../../../timelines/components/fields_browser';
import { useSessionViewNavigation } from '../../../timelines/components/timeline/tabs/session/use_session_view';
import { getCombinedFilterQuery } from './helpers';
import { useTimelineEvents } from './use_timelines_events';
import { EmptyTable, TableContext, TableLoading } from './shared';
@ -128,7 +125,6 @@ const StatefulEventsViewerComponent: React.FC<EventsViewerProps & PropsFromRedux
columns,
defaultColumns,
deletedEventIds,
graphEventId, // If truthy, the graph viewer (Resolver) is showing
itemsPerPage,
itemsPerPageOptions,
showCheckboxes,
@ -196,15 +192,6 @@ const StatefulEventsViewerComponent: React.FC<EventsViewerProps & PropsFromRedux
const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]);
// TODO remove this when session view is fully migrated to the flyout and the advanced settings is removed
const { Navigation } = useSessionViewNavigation({
scopeId: tableId,
});
const graphOverlay = useMemo(() => {
const shouldShowOverlay = graphEventId != null && graphEventId.length > 0;
return shouldShowOverlay ? <GraphOverlay scopeId={tableId} Navigation={Navigation} /> : null;
}, [graphEventId, tableId, Navigation]);
const setQuery = useCallback(
({ id, inspect, loading, refetch }: SetQuery) =>
dispatch(
@ -505,8 +492,6 @@ const StatefulEventsViewerComponent: React.FC<EventsViewerProps & PropsFromRedux
<div data-test-subj="events-viewer-panel">
{showFullLoading && <TableLoading height="short" />}
{graphOverlay}
{canQueryTimeline && (
<TableContext.Provider value={tableContext}>
<div
@ -516,7 +501,7 @@ const StatefulEventsViewerComponent: React.FC<EventsViewerProps & PropsFromRedux
position: relative;
`}
>
{!loading && !graphOverlay && (
{!loading && (
<div
css={css`
position: absolute;
@ -536,12 +521,12 @@ const StatefulEventsViewerComponent: React.FC<EventsViewerProps & PropsFromRedux
</div>
)}
{!hasAlerts && !loading && !graphOverlay && <EmptyTable />}
{!hasAlerts && !loading && <EmptyTable />}
{hasAlerts && (
<EuiFlexItem
css={css`
display: ${!graphEventId && graphOverlay == null ? 'flex' : 'none'};
display: flex;
overflow: auto;
`}
>

View file

@ -36,6 +36,7 @@ import type { ESQuery } from '../../../../common/typed_json';
import type { AlertWorkflowStatus } from '../../types';
import { getSearchTransactionName, useStartTransaction } from '../../lib/apm/use_start_transaction';
import { useFetchNotes } from '../../../notes/hooks/use_fetch_notes';
export type InspectResponse = Inspect & { response: string[] };
export const detectionsTimelineIds = [TableId.alertsOnAlertsPage, TableId.alertsOnRuleDetailsPage];
@ -164,7 +165,6 @@ export const useTimelineEventsHandler = ({
const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(0);
const [timelineRequest, setTimelineRequest] = useState<TimelineRequest | null>(null);
const [prevFilterStatus, setFilterStatus] = useState(filterStatus);
const prevTimelineRequest = useRef<TimelineRequest | null>(null);
const clearSignalsState = useCallback(() => {
@ -262,10 +262,6 @@ export const useTimelineEventsHandler = ({
if (onNextHandler) onNextHandler(newTimelineResponse);
return newTimelineResponse;
});
if (prevFilterStatus !== request.filterStatus) {
dispatch(dataTableActions.updateGraphEventId({ id, graphEventId: '' }));
}
setFilterStatus(request.filterStatus);
setLoading(false);
searchSubscription$.current.unsubscribe();
@ -286,7 +282,7 @@ export const useTimelineEventsHandler = ({
asyncSearch();
refetch.current = asyncSearch;
},
[skip, data, entityType, dataViewId, startTracking, dispatch, id, prevFilterStatus]
[skip, data, entityType, dataViewId, startTracking]
);
useEffect(() => {

View file

@ -18,14 +18,13 @@ exports[`rendering renders correctly 1`] = `
color="subdued"
paddingSize="none"
>
<FiltersGlobalContainer
<header
data-test-subj="filters-global-container"
show={true}
>
<p>
Additional filters here.
</p>
</FiltersGlobalContainer>
</header>
</EuiPanel>
</InPortal>
`;

View file

@ -44,21 +44,4 @@ describe('rendering', () => {
).not.toHaveStyleRule('display', 'none');
});
});
describe('when show is false', () => {
test('it renders the container with a `display: none` style', () => {
const wrapper = mount(
<TestProviders>
<FiltersGlobal show={false}>
<p>{'Filter content'}</p>
</FiltersGlobal>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="filters-global-container"]').first()).toHaveStyleRule(
'display',
'none'
);
});
});
});

View file

@ -6,31 +6,21 @@
*/
import React from 'react';
import styled from 'styled-components';
import { InPortal } from 'react-reverse-portal';
import { EuiPanel } from '@elastic/eui';
import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal';
const FiltersGlobalContainer = styled.header<{ show: boolean }>`
display: ${({ show }) => (show ? 'block' : 'none')};
`;
FiltersGlobalContainer.displayName = 'FiltersGlobalContainer';
export interface FiltersGlobalProps {
children: React.ReactNode;
show?: boolean;
}
export const FiltersGlobal = React.memo<FiltersGlobalProps>(({ children, show = true }) => {
export const FiltersGlobal = React.memo<FiltersGlobalProps>(({ children }) => {
const { globalKQLHeaderPortalNode } = useGlobalHeaderPortal();
return (
<InPortal node={globalKQLHeaderPortalNode}>
<EuiPanel borderRadius="none" color="subdued" paddingSize="none">
<FiltersGlobalContainer data-test-subj="filters-global-container" show={show}>
{children}
</FiltersGlobalContainer>
<header data-test-subj="filters-global-container">{children}</header>
</EuiPanel>
</InPortal>
);

View file

@ -5,14 +5,10 @@
* 2.0.
*/
import { isEmpty } from 'lodash/fp';
import type { TimelineType } from '../../../../common/api/timeline';
import { appendSearch } from './helpers';
export const getTimelineTabsUrl = (tabName: TimelineType, search?: string) =>
`/${tabName}${appendSearch(search)}`;
export const getTimelineUrl = (id: string, graphEventId?: string) =>
`?timeline=(id:'${id}',isOpen:!t${
isEmpty(graphEventId) ? ')' : `,graphEventId:'${graphEventId}')`
}`;
export const getTimelineUrl = (id: string) => `?timeline=(id:'${id}',isOpen:!t)`;

View file

@ -75,11 +75,9 @@ export const TimelineParser: Plugin = function () {
const parseTimelineUrlSearch = parse(timelineSearch[1]) as { timeline: string };
const decodedTimeline = safeDecode(parseTimelineUrlSearch.timeline ?? '') as {
id?: string;
graphEventId?: string;
} | null;
const { id: timelineId = '', graphEventId = '' } = decodedTimeline ?? {
const { id: timelineId = '' } = decodedTimeline ?? {
id: null,
graphEventId: '',
};
if (!timelineId) {
@ -94,7 +92,6 @@ export const TimelineParser: Plugin = function () {
type: ID,
id: timelineId,
title: timelineTitle,
graphEventId,
});
} catch {
this.file.info(i18n.TIMELINE_URL_IS_NOT_VALID(timelineUrl), {

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import React, { useCallback, memo } from 'react';
import type { EuiSelectableOption, EuiMarkdownEditorUiPlugin } from '@elastic/eui';
import { EuiModalBody, EuiModalHeader, EuiCodeBlock } from '@elastic/eui';
import React, { memo, useCallback } from 'react';
import type { EuiMarkdownEditorUiPlugin, EuiSelectableOption } from '@elastic/eui';
import { EuiCodeBlock, EuiModalBody, EuiModalHeader } from '@elastic/eui';
import { TimelineTypeEnum } from '../../../../../../common/api/timeline';
import { SelectableTimeline } from '../../../../../timelines/components/timeline/selectable_timeline';
@ -45,8 +45,8 @@ const TimelineEditorComponent: React.FC<TimelineEditorProps> = ({ onClosePopover
);
const handleTimelineChange = useCallback(
(timelineTitle: string, timelineId: string | null, graphEventId?: string) => {
const url = formatUrl(getTimelineUrl(timelineId ?? '', graphEventId), {
(timelineTitle: string, timelineId: string | null) => {
const url = formatUrl(getTimelineUrl(timelineId ?? ''), {
absolute: true,
skipSearch: true,
});

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import React, { useCallback, memo } from 'react';
import { EuiToolTip, EuiLink } from '@elastic/eui';
import React, { memo, useCallback } from 'react';
import { EuiLink, EuiToolTip } from '@elastic/eui';
import { useUpsellingMessage } from '../../../../hooks/use_upselling';
import { useTimelineClick } from '../../../../utils/timeline/use_timeline_click';
@ -15,11 +15,7 @@ import * as i18n from './translations';
import { useAppToasts } from '../../../../hooks/use_app_toasts';
import { useUserPrivileges } from '../../../user_privileges';
export const TimelineMarkDownRendererComponent: React.FC<TimelineProps> = ({
id,
title,
graphEventId,
}) => {
export const TimelineMarkDownRendererComponent: React.FC<TimelineProps> = ({ id, title }) => {
const { addError } = useAppToasts();
const interactionsUpsellingMessage = useUpsellingMessage('investigation_guide_interactions');
@ -41,8 +37,8 @@ export const TimelineMarkDownRendererComponent: React.FC<TimelineProps> = ({
);
const onClickTimeline = useCallback(
() => handleTimelineClick(id ?? '', onError, graphEventId),
[id, graphEventId, handleTimelineClick, onError]
() => handleTimelineClick(id ?? '', onError),
[id, handleTimelineClick, onError]
);
return (
<EuiToolTip content={interactionsUpsellingMessage ?? i18n.TIMELINE_ID(id ?? '')}>

View file

@ -10,7 +10,7 @@ import type { ID } from './constants';
export interface TimelineConfiguration {
id: string | null;
title: string;
graphEventId?: string;
[key: string]: string | null | undefined;
}

View file

@ -29,7 +29,6 @@ export const useInitTimelineFromUrlParam = () => {
queryTimelineById({
activeTimelineTab: initialState.activeTab,
duplicate: false,
graphEventId: initialState.graphEventId,
timelineId: initialState.id,
openTimeline: initialState.isOpen,
savedSearchId: initialState.savedSearchId,
@ -71,8 +70,6 @@ function hasTimelineStateChanged(
return (
activeTimeline &&
newState &&
(activeTimeline.id !== newState.id ||
activeTimeline.savedSearchId !== newState.savedSearchId ||
activeTimeline.graphEventId !== newState.graphEventId)
(activeTimeline.id !== newState.id || activeTimeline.savedSearchId !== newState.savedSearchId)
);
}

View file

@ -42,8 +42,8 @@ jest.mock('react-redux', () => {
describe('queryTimelineByIdOnUrlChange', () => {
const oldTestTimelineId = '04e8ffb0-2c2a-11ec-949c-39005af91f70';
const newTestTimelineId = `${oldTestTimelineId}-newId`;
const oldTimelineRisonSearchString = `?timeline=(activeTab:query,graphEventId:%27%27,id:%27${oldTestTimelineId}%27,isOpen:!t)`;
const newTimelineRisonSearchString = `?timeline=(activeTab:query,graphEventId:%27%27,id:%27${newTestTimelineId}%27,isOpen:!t)`;
const oldTimelineRisonSearchString = `?timeline=(activeTab:query,id:%27${oldTestTimelineId}%27,isOpen:!t)`;
const newTimelineRisonSearchString = `?timeline=(activeTab:query,id:%27${newTestTimelineId}%27,isOpen:!t)`;
const mockQueryTimelineById = jest.fn();
beforeEach(() => {
@ -66,7 +66,6 @@ describe('queryTimelineByIdOnUrlChange', () => {
expect.objectContaining({
activeTimelineTab: 'query',
duplicate: false,
graphEventId: '',
timelineId: newTestTimelineId,
openTimeline: true,
})

View file

@ -57,7 +57,7 @@ export const useQueryTimelineByIdOnUrlChange = () => {
}, [oldSearch, search]);
const oldId = previousTimeline?.id;
const { id: newId, activeTab, graphEventId } = currentTimeline || {};
const { id: newId, activeTab } = currentTimeline || {};
const queryTimelineById = useQueryTimelineById();
@ -66,12 +66,11 @@ export const useQueryTimelineByIdOnUrlChange = () => {
queryTimelineById({
activeTimelineTab: activeTab ?? TimelineTabs.query,
duplicate: false,
graphEventId,
timelineId: newId,
openTimeline: true,
});
}
}, [timelineIdFromReduxStore, oldId, newId, activeTab, graphEventId, queryTimelineById]);
}, [timelineIdFromReduxStore, oldId, newId, activeTab, queryTimelineById]);
};
export const getQueryStringKeyValue = ({ search, urlKey }: { search: string; urlKey: string }) =>

View file

@ -17,7 +17,7 @@ import { URL_PARAM_KEY } from '../use_url_state';
export const useSyncTimelineUrlParam = () => {
const updateUrlParam = useUpdateUrlParam<TimelineUrl>(URL_PARAM_KEY.timeline);
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const { activeTab, graphEventId, show, savedObjectId, savedSearchId } = useShallowEqualSelector(
const { activeTab, show, savedObjectId, savedSearchId } = useShallowEqualSelector(
(state) => getTimeline(state, TimelineId.active) ?? {}
);
@ -26,9 +26,8 @@ export const useSyncTimelineUrlParam = () => {
...(savedObjectId ? { id: savedObjectId } : {}),
isOpen: show,
activeTab,
graphEventId: graphEventId ?? '',
savedSearchId: savedSearchId ? savedSearchId : undefined,
};
updateUrlParam(params);
}, [activeTab, graphEventId, savedObjectId, show, updateUrlParam, savedSearchId]);
}, [activeTab, savedObjectId, show, updateUrlParam, savedSearchId]);
};

View file

@ -33,8 +33,7 @@ describe('useResolveConflict', () => {
// Mock rison format in actual url
(useLocation as jest.Mock).mockReturnValue({
pathname: 'my/cool/path',
search:
'timeline=(activeTab:query,graphEventId:%27%27,id:%2704e8ffb0-2c2a-11ec-949c-39005af91f70%27,isOpen:!t)',
search: 'timeline=(activeTab:query,id:%2704e8ffb0-2c2a-11ec-949c-39005af91f70%27,isOpen:!t)',
});
(useKibana as jest.Mock).mockReturnValue({
services: {
@ -58,7 +57,6 @@ describe('useResolveConflict', () => {
(useDeepEqualSelector as jest.Mock).mockImplementation(() => ({
savedObjectId: 'current-saved-object-id',
activeTab: 'some-tab',
graphEventId: 'current-graph-event-id',
show: false,
}));
const { result } = renderHook(() => useResolveConflict());
@ -75,7 +73,6 @@ describe('useResolveConflict', () => {
},
savedObjectId: 'current-saved-object-id',
activeTab: 'some-tab',
graphEventId: 'current-graph-event-id',
show: false,
}));
const { result } = renderHook(() => useResolveConflict());
@ -114,7 +111,7 @@ describe('useResolveConflict', () => {
currentObjectId: '04e8ffb0-2c2a-11ec-949c-39005af91f70',
otherObjectId: 'new-id',
otherObjectPath:
'my/cool/path?timeline=%28activeTab%3Aquery%2CgraphEventId%3A%27%27%2Cid%3Anew-id%2CisOpen%3A%21t%29',
'my/cool/path?timeline=%28activeTab%3Aquery%2Cid%3Anew-id%2CisOpen%3A%21t%29',
});
expect(result.current).toMatchInlineSnapshot(`
<React.Fragment>
@ -137,7 +134,6 @@ describe('useResolveConflict', () => {
},
savedObjectId: 'current-saved-object-id',
activeTab: 'some-tab',
graphEventId: 'current-graph-event-id',
show: false,
}));
mockGetLegacyUrlConflict.mockImplementation(() => mockTextContent);
@ -148,7 +144,7 @@ describe('useResolveConflict', () => {
currentObjectId: 'current-saved-object-id',
otherObjectId: 'new-id',
otherObjectPath:
'my/cool/path?foo=bar&timeline=%28activeTab%3Asome-tab%2CgraphEventId%3Acurrent-graph-event-id%2Cid%3Anew-id%2CisOpen%3A%21f%29',
'my/cool/path?foo=bar&timeline=%28activeTab%3Asome-tab%2Cid%3Anew-id%2CisOpen%3A%21f%29',
});
expect(result.current).toMatchInlineSnapshot(`
<React.Fragment>

View file

@ -8,7 +8,7 @@
import React, { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { EuiSpacer } from '@elastic/eui';
import { safeDecode, encode } from '@kbn/rison';
import { encode, safeDecode } from '@kbn/rison';
import { useDeepEqualSelector } from './use_selector';
import { TimelineId } from '../../../common/types/timeline';
import { timelineSelectors } from '../../timelines/store';
@ -25,8 +25,9 @@ export const useResolveConflict = () => {
const { search, pathname } = useLocation();
const { spaces } = useKibana().services;
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const { resolveTimelineConfig, savedObjectId, show, graphEventId, activeTab } =
useDeepEqualSelector((state) => getTimeline(state, TimelineId.active) ?? timelineDefaults);
const { resolveTimelineConfig, savedObjectId, show, activeTab } = useDeepEqualSelector(
(state) => getTimeline(state, TimelineId.active) ?? timelineDefaults
);
const getLegacyUrlConflictCallout = useCallback(() => {
// This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario
@ -38,7 +39,6 @@ export const useResolveConflict = () => {
id: savedObjectId ?? '',
isOpen: !!show,
activeTab,
graphEventId,
};
const timelineSearch =
(safeDecode(timelineRison ?? '') as TimelineUrl | null) ?? currentTimelineState;
@ -78,7 +78,6 @@ export const useResolveConflict = () => {
);
}, [
activeTab,
graphEventId,
pathname,
resolveTimelineConfig?.alias_target_id,
resolveTimelineConfig?.outcome,

View file

@ -34,8 +34,7 @@ describe('useResolveRedirect', () => {
// Mock rison format in actual url
(useLocation as jest.Mock).mockReturnValue({
pathname: 'my/cool/path',
search:
'timeline=(activeTab:query,graphEventId:%27%27,id:%2704e8ffb0-2c2a-11ec-949c-39005af91f70%27,isOpen:!t)',
search: 'timeline=(activeTab:query,id:%2704e8ffb0-2c2a-11ec-949c-39005af91f70%27,isOpen:!t)',
});
(useKibana as jest.Mock).mockReturnValue({
services: {
@ -57,7 +56,6 @@ describe('useResolveRedirect', () => {
(useDeepEqualSelector as jest.Mock).mockImplementation(() => ({
savedObjectId: 'current-saved-object-id',
activeTab: 'some-tab',
graphEventId: 'current-graph-event-id',
show: false,
}));
renderHook(() => useResolveRedirect());
@ -73,7 +71,6 @@ describe('useResolveRedirect', () => {
},
savedObjectId: 'current-saved-object-id',
activeTab: 'some-tab',
graphEventId: 'current-graph-event-id',
show: false,
}));
renderHook(() => useResolveRedirect());
@ -92,7 +89,7 @@ describe('useResolveRedirect', () => {
}));
renderHook(() => useResolveRedirect());
expect(mockRedirectLegacyUrl).toHaveBeenCalledWith({
path: 'my/cool/path?timeline=%28activeTab%3Aquery%2CgraphEventId%3A%27%27%2Cid%3Anew-id%2CisOpen%3A%21t%29',
path: 'my/cool/path?timeline=%28activeTab%3Aquery%2Cid%3Anew-id%2CisOpen%3A%21t%29',
aliasPurpose: 'savedObjectConversion',
objectNoun: 'timeline',
});
@ -112,12 +109,11 @@ describe('useResolveRedirect', () => {
},
savedObjectId: 'current-saved-object-id',
activeTab: 'some-tab',
graphEventId: 'current-graph-event-id',
show: false,
}));
renderHook(() => useResolveRedirect());
expect(mockRedirectLegacyUrl).toHaveBeenCalledWith({
path: 'my/cool/path?foo=bar&timeline=%28activeTab%3Asome-tab%2CgraphEventId%3Acurrent-graph-event-id%2Cid%3Anew-id%2CisOpen%3A%21f%29',
path: 'my/cool/path?foo=bar&timeline=%28activeTab%3Asome-tab%2Cid%3Anew-id%2CisOpen%3A%21f%29',
aliasPurpose: 'savedObjectConversion',
objectNoun: 'timeline',
});

View file

@ -7,7 +7,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { safeDecode, encode } from '@kbn/rison';
import { encode, safeDecode } from '@kbn/rison';
import { useDeepEqualSelector } from './use_selector';
import { TimelineId } from '../../../common/types/timeline';
import { timelineSelectors } from '../../timelines/store';
@ -28,8 +28,9 @@ export const useResolveRedirect = () => {
const { spaces } = useKibana().services;
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const { resolveTimelineConfig, savedObjectId, show, activeTab, graphEventId } =
useDeepEqualSelector((state) => getTimeline(state, TimelineId.active) ?? timelineDefaults);
const { resolveTimelineConfig, savedObjectId, show, activeTab } = useDeepEqualSelector(
(state) => getTimeline(state, TimelineId.active) ?? timelineDefaults
);
const redirect = useCallback(() => {
const searchQuery = new URLSearchParams(search);
@ -40,7 +41,6 @@ export const useResolveRedirect = () => {
id: savedObjectId ?? '',
isOpen: !!show,
activeTab,
graphEventId,
};
const timelineSearch =
(safeDecode(timelineRison ?? '') as TimelineUrl | null) ?? currentTimelineState;
@ -72,7 +72,6 @@ export const useResolveRedirect = () => {
updateHasRedirected(true);
}, [
activeTab,
graphEventId,
hasRedirected,
pathname,
resolveTimelineConfig,

View file

@ -421,7 +421,6 @@ export const mockGlobalState: State = {
sortDirection: 'desc',
},
],
graphEventId: '',
selectAll: false,
id: TableId.test,
title: '',

View file

@ -5,14 +5,14 @@
* 2.0.
*/
import {
isDetectionsPages,
getQueryStringFromLocation,
getParamFromQueryString,
getObjectFromQueryString,
useGetInitialUrlParamValue,
encodeQueryString,
useReplaceUrlParams,
createHistoryEntry,
encodeQueryString,
getObjectFromQueryString,
getParamFromQueryString,
getQueryStringFromLocation,
isDetectionsPages,
useGetInitialUrlParamValue,
useReplaceUrlParams,
} from './helpers';
import { renderHook } from '@testing-library/react';
import { createMemoryHistory } from 'history';
@ -22,7 +22,7 @@ import React from 'react';
const flyoutString =
"(left:(id:document-details-left,params:(id:'04cf6ea970ab702721f17755b718c1296b5f8b5fcf7b74b053eab9bc885ceb9c',indexName:.internal.alerts-security.alerts-default-000001,scopeId:alerts-page)),right:(id:document-details-right,params:(id:'04cf6ea970ab702721f17755b718c1296b5f8b5fcf7b74b053eab9bc885ceb9c',indexName:.internal.alerts-security.alerts-default-000001,scopeId:alerts-page)))";
const testString = `sourcerer=(default:(id:security-solution-default,selectedPatterns:!(.alerts-security.alerts-default)))&timerange=(global:(linkTo:!(),timerange:(from:%272024-05-14T23:00:00.000Z%27,fromStr:now%2Fd,kind:relative,to:%272024-05-15T22:59:59.999Z%27,toStr:now%2Fd)),timeline:(linkTo:!(),timerange:(from:%272024-05-14T09:32:36.347Z%27,kind:absolute,to:%272024-05-15T09:32:36.347Z%27)))&timeline=(activeTab:query,graphEventId:%27%27,isOpen:!f)&pageFilters=!((exclude:!f,existsSelected:!f,fieldName:kibana.alert.workflow_status,hideActionBar:!t,selectedOptions:!(open),title:Status),(exclude:!f,existsSelected:!f,fieldName:kibana.alert.severity,hideActionBar:!t,selectedOptions:!(),title:Severity),(exclude:!f,existsSelected:!f,fieldName:user.name,hideActionBar:!f,selectedOptions:!(),title:User),(exclude:!f,existsSelected:!f,fieldName:host.name,hideActionBar:!f,selectedOptions:!(),title:Host))&flyout=${flyoutString}&timelineFlyout=()`;
const testString = `sourcerer=(default:(id:security-solution-default,selectedPatterns:!(.alerts-security.alerts-default)))&timerange=(global:(linkTo:!(),timerange:(from:%272024-05-14T23:00:00.000Z%27,fromStr:now%2Fd,kind:relative,to:%272024-05-15T22:59:59.999Z%27,toStr:now%2Fd)),timeline:(linkTo:!(),timerange:(from:%272024-05-14T09:32:36.347Z%27,kind:absolute,to:%272024-05-15T09:32:36.347Z%27)))&timeline=(activeTab:query,isOpen:!f)&pageFilters=!((exclude:!f,existsSelected:!f,fieldName:kibana.alert.workflow_status,hideActionBar:!t,selectedOptions:!(open),title:Status),(exclude:!f,existsSelected:!f,fieldName:kibana.alert.severity,hideActionBar:!t,selectedOptions:!(),title:Severity),(exclude:!f,existsSelected:!f,fieldName:user.name,hideActionBar:!f,selectedOptions:!(),title:User),(exclude:!f,existsSelected:!f,fieldName:host.name,hideActionBar:!f,selectedOptions:!(),title:Host))&flyout=${flyoutString}&timelineFlyout=()`;
const flyoutObject = {
left: {

View file

@ -13,9 +13,8 @@ export const useTimelineClick = () => {
const queryTimelineById = useQueryTimelineById();
const handleTimelineClick = useCallback(
(timelineId: string, onError: TimelineErrorCallback, graphEventId?: string) => {
(timelineId: string, onError: TimelineErrorCallback) => {
queryTimelineById({
graphEventId,
timelineId,
onError,
});

View file

@ -102,7 +102,6 @@ import {
focusUtilityBarAction,
onTimelineTabKeyPressed,
resetKeyboardFocus,
showGlobalFilters,
} from '../../../../timelines/components/timeline/helpers';
import { useSourcererDataView } from '../../../../sourcerer/containers';
import { SourcererScopeName } from '../../../../sourcerer/store/model';
@ -228,9 +227,6 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
const containerElement = useRef<HTMLDivElement | null>(null);
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
const graphEventId = useShallowEqualSelector(
(state) => (getTable(state, TableId.alertsOnRuleDetailsPage) ?? tableDefaults).graphEventId
);
const updatedAt = useShallowEqualSelector(
(state) => (getTable(state, TableId.alertsOnRuleDetailsPage) ?? tableDefaults).updated
);
@ -638,7 +634,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
)}
<StyledFullHeightContainer onKeyDown={onKeyDown} ref={containerElement}>
<EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<FiltersGlobal>
<SiemSearchBar
id={InputsModelId.global}
pollForSignalIndex={pollForSignalIndex}

View file

@ -36,8 +36,6 @@ import { APP_ID, CASES_FEATURE_ID, VIEW_SELECTION } from '../../../../common/con
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model';
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
import { useSessionViewNavigation } from '../../../timelines/components/timeline/tabs/session/use_session_view';
import type { State } from '../../../common/store';
import { inputsSelectors } from '../../../common/store';
import { combineQueries } from '../../../common/lib/kuery';
@ -84,10 +82,10 @@ interface GridContainerProps {
hideLastPage: boolean;
}
export const FullWidthFlexGroupTable = styled(EuiFlexGroup)<{ $visible: boolean }>`
export const FullWidthFlexGroupTable = styled(EuiFlexGroup)`
overflow: hidden;
margin: 0;
display: ${({ $visible }) => ($visible ? 'flex' : 'none')};
display: flex;
`;
const EuiDataGridContainer = styled.div<GridContainerProps>`
@ -203,7 +201,6 @@ const DetectionEngineAlertsTableComponent: FC<Omit<DetectionEngineAlertTableProp
const {
initialized: isDataTableInitialized,
graphEventId,
viewMode: tableView = eventsDefaultModel.viewMode,
columns,
totalCount: count,
@ -383,15 +380,6 @@ const DetectionEngineAlertsTableComponent: FC<Omit<DetectionEngineAlertTableProp
);
}, [dispatch, tableType, finalColumns, isDataTableInitialized]);
const { Navigation } = useSessionViewNavigation({
scopeId: tableType,
});
const graphOverlay = useMemo(() => {
const shouldShowOverlay = graphEventId != null && graphEventId.length > 0;
return shouldShowOverlay ? <GraphOverlay scopeId={tableType} Navigation={Navigation} /> : null;
}, [graphEventId, tableType, Navigation]);
const toolbarVisibility = useMemo(
() => ({
showColumnSelector: !isEventRenderedView,
@ -438,9 +426,7 @@ const DetectionEngineAlertsTableComponent: FC<Omit<DetectionEngineAlertTableProp
}
return (
<div>
{graphOverlay}
<FullWidthFlexGroupTable $visible={!graphEventId && graphOverlay == null} gutterSize="none">
<FullWidthFlexGroupTable gutterSize="none">
<StatefulEventContext.Provider value={activeStatefulEventContext}>
<EuiDataGridContainer hideLastPage={false}>
<AlertTableCellContextProvider
@ -490,7 +476,6 @@ const DetectionEngineAlertsTableComponent: FC<Omit<DetectionEngineAlertTableProp
</EuiDataGridContainer>
</StatefulEventContext.Provider>
</FullWidthFlexGroupTable>
</div>
);
};

View file

@ -72,7 +72,6 @@ import {
focusUtilityBarAction,
onTimelineTabKeyPressed,
resetKeyboardFocus,
showGlobalFilters,
} from '../../../timelines/components/timeline/helpers';
import {
buildAlertAssigneesFilter,
@ -111,9 +110,6 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ()
const dispatch = useDispatch();
const containerElement = useRef<HTMLDivElement | null>(null);
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
const graphEventId = useShallowEqualSelector(
(state) => (getTable(state, TableId.alertsOnAlertsPage) ?? tableDefaults).graphEventId
);
const isTableLoading = useShallowEqualSelector(
(state) => (getTable(state, TableId.alertsOnAlertsPage) ?? tableDefaults).isLoading
@ -397,7 +393,7 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ()
) : !signalIndexNeedsInit && hasIndexRead && canUserREAD ? (
<StyledFullHeightContainer onKeyDown={onKeyDown} ref={containerElement}>
<EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<FiltersGlobal>
<SiemSearchBar
id={InputsModelId.global}
pollForSignalIndex={pollForSignalIndex}

View file

@ -15,11 +15,9 @@ import {
import { noop } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import type { Filter } from '@kbn/es-query';
import { buildEsQuery } from '@kbn/es-query';
import { getEsQueryConfig } from '@kbn/data-plugin/common';
import { dataTableSelectors, tableDefaults, TableId } from '@kbn/securitysolution-data-table';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import type { NarrowDateRange } from '../../../../common/components/ml/types';
import { dataViewSpecToViewBase } from '../../../../common/lib/kuery';
@ -36,7 +34,7 @@ import { AlertsByStatus } from '../../../../overview/components/detection_respon
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
import { useAlertsPrivileges } from '../../../../detections/containers/detection_engine/alerts/use_alerts_privileges';
import { InputsModelId } from '../../../../common/store/inputs/constants';
import { LastEventIndexKey, type HostItem } from '../../../../../common/search_strategy';
import { type HostItem, LastEventIndexKey } from '../../../../../common/search_strategy';
import { EntityType } from '../../../../../common/entity_analytics/types';
import { SecurityPageName } from '../../../../app/types';
import { FiltersGlobal } from '../../../../common/components/filters_global';
@ -60,19 +58,14 @@ import { inputsSelectors } from '../../../../common/store';
import { setHostDetailsTablesActivePageToZero } from '../../store/actions';
import { setAbsoluteRangeDatePicker } from '../../../../common/store/inputs/actions';
import { SpyRoute } from '../../../../common/utils/route/spy_routes';
import { HostDetailsTabs } from './details_tabs';
import { navTabsHostDetails } from './nav_tabs';
import type { HostDetailsProps } from './types';
import { HostsType } from '../../store/model';
import { getHostDetailsPageFilters } from './helpers';
import { showGlobalFilters } from '../../../../timelines/components/timeline/helpers';
import { useGlobalFullScreen } from '../../../../common/containers/use_full_screen';
import { Display } from '../display';
import {
useDeepEqualSelector,
useShallowEqualSelector,
} from '../../../../common/hooks/use_selector';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { ID, useHostDetails } from '../../containers/hosts/details';
import { manageQuery } from '../../../../common/components/page/manage_query';
import { useInvalidFilterQuery } from '../../../../common/hooks/use_invalid_filter_query';
@ -92,10 +85,6 @@ const HostOverviewManage = manageQuery(HostOverview);
const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDetailsPagePath }) => {
const dispatch = useDispatch();
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
const graphEventId = useShallowEqualSelector(
(state) => (getTable(state, TableId.hostsPageEvents) ?? tableDefaults).graphEventId
);
const getGlobalFiltersQuerySelector = useMemo(
() => inputsSelectors.globalFiltersQuerySelector(),
[]
@ -230,7 +219,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
{indicesExist ? (
<>
<EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<FiltersGlobal>
<SiemSearchBar id={InputsModelId.global} sourcererDataView={sourcererDataView} />
</FiltersGlobal>

View file

@ -13,7 +13,6 @@ import { useParams } from 'react-router-dom';
import type { Filter } from '@kbn/es-query';
import { isTab } from '@kbn/timelines-plugin/public';
import { getEsQueryConfig } from '@kbn/data-plugin/common';
import { dataTableSelectors, tableDefaults, TableId } from '@kbn/securitysolution-data-table';
import { LastEventIndexKey } from '@kbn/timelines-plugin/common';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { InputsModelId } from '../../../common/store/inputs/constants';
@ -33,7 +32,6 @@ import { useKibana } from '../../../common/lib/kibana';
import { convertToBuildEsQuery } from '../../../common/lib/kuery';
import type { State } from '../../../common/store';
import { inputsSelectors } from '../../../common/store';
import { SpyRoute } from '../../../common/utils/route/spy_routes';
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
import { Display } from './display';
@ -46,10 +44,9 @@ import { HostsTableType } from '../store/model';
import {
onTimelineTabKeyPressed,
resetKeyboardFocus,
showGlobalFilters,
} from '../../../timelines/components/timeline/helpers';
import { useSourcererDataView } from '../../../sourcerer/containers';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
import { ID } from '../containers/hosts';
import { EmptyPrompt } from '../../../common/components/empty_prompt';
@ -70,10 +67,6 @@ const StyledFullHeightContainer = styled.div`
const HostsComponent = () => {
const containerElement = useRef<HTMLDivElement | null>(null);
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
const graphEventId = useShallowEqualSelector(
(state) => (getTable(state, TableId.hostsPageEvents) ?? tableDefaults).graphEventId
);
const getGlobalFiltersQuerySelector = useMemo(
() => inputsSelectors.globalFiltersQuerySelector(),
[]
@ -190,7 +183,7 @@ const HostsComponent = () => {
{indicesExist ? (
<StyledFullHeightContainer onKeyDown={onKeyDown} ref={containerElement}>
<EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<FiltersGlobal>
<SiemSearchBar id={InputsModelId.global} sourcererDataView={sourcererDataView} />
</FiltersGlobal>

View file

@ -10,10 +10,8 @@ import { noop } from 'lodash/fp';
import React, { useCallback, useMemo, useRef } from 'react';
import { useParams } from 'react-router-dom';
import styled from '@emotion/styled';
import { isTab } from '@kbn/timelines-plugin/public';
import { getEsQueryConfig } from '@kbn/data-plugin/common';
import { dataTableSelectors, tableDefaults, TableId } from '@kbn/securitysolution-data-table';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { InputsModelId } from '../../../common/store/inputs/constants';
import { SecurityPageName } from '../../../app/types';
@ -22,7 +20,6 @@ import { FiltersGlobal } from '../../../common/components/filters_global';
import { HeaderPage } from '../../../common/components/header_page';
import { LastEventTime } from '../../../common/components/last_event_time';
import { TabNavigation } from '../../../common/components/navigation/tab_navigation';
import { NetworkKpiComponent } from '../components/kpi_network';
import { SiemSearchBar } from '../../../common/components/search_bar';
import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper';
@ -42,16 +39,16 @@ import { NetworkRouteType } from './navigation/types';
import {
onTimelineTabKeyPressed,
resetKeyboardFocus,
showGlobalFilters,
} from '../../../timelines/components/timeline/helpers';
import { useSourcererDataView } from '../../../sourcerer/containers';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
import { sourceOrDestinationIpExistsFilter } from '../../../common/components/visualization_actions/utils';
import { EmptyPrompt } from '../../../common/components/empty_prompt';
import { useDataView } from '../../../data_view_manager/hooks/use_data_view';
import { useDataViewSpec } from '../../../data_view_manager/hooks/use_data_view_spec';
import { useSelectedPatterns } from '../../../data_view_manager/hooks/use_selected_patterns';
/**
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
*/
@ -66,10 +63,6 @@ const ID = 'NetworkQueryId';
const NetworkComponent = React.memo<NetworkComponentProps>(
({ hasMlUserPermissions, capabilitiesFetched }) => {
const containerElement = useRef<HTMLDivElement | null>(null);
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
const graphEventId = useShallowEqualSelector(
(state) => (getTable(state, TableId.networkPageEvents) ?? tableDefaults).graphEventId
);
const getGlobalFiltersQuerySelector = useMemo(
() => inputsSelectors.globalFiltersQuerySelector(),
[]
@ -157,7 +150,7 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
{indicesExist ? (
<StyledFullHeightContainer onKeyDown={onKeyDown} ref={containerElement}>
<EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<FiltersGlobal>
<SiemSearchBar sourcererDataView={sourcererDataView} id={InputsModelId.global} />
</FiltersGlobal>

View file

@ -15,11 +15,9 @@ import {
import { noop } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { getEsQueryConfig } from '@kbn/data-plugin/common';
import type { Filter } from '@kbn/es-query';
import { buildEsQuery } from '@kbn/es-query';
import { dataTableSelectors, TableId } from '@kbn/securitysolution-data-table';
import { LastEventIndexKey } from '@kbn/timelines-plugin/common';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { dataViewSpecToViewBase } from '../../../../common/lib/kuery';
@ -49,28 +47,21 @@ import { useAlertsPrivileges } from '../../../../detections/containers/detection
import { setUsersDetailsTablesActivePageToZero } from '../../store/actions';
import { setAbsoluteRangeDatePicker } from '../../../../common/store/inputs/actions';
import { SpyRoute } from '../../../../common/utils/route/spy_routes';
import { UsersDetailsTabs } from './details_tabs';
import { navTabsUsersDetails } from './nav_tabs';
import type { UsersDetailsProps } from './types';
import { getUsersDetailsPageFilters } from './helpers';
import { showGlobalFilters } from '../../../../timelines/components/timeline/helpers';
import { useGlobalFullScreen } from '../../../../common/containers/use_full_screen';
import { timelineDefaults } from '../../../../timelines/store/defaults';
import { useSourcererDataView } from '../../../../sourcerer/containers';
import {
useDeepEqualSelector,
useShallowEqualSelector,
} from '../../../../common/hooks/use_selector';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { useInvalidFilterQuery } from '../../../../common/hooks/use_invalid_filter_query';
import { LastEventTime } from '../../../../common/components/last_event_time';
import { EntityType } from '../../../../../common/entity_analytics/types';
import { AnomalyTableProvider } from '../../../../common/components/ml/anomaly/anomaly_table_provider';
import type { UserSummaryProps } from '../../../../overview/components/user_overview';
import {
UserOverview,
USER_OVERVIEW_RISK_SCORE_QUERY_ID,
UserOverview,
} from '../../../../overview/components/user_overview';
import { useObservedUserDetails } from '../../containers/users/observed_details';
import { useQueryInspector } from '../../../../common/components/page/manage_query';
@ -93,10 +84,6 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
usersDetailsPagePath,
}) => {
const dispatch = useDispatch();
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
const graphEventId = useShallowEqualSelector(
(state) => (getTable(state, TableId.hostsPageEvents) ?? timelineDefaults).graphEventId
);
const getGlobalFiltersQuerySelector = useMemo(
() => inputsSelectors.globalFiltersQuerySelector(),
[]
@ -230,7 +217,7 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
{indicesExist ? (
<>
<EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<FiltersGlobal>
<SiemSearchBar sourcererDataView={sourcererDataView} id={InputsModelId.global} />
</FiltersGlobal>

View file

@ -1,211 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { cleanup, render } from '@testing-library/react';
import React from 'react';
import '@testing-library/jest-dom';
// Necessary until components being tested are migrated of styled-components https://github.com/elastic/kibana/issues/219037
import 'jest-styled-components';
import {
useGlobalFullScreen,
useTimelineFullScreen,
} from '../../../common/containers/use_full_screen';
import { createMockStore, mockGlobalState, TestProviders } from '../../../common/mock';
import { TimelineId } from '../../../../common/types/timeline';
import { GraphOverlay } from '.';
import { useStateSyncingActions } from '../../../resolver/view/use_state_syncing_actions';
import { TableId } from '@kbn/securitysolution-data-table';
jest.mock('../../../common/containers/use_full_screen', () => ({
useGlobalFullScreen: jest.fn(),
useTimelineFullScreen: jest.fn(),
}));
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
});
jest.mock('../../../resolver/view/use_resolver_query_params_cleaner');
jest.mock('../../../resolver/view/use_state_syncing_actions');
const useStateSyncingActionsMock = useStateSyncingActions as jest.Mock;
jest.mock('../../../resolver/view/use_sync_selected_node');
jest.mock('../../../common/lib/kibana', () => {
const original = jest.requireActual('../../../common/lib/kibana');
return {
...original,
useKibana: () => ({
services: {
sessionView: {
getSessionView: () => <div />,
},
data: {
search: {
search: jest.fn(),
},
},
},
}),
};
});
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
describe('GraphOverlay', () => {
beforeEach(() => {
jest.clearAllMocks();
(useGlobalFullScreen as jest.Mock).mockReturnValue({
globalFullScreen: false,
setGlobalFullScreen: jest.fn(),
});
(useTimelineFullScreen as jest.Mock).mockReturnValue({
timelineFullScreen: false,
setTimelineFullScreen: jest.fn(),
});
});
describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => {
test('it has 100% width when NOT in full screen mode', () => {
const wrapper = render(
<TestProviders>
<GraphOverlay Navigation={<div />} scopeId={TableId.test} />
</TestProviders>
);
const overlayContainer = wrapper.getByTestId('overlayContainer');
expect(overlayContainer).toHaveStyleRule('width', '100%');
});
test('it has a fixed position when in full screen mode', () => {
(useGlobalFullScreen as jest.Mock).mockReturnValue({
globalFullScreen: true,
setGlobalFullScreen: jest.fn(),
});
(useTimelineFullScreen as jest.Mock).mockReturnValue({
timelineFullScreen: false,
setTimelineFullScreen: jest.fn(),
});
const wrapper = render(
<TestProviders>
<GraphOverlay Navigation={<div />} scopeId={TableId.test} />
</TestProviders>
);
const overlayContainer = wrapper.getByTestId('overlayContainer');
expect(overlayContainer).toHaveStyleRule('position', 'fixed');
});
test('it gets index pattern from default data view', () => {
render(
<TestProviders
store={createMockStore({
...mockGlobalState,
timeline: {
...mockGlobalState.timeline,
timelineById: {
[TimelineId.test]: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
graphEventId: 'definitely-not-null',
},
},
},
})}
>
<GraphOverlay Navigation={<div />} scopeId={TableId.test} />
</TestProviders>
);
expect(useStateSyncingActionsMock.mock.calls[0][0].indices).toEqual(
mockGlobalState.sourcerer.sourcererScopes.analyzer.selectedPatterns
);
});
});
describe('when used in the active timeline', () => {
const timelineId = TimelineId.active;
afterAll(() => {
cleanup();
});
test('it has 100% width when NOT in full screen mode', () => {
const wrapper = render(
<TestProviders>
<GraphOverlay Navigation={<div />} scopeId={timelineId} />
</TestProviders>
);
const overlayContainer = wrapper.getByTestId('overlayContainer');
expect(overlayContainer).toHaveStyleRule('width', '100%');
});
test('it has 100% width when the active timeline is in full screen mode', () => {
(useGlobalFullScreen as jest.Mock).mockReturnValue({
globalFullScreen: false,
setGlobalFullScreen: jest.fn(),
});
(useTimelineFullScreen as jest.Mock).mockReturnValue({
timelineFullScreen: true, // <-- true when the active timeline is in full screen mode
setTimelineFullScreen: jest.fn(),
});
const wrapper = render(
<TestProviders>
<GraphOverlay Navigation={<div />} scopeId={timelineId} />
</TestProviders>
);
const overlayContainer = wrapper.getByTestId('overlayContainer');
expect(overlayContainer).toHaveStyleRule('width', '100%');
});
test('it clears the graph event id on unmount', () => {
(useGlobalFullScreen as jest.Mock).mockReturnValue({
globalFullScreen: false,
setGlobalFullScreen: jest.fn(),
});
(useTimelineFullScreen as jest.Mock).mockReturnValue({
timelineFullScreen: true,
setTimelineFullScreen: jest.fn(),
});
const wrapper = render(
<TestProviders
store={createMockStore({
...mockGlobalState,
timeline: {
...mockGlobalState.timeline,
timelineById: {
[timelineId]: {
...mockGlobalState.timeline.timelineById[timelineId],
graphEventId: 'test_id',
},
},
},
})}
>
<GraphOverlay Navigation={<div />} scopeId={timelineId} />
</TestProviders>
);
wrapper.unmount();
expect(mockDispatch).toHaveBeenCalledWith({
payload: { id: 'timeline-1', graphEventId: '' },
type: 'x-pack/security_solution/local/timeline/UPDATE_TIMELINE_GRAPH_EVENT_ID',
});
});
});
});

View file

@ -1,159 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiLoadingSpinner } from '@elastic/eui';
import { useDispatch } from 'react-redux';
import styled, { css } from 'styled-components';
import { dataTableSelectors, tableDefaults } from '@kbn/securitysolution-data-table';
import {
getScopedActions,
isActiveTimeline,
isInTableScope,
isTimelineScope,
} from '../../../helpers';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { InputsModelId } from '../../../common/store/inputs/constants';
import {
useGlobalFullScreen,
useTimelineFullScreen,
} from '../../../common/containers/use_full_screen';
import { inputsActions } from '../../../common/store/actions';
import { Resolver } from '../../../resolver/view';
import { useTimelineDataFilters } from '../../containers/use_timeline_data_filters';
import { timelineSelectors } from '../../store';
import { timelineDefaults } from '../../store/defaults';
import { isFullScreen } from '../timeline/helpers';
const OverlayStyle = css`
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
`;
const OverlayContainer = styled.div`
${OverlayStyle}
`;
const FullScreenOverlayStyles = css`
background-color: ${({ theme }) => `${theme.eui.euiColorEmptyShade};`}
position: fixed;
top: 0;
bottom: 2em;
left: 0;
right: 0;
z-index: 3000;
`;
const FullScreenOverlayContainer = styled.div`
${FullScreenOverlayStyles}
`;
const StyledResolver = styled(Resolver)`
height: 100%;
`;
interface GraphOverlayProps {
scopeId: string;
Navigation: JSX.Element | null;
}
const GraphOverlayComponent: React.FC<GraphOverlayProps> = ({ Navigation, scopeId }) => {
const dispatch = useDispatch();
const { globalFullScreen } = useGlobalFullScreen();
const { timelineFullScreen } = useTimelineFullScreen();
const getScope = useMemo(() => {
if (isInTableScope(scopeId)) {
return dataTableSelectors.getTableByIdSelector();
} else if (isTimelineScope(scopeId)) {
return timelineSelectors.getTimelineByIdSelector();
}
}, [scopeId]);
const defaults = isInTableScope(scopeId) ? tableDefaults : timelineDefaults;
const { graphEventId } = useDeepEqualSelector(
(state) => (getScope && getScope(state, scopeId)) ?? defaults
);
const fullScreen = useMemo(
() =>
isFullScreen({
globalFullScreen,
isActiveTimelines: isActiveTimeline(scopeId),
timelineFullScreen,
}),
[globalFullScreen, scopeId, timelineFullScreen]
);
useEffect(() => {
return () => {
const scopedActions = getScopedActions(scopeId);
if (scopedActions) {
dispatch(scopedActions.updateGraphEventId({ id: scopeId, graphEventId: '' }));
}
if (isActiveTimeline(scopeId)) {
dispatch(inputsActions.setFullScreen({ id: InputsModelId.timeline, fullScreen: false }));
} else {
dispatch(inputsActions.setFullScreen({ id: InputsModelId.global, fullScreen: false }));
}
};
}, [dispatch, scopeId]);
const { from, to, shouldUpdate, selectedPatterns } = useTimelineDataFilters(
isActiveTimeline(scopeId)
);
const filters = useMemo(() => {
return { from, to };
}, [from, to]);
const resolver = useMemo(
() =>
graphEventId !== undefined ? (
<StyledResolver
databaseDocumentID={graphEventId}
resolverComponentInstanceID={scopeId}
indices={selectedPatterns}
shouldUpdate={shouldUpdate}
filters={filters}
/>
) : (
<EuiFlexGroup alignItems="center" justifyContent="center" css={{ height: '100%' }}>
<EuiLoadingSpinner size="xl" />
</EuiFlexGroup>
),
[graphEventId, scopeId, selectedPatterns, shouldUpdate, filters]
);
if (fullScreen && !isActiveTimeline(scopeId)) {
return (
<FullScreenOverlayContainer data-test-subj="overlayContainer">
<EuiHorizontalRule margin="none" />
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>{Navigation}</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="none" />
{resolver}
</FullScreenOverlayContainer>
);
} else {
return (
<OverlayContainer data-test-subj="overlayContainer">
<EuiHorizontalRule margin="none" />
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>{Navigation}</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="none" />
{resolver}
</OverlayContainer>
);
}
};
export const GraphOverlay = React.memo(GraphOverlayComponent);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiContextMenuPanel, EuiContextMenuItem, EuiPopover, EuiButtonEmpty } from '@elastic/eui';
import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import type { CaseUI } from '@kbn/cases-plugin/common';
@ -17,7 +17,7 @@ import { setInsertTimeline, showTimeline } from '../../../store/actions';
import { useKibana } from '../../../../common/lib/kibana';
import { TimelineId } from '../../../../../common/types/timeline';
import { TimelineStatusEnum, TimelineTypeEnum } from '../../../../../common/api/timeline';
import { getCreateCaseUrl, getCaseDetailsUrl } from '../../../../common/components/link_to';
import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../../common/components/link_to';
import { SecurityPageName } from '../../../../app/types';
import * as i18n from './translations';
@ -34,7 +34,6 @@ interface AttachToCaseButtonProps {
export const AttachToCaseButton = React.memo<AttachToCaseButtonProps>(({ timelineId }) => {
const dispatch = useDispatch();
const {
graphEventId,
savedObjectId,
status: timelineStatus,
title: timelineTitle,
@ -62,22 +61,13 @@ export const AttachToCaseButton = React.memo<AttachToCaseButtonProps>(({ timelin
});
dispatch(
setInsertTimeline({
graphEventId,
timelineId,
timelineSavedObjectId: savedObjectId,
timelineTitle,
})
);
},
[
closeCaseModal,
dispatch,
graphEventId,
navigateToApp,
savedObjectId,
timelineId,
timelineTitle,
]
[closeCaseModal, dispatch, navigateToApp, savedObjectId, timelineId, timelineTitle]
);
const attachToNewCase = useCallback(() => {
@ -89,7 +79,6 @@ export const AttachToCaseButton = React.memo<AttachToCaseButtonProps>(({ timelin
}).then(() => {
dispatch(
setInsertTimeline({
graphEventId,
timelineId,
timelineSavedObjectId: savedObjectId,
timelineTitle: timelineTitle.length > 0 ? timelineTitle : UNTITLED_TIMELINE,
@ -97,15 +86,7 @@ export const AttachToCaseButton = React.memo<AttachToCaseButtonProps>(({ timelin
);
dispatch(showTimeline({ id: TimelineId.active, show: false }));
});
}, [
dispatch,
graphEventId,
navigateToApp,
savedObjectId,
timelineId,
timelineTitle,
togglePopover,
]);
}, [dispatch, navigateToApp, savedObjectId, timelineId, timelineTitle, togglePopover]);
const attachToExistingCase = useCallback(() => {
togglePopover();

View file

@ -6,30 +6,30 @@
*/
import { cloneDeep, omit } from 'lodash/fp';
import { waitFor, renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import { mockTimelineResults } from '../../../common/mock';
import { timelineDefaults } from '../../store/defaults';
import type { QueryTimelineById } from './helpers';
import {
defaultTimelineToTimelineModel,
formatTimelineResponseToModel,
getNotesCount,
getPinnedEventCount,
isUntitled,
useQueryTimelineById,
formatTimelineResponseToModel,
} from './helpers';
import type { OpenTimelineResult } from './types';
import { TimelineId } from '../../../../common/types/timeline';
import {
TimelineTypeEnum,
TimelineStatusEnum,
type ColumnHeaderResult,
type RowRendererId,
TimelineStatusEnum,
TimelineTypeEnum,
} from '../../../../common/api/timeline';
import {
mockTimeline as mockSelectedTimeline,
mockTemplate as mockSelectedTemplate,
mockTimeline as mockSelectedTimeline,
} from './__mocks__';
import { resolveTimeline } from '../../containers/api';
import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers';
@ -623,7 +623,6 @@ describe('helpers', () => {
const args: QueryTimelineById = {
duplicate: false,
graphEventId: '',
timelineId: '',
timelineType: TimelineTypeEnum.default,
onError,
@ -669,7 +668,6 @@ describe('helpers', () => {
const onOpenTimeline = jest.fn();
const args: QueryTimelineById = {
duplicate: false,
graphEventId: '',
timelineId: '',
timelineType: TimelineTypeEnum.default,
openTimeline: true,
@ -699,7 +697,6 @@ describe('helpers', () => {
expect(mockUpdateTimeline).toHaveBeenCalledWith({
timeline: {
...timeline,
graphEventId: '',
show: true,
dateRange: {
start: '2020-07-07T08:20:18.966Z',
@ -724,7 +721,6 @@ describe('helpers', () => {
(resolveTimeline as jest.Mock).mockResolvedValue(selectedTimeline);
const newArgs: QueryTimelineById = {
duplicate: false,
graphEventId: '',
timelineId: undefined,
timelineType: TimelineTypeEnum.default,
onOpenTimeline,
@ -798,7 +794,6 @@ describe('helpers', () => {
const onOpenTimeline = jest.fn();
const args = {
duplicate: false,
graphEventId: '',
timelineId: '',
timelineType: TimelineTypeEnum.template,
onOpenTimeline,

View file

@ -11,13 +11,14 @@ import { v4 as uuidv4 } from 'uuid';
import deepMerge from 'deepmerge';
import { useDiscoverInTimelineContext } from '../../../common/components/discover_in_timeline/use_discover_in_timeline_context';
import type { ColumnHeaderOptions } from '../../../../common/types/timeline';
import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
import type {
TimelineResponse,
ColumnHeaderResult,
FilterTimelineResult,
DataProviderResult,
PinnedEvent,
FilterTimelineResult,
Note,
PinnedEvent,
TimelineResponse,
} from '../../../../common/api/timeline';
import {
DataProviderTypeEnum,
@ -26,7 +27,6 @@ import {
type TimelineType,
TimelineTypeEnum,
} from '../../../../common/api/timeline';
import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
import { useUpdateTimeline } from './use_update_timeline';
import type { TimelineModel } from '../../store/model';
@ -292,7 +292,6 @@ export const formatTimelineResponseToModel = (
export interface QueryTimelineById {
activeTimelineTab?: TimelineTabs;
duplicate?: boolean;
graphEventId?: string;
timelineId?: string;
timelineType?: TimelineType;
onError?: TimelineErrorCallback;
@ -308,7 +307,6 @@ export const useQueryTimelineById = () => {
return ({
activeTimelineTab = TimelineTabs.query,
duplicate = false,
graphEventId = '',
timelineId,
timelineType,
onError,
@ -369,7 +367,6 @@ export const useQueryTimelineById = () => {
timeline: {
...timeline,
activeTab: activeTimelineTab,
graphEventId,
show: openTimeline,
dateRange: { start: from, end: to },
savedSearchId: timeline.savedSearchId,

View file

@ -19,7 +19,6 @@ import {
handleIsOperator,
isFullScreen,
isPrimitiveArray,
showGlobalFilters,
} from './helpers';
import { mockBrowserFields } from '../../../common/containers/source/mock';
@ -246,32 +245,6 @@ describe('Build KQL Query', () => {
'(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5") or (name : "Provider 3") or (name : "Provider 4") or (name : "Provider 5") or (name : "Provider 6") or (name : "Provider 7") or (name : "Provider 8") or (name : "Provider 9") or (name : "Provider 10")'
);
});
describe('showGlobalFilters', () => {
test('it returns false when `globalFullScreen` is true and `graphEventId` is NOT an empty string, because Resolver IS showing', () => {
expect(showGlobalFilters({ globalFullScreen: true, graphEventId: 'a valid id' })).toBe(false);
});
test('it returns true when `globalFullScreen` is true and `graphEventId` is undefined, because Resolver is NOT showing', () => {
expect(showGlobalFilters({ globalFullScreen: true, graphEventId: undefined })).toBe(true);
});
test('it returns true when `globalFullScreen` is true and `graphEventId` is an empty string, because Resolver is NOT showing', () => {
expect(showGlobalFilters({ globalFullScreen: true, graphEventId: '' })).toBe(true);
});
test('it returns true when `globalFullScreen` is false and `graphEventId` is NOT an empty string, because Resolver IS showing', () => {
expect(showGlobalFilters({ globalFullScreen: false, graphEventId: 'a valid id' })).toBe(true);
});
test('it returns true when `globalFullScreen` is false and `graphEventId` is undefined, because Resolver is NOT showing', () => {
expect(showGlobalFilters({ globalFullScreen: false, graphEventId: undefined })).toBe(true);
});
test('it returns true when `globalFullScreen` is false and `graphEventId` is an empty string, because Resolver is NOT showing', () => {
expect(showGlobalFilters({ globalFullScreen: false, graphEventId: '' })).toBe(true);
});
});
});
describe('isStringOrNumberArray', () => {

View file

@ -20,11 +20,11 @@ import { prepareKQLParam, prepareKQLStringParam } from '../../../../common/utils
import { assertUnreachable } from '../../../../common/utility_types';
import type { BrowserFields } from '../../../common/containers/source';
import {
convertDateFieldToQuery,
checkIfFieldTypeIsDate,
convertNestedFieldToQuery,
convertNestedFieldToExistQuery,
checkIfFieldTypeIsNested,
convertDateFieldToQuery,
convertNestedFieldToExistQuery,
convertNestedFieldToQuery,
type PrimitiveOrArrayOfPrimitives,
} from '../../../common/lib/kuery';
import type { DataProvider, DataProvidersAnd } from './data_providers/data_provider';
@ -91,39 +91,8 @@ export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: B
*/
export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view';
export const resolverIsShowing = (graphEventId: string | undefined): boolean =>
graphEventId != null && graphEventId !== '';
export const showGlobalFilters = ({
globalFullScreen,
graphEventId,
}: {
globalFullScreen: boolean;
graphEventId: string | undefined;
}): boolean => (globalFullScreen && resolverIsShowing(graphEventId) ? false : true);
/**
* The `aria-colindex` of the Timeline actions column
*/
export const ACTIONS_COLUMN_ARIA_COL_INDEX = '1';
/**
* Every column index offset by `2`, because, per https://www.w3.org/TR/wai-aria-practices-1.1/examples/grid/dataGrids.html
* the `aria-colindex` attribute starts at `1`, and the "actions column" is always the first column
*/
export const ARIA_COLUMN_INDEX_OFFSET = 2;
export const EVENTS_COUNT_BUTTON_CLASS_NAME = 'local-events-count-button';
/** Calculates the total number of pages in a (timeline) events view */
export const calculateTotalPages = ({
itemsCount,
itemsPerPage,
}: {
itemsCount: number;
itemsPerPage: number;
}): number => (itemsCount === 0 || itemsPerPage === 0 ? 0 : Math.ceil(itemsCount / itemsPerPage));
/** Returns true if the events table has focus */
export const tableHasFocus = (containerElement: HTMLElement | null): boolean =>
elementOrChildrenHasFocus(

View file

@ -78,7 +78,6 @@ const StatefulTimelineComponent: React.FC<Props> = ({
const {
dataViewId: selectedDataViewIdTimeline,
indexNames: selectedPatternsTimeline,
graphEventId,
savedObjectId,
timelineType,
description,
@ -88,7 +87,6 @@ const StatefulTimelineComponent: React.FC<Props> = ({
[
'indexNames',
'dataViewId',
'graphEventId',
'savedObjectId',
'timelineType',
'description',
@ -230,7 +228,6 @@ const StatefulTimelineComponent: React.FC<Props> = ({
</HideShowContainer>
<TabsContent
graphEventId={graphEventId}
renderCellValue={renderCellValue}
rowRenderers={rowRenderers}
timelineId={timelineId}

View file

@ -7,20 +7,20 @@
import type { EuiSelectableOption, EuiSelectableProps } from '@elastic/eui';
import {
EuiSelectable,
EuiHighlight,
EuiFilterButton,
EuiFlexGroup,
EuiFlexItem,
EuiHighlight,
EuiIcon,
EuiSelectable,
EuiTextColor,
EuiFilterButton,
EuiToolTip,
} from '@elastic/eui';
import { isEmpty, debounce } from 'lodash/fp';
import React, { memo, useCallback, useMemo, useState, useEffect, useRef } from 'react';
import { debounce, isEmpty } from 'lodash/fp';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import { type TimelineType, SortFieldTimelineEnum } from '../../../../../common/api/timeline';
import { SortFieldTimelineEnum, type TimelineType } from '../../../../../common/api/timeline';
import { useGetAllTimeline } from '../../../containers/all';
import { isUntitled } from '../../open_timeline/helpers';
@ -52,9 +52,7 @@ const TIMELINE_ITEM_HEIGHT = 50;
*/
const replaceTitleInOptions = (
options: EuiSelectableOption[]
): Array<
EuiSelectableOption<{ timelineTitle: string; description?: string; graphEveId?: string }>
> =>
): Array<EuiSelectableOption<{ timelineTitle: string; description?: string }>> =>
options.map(({ title, ...props }) => ({
...props,
title: undefined,
@ -77,11 +75,7 @@ export interface SelectableTimelineProps {
searchTimelineValue,
}: GetSelectableOptions) => EuiSelectableOption[];
onClosePopover: () => void;
onTimelineChange: (
timelineTitle: string,
timelineId: string | null,
graphEventId?: string
) => void;
onTimelineChange: (timelineTitle: string, timelineId: string | null) => void;
timelineType: TimelineType;
placeholder?: string;
}
@ -143,7 +137,6 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
EuiSelectableProps<{
timelineTitle: string;
description?: string;
graphEventId?: string;
favorite?: FavoriteTimelineResult[];
}>['renderOption']
>
@ -198,7 +191,6 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
EuiSelectableProps<{
timelineTitle: string;
description?: string;
graphEventId?: string;
}>['onChange']
>
>(
@ -211,8 +203,7 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
: selectedTimeline[0].timelineTitle,
selectedTimeline[0].id === '-1'
? null
: (selectedTimeline[0].id as unknown as string | null),
selectedTimeline[0].graphEventId ?? ''
: (selectedTimeline[0].id as unknown as string | null)
);
}
onClosePopover();
@ -288,7 +279,6 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
<EuiSelectable<{
timelineTitle: string;
description?: string;
graphEventId?: string;
favorite?: FavoriteTimelineResult[];
}>
data-test-subj="selectable-input"

View file

@ -14,7 +14,7 @@ import { TestProviders } from '../../../../../common/mock/test_providers';
import type { Props as EqlTabContentComponentProps } from '.';
import EqlTabContentComponent from '.';
import { TimelineId } from '../../../../../../common/types/timeline';
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
import { useTimelineEvents } from '../../../../containers';
import { useTimelineEventsDetails } from '../../../../containers/details';
import { useSourcererDataView } from '../../../../../sourcerer/containers';
@ -28,7 +28,6 @@ import { timelineActions } from '../../../../store';
import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer';
import { defaultRowRenderers } from '../../body/renderers';
import { useDispatch } from 'react-redux';
import { TimelineTabs } from '@kbn/securitysolution-data-table';
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
import { initialUserPrivilegesState } from '../../../../../common/components/user_privileges/user_privileges_context';

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo } from 'react';
import { timelineSelectors } from '../../../../store';
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
import type { TimelineId } from '../../../../../../common/types/timeline';
import { GraphOverlay } from '../../../graph_overlay';
import { useSessionViewNavigation } from '../session/use_session_view';
interface GraphTabContentProps {
timelineId: TimelineId;
}
const GraphTabContentComponent: React.FC<GraphTabContentProps> = ({ timelineId }) => {
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const graphEventId = useShallowEqualSelector(
(state) => getTimeline(state, timelineId)?.graphEventId
);
const { Navigation } = useSessionViewNavigation({
scopeId: timelineId,
});
if (!graphEventId) {
return null;
}
return (
<>
<GraphOverlay scopeId={timelineId} Navigation={Navigation} />
</>
);
};
GraphTabContentComponent.displayName = 'GraphTabContentComponent';
const GraphTabContent = React.memo(GraphTabContentComponent);
// eslint-disable-next-line import/no-default-export
export { GraphTabContent as default };

View file

@ -15,7 +15,6 @@ import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
import { TimelineTypeEnum } from '../../../../../common/api/timeline';
import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability';
import { render, screen, waitFor } from '@testing-library/react';
import { useLicense } from '../../../../common/hooks/use_license';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
jest.mock('../../../../common/hooks/use_license');
@ -112,21 +111,6 @@ describe('Timeline', () => {
});
});
describe('analyzer tab and session view tab', () => {
const analyzerTabSubj = `timelineTabs-${TimelineTabs.graph}`;
it('should not show the analyzer tab when the advanced setting is enabled', async () => {
mockUseUiSetting.mockReturnValue([true]);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
render(
<TestProviders>
<TabsContent {...defaultProps} />
</TestProviders>
);
expect(screen.queryByTestId(analyzerTabSubj)).not.toBeInTheDocument();
});
});
describe('privileges', () => {
it('should show notes and pinned tabs for users with the required privileges', () => {
(useUserPrivileges as jest.Mock).mockReturnValue({

View file

@ -69,10 +69,6 @@ const EqlTab = tabWithSuspense(
lazy(() => import('./eql')),
<TimelineTabFallback />
);
const GraphTab = tabWithSuspense(
lazy(() => import('./graph')),
<TimelineTabFallback />
);
const NotesTab = tabWithSuspense(
lazy(() => import('./notes')),
<TimelineTabFallback />
@ -92,7 +88,6 @@ interface BasicTimelineTab {
timelineFullScreen?: boolean;
timelineId: TimelineId;
timelineType: TimelineType;
graphEventId?: string;
timelineDescription: string;
}
@ -118,24 +113,6 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
() => isEsqlAdvancedSettingEnabled || timelineESQLSavedSearch != null,
[isEsqlAdvancedSettingEnabled, timelineESQLSavedSearch]
);
const getTab = useCallback(
(tab: TimelineTabs) => {
switch (tab) {
case TimelineTabs.graph:
return <GraphTab timelineId={timelineId} />;
case TimelineTabs.notes:
return <NotesTab timelineId={timelineId} />;
default:
return null;
}
},
[timelineId]
);
const isGraphOrNotesTabs = useMemo(
() => [TimelineTabs.graph, TimelineTabs.notes].includes(activeTimelineTab),
[activeTimelineTab]
);
return (
<>
@ -185,10 +162,10 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
)}
<LazyTimelineTabRenderer
timelineId={timelineId}
shouldShowTab={isGraphOrNotesTabs}
dataTestSubj={`timeline-tab-content-${TimelineTabs.graph}-${TimelineTabs.notes}`}
shouldShowTab={activeTimelineTab === TimelineTabs.notes}
dataTestSubj={`timeline-tab-content-${TimelineTabs.notes}`}
>
{isGraphOrNotesTabs ? getTab(activeTimelineTab) : null}
<NotesTab timelineId={timelineId} />
</LazyTimelineTabRenderer>
</>
);
@ -228,7 +205,6 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
timelineId,
timelineFullScreen,
timelineType,
graphEventId,
timelineDescription,
}) => {
const dispatch = useDispatch();
@ -353,14 +329,13 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
}, [setActiveTab, dispatch, timelineId]);
useEffect(() => {
// we redirect to the Query tab in the following situations:
// - for the Analyzer tab but the graphEventId is missing from the url
// - for urls with Timeline saved with the Session tab, we automatically (as Session View is now rendered in the flyout)
// we redirect to the Query tab when we try to load Timeline urls with the Session or Analyzer tabs active
// Session View and Analyzer graph are now only rendered in the flyout
// @ts-ignore
if ((!graphEventId && activeTab === TimelineTabs.graph) || activeTab === 'session') {
if (activeTab === 'graph' || activeTab === 'session') {
setQueryAsActiveTab();
}
}, [activeTab, graphEventId, setQueryAsActiveTab]);
}, [activeTab, setQueryAsActiveTab]);
return (
<>

View file

@ -1,29 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const CLOSE_ANALYZER = i18n.translate(
'xpack.securitySolution.timeline.graphOverlay.closeAnalyzerButton',
{
defaultMessage: 'Close analyzer',
}
);
export const CLOSE_SESSION = i18n.translate(
'xpack.securitySolution.timeline.graphOverlay.closeSessionButton',
{
defaultMessage: 'Close session viewer',
}
);
export const FULL_SCREEN = i18n.translate(
'xpack.securitySolution.timeline.graphOverlay.fullScreenButton',
{
defaultMessage: 'Full screen',
}
);

View file

@ -1,194 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { PropsWithChildren } from 'react';
import React, { memo } from 'react';
import { render, renderHook } from '@testing-library/react';
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
import { mockTimelineModel, TestProviders } from '../../../../../common/mock';
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
import {
useGlobalFullScreen,
useTimelineFullScreen,
} from '../../../../../common/containers/use_full_screen';
import { useSessionViewNavigation } from './use_session_view';
import { TableId } from '@kbn/securitysolution-data-table';
const mockDispatch = jest.fn();
jest.mock('../../../../../common/hooks/use_selector');
jest.mock('../../../../../common/containers/use_full_screen');
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
jest.mock('../../../../../common/lib/kibana', () => {
const originalModule = jest.requireActual('../../../../../common/lib/kibana');
return {
...originalModule,
useKibana: jest.fn().mockReturnValue({
services: {
application: {
navigateToApp: jest.fn(),
getUrlForApp: jest.fn(),
capabilities: {
siemV2: { crud_alerts: true, read_alerts: true },
},
},
data: {
search: jest.fn(),
query: jest.fn(),
},
uiSettings: {
get: jest.fn(),
},
savedObjects: {
client: {},
},
timelines: {
getLastUpdated: jest.fn(),
},
},
}),
};
});
describe('useSessionView with active timeline and graph event id', () => {
let setTimelineFullScreen: jest.Mock;
let setGlobalFullScreen: jest.Mock;
const Wrapper = memo<PropsWithChildren<unknown>>(({ children }) => {
return <TestProviders>{children}</TestProviders>;
});
Wrapper.displayName = 'Wrapper';
beforeEach(() => {
setTimelineFullScreen = jest.fn();
setGlobalFullScreen = jest.fn();
(useTimelineFullScreen as jest.Mock).mockImplementation(() => ({
setTimelineFullScreen,
}));
(useGlobalFullScreen as jest.Mock).mockImplementation(() => ({
setGlobalFullScreen,
}));
(useDeepEqualSelector as jest.Mock).mockImplementation(() => {
return {
...mockTimelineModel,
activeTab: TimelineTabs.graph,
graphEventId: 'current-graph-event-id',
show: true,
};
});
});
afterEach(() => {
(useDeepEqualSelector as jest.Mock).mockClear();
});
it('calls setTimelineFullScreen with false when onCloseOverlay is called and the app is not in full screen mode', () => {
const { result } = renderHook(
() => {
const testProps = {
scopeId: TimelineId.active,
};
return useSessionViewNavigation(testProps);
},
{ wrapper: Wrapper }
);
const navigation = result.current.Navigation;
const renderResult = render(<TestProviders>{navigation}</TestProviders>);
expect(renderResult.getByText('Close analyzer')).toBeTruthy();
});
describe('useSessionView with non active timeline and graph event id set', () => {
beforeEach(() => {
setTimelineFullScreen = jest.fn();
setGlobalFullScreen = jest.fn();
(useTimelineFullScreen as jest.Mock).mockImplementation(() => ({
setTimelineFullScreen,
}));
(useGlobalFullScreen as jest.Mock).mockImplementation(() => ({
setGlobalFullScreen,
}));
(useDeepEqualSelector as jest.Mock).mockImplementation(() => {
return {
...mockTimelineModel,
graphEventId: 'current-graph-event-id',
show: true,
};
});
});
afterEach(() => {
(useDeepEqualSelector as jest.Mock).mockClear();
});
it('renders the navigation component with the correct text for analyzer', () => {
const { result } = renderHook(
() => {
const testProps = {
scopeId: TableId.hostsPageEvents,
};
return useSessionViewNavigation(testProps);
},
{ wrapper: Wrapper }
);
const navigation = result.current.Navigation;
const renderResult = render(<TestProviders>{navigation}</TestProviders>);
expect(renderResult.getByText('Close analyzer')).toBeTruthy();
});
});
describe('useSessionViewNavigation should handle separate parts', () => {
beforeEach(() => {
setTimelineFullScreen = jest.fn();
setGlobalFullScreen = jest.fn();
(useTimelineFullScreen as jest.Mock).mockImplementation(() => ({
setTimelineFullScreen,
}));
(useGlobalFullScreen as jest.Mock).mockImplementation(() => ({
setGlobalFullScreen,
}));
(useDeepEqualSelector as jest.Mock).mockImplementation(() => {
return {
...mockTimelineModel,
activeTab: TimelineTabs.graph,
graphEventId: 'current-graph-event-id',
show: true,
};
});
});
afterEach(() => {
(useDeepEqualSelector as jest.Mock).mockClear();
});
it('useSessionViewNavigation should handle Navigation component and on close callback', () => {
const { result } = renderHook(
() => {
const testProps = {
scopeId: TableId.hostsPageEvents,
};
return useSessionViewNavigation(testProps);
},
{ wrapper: Wrapper }
);
expect(result.current).toHaveProperty('Navigation');
expect(result.current).toHaveProperty('onCloseOverlay');
expect(result.current).not.toHaveProperty('openDetailsPanel');
expect(result.current).not.toHaveProperty('shouldShowDetailsPanel');
expect(result.current).not.toHaveProperty('SessionView');
expect(result.current).not.toHaveProperty('DetailsPanel');
});
});
});

View file

@ -1,208 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useMemo } from 'react';
import { css } from '@emotion/react';
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
useEuiTheme,
} from '@elastic/eui';
import { useDispatch } from 'react-redux';
import { dataTableSelectors, tableDefaults } from '@kbn/securitysolution-data-table';
import {
getScopedActions,
isActiveTimeline,
isInTableScope,
isTimelineScope,
} from '../../../../../helpers';
import * as i18n from './translations';
import { TimelineTabs } from '../../../../../../common/types/timeline';
import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../../../common/constants';
import { EXIT_FULL_SCREEN } from '../../../../../common/components/exit_full_screen/translations';
import {
useGlobalFullScreen,
useTimelineFullScreen,
} from '../../../../../common/containers/use_full_screen';
import { timelineSelectors } from '../../../../store';
import { timelineDefaults } from '../../../../store/defaults';
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
import { isFullScreen } from '../../helpers';
interface NavigationProps {
fullScreen: boolean;
globalFullScreen: boolean;
onCloseOverlay: () => void;
isActiveTimelines: boolean;
timelineFullScreen: boolean;
toggleFullScreen: () => void;
graphEventId?: string;
activeTab?: TimelineTabs;
}
const NavigationComponent: React.FC<NavigationProps> = ({
fullScreen,
globalFullScreen,
onCloseOverlay,
isActiveTimelines,
timelineFullScreen,
toggleFullScreen,
graphEventId,
activeTab,
}) => {
const { euiTheme } = useEuiTheme();
const title = () => {
if (isActiveTimelines) {
return activeTab === TimelineTabs.graph ? i18n.CLOSE_ANALYZER : i18n.CLOSE_SESSION;
} else {
return graphEventId ? i18n.CLOSE_ANALYZER : i18n.CLOSE_SESSION;
}
};
return (
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="cross"
onClick={onCloseOverlay}
size="xs"
data-test-subj="close-overlay"
>
{title()}
</EuiButtonEmpty>
</EuiFlexItem>
{!isActiveTimelines && (
<EuiFlexItem grow={false}>
<EuiToolTip content={fullScreen ? EXIT_FULL_SCREEN : i18n.FULL_SCREEN}>
<EuiButtonIcon
aria-label={
isFullScreen({
globalFullScreen,
isActiveTimelines,
timelineFullScreen,
})
? EXIT_FULL_SCREEN
: i18n.FULL_SCREEN
}
display={fullScreen ? 'fill' : 'empty'}
color="primary"
data-test-subj="full-screen"
iconType="fullScreen"
onClick={toggleFullScreen}
css={css`
margin: ${euiTheme.size.xs} 0;
`}
/>
</EuiToolTip>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
NavigationComponent.displayName = 'NavigationComponent';
const Navigation = React.memo(NavigationComponent);
export const useSessionViewNavigation = ({ scopeId }: { scopeId: string }) => {
const dispatch = useDispatch();
const getScope = useMemo(() => {
if (isTimelineScope(scopeId)) {
return timelineSelectors.getTimelineByIdSelector();
} else if (isInTableScope(scopeId)) {
return dataTableSelectors.getTableByIdSelector();
}
}, [scopeId]);
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen();
const defaults = isTimelineScope(scopeId) ? timelineDefaults : tableDefaults;
const { graphEventId, activeTab } = useDeepEqualSelector((state) => ({
activeTab: timelineDefaults.activeTab,
prevActiveTab: timelineDefaults.prevActiveTab,
...((getScope && getScope(state, scopeId)) ?? defaults),
}));
const scopedActions = getScopedActions(scopeId);
const onCloseOverlay = useCallback(() => {
const isDataGridFullScreen = document.querySelector('.euiDataGrid--fullScreen') !== null;
// Since EUI changes these values directly as a side effect, need to add them back on close.
if (isDataGridFullScreen) {
if (isActiveTimeline(scopeId)) {
document.body.classList.add('euiDataGrid__restrictBody');
} else {
document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody');
}
} else {
if (isActiveTimeline(scopeId)) {
setTimelineFullScreen(false);
} else {
setGlobalFullScreen(false);
}
}
if (isActiveTimeline(scopeId) === false) {
if (scopedActions) {
dispatch(scopedActions.updateGraphEventId({ id: scopeId, graphEventId: '' }));
}
} else {
if (activeTab === TimelineTabs.graph) {
if (scopedActions) {
dispatch(scopedActions.updateGraphEventId({ id: scopeId, graphEventId: '' }));
}
}
}
}, [setTimelineFullScreen, setGlobalFullScreen, scopedActions, dispatch, scopeId, activeTab]);
const fullScreen = useMemo(
() =>
isFullScreen({
globalFullScreen,
isActiveTimelines: isActiveTimeline(scopeId),
timelineFullScreen,
}),
[globalFullScreen, scopeId, timelineFullScreen]
);
const toggleFullScreen = useCallback(() => {
if (isActiveTimeline(scopeId)) {
setTimelineFullScreen(!timelineFullScreen);
} else {
setGlobalFullScreen(!globalFullScreen);
}
}, [scopeId, setTimelineFullScreen, timelineFullScreen, setGlobalFullScreen, globalFullScreen]);
const navigation = useMemo(() => {
return (
<Navigation
fullScreen={fullScreen}
globalFullScreen={globalFullScreen}
activeTab={activeTab}
onCloseOverlay={onCloseOverlay}
isActiveTimelines={isActiveTimeline(scopeId)}
timelineFullScreen={timelineFullScreen}
toggleFullScreen={toggleFullScreen}
graphEventId={graphEventId}
/>
);
}, [
fullScreen,
globalFullScreen,
activeTab,
onCloseOverlay,
scopeId,
timelineFullScreen,
toggleFullScreen,
graphEventId,
]);
return {
onCloseOverlay,
Navigation: navigation,
};
};

View file

@ -9,7 +9,7 @@ import type { ComponentProps, FC, PropsWithChildren } from 'react';
import React, { useEffect } from 'react';
import type QueryTabContent from '.';
import { UnifiedTimeline } from '.';
import { TimelineId } from '../../../../../common/types/timeline';
import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
import { useTimelineEvents } from '../../../containers';
import { useTimelineEventsDetails } from '../../../containers/details';
import { useSourcererDataView } from '../../../../sourcerer/containers';
@ -20,7 +20,7 @@ import {
TestProviders,
} from '../../../../common/mock';
import { createMockStore } from '../../../../common/mock/create_store';
import { render, screen, fireEvent, cleanup, waitFor, within } from '@testing-library/react';
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { createStartServicesMock } from '../../../../common/lib/kibana/kibana_react.mock';
import type { StartServices } from '../../../../types';
import { useKibana } from '../../../../common/lib/kibana';
@ -29,7 +29,6 @@ import { timelineActions } from '../../../store';
import type { ExperimentalFeatures } from '../../../../../common';
import { allowedExperimentalValues } from '../../../../../common';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { TimelineTabs } from '@kbn/securitysolution-data-table';
import { DataLoadingState } from '@kbn/unified-data-table';
import { getColumnHeaders } from '../body/column_headers/helpers';
import { defaultUdtHeaders } from '../body/column_headers/default_headers';

View file

@ -644,7 +644,6 @@ describe('SiemLocalStorage', () => {
sort: [
{ columnId: '@timestamp', columnType: 'date', esTypes: ['date'], sortDirection: 'desc' },
],
graphEventId: undefined,
selectedEventIds: {},
selectAll: undefined,
id: 'alerts-page',
@ -885,7 +884,6 @@ describe('SiemLocalStorage', () => {
},
],
selectAll: false,
graphEventId: '',
columns: [
{
columnHeaderType: 'not-filtered',
@ -998,7 +996,6 @@ describe('SiemLocalStorage', () => {
},
],
selectAll: false,
graphEventId: '',
columns: [
{
columnHeaderType: 'not-filtered',
@ -1119,7 +1116,6 @@ describe('SiemLocalStorage', () => {
},
],
selectAll: false,
graphEventId: '',
columns: [
{
columnHeaderType: 'not-filtered',
@ -1555,7 +1551,6 @@ describe('SiemLocalStorage', () => {
sortDirection: 'desc',
},
],
graphEventId: undefined,
selectedEventIds: {},
selectAll: false,
id: 'alerts-page',

View file

@ -50,7 +50,6 @@ export const migrateLegacyTimelinesToSecurityDataTable = (legacyTimelineTables:
itemsPerPage: timelineModel.itemsPerPage,
itemsPerPageOptions: timelineModel.itemsPerPageOptions,
showCheckboxes: timelineModel.showCheckboxes,
graphEventId: timelineModel.graphEventId,
selectAll: timelineModel.selectAll,
id: timelineModel.id,
title: timelineModel.title,

View file

@ -62,10 +62,6 @@ export const removeProvider = actionCreator<{
andProviderId?: string;
}>('REMOVE_PROVIDER');
export const updateGraphEventId = actionCreator<{ id: string; graphEventId: string }>(
'UPDATE_TIMELINE_GRAPH_EVENT_ID'
);
export const unPinEvent = actionCreator<{ id: string; eventId: string }>('UN_PIN_EVENT');
export const updateTimeline = actionCreator<{

View file

@ -112,5 +112,4 @@ export const getTimelineManageDefaults = (id: string) => ({
isLoading: false,
queryFields: [],
title: '',
graphEventId: '',
});

View file

@ -7,7 +7,7 @@
import { cloneDeep } from 'lodash/fp';
import type { ColumnHeaderOptions } from '../../../common/types/timeline';
import { TimelineId, TimelineTabs } from '../../../common/types/timeline';
import { TimelineTabs } from '../../../common/types/timeline';
import {
DataProviderTypeEnum,
TimelineStatusEnum,
@ -32,7 +32,6 @@ import {
removeTimelineProvider,
updateTimelineColumns,
updateTimelineColumnWidth,
updateTimelineGraphEventId,
updateTimelineItemsPerPage,
updateTimelinePerPageOptions,
updateTimelineProviderEnabled,
@ -79,7 +78,7 @@ const basicDataProvider: DataProvider = {
};
const basicTimeline: TimelineModel = {
activeTab: TimelineTabs.query,
prevActiveTab: TimelineTabs.graph,
prevActiveTab: TimelineTabs.eql,
columns: [],
defaultColumns: [],
dataProviders: [{ ...basicDataProvider }],
@ -1818,77 +1817,6 @@ describe('Timeline', () => {
});
});
describe('#updateGraphEventId', () => {
test('should return a new reference and not the same reference', () => {
const update = updateTimelineGraphEventId({
id: 'foo',
graphEventId: '123',
timelineById: timelineByIdMock,
});
expect(update).not.toBe(timelineByIdMock);
});
test('should empty graphEventId', () => {
const update = updateTimelineGraphEventId({
id: 'foo',
graphEventId: '',
timelineById: timelineByIdMock,
});
expect(update.foo.graphEventId).toEqual('');
});
test('should empty graphEventId and not change activeTab and prevActiveTab because TimelineId !== TimelineId.active', () => {
const update = updateTimelineGraphEventId({
id: 'foo',
graphEventId: '',
timelineById: timelineByIdMock,
});
expect(update.foo.graphEventId).toEqual('');
expect(update.foo.activeTab).toEqual(timelineByIdMock.foo.activeTab);
expect(update.foo.prevActiveTab).toEqual(timelineByIdMock.foo.prevActiveTab);
});
test('should empty graphEventId if timeline is active and user go to another tab', () => {
const mock = cloneDeep(timelineByIdMock);
mock[TimelineId.active] = {
...timelineByIdMock.foo,
activeTab: TimelineTabs.eql,
prevActiveTab: TimelineTabs.graph,
};
delete mock.foo;
const update = updateTimelineGraphEventId({
id: TimelineId.active,
graphEventId: '',
timelineById: mock,
});
expect(update[TimelineId.active].graphEventId).toEqual('');
expect(update[TimelineId.active].activeTab).toEqual(TimelineTabs.eql);
expect(update[TimelineId.active].prevActiveTab).toEqual(TimelineTabs.graph);
});
test('should empty graphEventId and return to the previous tab if timeline is active and user close the graph', () => {
const mock = cloneDeep(timelineByIdMock);
mock[TimelineId.active] = {
...timelineByIdMock.foo,
activeTab: TimelineTabs.graph,
prevActiveTab: TimelineTabs.eql,
};
delete mock.foo;
const update = updateTimelineGraphEventId({
id: TimelineId.active,
graphEventId: '',
timelineById: mock,
});
expect(update[TimelineId.active].graphEventId).toEqual('');
expect(update[TimelineId.active].activeTab).toEqual(TimelineTabs.eql);
expect(update[TimelineId.active].prevActiveTab).toEqual(TimelineTabs.graph);
});
});
describe('#updateTimelineColumnWidth', () => {
let mockTimelineById: TimelineById;
beforeEach(() => {

View file

@ -28,7 +28,7 @@ import type {
SortColumnTimeline,
TimelinePersistInput,
} from '../../../common/types/timeline';
import { TimelineId, TimelineTabs } from '../../../common/types/timeline';
import { TimelineId } from '../../../common/types/timeline';
import { normalizeTimeRange } from '../../common/utils/normalize_time_range';
import { getTimelineManageDefaults, timelineDefaults } from './defaults';
import type { KqlMode, TimelineModel } from './model';
@ -246,32 +246,6 @@ export const updateTimelineShowTimeline = ({
};
};
export const updateTimelineGraphEventId = ({
id,
graphEventId,
timelineById,
}: {
id: string;
graphEventId: string;
timelineById: TimelineById;
}): TimelineById => {
const timeline = timelineById[id];
return {
...timelineById,
[id]: {
...timeline,
graphEventId,
// if use click close analyzer button, it will go back to the previous tab
...(graphEventId === '' &&
id === TimelineId.active &&
timeline.activeTab === TimelineTabs.graph
? { activeTab: timeline.prevActiveTab, prevActiveTab: timeline.activeTab }
: {}),
},
};
};
const queryMatchCustomizer = (dp1: QueryMatch, dp2: QueryMatch) => {
if (dp1.field === dp2.field && dp1.value === dp2.value && dp1.operator === dp2.operator) {
return true;

View file

@ -48,7 +48,6 @@ import {
updateDataProviderType,
updateDataView,
updateEqlOptions,
updateGraphEventId,
updateIsFavorite,
updateItemsPerPage,
updateItemsPerPageOptions,
@ -85,7 +84,6 @@ import {
updateSavedQuery,
updateTimelineColumns,
updateTimelineColumnWidth,
updateTimelineGraphEventId,
updateTimelineIsFavorite,
updateTimelineItemsPerPage,
updateTimelineKqlMode,
@ -161,14 +159,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: updateTimelineShowTimeline({ id, show, timelineById: state.timelineById }),
}))
.case(updateGraphEventId, (state, { id, graphEventId }) => ({
...state,
timelineById: updateTimelineGraphEventId({
id,
graphEventId,
timelineById: state.timelineById,
}),
}))
.case(pinEvent, (state, { id, eventId }) => ({
...state,
timelineById: pinTimelineEvent({ id, eventId, timelineById: state.timelineById }),