[8.0] [Response Ops][Cases] Fetch alerts within observability (#123883) (#125370)

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:
Christos Nasikas 2022-02-11 19:18:03 +02:00 committed by GitHub
parent 2149b1703b
commit 30d0ca85ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 562 additions and 72 deletions

View file

@ -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:

View file

@ -271,3 +271,5 @@ export interface Ecs {
}
export type CaseActionConnector = ActionConnector;
export type UseFetchAlertData = (alertIds: string[]) => [boolean, Record<string, unknown>];

View file

@ -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.

View file

@ -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 => {

View file

@ -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' } },
],

View file

@ -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);

View file

@ -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];
};

View file

@ -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;

View file

@ -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,
};
},
};

View file

@ -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,
};
};

View file

@ -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, {}]);
});
});
});

View file

@ -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));
};

View file

@ -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]);
});
});
});

View file

@ -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;
};

View file

@ -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(),
},
};
},
};

View file

@ -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]: {