mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Alert redirects w/ new flyout and share button (#157956)
## Summary
This PR ensures that alert redirection mechanism works the same way it
does for the legacy flyout (eg. that redirects done with
`kibana.alert.url` work),
and it introduces the share button, whenever `kibana.alert.url` is
present:

### Testing
The usual approach with switching the flag applies, it should work with
any alert created with detection rules.
### Checklist
Delete any items that are not applicable to this PR.
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [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
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Philippe Oberti <philippe.oberti@elastic.co>
This commit is contained in:
parent
6abee5af88
commit
96e4a83dcb
12 changed files with 328 additions and 36 deletions
|
@ -16,12 +16,11 @@ import {
|
|||
} from '../../../common/mock';
|
||||
import { createStore } from '../../../common/store';
|
||||
import { kibanaObservable } from '@kbn/timelines-plugin/public/mock';
|
||||
import {
|
||||
ALERTS_PATH,
|
||||
ALERT_DETAILS_REDIRECT_PATH,
|
||||
DEFAULT_ALERTS_INDEX,
|
||||
} from '../../../../common/constants';
|
||||
import { ALERTS_PATH, ALERT_DETAILS_REDIRECT_PATH } from '../../../../common/constants';
|
||||
import { mockHistory } from '../../../common/utils/route/mocks';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
|
||||
jest.mock('../../../common/hooks/use_experimental_features');
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
|
@ -67,7 +66,7 @@ describe('AlertDetailsRedirect', () => {
|
|||
expect(historyMock.replace).toHaveBeenCalledWith({
|
||||
hash: '',
|
||||
pathname: ALERTS_PATH,
|
||||
search: `?query=(language:kuery,query:'_id: ${testAlertId}')&timerange=(global:(linkTo:!(timeline,socTrends),timerange:(from:'${testTimestamp}',kind:absolute,to:'2023-04-20T12:05:00.000Z')),timeline:(linkTo:!(global,socTrends),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now/d,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now/d)))&pageFilters=!((exclude:!f,existsSelected:!f,fieldName:kibana.alert.workflow_status,selectedOptions:!(),title:Status))&eventFlyout=(panelView:eventDetail,params:(eventId:${testAlertId},indexName:${testIndex}))`,
|
||||
search: `?query=%28language%3Akuery%2Cquery%3A%27_id%3A+test-alert-id%27%29&timerange=%28global%3A%28linkTo%3A%21%28timeline%2CsocTrends%29%2Ctimerange%3A%28from%3A%272023-04-20T12%3A00%3A00.000Z%27%2Ckind%3Aabsolute%2Cto%3A%272023-04-20T12%3A05%3A00.000Z%27%29%29%2Ctimeline%3A%28linkTo%3A%21%28global%2CsocTrends%29%2Ctimerange%3A%28from%3A%272020-07-07T08%3A20%3A18.966Z%27%2CfromStr%3Anow%2Fd%2Ckind%3Arelative%2Cto%3A%272020-07-08T08%3A20%3A18.966Z%27%2CtoStr%3Anow%2Fd%29%29%29&pageFilters=%21%28%28exclude%3A%21f%2CexistsSelected%3A%21f%2CfieldName%3Akibana.alert.workflow_status%2CselectedOptions%3A%21%28%29%2Ctitle%3AStatus%29%29&eventFlyout=%28panelView%3AeventDetail%2Cparams%3A%28eventId%3Atest-alert-id%2CindexName%3A.someTestIndex%29%29`,
|
||||
state: undefined,
|
||||
});
|
||||
});
|
||||
|
@ -96,7 +95,7 @@ describe('AlertDetailsRedirect', () => {
|
|||
expect(historyMock.replace).toHaveBeenCalledWith({
|
||||
hash: '',
|
||||
pathname: ALERTS_PATH,
|
||||
search: `?query=(language:kuery,query:'_id: ${testAlertId}')&timerange=(global:(linkTo:!(timeline,socTrends),timerange:(from:'2020-07-07T08:20:18.966Z',kind:absolute,to:'2020-07-08T08:25:18.966Z')),timeline:(linkTo:!(global,socTrends),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now/d,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now/d)))&pageFilters=!((exclude:!f,existsSelected:!f,fieldName:kibana.alert.workflow_status,selectedOptions:!(),title:Status))&eventFlyout=(panelView:eventDetail,params:(eventId:${testAlertId},indexName:${testIndex}))`,
|
||||
search: `?query=%28language%3Akuery%2Cquery%3A%27_id%3A+test-alert-id%27%29&timerange=%28global%3A%28linkTo%3A%21%28timeline%2CsocTrends%29%2Ctimerange%3A%28from%3A%272020-07-07T08%3A20%3A18.966Z%27%2Ckind%3Aabsolute%2Cto%3A%272020-07-08T08%3A25%3A18.966Z%27%29%29%2Ctimeline%3A%28linkTo%3A%21%28global%2CsocTrends%29%2Ctimerange%3A%28from%3A%272020-07-07T08%3A20%3A18.966Z%27%2CfromStr%3Anow%2Fd%2Ckind%3Arelative%2Cto%3A%272020-07-08T08%3A20%3A18.966Z%27%2CtoStr%3Anow%2Fd%29%29%29&pageFilters=%21%28%28exclude%3A%21f%2CexistsSelected%3A%21f%2CfieldName%3Akibana.alert.workflow_status%2CselectedOptions%3A%21%28%29%2Ctitle%3AStatus%29%29&eventFlyout=%28panelView%3AeventDetail%2Cparams%3A%28eventId%3Atest-alert-id%2CindexName%3A.someTestIndex%29%29`,
|
||||
state: undefined,
|
||||
});
|
||||
});
|
||||
|
@ -124,9 +123,42 @@ describe('AlertDetailsRedirect', () => {
|
|||
expect(historyMock.replace).toHaveBeenCalledWith({
|
||||
hash: '',
|
||||
pathname: ALERTS_PATH,
|
||||
search: `?query=(language:kuery,query:'_id: ${testAlertId}')&timerange=(global:(linkTo:!(timeline,socTrends),timerange:(from:'2020-07-07T08:20:18.966Z',kind:absolute,to:'2020-07-08T08:25:18.966Z')),timeline:(linkTo:!(global,socTrends),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now/d,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now/d)))&pageFilters=!((exclude:!f,existsSelected:!f,fieldName:kibana.alert.workflow_status,selectedOptions:!(),title:Status))&eventFlyout=(panelView:eventDetail,params:(eventId:${testAlertId},indexName:.internal${DEFAULT_ALERTS_INDEX}-default))`,
|
||||
search: `?query=%28language%3Akuery%2Cquery%3A%27_id%3A+test-alert-id%27%29&timerange=%28global%3A%28linkTo%3A%21%28timeline%2CsocTrends%29%2Ctimerange%3A%28from%3A%272020-07-07T08%3A20%3A18.966Z%27%2Ckind%3Aabsolute%2Cto%3A%272020-07-08T08%3A25%3A18.966Z%27%29%29%2Ctimeline%3A%28linkTo%3A%21%28global%2CsocTrends%29%2Ctimerange%3A%28from%3A%272020-07-07T08%3A20%3A18.966Z%27%2CfromStr%3Anow%2Fd%2Ckind%3Arelative%2Cto%3A%272020-07-08T08%3A20%3A18.966Z%27%2CtoStr%3Anow%2Fd%29%29%29&pageFilters=%21%28%28exclude%3A%21f%2CexistsSelected%3A%21f%2CfieldName%3Akibana.alert.workflow_status%2CselectedOptions%3A%21%28%29%2Ctitle%3AStatus%29%29&eventFlyout=%28panelView%3AeventDetail%2Cparams%3A%28eventId%3Atest-alert-id%2CindexName%3A.internal.alerts-security.alerts-default%29%29`,
|
||||
state: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When expandable flyout is enabled', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(useIsExperimentalFeatureEnabled).mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe('when eventFlyout is not in the query', () => {
|
||||
it('redirects to the expected path with the correct query parameters', () => {
|
||||
const testSearch = `?index=${testIndex}×tamp=${testTimestamp}`;
|
||||
const historyMock = {
|
||||
...mockHistory,
|
||||
location: {
|
||||
hash: '',
|
||||
pathname: mockPathname,
|
||||
search: testSearch,
|
||||
state: '',
|
||||
},
|
||||
};
|
||||
render(
|
||||
<TestProviders store={store}>
|
||||
<Router history={historyMock}>
|
||||
<AlertDetailsRedirect />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const [{ search, pathname }] = historyMock.replace.mock.lastCall;
|
||||
|
||||
expect(search as string).toMatch(/eventFlyout.*right/);
|
||||
expect(pathname).toEqual(ALERTS_PATH);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,6 +17,9 @@ import { ALERTS_PATH, DEFAULT_ALERTS_INDEX } from '../../../../common/constants'
|
|||
import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state';
|
||||
import { inputsSelectors } from '../../../common/store';
|
||||
import { formatPageFilterSearchParam } from '../../../../common/utils/format_page_filter_search_param';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { resolveFlyoutParams } from './utils';
|
||||
import { FLYOUT_URL_PARAM } from '../../../flyout/url/use_sync_flyout_state_with_url';
|
||||
|
||||
export const AlertDetailsRedirect = () => {
|
||||
const { alertId } = useParams<{ alertId: string }>();
|
||||
|
@ -54,14 +57,6 @@ export const AlertDetailsRedirect = () => {
|
|||
},
|
||||
});
|
||||
|
||||
const flyoutString = encode({
|
||||
panelView: 'eventDetail',
|
||||
params: {
|
||||
eventId: alertId,
|
||||
indexName: index,
|
||||
},
|
||||
});
|
||||
|
||||
const kqlAppQuery = encode({ language: 'kuery', query: `_id: ${alertId}` });
|
||||
|
||||
const statusPageFilter: FilterItemObj = {
|
||||
|
@ -73,7 +68,21 @@ export const AlertDetailsRedirect = () => {
|
|||
|
||||
const pageFiltersQuery = encode(formatPageFilterSearchParam([statusPageFilter]));
|
||||
|
||||
const url = `${ALERTS_PATH}?${URL_PARAM_KEY.appQuery}=${kqlAppQuery}&${URL_PARAM_KEY.timerange}=${timerange}&${URL_PARAM_KEY.pageFilter}=${pageFiltersQuery}&${URL_PARAM_KEY.eventFlyout}=${flyoutString}`;
|
||||
const currentFlyoutParams = searchParams.get(FLYOUT_URL_PARAM);
|
||||
|
||||
const isSecurityFlyoutEnabled = useIsExperimentalFeatureEnabled('securityFlyoutEnabled');
|
||||
|
||||
const urlParams = new URLSearchParams({
|
||||
[URL_PARAM_KEY.appQuery]: kqlAppQuery,
|
||||
[URL_PARAM_KEY.timerange]: timerange,
|
||||
[URL_PARAM_KEY.pageFilter]: pageFiltersQuery,
|
||||
[URL_PARAM_KEY.eventFlyout]: resolveFlyoutParams(
|
||||
{ index, alertId, isSecurityFlyoutEnabled },
|
||||
currentFlyoutParams
|
||||
),
|
||||
});
|
||||
|
||||
const url = `${ALERTS_PATH}?${urlParams.toString()}`;
|
||||
|
||||
return <Redirect to={url} />;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { encode } from '@kbn/rison';
|
||||
import { expandableFlyoutStateFromEventMeta } from '../../../flyout/url/expandable_flyout_state_from_event_meta';
|
||||
|
||||
export interface ResolveFlyoutParamsConfig {
|
||||
index: string;
|
||||
alertId: string;
|
||||
isSecurityFlyoutEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves url parameters for the flyout, serialized as
|
||||
* rison string. NOTE: if user is already redirected to this route with flyout parameters set,
|
||||
* we simply use them. It will be the case when users are coming here using a link obtained
|
||||
* with Share Button on the Expandable Flyout
|
||||
*/
|
||||
export const resolveFlyoutParams = (
|
||||
{ index, alertId, isSecurityFlyoutEnabled }: ResolveFlyoutParamsConfig,
|
||||
currentParamsString: string | null
|
||||
) => {
|
||||
if (!isSecurityFlyoutEnabled) {
|
||||
const legacyFlyoutString = encode({
|
||||
panelView: 'eventDetail',
|
||||
params: {
|
||||
eventId: alertId,
|
||||
indexName: index,
|
||||
},
|
||||
});
|
||||
return legacyFlyoutString;
|
||||
}
|
||||
|
||||
if (currentParamsString) {
|
||||
return currentParamsString;
|
||||
}
|
||||
|
||||
const modernFlyoutString = encode(
|
||||
expandableFlyoutStateFromEventMeta({ index, eventId: alertId, scopeId: 'alerts-page' })
|
||||
);
|
||||
|
||||
return modernFlyoutString;
|
||||
};
|
|
@ -11,6 +11,7 @@ import { RightPanelContext } from '../context';
|
|||
import {
|
||||
FLYOUT_HEADER_RISK_SCORE_VALUE_TEST_ID,
|
||||
FLYOUT_HEADER_SEVERITY_TITLE_TEST_ID,
|
||||
FLYOUT_HEADER_SHARE_BUTTON_TEST_ID,
|
||||
FLYOUT_HEADER_TITLE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { HeaderTitle } from './header_title';
|
||||
|
@ -80,6 +81,36 @@ describe('<HeaderTitle />', () => {
|
|||
expect(getByTestId(FLYOUT_HEADER_TITLE_TEST_ID)).toHaveTextContent('test');
|
||||
});
|
||||
|
||||
it('should render share button in the title if document is an alert', () => {
|
||||
const contextValue = {
|
||||
dataFormattedForFieldBrowser: [
|
||||
{
|
||||
category: 'kibana',
|
||||
field: 'kibana.alert.rule.uuid',
|
||||
values: ['123'],
|
||||
originalValue: ['123'],
|
||||
isObjectArray: false,
|
||||
},
|
||||
{
|
||||
category: 'kibana',
|
||||
field: 'kibana.alert.url',
|
||||
values: ['http://kibana.url/alert/id'],
|
||||
originalValue: ['http://kibana.url/alert/id'],
|
||||
isObjectArray: false,
|
||||
},
|
||||
],
|
||||
getFieldsData: () => [],
|
||||
} as unknown as RightPanelContext;
|
||||
|
||||
const { getByTestId } = render(
|
||||
<RightPanelContext.Provider value={contextValue}>
|
||||
<HeaderTitle />
|
||||
</RightPanelContext.Provider>
|
||||
);
|
||||
|
||||
expect(getByTestId(FLYOUT_HEADER_SHARE_BUTTON_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render default document detail title if document is not an alert', () => {
|
||||
const contextValue = {
|
||||
dataFormattedForFieldBrowser: [
|
||||
|
|
|
@ -16,20 +16,28 @@ import { useBasicDataFromDetailsData } from '../../../timelines/components/side_
|
|||
import { useRightPanelContext } from '../context';
|
||||
import { PreferenceFormattedDate } from '../../../common/components/formatted_date';
|
||||
import { FLYOUT_HEADER_TITLE_TEST_ID } from './test_ids';
|
||||
import { ShareButton } from './share_button';
|
||||
|
||||
/**
|
||||
* Document details flyout right section header
|
||||
*/
|
||||
export const HeaderTitle: FC = memo(() => {
|
||||
const { dataFormattedForFieldBrowser } = useRightPanelContext();
|
||||
const { isAlert, ruleName, timestamp } = useBasicDataFromDetailsData(
|
||||
const { isAlert, ruleName, timestamp, alertUrl } = useBasicDataFromDetailsData(
|
||||
dataFormattedForFieldBrowser
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="s" data-test-subj={FLYOUT_HEADER_TITLE_TEST_ID}>
|
||||
<h4>{isAlert && !isEmpty(ruleName) ? ruleName : DOCUMENT_DETAILS}</h4>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<h4>{isAlert && !isEmpty(ruleName) ? ruleName : DOCUMENT_DETAILS}</h4>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{isAlert && alertUrl && <ShareButton alertUrl={alertUrl} />}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
{timestamp && <PreferenceFormattedDate value={new Date(timestamp)} />}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { copyToClipboard } from '@elastic/eui';
|
||||
import { ShareButton } from './share_button';
|
||||
import React from 'react';
|
||||
import { FLYOUT_URL_PARAM } from '../../url/use_sync_flyout_state_with_url';
|
||||
import { FLYOUT_HEADER_SHARE_BUTTON_TEST_ID } from './test_ids';
|
||||
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
...jest.requireActual('@elastic/eui'),
|
||||
copyToClipboard: jest.fn(),
|
||||
EuiCopy: jest.fn(({ children: functionAsChild }) => functionAsChild(jest.fn())),
|
||||
}));
|
||||
|
||||
describe('ShareButton', () => {
|
||||
const alertUrl = 'https://example.com/alert';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the share button', () => {
|
||||
render(<ShareButton alertUrl={alertUrl} />);
|
||||
|
||||
expect(screen.getByTestId(FLYOUT_HEADER_SHARE_BUTTON_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('copies the alert URL to clipboard', () => {
|
||||
const syncedFlyoutState = 'flyoutState';
|
||||
const query = `?${FLYOUT_URL_PARAM}=${syncedFlyoutState}`;
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
search: query,
|
||||
},
|
||||
});
|
||||
|
||||
render(<ShareButton alertUrl={alertUrl} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId(FLYOUT_HEADER_SHARE_BUTTON_TEST_ID));
|
||||
|
||||
expect(copyToClipboard).toHaveBeenCalledWith(
|
||||
`${alertUrl}&${FLYOUT_URL_PARAM}=${syncedFlyoutState}`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { copyToClipboard, EuiButtonEmpty, EuiCopy } from '@elastic/eui';
|
||||
import type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { FLYOUT_URL_PARAM } from '../../url/use_sync_flyout_state_with_url';
|
||||
import { FLYOUT_HEADER_SHARE_BUTTON_TEST_ID } from './test_ids';
|
||||
import { SHARE } from './translations';
|
||||
|
||||
interface ShareButtonProps {
|
||||
/**
|
||||
* Url retrieved from the kibana.alert.url field of the document
|
||||
*/
|
||||
alertUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts alertUrl to user's clipboard. If current query string contains synced flyout state,
|
||||
* it will be appended to the base alertUrl
|
||||
*/
|
||||
export const ShareButton: FC<ShareButtonProps> = ({ alertUrl }) => {
|
||||
return (
|
||||
<EuiCopy textToCopy={alertUrl}>
|
||||
{(copy) => (
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
// NOTE: currently, it is not possible to have textToCopy computed dynamically.
|
||||
// so, we are calling copy() here to trigger the ui tooltip, and then override the link manually
|
||||
copy();
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const alertDetailsLink = `${alertUrl}&${FLYOUT_URL_PARAM}=${query.get(
|
||||
FLYOUT_URL_PARAM
|
||||
)}`;
|
||||
copyToClipboard(alertDetailsLink);
|
||||
}}
|
||||
iconType="share"
|
||||
data-test-subj={FLYOUT_HEADER_SHARE_BUTTON_TEST_ID}
|
||||
>
|
||||
{SHARE}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiCopy>
|
||||
);
|
||||
};
|
||||
|
||||
ShareButton.displayName = 'ShareButton';
|
|
@ -21,6 +21,8 @@ export const FLYOUT_HEADER_RISK_SCORE_TITLE_TEST_ID =
|
|||
'securitySolutionAlertDetailsFlyoutHeaderRiskScoreTitle';
|
||||
export const FLYOUT_HEADER_RISK_SCORE_VALUE_TEST_ID =
|
||||
'securitySolutionAlertDetailsFlyoutHeaderRiskScoreValue';
|
||||
export const FLYOUT_HEADER_SHARE_BUTTON_TEST_ID =
|
||||
'securitySolutionAlertDetailsFlyoutHeaderShareButton';
|
||||
|
||||
/* Description section */
|
||||
|
||||
|
|
|
@ -279,6 +279,10 @@ export const ANALYZER_PREVIEW_TEXT = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SHARE = i18n.translate('xpack.securitySolution.flyout.documentDetails.share', {
|
||||
defaultMessage: 'Share Alert',
|
||||
});
|
||||
|
||||
export const INVESTIGATION_GUIDE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.investigationGuideText',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { ExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { RightPanelKey } from '../right';
|
||||
|
||||
interface RedirectParams {
|
||||
index: string;
|
||||
eventId: string;
|
||||
scopeId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds flyout state from basic event-related data, such as index name, event id and scope id.
|
||||
* This value can be used to open the flyout either by passing it directly to the flyout api (exposed via ref) or
|
||||
* by serializing it to the url & performing a redirect
|
||||
*/
|
||||
export const expandableFlyoutStateFromEventMeta = ({
|
||||
index,
|
||||
eventId,
|
||||
scopeId,
|
||||
}: RedirectParams): ExpandableFlyoutContext['panels'] => {
|
||||
return {
|
||||
right: {
|
||||
id: RightPanelKey,
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName: index,
|
||||
scopeId,
|
||||
},
|
||||
},
|
||||
left: undefined,
|
||||
preview: [],
|
||||
};
|
||||
};
|
|
@ -9,8 +9,9 @@ import { useCallback, useRef } from 'react';
|
|||
import type { ExpandableFlyoutApi, ExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { useSyncToUrl } from '@kbn/url-state';
|
||||
import last from 'lodash/last';
|
||||
import { URL_PARAM_KEY } from '../../common/hooks/use_url_state';
|
||||
|
||||
const URL_KEY = 'eventFlyout' as const;
|
||||
export const FLYOUT_URL_PARAM = URL_PARAM_KEY.eventFlyout;
|
||||
|
||||
type FlyoutState = Parameters<ExpandableFlyoutApi['openFlyout']>[0];
|
||||
|
||||
|
@ -21,7 +22,7 @@ type FlyoutState = Parameters<ExpandableFlyoutApi['openFlyout']>[0];
|
|||
export const useSyncFlyoutStateWithUrl = () => {
|
||||
const flyoutApi = useRef<ExpandableFlyoutApi>(null);
|
||||
|
||||
const syncStateToUrl = useSyncToUrl<FlyoutState>(URL_KEY, (data) => {
|
||||
const syncStateToUrl = useSyncToUrl<FlyoutState>(FLYOUT_URL_PARAM, (data) => {
|
||||
flyoutApi.current?.openFlyout(data);
|
||||
});
|
||||
|
||||
|
|
|
@ -12,16 +12,18 @@ import { getFieldValue } from '../../../../detections/components/host_isolation/
|
|||
import { DEFAULT_ALERTS_INDEX, DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants';
|
||||
|
||||
export interface GetBasicDataFromDetailsData {
|
||||
alertId: string;
|
||||
agentId?: string;
|
||||
isAlert: boolean;
|
||||
hostName: string;
|
||||
userName: string;
|
||||
ruleName: string;
|
||||
timestamp: string;
|
||||
alertId: string;
|
||||
alertUrl?: string;
|
||||
data: TimelineEventsDetailsItem[] | null;
|
||||
hostName: string;
|
||||
indexName?: string;
|
||||
isAlert: boolean;
|
||||
ruleDescription: string;
|
||||
ruleId: string;
|
||||
ruleName: string;
|
||||
timestamp: string;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
export const useBasicDataFromDetailsData = (
|
||||
|
@ -49,6 +51,16 @@ export const useBasicDataFromDetailsData = (
|
|||
|
||||
const alertId = useMemo(() => getFieldValue({ category: '_id', field: '_id' }, data), [data]);
|
||||
|
||||
const indexName = useMemo(
|
||||
() => getFieldValue({ category: '_index', field: '_index' }, data),
|
||||
[data]
|
||||
);
|
||||
|
||||
const alertUrl = useMemo(
|
||||
() => getFieldValue({ category: 'kibana', field: 'kibana.alert.url' }, data),
|
||||
[data]
|
||||
);
|
||||
|
||||
const agentId = useMemo(
|
||||
() => getFieldValue({ category: 'agent', field: 'agent.id' }, data),
|
||||
[data]
|
||||
|
@ -71,28 +83,32 @@ export const useBasicDataFromDetailsData = (
|
|||
|
||||
return useMemo(
|
||||
() => ({
|
||||
alertId,
|
||||
agentId,
|
||||
isAlert,
|
||||
hostName,
|
||||
userName,
|
||||
ruleName,
|
||||
timestamp,
|
||||
alertId,
|
||||
alertUrl,
|
||||
data,
|
||||
hostName,
|
||||
indexName,
|
||||
isAlert,
|
||||
ruleDescription,
|
||||
ruleId,
|
||||
ruleName,
|
||||
timestamp,
|
||||
userName,
|
||||
}),
|
||||
[
|
||||
agentId,
|
||||
alertId,
|
||||
alertUrl,
|
||||
data,
|
||||
hostName,
|
||||
indexName,
|
||||
isAlert,
|
||||
ruleDescription,
|
||||
ruleId,
|
||||
ruleName,
|
||||
timestamp,
|
||||
userName,
|
||||
data,
|
||||
ruleDescription,
|
||||
ruleId,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue