mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com>
This commit is contained in:
parent
2149b1703b
commit
30d0ca85ad
16 changed files with 562 additions and 72 deletions
|
@ -117,7 +117,7 @@ Arguments:
|
|||
| timelineIntegration?.hooks.useInsertTimeline | `(value: string, onChange: (newValue: string) => void): UseInsertTimelineReturn` |
|
||||
| timelineIntegration?.ui?.renderInvestigateInTimelineActionComponent? | `(alertIds: string[]) => JSX.Element;` space to render `InvestigateInTimelineActionComponent` |
|
||||
| timelineIntegration?.ui?renderTimelineDetailsPanel? | `() => JSX.Element;` space to render `TimelineDetailsPanel` |
|
||||
| useFetchAlertData | `(alertIds: string[]) => [boolean, Record<string, Ecs>];` fetch alerts |
|
||||
| useFetchAlertData | `(alertIds: string[]) => [boolean, Record<string, unknown>];` fetch alerts |
|
||||
| userCanCrud | `boolean;` user permissions to crud |
|
||||
|
||||
UI component:
|
||||
|
|
|
@ -271,3 +271,5 @@ export interface Ecs {
|
|||
}
|
||||
|
||||
export type CaseActionConnector = ActionConnector;
|
||||
|
||||
export type UseFetchAlertData = (alertIds: string[]) => [boolean, Record<string, unknown>];
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useMemo, useState, useRef, MutableRefObj
|
|||
import styled from 'styled-components';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent, EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
import { Case, Ecs, CaseViewRefreshPropInterface } from '../../../common/ui/types';
|
||||
import { Case, CaseViewRefreshPropInterface, UseFetchAlertData } from '../../../common/ui/types';
|
||||
import { CaseStatuses, CaseAttributes, CaseType, CaseConnector } from '../../../common/api';
|
||||
import { HeaderPage } from '../header_page';
|
||||
import { EditableTitle } from '../header_page/editable_title';
|
||||
|
@ -46,7 +46,7 @@ export interface CaseViewComponentProps {
|
|||
ruleDetailsNavigation?: CasesNavigation<string | null | undefined, 'configurable'>;
|
||||
showAlertDetails?: (alertId: string, index: string) => void;
|
||||
subCaseId?: string;
|
||||
useFetchAlertData: (alertIds: string[]) => [boolean, Record<string, Ecs>];
|
||||
useFetchAlertData: UseFetchAlertData;
|
||||
userCanCrud: boolean;
|
||||
/**
|
||||
* A React `Ref` that Exposes data refresh callbacks.
|
||||
|
|
|
@ -18,7 +18,7 @@ import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils/technical
|
|||
import React, { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import { Comment, Ecs } from '../../../common/ui/types';
|
||||
import { Comment } from '../../../common/ui/types';
|
||||
import {
|
||||
CaseFullExternalService,
|
||||
ActionConnector,
|
||||
|
@ -466,7 +466,7 @@ export const getFirstItem = (items?: string | string[] | null): string | null =>
|
|||
return Array.isArray(items) ? items[0] : items ?? null;
|
||||
};
|
||||
|
||||
export const getRuleId = (comment: CommentRequestAlertType, alertData?: Ecs): string | null =>
|
||||
export const getRuleId = (comment: CommentRequestAlertType, alertData?: unknown): string | null =>
|
||||
getRuleField({
|
||||
commentRuleField: comment?.rule?.id,
|
||||
alertData,
|
||||
|
@ -474,7 +474,7 @@ export const getRuleId = (comment: CommentRequestAlertType, alertData?: Ecs): st
|
|||
kibanaAlertFieldPath: ALERT_RULE_UUID,
|
||||
});
|
||||
|
||||
export const getRuleName = (comment: CommentRequestAlertType, alertData?: Ecs): string | null =>
|
||||
export const getRuleName = (comment: CommentRequestAlertType, alertData?: unknown): string | null =>
|
||||
getRuleField({
|
||||
commentRuleField: comment?.rule?.name,
|
||||
alertData,
|
||||
|
@ -489,7 +489,7 @@ const getRuleField = ({
|
|||
kibanaAlertFieldPath,
|
||||
}: {
|
||||
commentRuleField: string | string[] | null | undefined;
|
||||
alertData: Ecs | undefined;
|
||||
alertData: unknown | undefined;
|
||||
signalRuleFieldPath: string;
|
||||
kibanaAlertFieldPath: string;
|
||||
}): string | null => {
|
||||
|
|
|
@ -22,7 +22,6 @@ import {
|
|||
} from '../../containers/mock';
|
||||
import { UserActionTree } from '.';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { Ecs } from '../../../common/ui/types';
|
||||
|
||||
const fetchUserActions = jest.fn();
|
||||
const onUpdateField = jest.fn();
|
||||
|
@ -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' } },
|
||||
],
|
||||
|
|
|
@ -25,7 +25,7 @@ import * as i18n from './translations';
|
|||
import { useUpdateComment } from '../../containers/use_update_comment';
|
||||
import { useCurrentUser } from '../../common/lib/kibana';
|
||||
import { AddComment } from '../add_comment';
|
||||
import { Case, Ecs } from '../../../common/ui/types';
|
||||
import { Case, UseFetchAlertData } from '../../../common/ui/types';
|
||||
import { CaseUserActions } from '../../../common/ui';
|
||||
import {
|
||||
ActionConnector,
|
||||
|
@ -77,7 +77,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;
|
||||
}
|
||||
|
||||
|
@ -172,10 +172,13 @@ export const UserActionTree = React.memo(
|
|||
const { clearDraftComment, draftComment, hasIncomingLensState, openLensModal } =
|
||||
useLensDraftComment();
|
||||
|
||||
const [loadingAlertData, manualAlertsData] = useFetchAlertData(
|
||||
getManualAlertIdsWithNoRuleId(caseData.comments)
|
||||
const alertIdsWithoutRuleInfo = useMemo(
|
||||
() => getManualAlertIdsWithNoRuleId(caseData.comments),
|
||||
[caseData.comments]
|
||||
);
|
||||
|
||||
const [loadingAlertData, manualAlertsData] = useFetchAlertData(alertIdsWithoutRuleInfo);
|
||||
|
||||
const handleManageMarkdownEditId = useCallback(
|
||||
(id: string) => {
|
||||
clearDraftComment();
|
||||
|
@ -413,7 +416,7 @@ export const UserActionTree = React.memo(
|
|||
return comments;
|
||||
}
|
||||
|
||||
const alertField: Ecs | undefined = manualAlertsData[alertId];
|
||||
const alertField: unknown | undefined = manualAlertsData[alertId];
|
||||
const ruleId = getRuleId(comment, alertField);
|
||||
const ruleName = getRuleName(comment, alertField);
|
||||
|
||||
|
|
|
@ -1,55 +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 { useEffect, useState } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { usePluginContext } from '../../../../hooks/use_plugin_context';
|
||||
import { parseAlert } from '../../../../pages/alerts/components/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<Record<string, unknown>>('/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];
|
||||
};
|
|
@ -15,12 +15,13 @@ import {
|
|||
useFormatUrl,
|
||||
} from '../../../../pages/cases/links';
|
||||
import { Case } from '../../../../../../cases/common';
|
||||
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 '../../../..';
|
||||
import { useFetchAlertDetail } from '../../../../pages/cases/use_fetch_alert_detail';
|
||||
import { useFetchAlertData } from '../../../../pages/cases/use_fetch_alert_data';
|
||||
|
||||
interface Props {
|
||||
caseId: string;
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const casesUiStartMock = {
|
||||
createStart() {
|
||||
return {
|
||||
getCases: jest.fn(),
|
||||
getAllCasesSelectorModal: jest.fn(),
|
||||
getCreateCaseFlyout: jest.fn(),
|
||||
getRecentCases: jest.fn(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const embeddableStartMock = {
|
||||
createStart() {
|
||||
return {
|
||||
getEmbeddableFactory: jest.fn(),
|
||||
getEmbeddableFactories: jest.fn(),
|
||||
EmbeddablePanel: jest.fn(),
|
||||
getStateTransfer: jest.fn(),
|
||||
getAttributeService: jest.fn(),
|
||||
telemetry: null,
|
||||
inject: jest.fn(),
|
||||
extract: jest.fn(),
|
||||
getAllMigrations: jest.fn(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const observabilityPublicPluginsStartMock = {
|
||||
createStart() {
|
||||
return {
|
||||
cases: casesUiStartMock.createStart(),
|
||||
embeddable: embeddableStartMock.createStart(),
|
||||
triggersActionsUi: null,
|
||||
data: null,
|
||||
lens: null,
|
||||
discover: null,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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, {}]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { useCallback, useMemo } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '../../../../rule_registry/common/constants';
|
||||
import { useDataFetcher } from './use_data_fetcher';
|
||||
|
||||
export const useFetchAlertData = (alertIds: string[]): [boolean, Record<string, unknown>] => {
|
||||
const validIds = useMemo(() => getValidValues(alertIds), [alertIds]);
|
||||
const shouldExecuteApiCall = useCallback((ids: string[]) => ids.length > 0, []);
|
||||
|
||||
const { loading, data: alerts } = useDataFetcher<string[], Record<string, unknown> | undefined>({
|
||||
paramsForApiCall: validIds,
|
||||
initialDataState: undefined,
|
||||
executeApiCall: fetchAlerts,
|
||||
shouldExecuteApiCall,
|
||||
});
|
||||
|
||||
return [loading, alerts ?? {}];
|
||||
};
|
||||
|
||||
const fetchAlerts = async (
|
||||
ids: string[],
|
||||
abortCtrl: AbortController,
|
||||
http: HttpSetup
|
||||
): Promise<Record<string, unknown> | undefined> => {
|
||||
try {
|
||||
const response = await http.post<estypes.SearchResponse<Record<string, unknown>>>(
|
||||
`${BASE_RAC_ALERTS_API_PATH}/find`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
query: {
|
||||
ids: {
|
||||
values: ids,
|
||||
},
|
||||
},
|
||||
track_total_hits: false,
|
||||
size: 10000,
|
||||
}),
|
||||
signal: abortCtrl.signal,
|
||||
}
|
||||
);
|
||||
|
||||
if (response) {
|
||||
return getAlertsGroupedById(response);
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore the failure
|
||||
}
|
||||
};
|
||||
|
||||
const getAlertsGroupedById = (
|
||||
data: estypes.SearchResponse<Record<string, unknown>>
|
||||
): Record<string, unknown> => {
|
||||
return data.hits.hits.reduce(
|
||||
(acc, { _id, _index, _source }) => ({
|
||||
...acc,
|
||||
[_id]: {
|
||||
_id,
|
||||
_index,
|
||||
..._source,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
const getValidValues = (ids: string[]): string[] => {
|
||||
return ids.filter((id) => !isEmpty(id));
|
||||
};
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* 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 { TopAlert } from '../alerts';
|
||||
import * as pluginContext from '../../hooks/use_plugin_context';
|
||||
import { createObservabilityRuleTypeRegistryMock } from '../..';
|
||||
import { PluginContextValue } from '../../context/plugin_context';
|
||||
import { useFetchAlertDetail } from './use_fetch_alert_detail';
|
||||
|
||||
const mockUseKibanaReturnValue = kibanaStartMock.startContract();
|
||||
|
||||
jest.mock('../../utils/kibana_react', () => ({
|
||||
__esModule: true,
|
||||
useKibana: jest.fn(() => mockUseKibanaReturnValue),
|
||||
}));
|
||||
|
||||
describe('useFetchAlertDetail', () => {
|
||||
const getResult = {
|
||||
'kibana.alert.rule.category': 'Metric threshold',
|
||||
'kibana.alert.rule.consumer': 'infrastructure',
|
||||
'kibana.alert.rule.execution.uuid': 'e62c418d-734d-47e7-bbeb-e6f182f5fb45',
|
||||
'kibana.alert.rule.name': 'A super rule',
|
||||
'kibana.alert.rule.producer': 'infrastructure',
|
||||
'kibana.alert.rule.rule_type_id': 'metrics.alert.threshold',
|
||||
'kibana.alert.rule.uuid': '69411af0-82a2-11ec-8139-c1568734434e',
|
||||
'kibana.space_ids': ['default'],
|
||||
'kibana.alert.rule.tags': [],
|
||||
'@timestamp': '2022-01-31T18:20:57.204Z',
|
||||
'kibana.alert.reason': 'Document count reported no data in the last 1 hour for all hosts',
|
||||
'kibana.alert.duration.us': 13793555000,
|
||||
'kibana.alert.instance.id': '*',
|
||||
'kibana.alert.start': '2022-01-31T14:31:03.649Z',
|
||||
'kibana.alert.uuid': '73c0d0cd-2df4-4550-862c-1d447e9c1db2',
|
||||
'kibana.alert.status': 'active',
|
||||
'kibana.alert.workflow_status': 'open',
|
||||
'event.kind': 'signal',
|
||||
'event.action': 'active',
|
||||
'kibana.version': '8.1.0',
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const id = '123';
|
||||
const ruleType = createObservabilityRuleTypeRegistryMock();
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseKibanaReturnValue.services.http.get.mockImplementation(async () => getResult);
|
||||
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
observabilityRuleTypeRegistry: ruleType,
|
||||
} as unknown as PluginContextValue)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('initially is not loading and does not have data', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, [boolean, TopAlert | null]>(() =>
|
||||
useFetchAlertDetail(id)
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual([false, null]);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns no data when an error occurs', async () => {
|
||||
mockUseKibanaReturnValue.services.http.get.mockImplementation(async () => {
|
||||
throw new Error('an http error');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, [boolean, TopAlert | null]>(() =>
|
||||
useFetchAlertDetail('123')
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual([false, null]);
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves the alert data', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, [boolean, TopAlert | null]>(() =>
|
||||
useFetchAlertDetail(id)
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
false,
|
||||
Object {
|
||||
"0": "a",
|
||||
"1": " ",
|
||||
"2": "r",
|
||||
"3": "e",
|
||||
"4": "a",
|
||||
"5": "s",
|
||||
"6": "o",
|
||||
"7": "n",
|
||||
"active": true,
|
||||
"fields": Object {
|
||||
"@timestamp": "2022-01-31T18:20:57.204Z",
|
||||
"event.action": "active",
|
||||
"event.kind": "signal",
|
||||
"kibana.alert.duration.us": 13793555000,
|
||||
"kibana.alert.evaluation.threshold": undefined,
|
||||
"kibana.alert.evaluation.value": undefined,
|
||||
"kibana.alert.instance.id": "*",
|
||||
"kibana.alert.reason": "Document count reported no data in the last 1 hour for all hosts",
|
||||
"kibana.alert.rule.category": "Metric threshold",
|
||||
"kibana.alert.rule.consumer": "infrastructure",
|
||||
"kibana.alert.rule.execution.uuid": "e62c418d-734d-47e7-bbeb-e6f182f5fb45",
|
||||
"kibana.alert.rule.name": "A super rule",
|
||||
"kibana.alert.rule.producer": "infrastructure",
|
||||
"kibana.alert.rule.rule_type_id": "metrics.alert.threshold",
|
||||
"kibana.alert.rule.tags": Array [],
|
||||
"kibana.alert.rule.uuid": "69411af0-82a2-11ec-8139-c1568734434e",
|
||||
"kibana.alert.start": "2022-01-31T14:31:03.649Z",
|
||||
"kibana.alert.status": "active",
|
||||
"kibana.alert.uuid": "73c0d0cd-2df4-4550-862c-1d447e9c1db2",
|
||||
"kibana.alert.workflow_status": "open",
|
||||
"kibana.space_ids": Array [
|
||||
"default",
|
||||
],
|
||||
"kibana.version": "8.1.0",
|
||||
"tags": Array [],
|
||||
},
|
||||
"link": undefined,
|
||||
"reason": "A super rule",
|
||||
"start": 1643639463649,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not populate the results when the request is canceled', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate, unmount } = renderHook<string, [boolean, TopAlert | null]>(
|
||||
() => useFetchAlertDetail('123')
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
unmount();
|
||||
|
||||
expect(result.current).toEqual([false, null]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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, useMemo } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { TopAlert, parseAlert } from '../../pages/alerts/';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '../../../../rule_registry/common/constants';
|
||||
import { ObservabilityRuleTypeRegistry } from '../..';
|
||||
import { useDataFetcher } from './use_data_fetcher';
|
||||
|
||||
interface AlertDetailParams {
|
||||
id: string;
|
||||
ruleType: ObservabilityRuleTypeRegistry;
|
||||
}
|
||||
|
||||
export const useFetchAlertDetail = (id: string): [boolean, TopAlert | null] => {
|
||||
const { observabilityRuleTypeRegistry } = usePluginContext();
|
||||
|
||||
const params = useMemo(
|
||||
() => ({ id, ruleType: observabilityRuleTypeRegistry }),
|
||||
[id, observabilityRuleTypeRegistry]
|
||||
);
|
||||
|
||||
const shouldExecuteApiCall = useCallback(
|
||||
(apiCallParams: AlertDetailParams) => !isEmpty(apiCallParams.id),
|
||||
[]
|
||||
);
|
||||
|
||||
const { loading, data: alert } = useDataFetcher<AlertDetailParams, TopAlert | null>({
|
||||
paramsForApiCall: params,
|
||||
initialDataState: null,
|
||||
executeApiCall: fetchAlert,
|
||||
shouldExecuteApiCall,
|
||||
});
|
||||
|
||||
return [loading, alert];
|
||||
};
|
||||
|
||||
const fetchAlert = async (
|
||||
params: AlertDetailParams,
|
||||
abortController: AbortController,
|
||||
http: HttpSetup
|
||||
): Promise<TopAlert | null> => {
|
||||
const { id, ruleType } = params;
|
||||
try {
|
||||
const response = await http.get<Record<string, unknown>>(BASE_RAC_ALERTS_API_PATH, {
|
||||
query: {
|
||||
id,
|
||||
},
|
||||
signal: abortController.signal,
|
||||
});
|
||||
if (response !== undefined) {
|
||||
return parseAlert(ruleType)(response);
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore error for retrieving alert
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { coreMock, notificationServiceMock, overlayServiceMock } from 'src/core/public/mocks';
|
||||
import { observabilityPublicPluginsStartMock } from '../observability_public_plugins_start.mock';
|
||||
|
||||
export const kibanaStartMock = {
|
||||
startContract() {
|
||||
return {
|
||||
notifications: notificationServiceMock.createStartContract(),
|
||||
overlays: overlayServiceMock.createStartContract(),
|
||||
services: {
|
||||
...coreMock.createStart(),
|
||||
...observabilityPublicPluginsStartMock.createStart(),
|
||||
storage: coreMock.createStorage(),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
|
@ -101,7 +101,7 @@ export interface Alert {
|
|||
signal: Signal;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export const useFetchAlertData = (alertIds: string[]): [boolean, Record<string, Ecs>] => {
|
||||
export const useFetchAlertData = (alertIds: string[]): [boolean, Record<string, unknown>] => {
|
||||
const { selectedPatterns } = useSourcererDataView(SourcererScopeName.detections);
|
||||
const alertsQuery = useMemo(() => buildAlertsQuery(alertIds), [alertIds]);
|
||||
|
||||
|
@ -112,7 +112,7 @@ export const useFetchAlertData = (alertIds: string[]): [boolean, Record<string,
|
|||
|
||||
const alerts = useMemo(
|
||||
() =>
|
||||
alertsData?.hits.hits.reduce<Record<string, Ecs>>(
|
||||
alertsData?.hits.hits.reduce<Record<string, unknown>>(
|
||||
(acc, { _id, _index, _source }) => ({
|
||||
...acc,
|
||||
[_id]: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue