Skip to content

Commit

Permalink
[RAC] [Observability] [Security Solution] Use correct url to manageme…
Browse files Browse the repository at this point in the history
…nt app for observability cases, use normalized ids (#108775)

* Use correct url to management app for observability cases, use normalized ids in timelines

* Update failing test

* Load alert details data to render flyout in case detail view
  • Loading branch information
kqualters-elastic authored Aug 17, 2021
1 parent ab63730 commit 87c93ab
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,52 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEffect, useState } from 'react';
import { isEmpty } from 'lodash';

import { usePluginContext } from '../../../../hooks/use_plugin_context';
import { parseAlert } from '../../../../pages/alerts/parse_alert';
import { TopAlert } from '../../../../pages/alerts/';
import { useKibana } from '../../../../utils/kibana_react';
import { Ecs } from '../../../../../../cases/common';

// no alerts in observability so far
// dummy hook for now as hooks cannot be called conditionally
export const useFetchAlertData = (): [boolean, Record<string, Ecs>] => [false, {}];

export const useFetchAlertDetail = (alertId: string): [boolean, TopAlert | null] => {
const { http } = useKibana().services;
const [loading, setLoading] = useState(false);
const { observabilityRuleTypeRegistry } = usePluginContext();
const [alert, setAlert] = useState<TopAlert | null>(null);

useEffect(() => {
const abortCtrl = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const response = await http.get('/internal/rac/alerts', {
query: {
id: alertId,
},
});
if (response) {
const parsedAlert = parseAlert(observabilityRuleTypeRegistry)(response);
setAlert(parsedAlert);
setLoading(false);
}
} catch (error) {
setAlert(null);
}
};

if (!isEmpty(alertId) && loading === false && alert === null) {
fetchData();
}
return () => {
abortCtrl.abort();
};
}, [http, alertId, alert, loading, observabilityRuleTypeRegistry]);

