mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[RCA] Start investigation from alert details page (#190307)
Resolves https://github.com/elastic/kibana/issues/190320 and https://github.com/elastic/kibana/issues/190396 - Start investigation from Custom threshold alert details page - Go to ongoing investigation instead of creating new one if one already exists - Initial investigation status is set as `ongoing` - Investigation origin is set as `alert` "Start investigation" is hidden for other alert types and when investigate plugin is disabled. ### Testing - Add the following in `kibana.dev.yml` ``` xpack.investigate.enabled: true xpack.investigateApp.enabled: true ``` - Create Custom threshold rule - Open Custom threshold alert details page - Click on "Start investigation" - Verify that a new saved object is created for the investigation https://github.com/user-attachments/assets/6dfe8a5f-287b-4cc5-92ae-e4c315c7420b --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kevin Delemme <kdelemme@gmail.com>
This commit is contained in:
parent
abc8495337
commit
95736fb536
26 changed files with 477 additions and 22 deletions
|
@ -14,3 +14,11 @@ export type {
|
|||
export { mergePlainObjects } from './utils/merge_plain_objects';
|
||||
|
||||
export { InvestigateWidgetColumnSpan } from './types';
|
||||
|
||||
export type { CreateInvestigationInput, CreateInvestigationResponse } from './schema/create';
|
||||
export type { GetInvestigationParams } from './schema/get';
|
||||
export type { FindInvestigationsResponse } from './schema/find';
|
||||
|
||||
export { createInvestigationParamsSchema } from './schema/create';
|
||||
export { getInvestigationParamsSchema } from './schema/get';
|
||||
export { findInvestigationsParamsSchema } from './schema/find';
|
||||
|
|
|
@ -6,14 +6,16 @@
|
|||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { investigationResponseSchema } from './investigation';
|
||||
import { alertOriginSchema, blankOriginSchema } from './origin';
|
||||
|
||||
const createInvestigationParamsSchema = t.type({
|
||||
body: t.type({
|
||||
id: t.string,
|
||||
title: t.string,
|
||||
parameters: t.type({
|
||||
params: t.type({
|
||||
timeRange: t.type({ from: t.number, to: t.number }),
|
||||
}),
|
||||
origin: t.union([alertOriginSchema, blankOriginSchema]),
|
||||
}),
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@ import { investigationResponseSchema } from './investigation';
|
|||
|
||||
const findInvestigationsParamsSchema = t.partial({
|
||||
query: t.partial({
|
||||
alertId: t.string,
|
||||
page: t.string,
|
||||
perPage: t.string,
|
||||
}),
|
|
@ -5,15 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { alertOriginSchema, blankOriginSchema } from './origin';
|
||||
|
||||
const investigationResponseSchema = t.type({
|
||||
id: t.string,
|
||||
title: t.string,
|
||||
createdAt: t.number,
|
||||
createdBy: t.string,
|
||||
parameters: t.type({
|
||||
params: t.type({
|
||||
timeRange: t.type({ from: t.number, to: t.number }),
|
||||
}),
|
||||
origin: t.union([alertOriginSchema, blankOriginSchema]),
|
||||
status: t.union([t.literal('ongoing'), t.literal('closed')]),
|
||||
});
|
||||
|
||||
type InvestigationResponse = t.OutputOf<typeof investigationResponseSchema>;
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 * as t from 'io-ts';
|
||||
|
||||
const blankOriginSchema = t.type({ type: t.literal('blank') });
|
||||
const alertOriginSchema = t.type({ type: t.literal('alert'), id: t.string });
|
||||
|
||||
type AlertOrigin = t.OutputOf<typeof alertOriginSchema>;
|
||||
type BlankOrigin = t.OutputOf<typeof blankOriginSchema>;
|
||||
|
||||
export { alertOriginSchema, blankOriginSchema };
|
||||
|
||||
export type { AlertOrigin, BlankOrigin };
|
|
@ -9,6 +9,7 @@ export const investigationKeys = {
|
|||
all: ['investigation'] as const,
|
||||
list: (params: { page: number; perPage: number }) =>
|
||||
[...investigationKeys.all, 'list', params] as const,
|
||||
fetch: (params: { id: string }) => [...investigationKeys.all, 'fetch', params] as const,
|
||||
};
|
||||
|
||||
export type InvestigationKeys = typeof investigationKeys;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { FindInvestigationsResponse } from '../../common/schema/find';
|
||||
import { FindInvestigationsResponse } from '@kbn/investigate-plugin/common';
|
||||
import { investigationKeys } from './query_key_factory';
|
||||
import { useKibana } from './use_kibana';
|
||||
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import { BASE_RAC_ALERTS_API_PATH, EcsFieldsResponse } from '@kbn/rule-registry-plugin/common';
|
||||
import { useKibana } from './use_kibana';
|
||||
|
||||
export interface AlertParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface UseFetchAlertResponse {
|
||||
isInitialLoading: boolean;
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isSuccess: boolean;
|
||||
isError: boolean;
|
||||
data: EcsFieldsResponse | undefined | null;
|
||||
}
|
||||
|
||||
export function useFetchAlert({ id }: AlertParams): UseFetchAlertResponse {
|
||||
const {
|
||||
core: {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
|
||||
queryKey: ['fetchAlert', id],
|
||||
queryFn: async ({ signal }) => {
|
||||
return await http.get<EcsFieldsResponse>(BASE_RAC_ALERTS_API_PATH, {
|
||||
query: {
|
||||
id,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
},
|
||||
cacheTime: 0,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: (failureCount, error) => {
|
||||
if (String(error) === 'Error: Forbidden') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return failureCount < 3;
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toasts.addError(error, {
|
||||
title: 'Something went wrong while fetching alert',
|
||||
});
|
||||
},
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
isInitialLoading,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import { GetInvestigationResponse } from '@kbn/investigate-plugin/common/schema/get';
|
||||
import { investigationKeys } from './query_key_factory';
|
||||
import { useKibana } from './use_kibana';
|
||||
|
||||
export interface FetchInvestigationParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface UseFetchInvestigationResponse {
|
||||
isInitialLoading: boolean;
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isSuccess: boolean;
|
||||
isError: boolean;
|
||||
data: GetInvestigationResponse | undefined;
|
||||
}
|
||||
|
||||
export function useFetchInvestigation({
|
||||
id,
|
||||
}: FetchInvestigationParams): UseFetchInvestigationResponse {
|
||||
const {
|
||||
core: {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
|
||||
queryKey: investigationKeys.fetch({ id }),
|
||||
queryFn: async ({ signal }) => {
|
||||
return await http.get<GetInvestigationResponse>(`/api/observability/investigations/${id}`, {
|
||||
version: '2023-10-31',
|
||||
signal,
|
||||
});
|
||||
},
|
||||
cacheTime: 0,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: (failureCount, error) => {
|
||||
if (String(error) === 'Error: Forbidden') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return failureCount < 3;
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toasts.addError(error, {
|
||||
title: 'Something went wrong while fetching Investigation',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
isInitialLoading,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
};
|
||||
}
|
|
@ -5,11 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { EuiButton, EuiButtonEmpty, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { ALERT_RULE_CATEGORY } from '@kbn/rule-data-utils/src/default_alerts_as_data';
|
||||
import { AlertOrigin } from '@kbn/investigate-plugin/common/schema/origin';
|
||||
import { paths } from '../../../common/paths';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { useFetchInvestigation } from '../../hooks/use_get_investigation_details';
|
||||
import { useInvestigateParams } from '../../hooks/use_investigate_params';
|
||||
import { useFetchAlert } from '../../hooks/use_get_alert_details';
|
||||
import { InvestigationDetails } from './components/investigation_details';
|
||||
|
||||
export function InvestigationDetailsPage() {
|
||||
|
@ -22,8 +27,46 @@ export function InvestigationDetailsPage() {
|
|||
},
|
||||
} = useKibana();
|
||||
|
||||
const {
|
||||
path: { id },
|
||||
} = useInvestigateParams('/{id}');
|
||||
|
||||
const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate;
|
||||
|
||||
const {
|
||||
data: investigationDetails,
|
||||
isLoading: isFetchInvestigationLoading,
|
||||
isError: isFetchInvestigationError,
|
||||
} = useFetchInvestigation({ id });
|
||||
|
||||
const alertId = investigationDetails ? (investigationDetails.origin as AlertOrigin).id : '';
|
||||
|
||||
const {
|
||||
data: alertDetails,
|
||||
isLoading: isFetchAlertLoading,
|
||||
isError: isFetchAlertError,
|
||||
} = useFetchAlert({ id: alertId });
|
||||
|
||||
if (isFetchInvestigationLoading || isFetchAlertLoading) {
|
||||
return (
|
||||
<h1>
|
||||
{i18n.translate('xpack.investigateApp.fetchInvestigation.loadingLabel', {
|
||||
defaultMessage: 'Loading...',
|
||||
})}
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
if (isFetchInvestigationError || isFetchAlertError) {
|
||||
return (
|
||||
<h1>
|
||||
{i18n.translate('xpack.investigateApp.fetchInvestigation.errorLabel', {
|
||||
defaultMessage: 'Error while fetching investigation',
|
||||
})}
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ObservabilityPageTemplate
|
||||
pageHeader={{
|
||||
|
@ -40,12 +83,26 @@ export function InvestigationDetailsPage() {
|
|||
}),
|
||||
},
|
||||
],
|
||||
pageTitle: i18n.translate('xpack.investigateApp.detailsPage.title', {
|
||||
defaultMessage: 'New investigation',
|
||||
}),
|
||||
pageTitle: (
|
||||
<>
|
||||
{alertDetails && (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="investigationDetailsAlertLink"
|
||||
iconType="arrowLeft"
|
||||
size="xs"
|
||||
href={basePath.prepend(`/app/observability/alerts/${alertId}`)}
|
||||
>
|
||||
<EuiText size="s">
|
||||
{`[Alert] ${alertDetails?.[ALERT_RULE_CATEGORY]} breached`}
|
||||
</EuiText>
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
{investigationDetails && <div>{investigationDetails.title}</div>}
|
||||
</>
|
||||
),
|
||||
rightSideItems: [
|
||||
<EuiButton fill data-test-subj="investigateAppInvestigateDetailsPageEscalateButton">
|
||||
{i18n.translate('xpack.investigateApp.investigateDetailsPage.escalateButtonLabel', {
|
||||
<EuiButton fill data-test-subj="investigationDetailsEscalateButton">
|
||||
{i18n.translate('xpack.investigateApp.investigationDetails.escalateButtonLabel', {
|
||||
defaultMessage: 'Escalate',
|
||||
})}
|
||||
</EuiButton>,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { alertOriginSchema, blankOriginSchema } from '@kbn/investigate-plugin/common/schema/origin';
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export const investigationSchema = t.type({
|
||||
|
@ -12,9 +13,11 @@ export const investigationSchema = t.type({
|
|||
title: t.string,
|
||||
createdAt: t.number,
|
||||
createdBy: t.string,
|
||||
parameters: t.type({
|
||||
params: t.type({
|
||||
timeRange: t.type({ from: t.number, to: t.number }),
|
||||
}),
|
||||
origin: t.union([alertOriginSchema, blankOriginSchema]),
|
||||
status: t.union([t.literal('ongoing'), t.literal('closed')]),
|
||||
});
|
||||
|
||||
export type Investigation = t.TypeOf<typeof investigationSchema>;
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { findInvestigationsParamsSchema } from '../../common/schema/find';
|
||||
import { createInvestigationParamsSchema } from '../../common/schema/create';
|
||||
import { createInvestigationParamsSchema } from '@kbn/investigate-plugin/common';
|
||||
import { findInvestigationsParamsSchema } from '@kbn/investigate-plugin/common';
|
||||
import { getInvestigationParamsSchema } from '@kbn/investigate-plugin/common';
|
||||
import { createInvestigation } from '../services/create_investigation';
|
||||
import { investigationRepositoryFactory } from '../services/investigation_repository';
|
||||
import { createInvestigateAppServerRoute } from './create_investigate_app_server_route';
|
||||
import { findInvestigations } from '../services/find_investigations';
|
||||
import { getInvestigationParamsSchema } from '../../common/schema/get';
|
||||
import { getInvestigation } from '../services/get_investigation';
|
||||
|
||||
const createInvestigationRoute = createInvestigateAppServerRoute({
|
||||
|
|
|
@ -15,12 +15,18 @@ export const investigation: SavedObjectsType = {
|
|||
name: SO_INVESTIGATION_TYPE,
|
||||
hidden: false,
|
||||
namespaceType: 'multiple-isolated',
|
||||
switchToModelVersionAt: '8.10.0',
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
properties: {
|
||||
id: { type: 'keyword' },
|
||||
title: { type: 'text' },
|
||||
origin: {
|
||||
properties: {
|
||||
type: { type: 'keyword' },
|
||||
id: { type: 'keyword' },
|
||||
},
|
||||
},
|
||||
status: { type: 'keyword' },
|
||||
},
|
||||
},
|
||||
management: {
|
||||
|
|
|
@ -5,14 +5,27 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CreateInvestigationInput, CreateInvestigationResponse } from '../../common/schema/create';
|
||||
import {
|
||||
CreateInvestigationInput,
|
||||
CreateInvestigationResponse,
|
||||
} from '@kbn/investigate-plugin/common';
|
||||
import { InvestigationRepository } from './investigation_repository';
|
||||
|
||||
enum InvestigationStatus {
|
||||
ongoing = 'ongoing',
|
||||
closed = 'closed',
|
||||
}
|
||||
|
||||
export async function createInvestigation(
|
||||
params: CreateInvestigationInput,
|
||||
repository: InvestigationRepository
|
||||
): Promise<CreateInvestigationResponse> {
|
||||
const investigation = { ...params, createdAt: Date.now(), createdBy: 'elastic' };
|
||||
const investigation = {
|
||||
...params,
|
||||
createdAt: Date.now(),
|
||||
createdBy: 'elastic',
|
||||
status: InvestigationStatus.ongoing,
|
||||
};
|
||||
await repository.save(investigation);
|
||||
|
||||
return investigation;
|
||||
|
|
|
@ -5,18 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FindInvestigationsResponse } from '@kbn/investigate-plugin/common';
|
||||
import {
|
||||
FindInvestigationsParams,
|
||||
FindInvestigationsResponse,
|
||||
findInvestigationsResponseSchema,
|
||||
} from '../../common/schema/find';
|
||||
} from '@kbn/investigate-plugin/common/schema/find';
|
||||
import { InvestigationRepository } from './investigation_repository';
|
||||
|
||||
export async function findInvestigations(
|
||||
params: FindInvestigationsParams,
|
||||
repository: InvestigationRepository
|
||||
): Promise<FindInvestigationsResponse> {
|
||||
const investigations = await repository.search(toPagination(params));
|
||||
const investigations = await repository.search(toFilter(params), toPagination(params));
|
||||
|
||||
return findInvestigationsResponseSchema.encode(investigations);
|
||||
}
|
||||
|
@ -29,3 +29,10 @@ function toPagination(params: FindInvestigationsParams) {
|
|||
perPage: params.perPage ? parseInt(params.perPage, 10) : DEFAULT_PER_PAGE,
|
||||
};
|
||||
}
|
||||
|
||||
function toFilter(params: FindInvestigationsParams) {
|
||||
if (params.alertId) {
|
||||
return `investigation.attributes.origin.id:(${params.alertId}) AND investigation.attributes.status: ongoing`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { GetInvestigationParams, GetInvestigationResponse } from '../../common/schema/get';
|
||||
import { GetInvestigationParams } from '@kbn/investigate-plugin/common';
|
||||
import { GetInvestigationResponse } from '@kbn/investigate-plugin/common/schema/get';
|
||||
import { InvestigationRepository } from './investigation_repository';
|
||||
|
||||
export async function getInvestigation(
|
||||
|
|
|
@ -15,7 +15,7 @@ export interface InvestigationRepository {
|
|||
save(investigation: Investigation): Promise<void>;
|
||||
findById(id: string): Promise<Investigation>;
|
||||
deleteById(id: string): Promise<void>;
|
||||
search(pagination: Pagination): Promise<Paginated<Investigation>>;
|
||||
search(filter: string, pagination: Pagination): Promise<Paginated<Investigation>>;
|
||||
}
|
||||
|
||||
export function investigationRepositoryFactory({
|
||||
|
@ -89,11 +89,12 @@ export function investigationRepositoryFactory({
|
|||
await soClient.delete(SO_INVESTIGATION_TYPE, response.saved_objects[0].id);
|
||||
},
|
||||
|
||||
async search(pagination: Pagination): Promise<Paginated<Investigation>> {
|
||||
async search(filter: string, pagination: Pagination): Promise<Paginated<Investigation>> {
|
||||
const response = await soClient.find<StoredInvestigation>({
|
||||
type: SO_INVESTIGATION_TYPE,
|
||||
page: pagination.page,
|
||||
perPage: pagination.perPage,
|
||||
filter,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -52,5 +52,7 @@
|
|||
"@kbn/esql-datagrid",
|
||||
"@kbn/server-route-repository-utils",
|
||||
"@kbn/core-saved-objects-server",
|
||||
"@kbn/rule-registry-plugin",
|
||||
"@kbn/rule-data-utils",
|
||||
],
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"serverless",
|
||||
"guidedOnboarding",
|
||||
"observabilityAIAssistant",
|
||||
"investigate"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"data",
|
||||
|
|
|
@ -34,6 +34,10 @@ const mockHttp = {
|
|||
},
|
||||
};
|
||||
|
||||
const mockNavigateToApp = {
|
||||
mockNavigateToApp: jest.fn(),
|
||||
};
|
||||
|
||||
const mockGetEditRuleFlyout = jest.fn(() => (
|
||||
<div data-test-subj="edit-rule-flyout">mocked component</div>
|
||||
));
|
||||
|
@ -48,6 +52,7 @@ const mockKibana = () => {
|
|||
},
|
||||
cases: mockCases,
|
||||
http: mockHttp,
|
||||
application: mockNavigateToApp,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -25,13 +25,22 @@ import {
|
|||
ALERT_RULE_UUID,
|
||||
ALERT_STATUS_ACTIVE,
|
||||
ALERT_UUID,
|
||||
ALERT_RULE_CATEGORY,
|
||||
ALERT_START,
|
||||
ALERT_END,
|
||||
ALERT_RULE_TYPE_ID,
|
||||
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
|
||||
} from '@kbn/rule-data-utils';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { useFetchRule } from '../../../hooks/use_fetch_rule';
|
||||
import type { TopAlert } from '../../../typings/alerts';
|
||||
import { paths } from '../../../../common/locators/paths';
|
||||
import { useBulkUntrackAlerts } from '../hooks/use_bulk_untrack_alerts';
|
||||
import { useCreateInvestigation } from '../hooks/use_create_investigation';
|
||||
import { useFetchInvestigationsByAlert } from '../hooks/use_fetch_investigations_by_alert';
|
||||
|
||||
export interface HeaderActionsProps {
|
||||
alert: TopAlert | null;
|
||||
|
@ -52,12 +61,18 @@ export function HeaderActions({
|
|||
},
|
||||
triggersActionsUi: { getEditRuleFlyout: EditRuleFlyout, getRuleSnoozeModal: RuleSnoozeModal },
|
||||
http,
|
||||
application: { navigateToApp },
|
||||
investigate: investigatePlugin,
|
||||
} = useKibana().services;
|
||||
|
||||
const { rule, refetch } = useFetchRule({
|
||||
ruleId: alert?.fields[ALERT_RULE_UUID] || '',
|
||||
});
|
||||
|
||||
const { data: investigations } = useFetchInvestigationsByAlert({
|
||||
alertId: alert?.fields[ALERT_UUID] ?? '',
|
||||
});
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
const [ruleConditionsFlyoutOpen, setRuleConditionsFlyoutOpen] = useState<boolean>(false);
|
||||
const [snoozeModalOpen, setSnoozeModalOpen] = useState<boolean>(false);
|
||||
|
@ -109,9 +124,67 @@ export function HeaderActions({
|
|||
setSnoozeModalOpen(true);
|
||||
};
|
||||
|
||||
const { mutateAsync: createInvestigation } = useCreateInvestigation();
|
||||
|
||||
const alertStart = alert?.fields[ALERT_START];
|
||||
const alertEnd = alert?.fields[ALERT_END];
|
||||
|
||||
const createOrOpenInvestigation = async () => {
|
||||
if (!alert) return;
|
||||
|
||||
if (!investigations || investigations.results.length === 0) {
|
||||
const paddedAlertTimeRange = getPaddedAlertTimeRange(alertStart!, alertEnd);
|
||||
|
||||
const investigationResponse = await createInvestigation({
|
||||
investigation: {
|
||||
id: uuidv4(),
|
||||
title: `Investigate ${alert.fields[ALERT_RULE_CATEGORY]} breached`,
|
||||
params: {
|
||||
timeRange: {
|
||||
from: new Date(paddedAlertTimeRange.from).getTime(),
|
||||
to: new Date(paddedAlertTimeRange.to).getTime(),
|
||||
},
|
||||
},
|
||||
origin: {
|
||||
type: 'alert',
|
||||
id: alert.fields[ALERT_UUID],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
navigateToApp('investigate', { path: `/${investigationResponse.id}`, replace: false });
|
||||
} else {
|
||||
navigateToApp('investigate', {
|
||||
path: `/${investigations.results[0].id}`,
|
||||
replace: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="row" gutterSize="s" justifyContent="flexEnd">
|
||||
{Boolean(investigatePlugin) &&
|
||||
alert?.fields[ALERT_RULE_TYPE_ID] === OBSERVABILITY_THRESHOLD_RULE_TYPE_ID && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={() => {
|
||||
createOrOpenInvestigation();
|
||||
}}
|
||||
fill
|
||||
data-test-subj="investigate-alert-button"
|
||||
>
|
||||
<EuiText size="s">
|
||||
{i18n.translate('xpack.observability.alertDetails.investigateAlert', {
|
||||
defaultMessage:
|
||||
!investigations || investigations.results.length === 0
|
||||
? 'Start investigation'
|
||||
: 'Ongoing investigation',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
|
|
|
@ -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 { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
|
||||
import { QueryKey, useMutation } from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
CreateInvestigationInput,
|
||||
CreateInvestigationResponse,
|
||||
} from '@kbn/investigate-plugin/common';
|
||||
import { FindInvestigationsResponse } from '@kbn/investigate-plugin/common';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
type ServerError = IHttpFetchError<ResponseErrorBody>;
|
||||
|
||||
export function useCreateInvestigation() {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
return useMutation<
|
||||
CreateInvestigationResponse,
|
||||
ServerError,
|
||||
{ investigation: CreateInvestigationInput },
|
||||
{ previousData?: FindInvestigationsResponse; queryKey?: QueryKey }
|
||||
>(
|
||||
['createInvestigation'],
|
||||
({ investigation }) => {
|
||||
const body = JSON.stringify(investigation);
|
||||
return http.post<CreateInvestigationResponse>(`/api/observability/investigations`, { body });
|
||||
},
|
||||
{
|
||||
onError: (error, { investigation }, context) => {
|
||||
toasts.addError(new Error(error.body?.message ?? error.message), {
|
||||
title: i18n.translate('xpack.observability.create.errorNotification', {
|
||||
defaultMessage: 'Something went wrong while creating investigation',
|
||||
}),
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import { FindInvestigationsResponse } from '@kbn/investigate-plugin/common';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
export interface InvestigationsByAlertParams {
|
||||
alertId: string;
|
||||
}
|
||||
|
||||
export interface UseFetchInvestigationsByAlertResponse {
|
||||
isInitialLoading: boolean;
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isSuccess: boolean;
|
||||
isError: boolean;
|
||||
data: FindInvestigationsResponse | undefined;
|
||||
}
|
||||
|
||||
export function useFetchInvestigationsByAlert({
|
||||
alertId,
|
||||
}: InvestigationsByAlertParams): UseFetchInvestigationsByAlertResponse {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
investigate: investigatePlugin,
|
||||
} = useKibana().services;
|
||||
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
|
||||
queryKey: ['fetchInvestigationsByAlert', alertId],
|
||||
queryFn: async ({ signal }) => {
|
||||
return await http.get<FindInvestigationsResponse>('/api/observability/investigations', {
|
||||
query: { alertId },
|
||||
version: '2023-10-31',
|
||||
signal,
|
||||
});
|
||||
},
|
||||
cacheTime: 0,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: (failureCount, error) => {
|
||||
if (String(error) === 'Error: Forbidden') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return failureCount < 3;
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toasts.addError(error, {
|
||||
title: 'Something went wrong while fetching Investigations',
|
||||
});
|
||||
},
|
||||
enabled: Boolean(investigatePlugin),
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
isInitialLoading,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
};
|
||||
}
|
|
@ -70,6 +70,7 @@ import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'
|
|||
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
|
||||
import { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
|
||||
import { LicenseManagementUIPluginSetup } from '@kbn/license-management-plugin/public';
|
||||
import { InvestigatePublicStart } from '@kbn/investigate-plugin/public';
|
||||
import { observabilityAppId, observabilityFeatureId } from '../common';
|
||||
import {
|
||||
ALERTS_PATH,
|
||||
|
@ -161,6 +162,7 @@ export interface ObservabilityPublicPluginsStart {
|
|||
theme: CoreStart['theme'];
|
||||
dataViewFieldEditor: DataViewFieldEditorStart;
|
||||
toastNotifications: ToastsStart;
|
||||
investigate?: InvestigatePublicStart;
|
||||
}
|
||||
export type ObservabilityPublicStart = ReturnType<Plugin['start']>;
|
||||
|
||||
|
|
|
@ -110,6 +110,7 @@
|
|||
"@kbn/license-management-plugin",
|
||||
"@kbn/observability-alerting-rule-utils",
|
||||
"@kbn/core-ui-settings-server-mocks",
|
||||
"@kbn/investigate-plugin",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue