Skip to content
Closed

WIP #148827

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ export const allowedExperimentalValues = Object.freeze({
* Enables the alert details page currently only accessible via the alert details flyout and alert table context menu
*/
alertDetailsPageEnabled: false,

/**
* Enables the new security flyout over the current alert details flyout
*/
securityFlyoutEnabled: false,
/**
* Enables the `get-file` endpoint response action
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { EuiThemeProvider, useEuiTheme } from '@elastic/eui';
import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { SecurityFlyout } from '../../../flyout';
import { useSecuritySolutionNavigation } from '../../../common/components/navigation/use_security_solution_navigation';
import { TimelineId } from '../../../../common/types/timeline';
import { getTimelineShowStatusByIdSelector } from '../../../timelines/components/flyout/selectors';
Expand All @@ -25,6 +27,7 @@ import { useShowTimeline } from '../../../common/utils/timeline/use_show_timelin
import { useShowPagesWithEmptyView } from '../../../common/utils/empty_view/use_show_pages_with_empty_view';
import { useIsPolicySettingsBarVisible } from '../../../management/pages/policy/view/policy_hooks';
import { useIsGroupedNavigationEnabled } from '../../../common/components/navigation/helpers';
import { useSourcererDataView } from '../../../common/containers/sourcerer';

const NO_DATA_PAGE_MAX_WIDTH = 950;

Expand Down Expand Up @@ -59,6 +62,8 @@ export const SecuritySolutionTemplateWrapper: React.FC<Omit<KibanaPageTemplatePr
const solutionNav = useSecuritySolutionNavigation();
const isPolicySettingsVisible = useIsPolicySettingsBarVisible();
const [isTimelineBottomBarVisible] = useShowTimeline();
const { browserFields, runtimeMappings } = useSourcererDataView(SourcererScopeName.timeline);

const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []);
const { show: isShowingTimelineOverlay } = useDeepEqualSelector((state) =>
getTimelineShowStatus(state, TimelineId.active)
Expand Down Expand Up @@ -107,6 +112,7 @@ export const SecuritySolutionTemplateWrapper: React.FC<Omit<KibanaPageTemplatePr
</EuiThemeProvider>
</KibanaPageTemplate.BottomBar>
)}
<SecurityFlyout flyoutScope="globalFlyout" />
</StyledKibanaPageTemplate>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/
import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy';
import type { ColumnHeaderOptions, OnRowSelected } from '../../../../../common/types/timeline';
import { dataTableActions } from '../../../store/data_table';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
import { openSecurityFlyoutByScope } from '../../../store/flyout/actions';

type Props = EuiDataGridCellValueElementProps & {
columnHeaders: ColumnHeaderOptions[];
Expand Down Expand Up @@ -71,6 +73,7 @@ const RowActionComponent = ({
}, [data, pageRowIndex]);

const dispatch = useDispatch();
const isSecurityFlyoutEnabled = useIsExperimentalFeatureEnabled('securityFlyoutEnabled');

const columnValues = useMemo(
() =>
Expand All @@ -96,14 +99,29 @@ const RowActionComponent = ({
},
};

dispatch(
dataTableActions.toggleDetailPanel({
...updatedExpandedDetail,
tabType,
id: tableId,
})
);
}, [dispatch, eventId, indexName, tabType, tableId]);
if (isSecurityFlyoutEnabled && eventId && indexName) {
dispatch(
openSecurityFlyoutByScope({
flyoutScope: 'globalFlyout',
right: {
panelKind: 'event',
params: {
eventId,
indexName,
},
},
})
);
} else {
dispatch(
dataTableActions.toggleDetailPanel({
...updatedExpandedDetail,
tabType,
id: tableId,
})
);
}
}, [dispatch, eventId, indexName, isSecurityFlyoutEnabled, tabType, tableId]);

const Action = controlColumn.rowCellRender;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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 type { EuiFlyoutProps } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiFlyout } from '@elastic/eui';
import { css } from '@emotion/react';
import { useExpandableFlyoutContext } from '../../../flyout/context';
import type { SecurityFlyoutPanel } from '../../store/flyout/model';

// The expandable flyout should only worry about visual information and rendering components based on the ID provided.
// This *should* be able to be exported to a package
export interface ExpandableFlyoutViews {
panelKind?: string;
component: (props: SecurityFlyoutPanel) => React.ReactElement; // TODO: genericize SecurityFlyoutPanel to allow it to work in any solution
size: number;
}

export interface ExpandableFlyoutProps extends EuiFlyoutProps {
panels: ExpandableFlyoutViews[];
}

export const ExpandableFlyout: React.FC<ExpandableFlyoutProps> = ({ panels, ...flyoutProps }) => {
const { flyoutPanels } = useExpandableFlyoutContext();
const { left, right, preview } = flyoutPanels;

const leftSection = useMemo(
() => panels.find((panel) => panel.panelKind === left?.panelKind),
[left, panels]
);

const rightSection = useMemo(
() => panels.find((panel) => panel.panelKind === right?.panelKind),
[right, panels]
);

// const previewSection = useMemo(
// () => panels.find((panel) => panel.panelKind === preview?.panelKind),
// [preview, panels]
// );

const flyoutSize = (leftSection?.size ?? 0) + (rightSection?.size ?? 0);
return (
<EuiFlyout
css={css`
overflow-y: scroll;
`}
{...flyoutProps}
size={flyoutSize}
ownFocus={false}
>
<EuiFlexGroup
direction={leftSection ? 'row' : 'column'}
wrap={false}
style={{ height: '100%' }}
>
{leftSection && left ? (
<EuiFlexItem grow>
<EuiFlexGroup direction="column" style={{ maxWidth: leftSection.size, width: 'auto' }}>
{leftSection.component({ ...left })}
</EuiFlexGroup>
</EuiFlexItem>
) : null}
{rightSection && right ? (
<EuiFlexItem grow={false} style={{ height: '100%', borderLeft: '1px solid #ccc' }}>
<EuiFlexGroup direction="column" style={{ width: rightSection.size }}>
{rightSection.component({ ...right })}
</EuiFlexGroup>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFlyout>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { useQuery } from '@tanstack/react-query';
import { useHttp } from '../../lib/kibana';
import { useTimelineDataFilters } from '../../../timelines/containers/use_timeline_data_filters';
import { useGlobalOrTimelineFilters } from '../../hooks/use_global_or_timeline_filters';

export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count';

Expand Down Expand Up @@ -99,7 +99,7 @@ export function useAlertPrevalenceFromProcessTree({
}: UseAlertPrevalenceFromProcessTree): UserAlertPrevalenceFromProcessTreeResult {
const http = useHttp();

const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline);
const { selectedPatterns } = useGlobalOrTimelineFilters(isActiveTimeline);
const alertAndOriginalIndices = [...new Set(selectedPatterns.concat(indices))];
const { loading, id, schema } = useAlertDocumentAnalyzerSchema({
documentId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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 { useInitFlyoutsFromUrlParam } from './use_init_flyouts_url_params';
import { useSyncFlyoutsUrlParam } from './use_sync_flyouts_url_params';

export const useFlyoutsUrlStateSync = () => {
useInitFlyoutsFromUrlParam();
useSyncFlyoutsUrlParam();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 { useCallback } from 'react';

import { useDispatch } from 'react-redux';
import { initializeSecurityFlyoutFromUrl } from '../../store/flyout/actions';
import type { SecurityFlyoutState } from '../../store/flyout/model';
import { useInitializeUrlParam } from '../../utils/global_query_string';
import { URL_PARAM_KEY } from '../use_url_state';

export const useInitFlyoutsFromUrlParam = () => {
const dispatch = useDispatch();

const onInitialize = useCallback(
(initialState: Required<SecurityFlyoutState> | null) => {
if (initialState != null) {
dispatch(initializeSecurityFlyoutFromUrl(initialState));
}
},
[dispatch]
);

useInitializeUrlParam(URL_PARAM_KEY.flyouts, onInitialize);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 { useEffect } from 'react';

import { useSelector } from 'react-redux';
import { useUpdateUrlParam } from '../../utils/global_query_string';
import { URL_PARAM_KEY } from '../use_url_state';
import { flyoutsSelector } from '../../store/flyout/selectors';
import type { SecurityFlyoutReducerByScope } from '../../store/flyout/model';
import { areUrlParamsValidSecurityFlyoutParams } from '../../store/flyout/helpers';

export const useSyncFlyoutsUrlParam = () => {
const updateUrlParam = useUpdateUrlParam<SecurityFlyoutReducerByScope>(URL_PARAM_KEY.flyouts);
const flyouts = useSelector(flyoutsSelector);

useEffect(() => {
if (areUrlParamsValidSecurityFlyoutParams(flyouts)) {
// TODO: It may be better to allow for graceful failure of either flyout rather than making them interdependent
// When they shouldn't be in any way
updateUrlParam(flyouts);
} else {
updateUrlParam(null);
}
}, [flyouts, updateUrlParam]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import {
mockGlobalState,
SUB_PLUGINS_REDUCER,
TestProviders,
kibanaObservable,
createSecuritySolutionStorageMock,
} from '../mock';
import { useGlobalOrTimelineFilters } from './use_global_or_timeline_filters';
import { createStore } from '../store';
import React from 'react';
import { SourcererScopeName } from '../store/sourcerer/model';

jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname }) };
});

const defaultDataViewPattern = 'test-dataview-patterns';
const timelinePattern = 'test-timeline-patterns';
const alertsPagePatterns = '.siem-signals-spacename';
const pathname = '/alerts';
const { storage } = createSecuritySolutionStorageMock();
const store = createStore(
{
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
defaultDataView: {
...mockGlobalState.sourcerer.defaultDataView,
patternList: [defaultDataViewPattern],
},
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
selectedPatterns: [timelinePattern],
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);

const wrapper = ({ children }: { children: React.ReactNode }) => (
<TestProviders store={store}>{children}</TestProviders>
);

describe('useGlobalOrTimelineFilters', () => {
describe('on alerts page', () => {
it('returns default data view patterns and alerts page patterns when isActiveTimelines is falsy', () => {
const isActiveTimelines = false;
const { result } = renderHook(() => useGlobalOrTimelineFilters(isActiveTimelines), {
wrapper,
});

expect(result.current.selectedPatterns).toEqual([alertsPagePatterns, defaultDataViewPattern]);
});

it('returns default data view patterns and timelinePatterns when isActiveTimelines is truthy', () => {
const isActiveTimelines = true;
const { result } = renderHook(() => useGlobalOrTimelineFilters(isActiveTimelines), {
wrapper,
});

expect(result.current.selectedPatterns).toEqual([timelinePattern, defaultDataViewPattern]);
});
});
});
Loading