return [loading, alert];
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useCallback, useState } from 'react';
import React, { useCallback, useState, Suspense } from 'react';
import {
casesBreadcrumbs,
getCaseDetailsUrl,
Expand All @@ -15,10 +15,12 @@ import {
useFormatUrl,
} from '../../../../pages/cases/links';
import { Case } from '../../../../../../cases/common';
import { useFetchAlertData } from './helpers';
import { useFetchAlertData, useFetchAlertDetail } from './helpers';
import { useKibana } from '../../../../utils/kibana_react';
import { usePluginContext } from '../../../../hooks/use_plugin_context';
import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs';
import { observabilityAppId } from '../../../../../common';
import { LazyAlertsFlyout } from '../../../..';

interface Props {
caseId: string;
Expand All @@ -41,14 +43,17 @@ export interface CaseProps extends Props {

export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => {
const [caseTitle, setCaseTitle] = useState<string | null>(null);
const { observabilityRuleTypeRegistry } = usePluginContext();

const {
cases: casesUi,
application: { getUrlForApp, navigateToUrl },
application: { getUrlForApp, navigateToUrl, navigateToApp },
} = useKibana().services;
const allCasesLink = getCaseUrl();
const { formatUrl } = useFormatUrl();
const href = formatUrl(allCasesLink);
const [selectedAlertId, setSelectedAlertId] = useState<string>('');

useBreadcrumbs([
{ ...casesBreadcrumbs.cases, href },
...(caseTitle !== null
Expand Down Expand Up @@ -80,41 +85,78 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
}),
[caseId, formatUrl, subCaseId]
);

const casesUrl = `${getUrlForApp(observabilityAppId)}/cases`;
return casesUi.getCaseView({
allCasesNavigation: {
href: allCasesHref,
onClick: async (ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToUrl(casesUrl);
},
},
caseDetailsNavigation: {
href: caseDetailsHref,
onClick: async (ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: caseId })}`);
},
},
caseId,
configureCasesNavigation: {
href: configureCasesHref,
onClick: async (ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToUrl(`${casesUrl}${configureCasesLink}`);
},
},
getCaseDetailHrefWithCommentId,
onCaseDataSuccess,
subCaseId,
useFetchAlertData,
userCanCrud,
});

const handleFlyoutClose = useCallback(() => {
setSelectedAlertId('');
}, []);

const [alertLoading, alert] = useFetchAlertDetail(selectedAlertId);

return (
<>
{alertLoading === false && alert && selectedAlertId !== '' && (
<Suspense fallback={null}>
<LazyAlertsFlyout
alert={alert}
observabilityRuleTypeRegistry={observabilityRuleTypeRegistry}
onClose={handleFlyoutClose}
/>
</Suspense>
)}
{casesUi.getCaseView({
allCasesNavigation: {
href: allCasesHref,
onClick: async (ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToUrl(casesUrl);
},
},
caseDetailsNavigation: {
href: caseDetailsHref,
onClick: async (ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: caseId })}`);
},
},
caseId,
configureCasesNavigation: {
href: configureCasesHref,
onClick: async (ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToUrl(`${casesUrl}${configureCasesLink}`);
},
},
ruleDetailsNavigation: {
href: (ruleId) => {
return getUrlForApp('management', {
path: `/insightsAndAlerting/triggersActions/rule/${ruleId}`,
});
},
onClick: async (ruleId, ev) => {
if (ev != null) {
ev.preventDefault();
}
return navigateToApp('management', {
path: `/insightsAndAlerting/triggersActions/rule/${ruleId}`,
});
},
},
getCaseDetailHrefWithCommentId,
onCaseDataSuccess,
subCaseId,
useFetchAlertData,
showAlertDetails: (alertId) => {
setSelectedAlertId(alertId);
},
userCanCrud,
})}
</>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ describe('AddToCaseAction', () => {
{...props}
event={{
_id: 'test-id',
data: [],
data: [{ field: 'kibana.alert.rule.id', value: ['rule-id'] }],
ecs: {
_id: 'test-id',
_index: 'test-index',
Expand All @@ -112,7 +112,7 @@ describe('AddToCaseAction', () => {
{...props}
event={{
_id: 'test-id',
data: [],
data: [{ field: 'kibana.alert.rule.id', value: ['rule-id'] }],
ecs: {
_id: 'test-id',
_index: 'test-index',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import React, { memo, useMemo, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { CaseStatuses, StatusAll } from '../../../../../../cases/common';
import { TimelineItem } from '../../../../../common/';
import { useAddToCase } from '../../../../hooks/use_add_to_case';
import { useAddToCase, normalizedEventFields } from '../../../../hooks/use_add_to_case';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { TimelinesStartServices } from '../../../../types';
import { CreateCaseFlyout } from './create/flyout';
Expand Down Expand Up @@ -38,7 +38,6 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
}) => {
const eventId = event?.ecs._id ?? '';
const eventIndex = event?.ecs._index ?? '';
const rule = event?.ecs.signal?.rule;
const dispatch = useDispatch();
const { cases } = useKibana<TimelinesStartServices>().services;
const {
Expand All @@ -52,13 +51,14 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
} = useAddToCase({ event, useInsertTimeline, casePermissions, appId, onClose });

const getAllCasesSelectorModalProps = useMemo(() => {
const { ruleId, ruleName } = normalizedEventFields(event);
return {
alertData: {
alertId: eventId,
index: eventIndex ?? '',
rule: {
id: rule?.id != null ? rule.id[0] : null,
name: rule?.name != null ? rule.name[0] : null,
id: ruleId,
name: ruleName,
},
owner: appId,
},
Expand All @@ -85,11 +85,10 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
goToCreateCase,
eventId,
eventIndex,
rule?.id,
rule?.name,
appId,
dispatch,
useInsertTimeline,
event,
]);

const closeCaseFlyoutOpen = useCallback(() => {
Expand Down
24 changes: 20 additions & 4 deletions x-pack/plugins/timelines/public/hooks/use_add_to_case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { isEmpty } from 'lodash';
import { useState, useCallback, useMemo, SyntheticEvent } from 'react';
import { useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { ALERT_RULE_ID, ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { Case, SubCase } from '../../../cases/common';
import { TimelinesStartServices } from '../types';
import { TimelineItem } from '../../common/';
import { tGridActions } from '../store/t_grid';
import { useDeepEqualSelector } from './use_selector';
import { createUpdateSuccessToaster } from '../components/actions/timeline/cases/helpers';
Expand Down Expand Up @@ -83,7 +85,6 @@ export const useAddToCase = ({
}: AddToCaseActionProps): UseAddToCase => {
const eventId = event?.ecs._id ?? '';
const eventIndex = event?.ecs._index ?? '';
const rule = event?.ecs.signal?.rule;
const dispatch = useDispatch();
// TODO: use correct value in standalone or integrated.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -154,6 +155,7 @@ export const useAddToCase = ({
updateCase?: (newCase: Case) => void
) => {
dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: false }));
const { ruleId, ruleName } = normalizedEventFields(event);
if (postComment) {
await postComment({
caseId: theCase.id,
Expand All @@ -162,16 +164,16 @@ export const useAddToCase = ({
alertId: eventId,
index: eventIndex ?? '',
rule: {
id: rule?.id != null ? rule.id[0] : null,
name: rule?.name != null ? rule.name[0] : null,
id: ruleId,
name: ruleName,
},
owner: appId,
},
updateCase,
});
}
},
[eventId, eventIndex, rule, appId, dispatch]
[eventId, eventIndex, appId, dispatch, event]
);
const onCaseSuccess = useCallback(
async (theCase: Case) => {
Expand Down Expand Up @@ -239,3 +241,17 @@ export const useAddToCase = ({
isCreateCaseFlyoutOpen,
};
};

export function normalizedEventFields(event?: TimelineItem) {
const ruleId = event && event.data.find(({ field }) => field === ALERT_RULE_ID);
const ruleUuid = event && event.data.find(({ field }) => field === ALERT_RULE_UUID);
const ruleName = event && event.data.find(({ field }) => field === ALERT_RULE_NAME);
const ruleIdValue = ruleId && ruleId.value && ruleId.value[0];
const ruleUuidValue = ruleUuid && ruleUuid.value && ruleUuid.value[0];
const ruleNameValue = ruleName && ruleName.value && ruleName.value[0];
const idToUse = ruleIdValue ? ruleIdValue : ruleUuidValue;
return {
ruleId: idToUse ?? null,
ruleName: ruleNameValue ?? null,
};
}

0 comments on commit 87c93ab

Please sign in to comment.