diff --git a/packages/kbn-investigation-shared/src/rest_specs/event.ts b/packages/kbn-investigation-shared/src/rest_specs/event.ts index df2f3941ad332..e63083f75c824 100644 --- a/packages/kbn-investigation-shared/src/rest_specs/event.ts +++ b/packages/kbn-investigation-shared/src/rest_specs/event.ts @@ -13,6 +13,7 @@ import { eventSchema } from '../schema'; const eventResponseSchema = eventSchema; type EventResponse = z.output; +type EventSchema = z.output; export { eventResponseSchema }; -export type { EventResponse }; +export type { EventResponse, EventSchema }; diff --git a/x-pack/packages/observability/observability_utils/chart/utils.ts b/x-pack/packages/observability/observability_utils/chart/utils.ts new file mode 100644 index 0000000000000..487b4df200c40 --- /dev/null +++ b/x-pack/packages/observability/observability_utils/chart/utils.ts @@ -0,0 +1,16 @@ +/* + * 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 { BrushEvent } from '@elastic/charts'; +import moment from 'moment'; + +export function getBrushData(e: BrushEvent) { + const [from, to] = [Number(e.x?.[0]), Number(e.x?.[1])]; + const [fromUtc, toUtc] = [moment(from).format(), moment(to).format()]; + + return { from: fromUtc, to: toUtc }; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/.storybook/get_mock_investigate_app_services.tsx b/x-pack/plugins/observability_solution/investigate_app/.storybook/get_mock_investigate_app_services.tsx index 13e2c008648e5..42ccc20d52a71 100644 --- a/x-pack/plugins/observability_solution/investigate_app/.storybook/get_mock_investigate_app_services.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/.storybook/get_mock_investigate_app_services.tsx @@ -53,6 +53,7 @@ export function getMockInvestigateAppContext(): DeeplyMockedKeys) => [...investigationKeys.all, 'userProfiles', ...profileIds] as const, tags: () => [...investigationKeys.all, 'tags'] as const, + events: (rangeFrom?: string, rangeTo?: string) => + [...investigationKeys.all, 'events', rangeFrom, rangeTo] as const, stats: () => [...investigationKeys.all, 'stats'] as const, lists: () => [...investigationKeys.all, 'list'] as const, list: (params: { page: number; perPage: number; search?: string; filter?: string }) => diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_events.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_events.ts new file mode 100644 index 0000000000000..61b0c441c1fc2 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_events.ts @@ -0,0 +1,70 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { useQuery } from '@tanstack/react-query'; +import { GetEventsResponse } from '@kbn/investigation-shared'; +import { investigationKeys } from './query_key_factory'; +import { useKibana } from './use_kibana'; + +export interface Response { + isInitialLoading: boolean; + isLoading: boolean; + isRefetching: boolean; + isSuccess: boolean; + isError: boolean; + data?: GetEventsResponse; +} + +export function useFetchEvents({ + rangeFrom, + rangeTo, +}: { + rangeFrom?: string; + rangeTo?: string; +}): Response { + const { + core: { + http, + notifications: { toasts }, + }, + } = useKibana(); + + const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({ + queryKey: investigationKeys.events(rangeFrom, rangeTo), + queryFn: async ({ signal }) => { + return await http.get(`/api/observability/events`, { + query: { + rangeFrom, + rangeTo, + }, + version: '2023-10-31', + signal, + }); + }, + cacheTime: 600 * 1000, // 10_minutes + staleTime: 0, + refetchOnWindowFocus: false, + retry: false, + onError: (error: Error) => { + toasts.addError(error, { + title: i18n.translate('xpack.investigateApp.events.fetch.error', { + defaultMessage: 'Something went wrong while fetching the events', + }), + }); + }, + }); + + return { + data, + isInitialLoading, + isLoading, + isRefetching, + isSuccess, + isError, + }; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/alert_event.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/alert_event.tsx new file mode 100644 index 0000000000000..2e5ab220054e4 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/alert_event.tsx @@ -0,0 +1,34 @@ +/* + * 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 { LineAnnotation, AnnotationDomainType } from '@elastic/charts'; +import { EuiIcon } from '@elastic/eui'; +import React from 'react'; +import moment from 'moment'; +import { EventSchema } from '@kbn/investigation-shared'; + +export const AlertEvent = ({ event }: { event: EventSchema }) => { + return ( + + + + } + markerPosition="bottom" + dataValues={[ + { + dataValue: moment(event.timestamp).valueOf(), + header: moment(event.timestamp).format('lll'), + details: event.description, + }, + ]} + /> + ); +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/annotation_event.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/annotation_event.tsx new file mode 100644 index 0000000000000..1db856575441a --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/annotation_event.tsx @@ -0,0 +1,36 @@ +/* + * 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 React from 'react'; +import moment from 'moment'; +import { AnnotationDomainType, LineAnnotation } from '@elastic/charts'; +import { EuiIcon } from '@elastic/eui'; +import { EventSchema } from '@kbn/investigation-shared'; + +export function AnnotationEvent({ event }: { event: EventSchema }) { + const timestamp = event.timestamp; + + return ( + + + + } + markerPosition="bottom" + /> + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/events_timeline.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/events_timeline.tsx new file mode 100644 index 0000000000000..70f4159924bd1 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/events_timeline.tsx @@ -0,0 +1,114 @@ +/* + * 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 React, { useMemo, useRef } from 'react'; +import moment from 'moment'; + +import { Chart, Axis, AreaSeries, Position, ScaleType, Settings } from '@elastic/charts'; +import { useActiveCursor } from '@kbn/charts-plugin/public'; +import { EuiSkeletonText } from '@elastic/eui'; +import { getBrushData } from '@kbn/observability-utils/chart/utils'; +import { AnnotationEvent } from './annotation_event'; +import { TIME_LINE_THEME } from './timeline_theme'; +import { useFetchEvents } from '../../../../hooks/use_fetch_events'; +import { useInvestigation } from '../../contexts/investigation_context'; +import { useKibana } from '../../../../hooks/use_kibana'; +import { AlertEvent } from './alert_event'; + +export const EventsTimeLine = () => { + const { dependencies } = useKibana(); + + const baseTheme = dependencies.start.charts.theme.useChartsBaseTheme(); + + const { globalParams, updateInvestigationParams } = useInvestigation(); + + const { data: events, isLoading } = useFetchEvents({ + rangeFrom: globalParams.timeRange.from, + rangeTo: globalParams.timeRange.to, + }); + + const chartRef = useRef(null); + const handleCursorUpdate = useActiveCursor(dependencies.start.charts.activeCursor, chartRef, { + isDateHistogram: true, + }); + + const data = useMemo(() => { + const points = [ + { x: moment(globalParams.timeRange.from).valueOf(), y: 0 }, + { x: moment(globalParams.timeRange.to).valueOf(), y: 0 }, + ]; + + // adding 100 fake points to the chart so the chart shows cursor on hover + for (let i = 0; i < 100; i++) { + const diff = + moment(globalParams.timeRange.to).valueOf() - moment(globalParams.timeRange.from).valueOf(); + points.push({ x: moment(globalParams.timeRange.from).valueOf() + (diff / 100) * i, y: 0 }); + } + return points; + }, [globalParams.timeRange.from, globalParams.timeRange.to]); + + if (isLoading) { + return ; + } + + const alertEvents = events?.filter((evt) => evt.eventType === 'alert'); + const annotations = events?.filter((evt) => evt.eventType === 'annotation'); + + return ( + <> + + { + const { from, to } = getBrushData(brush); + updateInvestigationParams({ + timeRange: { from, to }, + }); + }} + /> + + moment(d).format('LTS')} + style={{ + tickLine: { + visible: true, + strokeWidth: 1, + stroke: '#98A2B3', + }, + }} + /> + + {alertEvents?.map((event) => ( + + ))} + + {annotations?.map((annotation) => ( + + ))} + + false} + /> + + + ); +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/timeline_theme.ts b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/timeline_theme.ts new file mode 100644 index 0000000000000..a1d7441fee539 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/timeline_theme.ts @@ -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 { PartialTheme } from '@elastic/charts'; + +export const TIME_LINE_THEME: PartialTheme = { + highlighter: { + point: { + opacity: 0, + }, + }, + axes: { + gridLine: { + horizontal: { + visible: false, + }, + vertical: { + visible: false, + }, + }, + axisLine: { + strokeWidth: 1, + stroke: '#98A2B3', + }, + }, + chartMargins: { + bottom: 10, + top: 10, + }, + areaSeriesStyle: { + area: { + visible: false, + }, + line: { + visible: false, + }, + }, + lineAnnotation: { + line: { + opacity: 0, + }, + }, +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_items/investigation_items.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_items/investigation_items.tsx index a95c50274d198..bd03324a994ac 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_items/investigation_items.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_items/investigation_items.tsx @@ -6,8 +6,9 @@ */ import datemath from '@elastic/datemath'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React from 'react'; +import { EventsTimeLine } from '../events_timeline/events_timeline'; import { useInvestigation } from '../../contexts/investigation_context'; import { AddInvestigationItem } from '../add_investigation_item/add_investigation_item'; import { InvestigationItemsList } from '../investigation_items_list/investigation_items_list'; @@ -18,8 +19,8 @@ export function InvestigationItems() { const { globalParams, updateInvestigationParams, investigation } = useInvestigation(); return ( - - + <> + - - {investigation?.id && ( - + - )} - - - + + {investigation?.id && ( + + + + )} + + + + + + + - + ); } diff --git a/x-pack/plugins/observability_solution/investigate_app/public/plugin.tsx b/x-pack/plugins/observability_solution/investigate_app/public/plugin.tsx index a98d89fb314b2..5ec88f9d72468 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/plugin.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/plugin.tsx @@ -86,11 +86,13 @@ export class InvestigateAppPlugin ]); const services: InvestigateAppServices = { + ...coreStart, esql: createEsqlService({ data: pluginsStart.data, dataViews: pluginsStart.dataViews, lens: pluginsStart.lens, }), + charts: pluginsStart.charts, }; ReactDOM.render( @@ -130,6 +132,7 @@ export class InvestigateAppPlugin dataViews: pluginsStart.dataViews, lens: pluginsStart.lens, }), + charts: pluginsStart.charts, }, }); }); diff --git a/x-pack/plugins/observability_solution/investigate_app/public/services/types.ts b/x-pack/plugins/observability_solution/investigate_app/public/services/types.ts index 149cacf2434a7..257ed5a7aeaca 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/services/types.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/services/types.ts @@ -5,8 +5,10 @@ * 2.0. */ +import { ChartsPluginStart } from '@kbn/charts-plugin/public'; import type { EsqlService } from './esql'; export interface InvestigateAppServices { esql: EsqlService; + charts: ChartsPluginStart; } diff --git a/x-pack/plugins/observability_solution/investigate_app/public/types.ts b/x-pack/plugins/observability_solution/investigate_app/public/types.ts index 101d6993ab9c5..a2d7b5227a201 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/types.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/types.ts @@ -8,6 +8,7 @@ import type { ObservabilityAIAssistantPublicSetup, ObservabilityAIAssistantPublicStart, } from '@kbn/observability-ai-assistant-plugin/public'; +import { ChartsPluginStart } from '@kbn/charts-plugin/public'; import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { @@ -66,6 +67,7 @@ export interface InvestigateAppStartDependencies { unifiedSearch: UnifiedSearchPublicPluginStart; uiActions: UiActionsStart; security: SecurityPluginStart; + charts: ChartsPluginStart; } export interface InvestigateAppPublicSetup {} diff --git a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json index 377db42186f5e..7ea8234fba670 100644 --- a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json +++ b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json @@ -66,6 +66,7 @@ "@kbn/core-elasticsearch-server", "@kbn/calculate-auto", "@kbn/ml-random-sampler-utils", + "@kbn/charts-plugin", "@kbn/observability-utils", ], }