mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
feat(investigation): Add eventTypes filter on the API (#202829)
This commit is contained in:
parent
668f776583
commit
2ab38a3664
36 changed files with 538 additions and 439 deletions
|
@ -1,19 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
import { eventSchema } from '../schema';
|
||||
|
||||
const eventResponseSchema = eventSchema;
|
||||
|
||||
type EventResponse = z.output<typeof eventResponseSchema>;
|
||||
type EventSchema = z.output<typeof eventSchema>;
|
||||
|
||||
export { eventResponseSchema };
|
||||
export type { EventResponse, EventSchema };
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
import { eventResponseSchema } from './event';
|
||||
import { eventTypeSchema, eventSchema } from '../schema';
|
||||
|
||||
const getEventsParamsSchema = z
|
||||
.object({
|
||||
|
@ -17,12 +17,24 @@ const getEventsParamsSchema = z
|
|||
rangeFrom: z.string(),
|
||||
rangeTo: z.string(),
|
||||
filter: z.string(),
|
||||
eventTypes: z.string().transform((val, ctx) => {
|
||||
const eventTypes = val.split(',');
|
||||
const hasInvalidType = eventTypes.some((eventType) => !eventTypeSchema.parse(eventType));
|
||||
if (hasInvalidType) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Invalid event type',
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return val.split(',').map((v) => eventTypeSchema.parse(v));
|
||||
}),
|
||||
})
|
||||
.partial(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
const getEventsResponseSchema = z.array(eventResponseSchema);
|
||||
const getEventsResponseSchema = z.array(eventSchema);
|
||||
|
||||
type GetEventsParams = z.infer<typeof getEventsParamsSchema.shape.query>;
|
||||
type GetEventsResponse = z.output<typeof getEventsResponseSchema>;
|
||||
|
|
|
@ -25,7 +25,6 @@ export type * from './investigation_note';
|
|||
export type * from './update';
|
||||
export type * from './update_item';
|
||||
export type * from './update_note';
|
||||
export type * from './event';
|
||||
export type * from './get_events';
|
||||
export type * from './entity';
|
||||
export type * from './get_entities';
|
||||
|
@ -48,7 +47,6 @@ export * from './investigation_note';
|
|||
export * from './update';
|
||||
export * from './update_item';
|
||||
export * from './update_note';
|
||||
export * from './event';
|
||||
export * from './get_events';
|
||||
export * from './entity';
|
||||
export * from './get_entities';
|
||||
|
|
|
@ -17,8 +17,15 @@ const eventTypeSchema = z.union([
|
|||
z.literal('anomaly'),
|
||||
]);
|
||||
|
||||
const sourceSchema = z.record(z.string(), z.any());
|
||||
|
||||
const annotationEventSchema = z.object({
|
||||
eventType: z.literal('annotation'),
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
timestamp: z.number(),
|
||||
source: sourceSchema.optional(),
|
||||
annotationType: z.string().optional(),
|
||||
});
|
||||
|
||||
|
@ -31,21 +38,19 @@ const alertStatusSchema = z.union([
|
|||
|
||||
const alertEventSchema = z.object({
|
||||
eventType: z.literal('alert'),
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
timestamp: z.number(),
|
||||
source: sourceSchema.optional(),
|
||||
alertStatus: alertStatusSchema,
|
||||
});
|
||||
|
||||
const sourceSchema = z.record(z.string(), z.any());
|
||||
const eventSchema = z.discriminatedUnion('eventType', [annotationEventSchema, alertEventSchema]);
|
||||
|
||||
const eventSchema = z.intersection(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
timestamp: z.number(),
|
||||
eventType: eventTypeSchema,
|
||||
source: sourceSchema.optional(),
|
||||
}),
|
||||
z.discriminatedUnion('eventType', [annotationEventSchema, alertEventSchema])
|
||||
);
|
||||
type EventResponse = z.output<typeof eventSchema>;
|
||||
type AlertEventResponse = z.output<typeof alertEventSchema>;
|
||||
type AnnotationEventResponse = z.output<typeof annotationEventSchema>;
|
||||
|
||||
export { eventSchema };
|
||||
export type { EventResponse, AlertEventResponse, AnnotationEventResponse };
|
||||
export { eventSchema, eventTypeSchema, alertEventSchema, annotationEventSchema };
|
||||
|
|
|
@ -12,8 +12,12 @@ export const investigationKeys = {
|
|||
userProfiles: (profileIds: Set<string>) =>
|
||||
[...investigationKeys.all, 'userProfiles', ...profileIds] as const,
|
||||
tags: () => [...investigationKeys.all, 'tags'] as const,
|
||||
events: (rangeFrom?: string, rangeTo?: string, filter?: string) =>
|
||||
[...investigationKeys.all, 'events', rangeFrom, rangeTo, filter] as const,
|
||||
events: (params: {
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
filter?: string;
|
||||
eventTypes?: string[];
|
||||
}) => [...investigationKeys.all, 'events', params] as const,
|
||||
stats: () => [...investigationKeys.all, 'stats'] as const,
|
||||
lists: () => [...investigationKeys.all, 'list'] as const,
|
||||
list: (params: { page: number; perPage: number; search?: string; filter?: string }) =>
|
||||
|
|
|
@ -11,8 +11,9 @@ import {
|
|||
CreateInvestigationNoteParams,
|
||||
CreateInvestigationNoteResponse,
|
||||
} from '@kbn/investigation-shared';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useKibana } from './use_kibana';
|
||||
import { investigationKeys } from './query_key_factory';
|
||||
|
||||
type ServerError = IHttpFetchError<ResponseErrorBody>;
|
||||
|
||||
|
@ -23,6 +24,7 @@ export function useAddInvestigationNote() {
|
|||
notifications: { toasts },
|
||||
},
|
||||
} = useKibana();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
CreateInvestigationNoteResponse,
|
||||
|
@ -39,7 +41,12 @@ export function useAddInvestigationNote() {
|
|||
);
|
||||
},
|
||||
{
|
||||
onSuccess: (response, {}) => {
|
||||
onSuccess: (_, { investigationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: investigationKeys.detailNotes(investigationId),
|
||||
exact: false,
|
||||
});
|
||||
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.investigateApp.addInvestigationNote.successMessage', {
|
||||
defaultMessage: 'Note saved',
|
||||
|
|
|
@ -6,13 +6,15 @@
|
|||
*/
|
||||
|
||||
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from './use_kibana';
|
||||
import { investigationKeys } from './query_key_factory';
|
||||
|
||||
type ServerError = IHttpFetchError<ResponseErrorBody>;
|
||||
|
||||
export function useDeleteInvestigationNote() {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
core: {
|
||||
http,
|
||||
|
@ -34,7 +36,12 @@ export function useDeleteInvestigationNote() {
|
|||
);
|
||||
},
|
||||
{
|
||||
onSuccess: (response, {}) => {
|
||||
onSuccess: (response, { investigationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: investigationKeys.detailNotes(investigationId),
|
||||
exact: false,
|
||||
});
|
||||
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.investigateApp.useDeleteInvestigationNote.successMessage', {
|
||||
defaultMessage: 'Note deleted',
|
||||
|
|
|
@ -44,6 +44,7 @@ export function useFetchAlert({ investigation }: UseFetchAlertParams): UseFetchA
|
|||
});
|
||||
},
|
||||
staleTime: 60 * 1000,
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
onError: (error: Error) => {
|
||||
toasts.addError(error, {
|
||||
|
|
|
@ -51,8 +51,7 @@ export function useFetchAllInvestigationStats(): Response {
|
|||
};
|
||||
},
|
||||
retry: false,
|
||||
cacheTime: 600 * 1000, // 10 minutes
|
||||
staleTime: 0,
|
||||
staleTime: 15 * 1000,
|
||||
onError: (error: Error) => {
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.investigateApp.useFetchAllInvestigationStats.errorTitle', {
|
||||
|
|
|
@ -35,8 +35,7 @@ export function useFetchAllInvestigationTags(): Response {
|
|||
signal,
|
||||
});
|
||||
},
|
||||
cacheTime: 600 * 1000, // 10_minutes
|
||||
staleTime: 0,
|
||||
staleTime: 15 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
onError: (error: Error) => {
|
||||
|
|
|
@ -50,9 +50,7 @@ export function useFetchEntities({
|
|||
});
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
onError: (error: Error) => {
|
||||
// ignore error
|
||||
},
|
||||
retry: false,
|
||||
enabled: Boolean(investigationId && (serviceName || hostName || containerId)),
|
||||
});
|
||||
|
||||
|
|
|
@ -6,16 +6,14 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { GetEventsResponse } from '@kbn/investigation-shared';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { isArray } from 'lodash';
|
||||
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;
|
||||
}
|
||||
|
@ -24,10 +22,12 @@ export function useFetchEvents({
|
|||
rangeFrom,
|
||||
rangeTo,
|
||||
filter,
|
||||
eventTypes,
|
||||
}: {
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
filter?: string;
|
||||
eventTypes?: string[];
|
||||
}): Response {
|
||||
const {
|
||||
core: {
|
||||
|
@ -36,21 +36,20 @@ export function useFetchEvents({
|
|||
},
|
||||
} = useKibana();
|
||||
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
|
||||
queryKey: investigationKeys.events(rangeFrom, rangeTo, filter),
|
||||
const { isLoading, isError, data } = useQuery({
|
||||
queryKey: investigationKeys.events({ rangeFrom, rangeTo, filter, eventTypes }),
|
||||
queryFn: async ({ signal }) => {
|
||||
return await http.get<GetEventsResponse>(`/api/observability/events`, {
|
||||
return http.get<GetEventsResponse>(`/api/observability/events`, {
|
||||
query: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
filter,
|
||||
...(isArray(eventTypes) && eventTypes.length > 0 && { eventTypes: eventTypes.join(',') }),
|
||||
},
|
||||
version: '2023-10-31',
|
||||
signal,
|
||||
});
|
||||
},
|
||||
cacheTime: 600 * 1000, // 10_minutes
|
||||
staleTime: 0,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
onError: (error: Error) => {
|
||||
|
@ -64,10 +63,7 @@ export function useFetchEvents({
|
|||
|
||||
return {
|
||||
data,
|
||||
isInitialLoading,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -64,8 +64,6 @@ export function useFetchInvestigationList({
|
|||
retry: false,
|
||||
refetchInterval: 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
cacheTime: 600 * 1000, // 10 minutes
|
||||
staleTime: 0,
|
||||
onError: (error: Error) => {
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.investigateApp.useFetchInvestigationList.errorTitle', {
|
||||
|
|
|
@ -39,12 +39,16 @@ export function useUpdateInvestigationNote() {
|
|||
},
|
||||
{
|
||||
onSuccess: (response, { investigationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: investigationKeys.detailNotes(investigationId),
|
||||
exact: false,
|
||||
});
|
||||
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.investigateApp.useUpdateInvestigationNote.successMessage', {
|
||||
defaultMessage: 'Note updated',
|
||||
})
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: investigationKeys.detailNotes(investigationId) });
|
||||
},
|
||||
onError: (error, {}, context) => {
|
||||
toasts.addError(
|
||||
|
|
|
@ -10,8 +10,8 @@ import { css } from '@emotion/css';
|
|||
import { ESQLLangEditor } from '@kbn/esql/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { AddFromLibraryButton } from '../add_from_library_button';
|
||||
import { useInvestigation } from '../../contexts/investigation_context';
|
||||
import { AddFromLibraryButton } from '../add_from_library_button';
|
||||
import { EsqlWidgetPreview } from './esql_widget_preview';
|
||||
|
||||
const emptyPreview = css`
|
||||
|
|
|
@ -4,11 +4,10 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { RootCauseAnalysisEvent } from '@kbn/observability-ai-server/root_cause_analysis';
|
||||
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
ALERT_FLAPPING_HISTORY,
|
||||
ALERT_RULE_EXECUTION_TIMESTAMP,
|
||||
|
@ -17,9 +16,11 @@ import {
|
|||
EVENT_KIND,
|
||||
} from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names';
|
||||
import { isRequestAbortedError } from '@kbn/server-route-repository-client';
|
||||
import { omit } from 'lodash';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useKibana } from '../../../../hooks/use_kibana';
|
||||
import { useInvestigation } from '../../contexts/investigation_context';
|
||||
import { useUpdateInvestigation } from '../../../../hooks/use_update_investigation';
|
||||
import { useInvestigation } from '../../contexts/investigation_context';
|
||||
|
||||
export interface InvestigationContextualInsight {
|
||||
key: string;
|
||||
|
@ -27,7 +28,7 @@ export interface InvestigationContextualInsight {
|
|||
data: unknown;
|
||||
}
|
||||
|
||||
export function AssistantHypothesis({ investigationId }: { investigationId: string }) {
|
||||
export function AssistantHypothesis() {
|
||||
const {
|
||||
alert,
|
||||
globalParams: { timeRange },
|
||||
|
@ -87,7 +88,7 @@ export function AssistantHypothesis({ investigationId }: { investigationId: stri
|
|||
.stream('POST /internal/observability/investigation/root_cause_analysis', {
|
||||
params: {
|
||||
body: {
|
||||
investigationId,
|
||||
investigationId: investigation!.id,
|
||||
connectorId,
|
||||
context: `The user is investigating an alert for the ${serviceName} service,
|
||||
and wants to find the root cause. Here is the alert:
|
||||
|
@ -156,7 +157,7 @@ export function AssistantHypothesis({ investigationId }: { investigationId: stri
|
|||
setEvents([]);
|
||||
if (investigation?.rootCauseAnalysis) {
|
||||
updateInvestigation({
|
||||
investigationId,
|
||||
investigationId: investigation!.id,
|
||||
payload: {
|
||||
rootCauseAnalysis: {
|
||||
events: [],
|
||||
|
|
|
@ -1,114 +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 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-browser/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 <EuiSkeletonText />;
|
||||
}
|
||||
|
||||
const alertEvents = events?.filter((evt) => evt.eventType === 'alert');
|
||||
const annotations = events?.filter((evt) => evt.eventType === 'annotation');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Chart size={['100%', 100]} ref={chartRef}>
|
||||
<Settings
|
||||
xDomain={{
|
||||
min: moment(globalParams.timeRange.from).valueOf(),
|
||||
max: moment(globalParams.timeRange.to).valueOf(),
|
||||
}}
|
||||
theme={TIME_LINE_THEME}
|
||||
baseTheme={baseTheme}
|
||||
onPointerUpdate={handleCursorUpdate}
|
||||
externalPointerEvents={{
|
||||
tooltip: { visible: true },
|
||||
}}
|
||||
onBrushEnd={(brush) => {
|
||||
const { from, to } = getBrushData(brush);
|
||||
updateInvestigationParams({
|
||||
timeRange: { from, to },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Axis id="y" position={Position.Left} hide />
|
||||
<Axis
|
||||
id="x"
|
||||
position={Position.Bottom}
|
||||
tickFormat={(d) => moment(d).format('LTS')}
|
||||
style={{
|
||||
tickLine: {
|
||||
visible: true,
|
||||
strokeWidth: 1,
|
||||
stroke: '#98A2B3',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{alertEvents?.map((event) => (
|
||||
<AlertEvent key={event.id} event={event} />
|
||||
))}
|
||||
|
||||
{annotations?.map((annotation) => (
|
||||
<AnnotationEvent key={annotation.id} event={annotation} />
|
||||
))}
|
||||
|
||||
<AreaSeries
|
||||
id="Time"
|
||||
xScaleType={ScaleType.Time}
|
||||
xAccessor="x"
|
||||
yAccessors={['y']}
|
||||
data={data}
|
||||
filterSeriesInTooltip={() => false}
|
||||
/>
|
||||
</Chart>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -7,8 +7,8 @@
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
import { InvestigateTextButton } from '../../../../components/investigate_text_button';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
|
||||
export const GRID_ITEM_HEADER_HEIGHT = 40;
|
||||
|
||||
|
@ -21,8 +21,6 @@ interface GridItemProps {
|
|||
loading: boolean;
|
||||
}
|
||||
|
||||
const editTitleButtonClassName = `investigateGridItemTitleEditButton`;
|
||||
|
||||
const titleContainerClassName = css`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
@ -35,11 +33,6 @@ const titleItemClassName = css`
|
|||
}
|
||||
`;
|
||||
|
||||
const panelContainerClassName = css`
|
||||
overflow: clip;
|
||||
overflow-clip-margin: 20px;
|
||||
`;
|
||||
|
||||
const panelClassName = css`
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
@ -47,9 +40,6 @@ const panelClassName = css`
|
|||
const panelContentClassName = css`
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
> [data-shared-item] {
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export function GridItem({ id, title, children, onDelete, onCopy, loading }: GridItemProps) {
|
||||
|
@ -64,10 +54,6 @@ export function GridItem({ id, title, children, onDelete, onCopy, loading }: Gri
|
|||
max-width: 100%;
|
||||
transition: opacity ${theme.animation.normal} ${theme.animation.resistance};
|
||||
overflow: auto;
|
||||
|
||||
&:not(:hover) .${editTitleButtonClassName} {
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
|
@ -119,9 +105,7 @@ export function GridItem({ id, title, children, onDelete, onCopy, loading }: Gri
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow className={panelContainerClassName}>
|
||||
<div className={panelContentClassName}>{children}</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem className={panelContentClassName}>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
|
|
@ -72,12 +72,11 @@ export function InvestigationDetails({ user }: Props) {
|
|||
],
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup direction="row" responsive>
|
||||
<EuiFlexItem grow={8}>
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem grow={4}>
|
||||
<InvestigationItems />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiFlexItem>
|
||||
<InvestigationNotes user={user} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -5,52 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import datemath from '@elastic/datemath';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFlexGroup } 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';
|
||||
import { InvestigationSearchBar } from '../investigation_search_bar/investigation_search_bar';
|
||||
import { AssistantHypothesis } from '../assistant_hypothesis/assistant_hypothesis';
|
||||
import { InvestigationItemsList } from '../investigation_items_list/investigation_items_list';
|
||||
import { InvestigationTimeline } from '../investigation_timeline/investigation_timeline';
|
||||
|
||||
export function InvestigationItems() {
|
||||
const { globalParams, updateInvestigationParams, investigation } = useInvestigation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<InvestigationSearchBar
|
||||
dateRangeFrom={globalParams.timeRange.from}
|
||||
dateRangeTo={globalParams.timeRange.to}
|
||||
onQuerySubmit={async ({ dateRange }) => {
|
||||
const nextTimeRange = {
|
||||
from: datemath.parse(dateRange.from)!.toISOString(),
|
||||
to: datemath.parse(dateRange.to)!.toISOString(),
|
||||
};
|
||||
|
||||
updateInvestigationParams({ timeRange: nextTimeRange });
|
||||
}}
|
||||
/>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EventsTimeLine />
|
||||
</EuiFlexItem>
|
||||
|
||||
{investigation?.id && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssistantHypothesis investigationId={investigation.id} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<InvestigationItemsList />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFlexGroup direction="column" gutterSize="m" responsive>
|
||||
<InvestigationTimeline />
|
||||
<AssistantHypothesis />
|
||||
<InvestigationItemsList />
|
||||
<AddInvestigationItem />
|
||||
</>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export function InvestigationItemsList() {
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexGroup direction="column" gutterSize="m" responsive>
|
||||
{renderableItems.map((item) => {
|
||||
return (
|
||||
<EuiFlexItem grow={false} key={`item-${item.id}`}>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { InvestigationNoteResponse } from '@kbn/investigation-shared';
|
|||
import React, { useState } from 'react';
|
||||
import { ResizableTextInput } from './resizable_text_input';
|
||||
import { useInvestigation } from '../../contexts/investigation_context';
|
||||
import { useUpdateInvestigationNote } from '../../../../hooks/use_update_investigation_note';
|
||||
|
||||
interface Props {
|
||||
note: InvestigationNoteResponse;
|
||||
|
@ -19,11 +20,22 @@ interface Props {
|
|||
|
||||
export function EditNoteForm({ note, onClose }: Props) {
|
||||
const [noteInput, setNoteInput] = useState(note.content);
|
||||
const { updateNote, isUpdatingNote } = useInvestigation();
|
||||
const { investigation } = useInvestigation();
|
||||
const { mutate: updateNote, isLoading: isUpdatingNote } = useUpdateInvestigationNote();
|
||||
|
||||
const onUpdate = async () => {
|
||||
await updateNote(note.id, noteInput.trim());
|
||||
onClose();
|
||||
const onUpdate = () => {
|
||||
updateNote(
|
||||
{
|
||||
investigationId: investigation!.id,
|
||||
noteId: note.id,
|
||||
note: { content: noteInput.trim() },
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -18,6 +18,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import { InvestigationNoteResponse } from '@kbn/investigation-shared';
|
||||
import { AuthenticatedUser } from '@kbn/security-plugin/common';
|
||||
import React, { useState } from 'react';
|
||||
import { useAddInvestigationNote } from '../../../../hooks/use_add_investigation_note';
|
||||
import { useFetchInvestigationNotes } from '../../../../hooks/use_fetch_investigation_notes';
|
||||
import { useFetchUserProfiles } from '../../../../hooks/use_fetch_user_profiles';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
import { useInvestigation } from '../../contexts/investigation_context';
|
||||
|
@ -30,15 +32,25 @@ export interface Props {
|
|||
|
||||
export function InvestigationNotes({ user }: Props) {
|
||||
const theme = useTheme();
|
||||
const { investigation, addNote, isAddingNote } = useInvestigation();
|
||||
const { investigation } = useInvestigation();
|
||||
const { data: notes } = useFetchInvestigationNotes({
|
||||
investigationId: investigation!.id,
|
||||
});
|
||||
const { mutate: addNote, isLoading: isAddingNote } = useAddInvestigationNote();
|
||||
const { data: userProfiles, isLoading: isLoadingUserProfiles } = useFetchUserProfiles({
|
||||
profileIds: new Set(investigation?.notes.map((note) => note.createdBy)),
|
||||
});
|
||||
|
||||
const [noteInput, setNoteInput] = useState('');
|
||||
const onAddNote = async (content: string) => {
|
||||
await addNote(content);
|
||||
setNoteInput('');
|
||||
const onAddNote = (content: string) => {
|
||||
addNote(
|
||||
{ investigationId: investigation!.id, note: { content } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setNoteInput('');
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const panelClassName = css`
|
||||
|
@ -58,7 +70,7 @@ export function InvestigationNotes({ user }: Props) {
|
|||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
{investigation?.notes.map((currNote: InvestigationNoteResponse) => {
|
||||
{notes?.map((currNote: InvestigationNoteResponse) => {
|
||||
return (
|
||||
<Note
|
||||
key={currNote.id}
|
||||
|
|
|
@ -21,6 +21,7 @@ import React, { useState } from 'react';
|
|||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
import { useInvestigation } from '../../contexts/investigation_context';
|
||||
import { EditNoteForm } from './edit_note_form';
|
||||
import { useDeleteInvestigationNote } from '../../../../hooks/use_delete_investigation_note';
|
||||
|
||||
const textContainerClassName = css`
|
||||
padding-top: 2px;
|
||||
|
@ -36,7 +37,12 @@ interface Props {
|
|||
export function Note({ note, isOwner, userProfile, userProfileLoading }: Props) {
|
||||
const theme = useTheme();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const { deleteNote, isDeletingNote } = useInvestigation();
|
||||
const { investigation } = useInvestigation();
|
||||
const { mutate: deleteNote, isLoading: isDeletingNote } = useDeleteInvestigationNote();
|
||||
|
||||
const onDeleteNote = () => {
|
||||
deleteNote({ investigationId: investigation!.id, noteId: note.id });
|
||||
};
|
||||
|
||||
const timelineContainerClassName = css`
|
||||
padding-bottom: 16px;
|
||||
|
@ -98,7 +104,7 @@ export function Note({ note, isOwner, userProfile, userProfileLoading }: Props)
|
|||
iconSize="s"
|
||||
iconType="trash"
|
||||
disabled={isDeletingNote}
|
||||
onClick={async () => await deleteNote(note.id)}
|
||||
onClick={onDeleteNote}
|
||||
data-test-subj="deleteInvestigationNoteButton"
|
||||
className={actionButtonClassname}
|
||||
/>
|
||||
|
|
|
@ -1,56 +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 { css } from '@emotion/css';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { SearchBar } from '@kbn/unified-search-plugin/public';
|
||||
import React from 'react';
|
||||
import { useKibana } from '../../../../hooks/use_kibana';
|
||||
|
||||
const parentClassName = css`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
dateRangeFrom?: string;
|
||||
dateRangeTo?: string;
|
||||
onQuerySubmit: (payload: { dateRange: TimeRange }, isUpdate?: boolean) => void;
|
||||
onRefresh?: Required<React.ComponentProps<typeof SearchBar>>['onRefresh'];
|
||||
}
|
||||
|
||||
export function InvestigationSearchBar({
|
||||
dateRangeFrom,
|
||||
dateRangeTo,
|
||||
onQuerySubmit,
|
||||
onRefresh,
|
||||
}: Props) {
|
||||
const {
|
||||
dependencies: {
|
||||
start: { unifiedSearch },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
return (
|
||||
<div className={parentClassName}>
|
||||
<unifiedSearch.ui.SearchBar
|
||||
appName="investigate"
|
||||
onQuerySubmit={({ dateRange }) => {
|
||||
onQuerySubmit({ dateRange });
|
||||
}}
|
||||
showQueryInput={false}
|
||||
showFilterBar={false}
|
||||
showQueryMenu={false}
|
||||
showDatePicker
|
||||
showSubmitButton={true}
|
||||
dateRangeFrom={dateRangeFrom}
|
||||
dateRangeTo={dateRangeTo}
|
||||
onRefresh={onRefresh}
|
||||
displayStyle="inPage"
|
||||
disableQueryLanguageSwitcher
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -5,13 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LineAnnotation, AnnotationDomainType } from '@elastic/charts';
|
||||
import { AnnotationDomainType, LineAnnotation } from '@elastic/charts';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { AlertEventResponse } from '@kbn/investigation-shared';
|
||||
import moment from 'moment';
|
||||
import { EventSchema } from '@kbn/investigation-shared';
|
||||
import React from 'react';
|
||||
|
||||
export const AlertEvent = ({ event }: { event: EventSchema }) => {
|
||||
export const AlertEvent = ({ event }: { event: AlertEventResponse }) => {
|
||||
return (
|
||||
<LineAnnotation
|
||||
id={event.id}
|
|
@ -5,13 +5,13 @@
|
|||
* 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';
|
||||
import { AnnotationEventResponse } from '@kbn/investigation-shared';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
||||
export function AnnotationEvent({ event }: { event: EventSchema }) {
|
||||
export function AnnotationEvent({ event }: { event: AnnotationEventResponse }) {
|
||||
const timestamp = event.timestamp;
|
||||
|
||||
return (
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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 { AreaSeries, Axis, Chart, Position, ScaleType, Settings } from '@elastic/charts';
|
||||
import { EuiSkeletonText } from '@elastic/eui';
|
||||
import { useActiveCursor } from '@kbn/charts-plugin/public';
|
||||
import { getBrushData } from '@kbn/observability-utils-browser/chart/utils';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import moment from 'moment';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { useFetchEvents } from '../../../../../hooks/use_fetch_events';
|
||||
import { useKibana } from '../../../../../hooks/use_kibana';
|
||||
import { useInvestigation } from '../../../contexts/investigation_context';
|
||||
import { AlertEvent } from './alert_event';
|
||||
import { AnnotationEvent } from './annotation_event';
|
||||
import { TIMELINE_THEME } from './timeline_theme';
|
||||
|
||||
interface Props {
|
||||
eventTypes: string[];
|
||||
}
|
||||
|
||||
export const EventsTimeline = ({ eventTypes }: Props) => {
|
||||
const { dependencies } = useKibana();
|
||||
const baseTheme = dependencies.start.charts.theme.useChartsBaseTheme();
|
||||
const { globalParams, updateInvestigationParams } = useInvestigation();
|
||||
const chartRef = useRef(null);
|
||||
|
||||
const { data: events, isLoading } = useFetchEvents({
|
||||
rangeFrom: globalParams.timeRange.from,
|
||||
rangeTo: globalParams.timeRange.to,
|
||||
eventTypes,
|
||||
});
|
||||
|
||||
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 <EuiSkeletonText />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Chart size={['100%', 64]} ref={chartRef}>
|
||||
<Settings
|
||||
xDomain={{
|
||||
min: moment(globalParams.timeRange.from).valueOf(),
|
||||
max: moment(globalParams.timeRange.to).valueOf(),
|
||||
}}
|
||||
theme={TIMELINE_THEME}
|
||||
baseTheme={baseTheme}
|
||||
onPointerUpdate={handleCursorUpdate}
|
||||
externalPointerEvents={{
|
||||
tooltip: { visible: true },
|
||||
}}
|
||||
onBrushEnd={(brush) => {
|
||||
const { from, to } = getBrushData(brush);
|
||||
updateInvestigationParams({
|
||||
timeRange: { from, to },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Axis id="y" position={Position.Left} hide />
|
||||
<Axis
|
||||
id="x"
|
||||
position={Position.Bottom}
|
||||
tickFormat={(d) => moment(d).format('LTS')}
|
||||
style={{
|
||||
tickLine: {
|
||||
visible: true,
|
||||
strokeWidth: 1,
|
||||
stroke: '#98A2B3',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{events?.map((event) => {
|
||||
if (event.eventType === 'alert') {
|
||||
return <AlertEvent key={event.id} event={event} />;
|
||||
}
|
||||
if (event.eventType === 'annotation') {
|
||||
return <AnnotationEvent key={event.id} event={event} />;
|
||||
}
|
||||
assertNever(event);
|
||||
})}
|
||||
|
||||
<AreaSeries
|
||||
id="Time"
|
||||
xScaleType={ScaleType.Time}
|
||||
xAccessor="x"
|
||||
yAccessors={['y']}
|
||||
data={data}
|
||||
filterSeriesInTooltip={() => false}
|
||||
/>
|
||||
</Chart>
|
||||
);
|
||||
};
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { PartialTheme } from '@elastic/charts';
|
||||
|
||||
export const TIME_LINE_THEME: PartialTheme = {
|
||||
export const TIMELINE_THEME: PartialTheme = {
|
||||
highlighter: {
|
||||
point: {
|
||||
opacity: 0,
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiPanel } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import { EventsTimeline } from './events_timeline/events_timeline';
|
||||
import { InvestigationTimelineFilterBar } from './investigation_timeline_filter_bar/investigation_timeline_filter_bar';
|
||||
|
||||
export function InvestigationTimeline() {
|
||||
const [eventTypes, setEventTypes] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={true} grow={false} paddingSize="xs">
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<InvestigationTimelineFilterBar
|
||||
onEventTypesSelected={(selected: string[]) => setEventTypes(selected)}
|
||||
/>
|
||||
|
||||
<EventsTimeline eventTypes={eventTypes} />
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiSelectable,
|
||||
EuiSelectableOption,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
onSelected: (eventTypes: string[]) => void;
|
||||
}
|
||||
|
||||
export function InvestigationEventTypesFilter({ onSelected }: Props) {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [items, setItems] = useState<EuiSelectableOption[]>([
|
||||
{
|
||||
key: 'alert',
|
||||
label: i18n.translate('xpack.investigateApp.investigationEventTypesFilter.alertLabel', {
|
||||
defaultMessage: 'Alert',
|
||||
}),
|
||||
checked: 'on',
|
||||
},
|
||||
{
|
||||
key: 'annotation',
|
||||
label: i18n.translate('xpack.investigateApp.investigationEventTypesFilter.annotationLabel', {
|
||||
defaultMessage: 'Annotation',
|
||||
}),
|
||||
checked: 'on',
|
||||
},
|
||||
]);
|
||||
|
||||
const togglePopover = () => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
};
|
||||
const closePopover = () => {
|
||||
setIsPopoverOpen(false);
|
||||
};
|
||||
|
||||
const filterGroupPopoverId = useGeneratedHtmlId({
|
||||
prefix: 'filterGroupPopover',
|
||||
});
|
||||
|
||||
const handleChange = (newOptions: EuiSelectableOption[]) => {
|
||||
setItems(newOptions);
|
||||
|
||||
const selected = newOptions
|
||||
.filter((option) => option.checked === 'on')
|
||||
.map((option) => option.key!);
|
||||
onSelected(selected);
|
||||
};
|
||||
|
||||
const button = (
|
||||
<EuiFilterButton
|
||||
grow={false}
|
||||
iconType="arrowDown"
|
||||
badgeColor="success"
|
||||
onClick={togglePopover}
|
||||
isSelected={isPopoverOpen}
|
||||
numFilters={items.filter((item) => item.checked !== 'off').length}
|
||||
hasActiveFilters={!!items.find((item) => item.checked === 'on')}
|
||||
numActiveFilters={items.filter((item) => item.checked === 'on').length}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.investigateApp.investigationEventTypesFilter.filtersFilterButtonLabel',
|
||||
{ defaultMessage: 'Filters' }
|
||||
)}
|
||||
</EuiFilterButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFilterGroup compressed>
|
||||
<EuiPopover
|
||||
id={filterGroupPopoverId}
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiSelectable options={items} onChange={handleChange}>
|
||||
{(list) => (
|
||||
<div style={{ width: 200 }}>
|
||||
<EuiPopoverTitle paddingSize="s">
|
||||
{i18n.translate(
|
||||
'xpack.investigateApp.investigationEventTypesFilter.filterEventTypePopoverTitleLabel',
|
||||
{ defaultMessage: 'Filter event type' }
|
||||
)}
|
||||
</EuiPopoverTitle>
|
||||
{list}
|
||||
</div>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</EuiPopover>
|
||||
</EuiFilterGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { EuiDatePicker, EuiDatePickerRange, EuiFlexGroup } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { useInvestigation } from '../../../contexts/investigation_context';
|
||||
import { InvestigationEventTypesFilter } from './investigation_event_types_filter';
|
||||
|
||||
interface Props {
|
||||
onEventTypesSelected: (eventTypes: string[]) => void;
|
||||
}
|
||||
|
||||
export function InvestigationTimelineFilterBar({ onEventTypesSelected }: Props) {
|
||||
const { globalParams, updateInvestigationParams } = useInvestigation();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="flexStart"
|
||||
justifyContent="spaceBetween"
|
||||
css={css`
|
||||
padding: 8px 8px 0px 8px;
|
||||
max-height: fit-content;
|
||||
`}
|
||||
>
|
||||
<InvestigationEventTypesFilter onSelected={onEventTypesSelected} />
|
||||
|
||||
<EuiDatePickerRange
|
||||
compressed
|
||||
startDateControl={
|
||||
<EuiDatePicker
|
||||
selected={moment(globalParams.timeRange.from)}
|
||||
onChange={(date) => {
|
||||
if (!date) return;
|
||||
|
||||
updateInvestigationParams({
|
||||
timeRange: {
|
||||
from: date.toISOString(),
|
||||
to: globalParams.timeRange.to,
|
||||
},
|
||||
});
|
||||
}}
|
||||
showTimeSelect
|
||||
/>
|
||||
}
|
||||
endDateControl={
|
||||
<EuiDatePicker
|
||||
selected={moment(globalParams.timeRange.to)}
|
||||
onChange={(date) => {
|
||||
if (!date) return;
|
||||
|
||||
updateInvestigationParams({
|
||||
timeRange: {
|
||||
from: globalParams.timeRange.from,
|
||||
to: date.toISOString(),
|
||||
},
|
||||
});
|
||||
}}
|
||||
showTimeSelect
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -7,19 +7,16 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { type GlobalWidgetParameters } from '@kbn/investigate-plugin/public';
|
||||
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common';
|
||||
import { GetInvestigationResponse, InvestigationItem, Item } from '@kbn/investigation-shared';
|
||||
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common';
|
||||
import { isEqual } from 'lodash';
|
||||
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { useAddInvestigationItem } from '../../../hooks/use_add_investigation_item';
|
||||
import { useAddInvestigationNote } from '../../../hooks/use_add_investigation_note';
|
||||
import { useDeleteInvestigationItem } from '../../../hooks/use_delete_investigation_item';
|
||||
import { useDeleteInvestigationNote } from '../../../hooks/use_delete_investigation_note';
|
||||
import { useFetchInvestigation } from '../../../hooks/use_fetch_investigation';
|
||||
import { useFetchAlert } from '../../../hooks/use_fetch_alert';
|
||||
import { useFetchInvestigation } from '../../../hooks/use_fetch_investigation';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { useUpdateInvestigation } from '../../../hooks/use_update_investigation';
|
||||
import { useUpdateInvestigationNote } from '../../../hooks/use_update_investigation_note';
|
||||
|
||||
export type RenderedInvestigationItem = InvestigationItem & {
|
||||
loading: boolean;
|
||||
|
@ -36,13 +33,6 @@ interface InvestigationContextProps {
|
|||
deleteItem: (itemId: string) => Promise<void>;
|
||||
isAddingItem: boolean;
|
||||
isDeletingItem: boolean;
|
||||
// note
|
||||
addNote: (content: string) => Promise<void>;
|
||||
updateNote: (noteId: string, content: string) => Promise<void>;
|
||||
deleteNote: (noteId: string) => Promise<void>;
|
||||
isAddingNote: boolean;
|
||||
isUpdatingNote: boolean;
|
||||
isDeletingNote: boolean;
|
||||
}
|
||||
|
||||
export const InvestigationContext = createContext<InvestigationContextProps>({
|
||||
|
@ -54,13 +44,6 @@ export const InvestigationContext = createContext<InvestigationContextProps>({
|
|||
deleteItem: async () => {},
|
||||
isAddingItem: false,
|
||||
isDeletingItem: false,
|
||||
// note
|
||||
addNote: async () => {},
|
||||
updateNote: async (noteId: string, content: string) => {},
|
||||
deleteNote: async (noteId: string) => {},
|
||||
isAddingNote: false,
|
||||
isUpdatingNote: false,
|
||||
isDeletingNote: false,
|
||||
});
|
||||
|
||||
export function useInvestigation() {
|
||||
|
@ -90,32 +73,6 @@ export function InvestigationProvider({
|
|||
Record<string, { globalParams: GlobalWidgetParameters; item: RenderedInvestigationItem }>
|
||||
>({});
|
||||
|
||||
const { mutateAsync: addInvestigationNote, isLoading: isAddingNote } = useAddInvestigationNote();
|
||||
const { mutateAsync: updateInvestigationNote, isLoading: isUpdatingNote } =
|
||||
useUpdateInvestigationNote();
|
||||
const { mutateAsync: deleteInvestigationNote, isLoading: isDeletingNote } =
|
||||
useDeleteInvestigationNote();
|
||||
|
||||
const addNote = async (content: string) => {
|
||||
await addInvestigationNote({ investigationId: initialInvestigation.id, note: { content } });
|
||||
refetch();
|
||||
};
|
||||
|
||||
const updateNote = async (noteId: string, content: string) => {
|
||||
await updateInvestigationNote({
|
||||
investigationId: initialInvestigation.id,
|
||||
noteId,
|
||||
note: { content: content.trim() },
|
||||
});
|
||||
|
||||
refetch();
|
||||
};
|
||||
|
||||
const deleteNote = async (noteId: string) => {
|
||||
await deleteInvestigationNote({ investigationId: initialInvestigation.id, noteId });
|
||||
refetch();
|
||||
};
|
||||
|
||||
const { mutateAsync: updateInvestigation } = useUpdateInvestigation();
|
||||
const { mutateAsync: addInvestigationItem, isLoading: isAddingItem } = useAddInvestigationItem();
|
||||
const { mutateAsync: deleteInvestigationItem, isLoading: isDeletingItem } =
|
||||
|
@ -221,12 +178,6 @@ export function InvestigationProvider({
|
|||
deleteItem,
|
||||
isAddingItem,
|
||||
isDeletingItem,
|
||||
addNote,
|
||||
updateNote,
|
||||
deleteNote,
|
||||
isAddingNote,
|
||||
isUpdatingNote,
|
||||
isDeletingNote,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
GetEntitiesResponse,
|
||||
GetEventsResponse,
|
||||
createInvestigationItemParamsSchema,
|
||||
createInvestigationNoteParamsSchema,
|
||||
createInvestigationParamsSchema,
|
||||
|
@ -16,9 +18,7 @@ import {
|
|||
getAllInvestigationStatsParamsSchema,
|
||||
getAllInvestigationTagsParamsSchema,
|
||||
getEntitiesParamsSchema,
|
||||
GetEntitiesResponse,
|
||||
getEventsParamsSchema,
|
||||
GetEventsResponse,
|
||||
getInvestigationItemsParamsSchema,
|
||||
getInvestigationNotesParamsSchema,
|
||||
getInvestigationParamsSchema,
|
||||
|
@ -335,12 +335,17 @@ const getEventsRoute = createInvestigateAppServerRoute({
|
|||
const alertsClient: AlertsClient = await getAlertsClient({ plugins, request });
|
||||
const events: GetEventsResponse = [];
|
||||
|
||||
if (annotationsClient) {
|
||||
const includeAllEventTypes = !params?.query?.eventTypes || params.query.eventTypes.length === 0;
|
||||
|
||||
if (
|
||||
annotationsClient &&
|
||||
(includeAllEventTypes || params?.query?.eventTypes?.includes('annotation'))
|
||||
) {
|
||||
const annotationEvents = await getAnnotationEvents(params?.query ?? {}, annotationsClient);
|
||||
events.push(...annotationEvents);
|
||||
}
|
||||
|
||||
if (alertsClient) {
|
||||
if (alertsClient && (includeAllEventTypes || params?.query?.eventTypes?.includes('alert'))) {
|
||||
const alertEvents = await getAlertEvents(params?.query ?? {}, alertsClient);
|
||||
events.push(...alertEvents);
|
||||
}
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
|
||||
import datemath from '@elastic/datemath';
|
||||
import {
|
||||
AlertEventResponse,
|
||||
AnnotationEventResponse,
|
||||
GetEventsParams,
|
||||
GetEventsResponse,
|
||||
getEventsResponseSchema,
|
||||
alertEventSchema,
|
||||
annotationEventSchema,
|
||||
} from '@kbn/investigation-shared';
|
||||
import { ScopedAnnotationsClient } from '@kbn/observability-plugin/server';
|
||||
import {
|
||||
|
@ -19,13 +21,13 @@ import {
|
|||
ALERT_STATUS,
|
||||
ALERT_UUID,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { AlertsClient } from './get_alerts_client';
|
||||
import { rangeQuery } from '../lib/queries';
|
||||
import { AlertsClient } from './get_alerts_client';
|
||||
|
||||
export async function getAnnotationEvents(
|
||||
params: GetEventsParams,
|
||||
annotationsClient: ScopedAnnotationsClient
|
||||
): Promise<GetEventsResponse> {
|
||||
): Promise<AnnotationEventResponse[]> {
|
||||
const response = await annotationsClient.find({
|
||||
start: params?.rangeFrom,
|
||||
end: params?.rangeTo,
|
||||
|
@ -60,13 +62,13 @@ export async function getAnnotationEvents(
|
|||
};
|
||||
});
|
||||
|
||||
return getEventsResponseSchema.parse(events);
|
||||
return annotationEventSchema.array().parse(events);
|
||||
}
|
||||
|
||||
export async function getAlertEvents(
|
||||
params: GetEventsParams,
|
||||
alertsClient: AlertsClient
|
||||
): Promise<GetEventsResponse> {
|
||||
): Promise<AlertEventResponse[]> {
|
||||
const startInMs = datemath.parse(params?.rangeFrom ?? 'now-15m')!.valueOf();
|
||||
const endInMs = datemath.parse(params?.rangeTo ?? 'now')!.valueOf();
|
||||
const filterJSON = params?.filter ? JSON.parse(params.filter) : {};
|
||||
|
@ -101,5 +103,5 @@ export async function getAlertEvents(
|
|||
};
|
||||
});
|
||||
|
||||
return getEventsResponseSchema.parse(events);
|
||||
return alertEventSchema.array().parse(events);
|
||||
}
|
||||
|
|
|
@ -17,67 +17,68 @@
|
|||
".storybook/**/*.js"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/esql",
|
||||
"@kbn/alerting-plugin",
|
||||
"@kbn/apm-data-access-plugin",
|
||||
"@kbn/calculate-auto",
|
||||
"@kbn/charts-plugin",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/content-management-plugin",
|
||||
"@kbn/core-elasticsearch-server",
|
||||
"@kbn/core-saved-objects-server",
|
||||
"@kbn/core-security-common",
|
||||
"@kbn/core",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/expressions-plugin",
|
||||
"@kbn/kibana-utils-plugin",
|
||||
"@kbn/utility-types-jest",
|
||||
"@kbn/es-types",
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/dataset-quality-plugin",
|
||||
"@kbn/deeplinks-observability",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/unified-search-plugin",
|
||||
"@kbn/entities-schema",
|
||||
"@kbn/es-query",
|
||||
"@kbn/es-types",
|
||||
"@kbn/esql-utils",
|
||||
"@kbn/esql",
|
||||
"@kbn/expressions-plugin",
|
||||
"@kbn/field-types",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/i18n",
|
||||
"@kbn/inference-common",
|
||||
"@kbn/inference-plugin",
|
||||
"@kbn/investigate-plugin",
|
||||
"@kbn/investigation-shared",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/server-route-repository",
|
||||
"@kbn/server-route-repository-client",
|
||||
"@kbn/kibana-utils-plugin",
|
||||
"@kbn/lens-embeddable-utils",
|
||||
"@kbn/lens-plugin",
|
||||
"@kbn/licensing-plugin",
|
||||
"@kbn/logging",
|
||||
"@kbn/management-settings-ids",
|
||||
"@kbn/ml-random-sampler-utils",
|
||||
"@kbn/observability-ai-assistant-app-plugin",
|
||||
"@kbn/observability-ai-assistant-plugin",
|
||||
"@kbn/observability-ai-server",
|
||||
"@kbn/observability-plugin",
|
||||
"@kbn/observability-shared-plugin",
|
||||
"@kbn/observability-utils-browser",
|
||||
"@kbn/observability-utils-server",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/react-kibana-context-theme",
|
||||
"@kbn/rule-data-utils",
|
||||
"@kbn/rule-registry-plugin",
|
||||
"@kbn/saved-objects-finder-plugin",
|
||||
"@kbn/security-plugin",
|
||||
"@kbn/server-route-repository-client",
|
||||
"@kbn/server-route-repository",
|
||||
"@kbn/shared-ux-link-redirect-app",
|
||||
"@kbn/shared-ux-router",
|
||||
"@kbn/i18n",
|
||||
"@kbn/investigation-shared",
|
||||
"@kbn/lens-plugin",
|
||||
"@kbn/rule-registry-plugin",
|
||||
"@kbn/security-plugin",
|
||||
"@kbn/rule-data-utils",
|
||||
"@kbn/investigate-plugin",
|
||||
"@kbn/observability-utils-browser",
|
||||
"@kbn/lens-embeddable-utils",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/es-query",
|
||||
"@kbn/saved-objects-finder-plugin",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/observability-ai-server",
|
||||
"@kbn/charts-plugin",
|
||||
"@kbn/observability-shared-plugin",
|
||||
"@kbn/core-security-common",
|
||||
"@kbn/deeplinks-observability",
|
||||
"@kbn/logging",
|
||||
"@kbn/esql-utils",
|
||||
"@kbn/observability-ai-assistant-plugin",
|
||||
"@kbn/observability-ai-assistant-app-plugin",
|
||||
"@kbn/content-management-plugin",
|
||||
"@kbn/dataset-quality-plugin",
|
||||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/field-types",
|
||||
"@kbn/entities-schema",
|
||||
"@kbn/observability-plugin",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/visualization-utils",
|
||||
"@kbn/usage-collection-plugin",
|
||||
"@kbn/calculate-auto",
|
||||
"@kbn/ml-random-sampler-utils",
|
||||
"@kbn/zod",
|
||||
"@kbn/inference-common",
|
||||
"@kbn/core-elasticsearch-server",
|
||||
"@kbn/sse-utils",
|
||||
"@kbn/management-settings-ids",
|
||||
"@kbn/observability-utils-server",
|
||||
"@kbn/licensing-plugin",
|
||||
"@kbn/core-saved-objects-server",
|
||||
"@kbn/alerting-plugin",
|
||||
"@kbn/slo-plugin",
|
||||
"@kbn/inference-plugin",
|
||||
"@kbn/spaces-plugin",
|
||||
"@kbn/apm-data-access-plugin",
|
||||
"@kbn/sse-utils",
|
||||
"@kbn/std",
|
||||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/unified-search-plugin",
|
||||
"@kbn/usage-collection-plugin",
|
||||
"@kbn/utility-types-jest",
|
||||
"@kbn/visualization-utils",
|
||||
"@kbn/zod",
|
||||
],
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue