diff --git a/x-pack/plugins/observability_solution/investigate/common/index.ts b/x-pack/plugins/observability_solution/investigate/common/index.ts index 7846f257f64fe..71585c294f776 100644 --- a/x-pack/plugins/observability_solution/investigate/common/index.ts +++ b/x-pack/plugins/observability_solution/investigate/common/index.ts @@ -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'; diff --git a/x-pack/plugins/observability_solution/investigate_app/common/schema/create.ts b/x-pack/plugins/observability_solution/investigate/common/schema/create.ts similarity index 88% rename from x-pack/plugins/observability_solution/investigate_app/common/schema/create.ts rename to x-pack/plugins/observability_solution/investigate/common/schema/create.ts index 050fa67d5cbaf..f2ba04de9e88a 100644 --- a/x-pack/plugins/observability_solution/investigate_app/common/schema/create.ts +++ b/x-pack/plugins/observability_solution/investigate/common/schema/create.ts @@ -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]), }), }); diff --git a/x-pack/plugins/observability_solution/investigate_app/common/schema/find.ts b/x-pack/plugins/observability_solution/investigate/common/schema/find.ts similarity index 97% rename from x-pack/plugins/observability_solution/investigate_app/common/schema/find.ts rename to x-pack/plugins/observability_solution/investigate/common/schema/find.ts index dc76a39fce679..4eb9d6f9bf66c 100644 --- a/x-pack/plugins/observability_solution/investigate_app/common/schema/find.ts +++ b/x-pack/plugins/observability_solution/investigate/common/schema/find.ts @@ -9,6 +9,7 @@ import { investigationResponseSchema } from './investigation'; const findInvestigationsParamsSchema = t.partial({ query: t.partial({ + alertId: t.string, page: t.string, perPage: t.string, }), diff --git a/x-pack/plugins/observability_solution/investigate_app/common/schema/get.ts b/x-pack/plugins/observability_solution/investigate/common/schema/get.ts similarity index 100% rename from x-pack/plugins/observability_solution/investigate_app/common/schema/get.ts rename to x-pack/plugins/observability_solution/investigate/common/schema/get.ts diff --git a/x-pack/plugins/observability_solution/investigate_app/common/schema/investigation.ts b/x-pack/plugins/observability_solution/investigate/common/schema/investigation.ts similarity index 75% rename from x-pack/plugins/observability_solution/investigate_app/common/schema/investigation.ts rename to x-pack/plugins/observability_solution/investigate/common/schema/investigation.ts index 3046a5c4c6d8a..23247bba88ab8 100644 --- a/x-pack/plugins/observability_solution/investigate_app/common/schema/investigation.ts +++ b/x-pack/plugins/observability_solution/investigate/common/schema/investigation.ts @@ -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; diff --git a/x-pack/plugins/observability_solution/investigate/common/schema/origin.ts b/x-pack/plugins/observability_solution/investigate/common/schema/origin.ts new file mode 100644 index 0000000000000..1c00f5ad73de8 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate/common/schema/origin.ts @@ -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; +type BlankOrigin = t.OutputOf; + +export { alertOriginSchema, blankOriginSchema }; + +export type { AlertOrigin, BlankOrigin }; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts index a5ff06fa24a26..5ce96f5a2061c 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts @@ -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; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_investigation_list.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_investigation_list.ts index a3ca59519211a..fef9ce274d63e 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_investigation_list.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_investigation_list.ts @@ -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'; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_get_alert_details.tsx b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_get_alert_details.tsx new file mode 100644 index 0000000000000..5f3bc3f3c9b6a --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_get_alert_details.tsx @@ -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(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, + }; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_get_investigation_details.tsx b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_get_investigation_details.tsx new file mode 100644 index 0000000000000..796d8bc561575 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_get_investigation_details.tsx @@ -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(`/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, + }; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/investigation_details_page.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/investigation_details_page.tsx index 008ae1a18524a..ed749ce925ea1 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/investigation_details_page.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/investigation_details_page.tsx @@ -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 ( +

+ {i18n.translate('xpack.investigateApp.fetchInvestigation.loadingLabel', { + defaultMessage: 'Loading...', + })} +

+ ); + } + + if (isFetchInvestigationError || isFetchAlertError) { + return ( +

+ {i18n.translate('xpack.investigateApp.fetchInvestigation.errorLabel', { + defaultMessage: 'Error while fetching investigation', + })} +

+ ); + } + return ( + {alertDetails && ( + + + {`[Alert] ${alertDetails?.[ALERT_RULE_CATEGORY]} breached`} + + + )} + {investigationDetails &&
{investigationDetails.title}
} + + ), rightSideItems: [ - - {i18n.translate('xpack.investigateApp.investigateDetailsPage.escalateButtonLabel', { + + {i18n.translate('xpack.investigateApp.investigationDetails.escalateButtonLabel', { defaultMessage: 'Escalate', })} , diff --git a/x-pack/plugins/observability_solution/investigate_app/server/models/investigation.ts b/x-pack/plugins/observability_solution/investigate_app/server/models/investigation.ts index f079d922d1a9d..041b1d217c208 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/models/investigation.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/models/investigation.ts @@ -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; diff --git a/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts b/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts index 7565330316fed..f745b46bb54b8 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts @@ -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({ diff --git a/x-pack/plugins/observability_solution/investigate_app/server/saved_objects/investigation.ts b/x-pack/plugins/observability_solution/investigate_app/server/saved_objects/investigation.ts index 47fa9f17749c1..eeb937fb16cfa 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/saved_objects/investigation.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/saved_objects/investigation.ts @@ -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: { diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation.ts index 9d28136c06f6c..52b5953fb8095 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation.ts @@ -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 { - 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; diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/find_investigations.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/find_investigations.ts index 2c08125aff734..0db6f2eb587b8 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/find_investigations.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/find_investigations.ts @@ -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 { - 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 ''; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/get_investigation.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/get_investigation.ts index 9bd6025f8e8a2..e57111af99e32 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/get_investigation.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/get_investigation.ts @@ -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( diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/investigation_repository.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/investigation_repository.ts index 6d9c5ddbc0d03..090930351fc14 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/investigation_repository.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/investigation_repository.ts @@ -15,7 +15,7 @@ export interface InvestigationRepository { save(investigation: Investigation): Promise; findById(id: string): Promise; deleteById(id: string): Promise; - search(pagination: Pagination): Promise>; + search(filter: string, pagination: Pagination): Promise>; } 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> { + async search(filter: string, pagination: Pagination): Promise> { const response = await soClient.find({ type: SO_INVESTIGATION_TYPE, page: pagination.page, perPage: pagination.perPage, + filter, }); return { diff --git a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json index b4b586601ebaa..6647b16356984 100644 --- a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json +++ b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json @@ -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", ], } diff --git a/x-pack/plugins/observability_solution/observability/kibana.jsonc b/x-pack/plugins/observability_solution/observability/kibana.jsonc index 997b6196def4b..8e97ea707641d 100644 --- a/x-pack/plugins/observability_solution/observability/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability/kibana.jsonc @@ -51,6 +51,7 @@ "serverless", "guidedOnboarding", "observabilityAIAssistant", + "investigate" ], "requiredBundles": [ "data", diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/header_actions.test.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/header_actions.test.tsx index b3e28ba222c5a..d484773e0b14a 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/header_actions.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/header_actions.test.tsx @@ -34,6 +34,10 @@ const mockHttp = { }, }; +const mockNavigateToApp = { + mockNavigateToApp: jest.fn(), +}; + const mockGetEditRuleFlyout = jest.fn(() => (
mocked component
)); @@ -48,6 +52,7 @@ const mockKibana = () => { }, cases: mockCases, http: mockHttp, + application: mockNavigateToApp, }, }); }; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/header_actions.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/header_actions.tsx index 3d47e34002945..6ea43b165fe7a 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/header_actions.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/header_actions.tsx @@ -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(false); const [ruleConditionsFlyoutOpen, setRuleConditionsFlyoutOpen] = useState(false); const [snoozeModalOpen, setSnoozeModalOpen] = useState(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 ( <> + {Boolean(investigatePlugin) && + alert?.fields[ALERT_RULE_TYPE_ID] === OBSERVABILITY_THRESHOLD_RULE_TYPE_ID && ( + + { + createOrOpenInvestigation(); + }} + fill + data-test-subj="investigate-alert-button" + > + + {i18n.translate('xpack.observability.alertDetails.investigateAlert', { + defaultMessage: + !investigations || investigations.results.length === 0 + ? 'Start investigation' + : 'Ongoing investigation', + })} + + + + )} ; + +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(`/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', + }), + }); + }, + } + ); +} diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/hooks/use_fetch_investigations_by_alert.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/hooks/use_fetch_investigations_by_alert.tsx new file mode 100644 index 0000000000000..d9376f30d9579 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/hooks/use_fetch_investigations_by_alert.tsx @@ -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('/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, + }; +} diff --git a/x-pack/plugins/observability_solution/observability/public/plugin.ts b/x-pack/plugins/observability_solution/observability/public/plugin.ts index a97085f0c036d..b2d0e526f3c64 100644 --- a/x-pack/plugins/observability_solution/observability/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability/public/plugin.ts @@ -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; diff --git a/x-pack/plugins/observability_solution/observability/tsconfig.json b/x-pack/plugins/observability_solution/observability/tsconfig.json index 28f06a81d17c0..53390c6c2d535 100644 --- a/x-pack/plugins/observability_solution/observability/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability/tsconfig.json @@ -110,6 +110,7 @@ "@kbn/license-management-plugin", "@kbn/observability-alerting-rule-utils", "@kbn/core-ui-settings-server-mocks", + "@kbn/investigate-plugin", ], "exclude": [ "target/**/*"