Skip to content

Commit

Permalink
[Response Ops][Cases] Fetch alerts within observability (elastic#123883
Browse files Browse the repository at this point in the history
…) (elastic#125347)

Co-authored-by: Kibana Machine <[email protected]>
(cherry picked from commit 30ed7bb)

Co-authored-by: Jonathan Buttner <[email protected]>
  • Loading branch information
kibanamachine and jonathan-buttner authored Feb 11, 2022
1 parent 7a65488 commit 6f5ced9
Show file tree
Hide file tree
Showing 16 changed files with 493 additions and 72 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/cases/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Arguments:
| userCanCrud | `boolean;` user permissions to crud |
| owner | `string[];` owner ids of the cases |
| basePath | `string;` path to mount the Cases router on top of |
| useFetchAlertData | `(alertIds: string[]) => [boolean, Record<string, Ecs>];` fetch alerts |
| useFetchAlertData | `(alertIds: string[]) => [boolean, Record<string, unknown>];` fetch alerts |
| disableAlerts? | `boolean` (default: false) flag to not show alerts information |
| actionsNavigation? | <code>CasesNavigation<string, 'configurable'></code> |
| ruleDetailsNavigation? | <code>CasesNavigation<string &vert; null &vert; undefined, 'configurable'></code> |
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/cases/common/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,5 @@ export interface Ecs {
}

export type CaseActionConnector = ActionConnector;

export type UseFetchAlertData = (alertIds: string[]) => [boolean, Record<string, unknown>];
4 changes: 2 additions & 2 deletions x-pack/plugins/cases/public/components/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { MutableRefObject } from 'react';
import { Ecs, CaseViewRefreshPropInterface } from '../../../common/ui/types';
import { CaseViewRefreshPropInterface, UseFetchAlertData } from '../../../common/ui/types';
import { CasesNavigation } from '../links';
import { CasesTimelineIntegration } from '../timeline_context';

Expand All @@ -15,7 +15,7 @@ export interface CasesRoutesProps {
actionsNavigation?: CasesNavigation<string, 'configurable'>;
ruleDetailsNavigation?: CasesNavigation<string | null | undefined, 'configurable'>;
showAlertDetails?: (alertId: string, index: string) => void;
useFetchAlertData: (alertIds: string[]) => [boolean, Record<string, Ecs>];
useFetchAlertData: UseFetchAlertData;
/**
* A React `Ref` that Exposes data refresh callbacks.
* **NOTE**: Do not hold on to the `.current` object, as it could become stale
Expand Down
5 changes: 3 additions & 2 deletions x-pack/plugins/cases/public/components/case_view/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@
import { MutableRefObject } from 'react';
import { CasesTimelineIntegration } from '../timeline_context';
import { CasesNavigation } from '../links';
import { CaseViewRefreshPropInterface, Ecs, Case } from '../../../common';
import { CaseViewRefreshPropInterface, Case } from '../../../common';
import { UseGetCase } from '../../containers/use_get_case';
import { UseFetchAlertData } from '../../../common/ui';

export interface CaseViewBaseProps {
onComponentInitialized?: () => void;
actionsNavigation?: CasesNavigation<string, 'configurable'>;
ruleDetailsNavigation?: CasesNavigation<string | null | undefined, 'configurable'>;
showAlertDetails?: (alertId: string, index: string) => void;
useFetchAlertData: (alertIds: string[]) => [boolean, Record<string, Ecs>];
useFetchAlertData: UseFetchAlertData;
/**
* A React `Ref` that Exposes data refresh callbacks.
* **NOTE**: Do not hold on to the `.current` object, as it could become stale
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { UserActionUsernameWithAvatar } from '../avatar_username';
import { AlertCommentEvent } from './alert_event';
import { UserActionCopyLink } from '../copy_link';
import { UserActionShowAlert } from './show_alert';
import { Ecs } from '../../../containers/types';

type BuilderArgs = Pick<
UserActionBuilderArgs,
Expand Down Expand Up @@ -49,7 +48,7 @@ export const createAlertAttachmentUserActionBuilder = ({
return [];
}

const alertField: Ecs | undefined = alertData[alertId];
const alertField: unknown | undefined = alertData[alertId];
const ruleId = getRuleId(comment, alertField);
const ruleName = getRuleName(comment, alertField);

Expand Down Expand Up @@ -101,15 +100,15 @@ const getFirstItem = (items?: string | string[] | null): string | null => {
return Array.isArray(items) ? items[0] : items ?? null;
};

export const getRuleId = (comment: BuilderArgs['comment'], alertData?: Ecs): string | null =>
export const getRuleId = (comment: BuilderArgs['comment'], alertData?: unknown): string | null =>
getRuleField({
commentRuleField: comment?.rule?.id,
alertData,
signalRuleFieldPath: 'signal.rule.id',
kibanaAlertFieldPath: ALERT_RULE_UUID,
});

export const getRuleName = (comment: BuilderArgs['comment'], alertData?: Ecs): string | null =>
export const getRuleName = (comment: BuilderArgs['comment'], alertData?: unknown): string | null =>
getRuleField({
commentRuleField: comment?.rule?.name,
alertData,
Expand All @@ -124,7 +123,7 @@ const getRuleField = ({
kibanaAlertFieldPath,
}: {
commentRuleField: string | string[] | null | undefined;
alertData: Ecs | undefined;
alertData: unknown | undefined;
signalRuleFieldPath: string;
kibanaAlertFieldPath: string;
}): string | null => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
} from '../../containers/mock';
import { UserActions } from '.';
import { TestProviders } from '../../common/mock';
import { Ecs } from '../../../common/ui/types';
import { Actions } from '../../../common/api';

const fetchUserActions = jest.fn();
Expand All @@ -46,7 +45,7 @@ const defaultProps = {
statusActionButton: null,
updateCase,
userCanCrud: true,
useFetchAlertData: (): [boolean, Record<string, Ecs>] => [
useFetchAlertData: (): [boolean, Record<string, unknown>] => [
false,
{ 'some-id': { _id: 'some-id' } },
],
Expand Down
7 changes: 5 additions & 2 deletions x-pack/plugins/cases/public/components/user_actions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,13 @@ export const UserActions = React.memo(
const [initLoading, setInitLoading] = useState(true);
const currentUser = useCurrentUser();

const [loadingAlertData, manualAlertsData] = useFetchAlertData(
getManualAlertIdsWithNoRuleId(caseData.comments)
const alertIdsWithoutRuleInfo = useMemo(
() => getManualAlertIdsWithNoRuleId(caseData.comments),
[caseData.comments]
);

const [loadingAlertData, manualAlertsData] = useFetchAlertData(alertIdsWithoutRuleInfo);

const {
loadingCommentIds,
commentRefs,
Expand Down
6 changes: 3 additions & 3 deletions x-pack/plugins/cases/public/components/user_actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { EuiCommentProps } from '@elastic/eui';
import { SnakeToCamelCase } from '../../../common/types';
import { ActionTypes, UserActionWithResponse } from '../../../common/api';
import { Case, CaseUserActions, Ecs, Comment } from '../../containers/types';
import { Case, CaseUserActions, Comment, UseFetchAlertData } from '../../containers/types';
import { CaseServices } from '../../containers/use_get_case_user_actions';
import { AddCommentRefObject } from '../add_comment';
import { UserActionMarkdownRefObject } from './markdown_form';
Expand All @@ -31,7 +31,7 @@ export interface UserActionTreeProps {
renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element;
statusActionButton: JSX.Element | null;
updateCase: (newCase: Case) => void;
useFetchAlertData: (alertIds: string[]) => [boolean, Record<string, Ecs>];
useFetchAlertData: UseFetchAlertData;
userCanCrud: boolean;
}

Expand All @@ -52,7 +52,7 @@ export interface UserActionBuilderArgs {
selectedOutlineCommentId: string;
loadingCommentIds: string[];
loadingAlertData: boolean;
alertData: Record<string, Ecs>;
alertData: Record<string, unknown>;
handleOutlineComment: (id: string) => void;
handleManageMarkdownEditId: (id: string) => void;
handleSaveComment: ({ id, version }: { id: string; version: string }, content: string) => void;
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/observability/public/pages/cases/cases.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import React, { Suspense, useCallback, useState } from 'react';

import { useKibana } from '../../utils/kibana_react';
import { useFetchAlertData, useFetchAlertDetail } from './helpers';
import { CASES_OWNER, CASES_PATH } from './constants';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { LazyAlertsFlyout } from '../..';
import { useFetchAlertDetail } from './use_fetch_alert_detail';
import { useFetchAlertData } from './use_fetch_alert_data';

interface CasesProps {
userCanCrud: boolean;
Expand Down
53 changes: 0 additions & 53 deletions x-pack/plugins/observability/public/pages/cases/helpers.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 { useState, useMemo, useEffect } from 'react';

import { HttpSetup } from 'kibana/public';
import { useKibana } from '../../utils/kibana_react';

type DataFetcher<T, R> = (params: T, ctrl: AbortController, http: HttpSetup) => Promise<R>;

export const useDataFetcher = <ApiCallParams, AlertDataType>({
paramsForApiCall,
initialDataState,
executeApiCall,
shouldExecuteApiCall,
}: {
paramsForApiCall: ApiCallParams;
initialDataState: AlertDataType;
executeApiCall: DataFetcher<ApiCallParams, AlertDataType>;
shouldExecuteApiCall: (params: ApiCallParams) => boolean;
}) => {
const { http } = useKibana().services;
const [loading, setLoading] = useState(false);
const [data, setData] = useState<AlertDataType>(initialDataState);

const { fetch, cancel } = useMemo(() => {
const abortController = new AbortController();
let isCanceled = false;

return {
fetch: async () => {
if (shouldExecuteApiCall(paramsForApiCall)) {
setLoading(true);

const results = await executeApiCall(paramsForApiCall, abortController, http);
if (!isCanceled) {
setLoading(false);
setData(results);
}
}
},
cancel: () => {
isCanceled = true;
abortController.abort();
},
};
}, [executeApiCall, http, paramsForApiCall, shouldExecuteApiCall]);

useEffect(() => {
fetch();

return () => {
cancel();
};
}, [fetch, cancel]);

return {
loading,
data,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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 { act, renderHook } from '@testing-library/react-hooks';
import { kibanaStartMock } from '../../utils/kibana_react.mock';
import { useFetchAlertData } from './use_fetch_alert_data';

const mockUseKibanaReturnValue = kibanaStartMock.startContract();

jest.mock('../../utils/kibana_react', () => ({
__esModule: true,
useKibana: jest.fn(() => mockUseKibanaReturnValue),
}));

describe('useFetchAlertData', () => {
const testIds = ['123'];

beforeEach(() => {
mockUseKibanaReturnValue.services.http.post.mockImplementation(async () => ({
hits: {
hits: [
{
_id: '123',
_index: 'index',
_source: {
testField: 'test',
},
},
],
},
}));
});

afterEach(() => {
jest.clearAllMocks();
});

it('initially is not loading and does not have data', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, [boolean, Record<string, unknown>]>(
() => useFetchAlertData(testIds)
);

await waitForNextUpdate();

expect(result.current).toEqual([false, {}]);
});
});

it('returns no data when an error occurs', async () => {
mockUseKibanaReturnValue.services.http.post.mockImplementation(async () => {
throw new Error('an http error');
});

await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, [boolean, Record<string, unknown>]>(
() => useFetchAlertData(testIds)
);

await waitForNextUpdate();

expect(result.current).toEqual([false, {}]);
});
});

it('retrieves the alert data', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, [boolean, Record<string, unknown>]>(
() => useFetchAlertData(testIds)
);

await waitForNextUpdate();
await waitForNextUpdate();

expect(result.current).toEqual([
false,
{ '123': { _id: '123', _index: 'index', testField: 'test' } },
]);
});
});

it('does not populate the results when the request is canceled', async () => {
await act(async () => {
const { result, waitForNextUpdate, unmount } = renderHook<
string,
[boolean, Record<string, unknown>]
>(() => useFetchAlertData(testIds));

await waitForNextUpdate();
unmount();

expect(result.current).toEqual([false, {}]);
});
});
});
Loading

0 comments on commit 6f5ced9

Please sign in to comment.