chore(rca): Add notes related APIs (#190362)

This commit is contained in:
Kevin Delemme 2024-08-14 14:32:03 -04:00 committed by GitHub
parent 19e9bfb26e
commit 47c41c16c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 587 additions and 258 deletions

1
.github/CODEOWNERS vendored
View file

@ -513,6 +513,7 @@ test/interactive_setup_api_integration/plugins/test_endpoints @elastic/kibana-se
packages/kbn-interpreter @elastic/kibana-visualizations
x-pack/plugins/observability_solution/investigate_app @elastic/obs-ux-management-team
x-pack/plugins/observability_solution/investigate @elastic/obs-ux-management-team
packages/kbn-investigation-shared @elastic/obs-ux-management-team
packages/kbn-io-ts-utils @elastic/obs-knowledge-team
packages/kbn-ipynb @elastic/search-kibana
packages/kbn-jest-serializers @elastic/kibana-operations

View file

@ -556,6 +556,7 @@
"@kbn/interpreter": "link:packages/kbn-interpreter",
"@kbn/investigate-app-plugin": "link:x-pack/plugins/observability_solution/investigate_app",
"@kbn/investigate-plugin": "link:x-pack/plugins/observability_solution/investigate",
"@kbn/investigation-shared": "link:packages/kbn-investigation-shared",
"@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils",
"@kbn/ipynb": "link:packages/kbn-ipynb",
"@kbn/json-schemas": "link:x-pack/packages/ml/json_schemas",

View file

@ -0,0 +1,3 @@
# @kbn/investigation-shared
Empty package generated by @kbn/generate

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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 or the Server
* Side Public License, v 1.
*/
export type * from './src/schema/create';
export type * from './src/schema/create_notes';
export type * from './src/schema/delete';
export type * from './src/schema/find';
export type * from './src/schema/get';
export type * from './src/schema/get_notes';
export type * from './src/schema/origin';
export * from './src/schema/create';
export * from './src/schema/create_notes';
export * from './src/schema/delete';
export * from './src/schema/find';
export * from './src/schema/get';
export * from './src/schema/get_notes';
export * from './src/schema/origin';

View file

@ -0,0 +1,13 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-investigation-shared'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/investigation-shared",
"owner": "@elastic/obs-ux-management-team"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/investigation-shared",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -1,9 +1,11 @@
/*
* 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.
* 2.0 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 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { investigationResponseSchema } from './investigation';
import { alertOriginSchema, blankOriginSchema } from './origin';

View file

@ -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 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 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { investigationNoteResponseSchema } from './investigation_note';
const createInvestigationNoteParamsSchema = t.type({
path: t.type({
id: t.string,
}),
body: t.type({
content: t.string,
}),
});
const createInvestigationNoteResponseSchema = investigationNoteResponseSchema;
type CreateInvestigationNoteInput = t.OutputOf<
typeof createInvestigationNoteParamsSchema.props.body
>;
type CreateInvestigationNoteParams = t.TypeOf<
typeof createInvestigationNoteParamsSchema.props.body
>;
type CreateInvestigationNoteResponse = t.OutputOf<typeof createInvestigationNoteResponseSchema>;
export { createInvestigationNoteParamsSchema, createInvestigationNoteResponseSchema };
export type {
CreateInvestigationNoteInput,
CreateInvestigationNoteParams,
CreateInvestigationNoteResponse,
};

View file

@ -0,0 +1,20 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
const deleteInvestigationParamsSchema = t.type({
path: t.type({
id: t.string,
}),
});
type DeleteInvestigationParams = t.TypeOf<typeof deleteInvestigationParamsSchema.props.path>; // Parsed payload used by the backend
export { deleteInvestigationParamsSchema };
export type { DeleteInvestigationParams };

View file

@ -1,9 +1,11 @@
/*
* 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.
* 2.0 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 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { investigationResponseSchema } from './investigation';

View file

@ -1,9 +1,11 @@
/*
* 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.
* 2.0 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 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { investigationResponseSchema } from './investigation';

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { investigationNoteResponseSchema } from './investigation_note';
const getInvestigationNotesParamsSchema = t.type({
path: t.type({
id: t.string,
}),
});
const getInvestigationNotesResponseSchema = t.array(investigationNoteResponseSchema);
type GetInvestigationNotesResponse = t.OutputOf<typeof getInvestigationNotesResponseSchema>;
export { getInvestigationNotesParamsSchema, getInvestigationNotesResponseSchema };
export type { GetInvestigationNotesResponse };

View file

@ -1,11 +1,14 @@
/*
* 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.
* 2.0 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 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { alertOriginSchema, blankOriginSchema } from './origin';
import { investigationNoteResponseSchema } from './investigation_note';
const investigationResponseSchema = t.type({
id: t.string,
@ -17,6 +20,7 @@ const investigationResponseSchema = t.type({
}),
origin: t.union([alertOriginSchema, blankOriginSchema]),
status: t.union([t.literal('ongoing'), t.literal('closed')]),
notes: t.array(investigationNoteResponseSchema),
});
type InvestigationResponse = t.OutputOf<typeof investigationResponseSchema>;

View file

@ -0,0 +1,21 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
const investigationNoteResponseSchema = t.type({
id: t.string,
content: t.string,
createdAt: t.number,
createdBy: t.string,
});
type InvestigationNoteResponse = t.OutputOf<typeof investigationNoteResponseSchema>;
export { investigationNoteResponseSchema };
export type { InvestigationNoteResponse };

View file

@ -1,17 +1,14 @@
/*
* 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.
* 2.0 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 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
const blankOriginSchema = t.type({ type: t.literal('blank') });
const alertOriginSchema = t.type({ type: t.literal('alert'), id: t.string });
type AlertOrigin = t.OutputOf<typeof alertOriginSchema>;
type BlankOrigin = t.OutputOf<typeof blankOriginSchema>;
export { alertOriginSchema, blankOriginSchema };
export type { AlertOrigin, BlankOrigin };

View file

@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*"
],
"kbn_references": []
}

View file

@ -1020,6 +1020,8 @@
"@kbn/investigate-app-plugin/*": ["x-pack/plugins/observability_solution/investigate_app/*"],
"@kbn/investigate-plugin": ["x-pack/plugins/observability_solution/investigate"],
"@kbn/investigate-plugin/*": ["x-pack/plugins/observability_solution/investigate/*"],
"@kbn/investigation-shared": ["packages/kbn-investigation-shared"],
"@kbn/investigation-shared/*": ["packages/kbn-investigation-shared/*"],
"@kbn/io-ts-utils": ["packages/kbn-io-ts-utils"],
"@kbn/io-ts-utils/*": ["packages/kbn-io-ts-utils/*"],
"@kbn/ipynb": ["packages/kbn-ipynb"],

View file

@ -14,11 +14,3 @@ 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';

View file

@ -35,7 +35,7 @@ export interface Investigation {
export interface InvestigationNote {
id: string;
createdAt: number;
createdBy: AuthenticatedUser;
createdBy: string;
content: string;
}

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useContext, createContext } from 'react';
import { createContext } from 'react';
import type { InvestigateWidgetCreate } from '../../common';
export interface UseInvestigateWidgetApi<
@ -17,9 +17,3 @@ export interface UseInvestigateWidgetApi<
const InvestigateWidgetApiContext = createContext<UseInvestigateWidgetApi | undefined>(undefined);
export const InvestigateWidgetApiContextProvider = InvestigateWidgetApiContext.Provider;
export function useInvestigateWidget(): UseInvestigateWidgetApi | undefined {
const context = useContext(InvestigateWidgetApiContext);
return context;
}

View file

@ -159,7 +159,7 @@ export function createInvestigationStore({
notes: prevInvestigation.notes.concat({
id: v4(),
createdAt: Date.now(),
createdBy: user,
createdBy: user.username,
content: note,
}),
};

View file

@ -4,10 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { CoreSetup, CoreStart, PluginInitializerContext, Plugin } from '@kbn/core/public';
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
import type { Logger } from '@kbn/logging';
import { useMemo } from 'react';
import { useInvestigateWidget } from './hooks/use_investigate_widget';
import { createUseInvestigation } from './hooks/use_investigation';
import type {
ConfigSchema,
@ -73,7 +72,6 @@ export class InvestigatePlugin
to,
});
},
useInvestigateWidget,
};
}
}

View file

@ -6,13 +6,12 @@
*/
/* eslint-disable @typescript-eslint/no-empty-interface*/
import type { FromSchema } from 'json-schema-to-ts';
import type { CompatibleJSONSchema } from '@kbn/observability-ai-assistant-plugin/public';
import type { AuthenticatedUser } from '@kbn/core/public';
import type { CompatibleJSONSchema } from '@kbn/observability-ai-assistant-plugin/public';
import type { FromSchema } from 'json-schema-to-ts';
import type { InvestigateWidget } from '../common';
import type { GlobalWidgetParameters, InvestigateWidgetCreate } from '../common/types';
import type { UseInvestigationApi } from './hooks/use_investigation';
import type { UseInvestigateWidgetApi } from './hooks/use_investigate_widget';
export enum ChromeOption {
disabled = 'disabled',
@ -84,5 +83,4 @@ export interface InvestigatePublicStart {
from: string;
to: string;
}) => UseInvestigationApi;
useInvestigateWidget: () => UseInvestigateWidgetApi | undefined;
}

View file

@ -9,8 +9,7 @@
"public/**/*",
"typings/**/*",
"public/**/*.json",
"server/**/*"
],
"server/**/*", ],
"kbn_references": [
"@kbn/core",
"@kbn/logging",

View file

@ -7,14 +7,14 @@
import type { CoreStart, CoreTheme } from '@kbn/core/public';
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import { Route, Router, Routes } from '@kbn/shared-ux-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { History } from 'history';
import React, { useMemo } from 'react';
import type { Observable } from 'rxjs';
import { InvestigateAppContextProvider } from './components/investigate_app_context_provider';
import { InvestigateAppKibanaContext } from './hooks/use_kibana';
import { investigateRouter } from './routes/config';
import { getRoutes } from './routes/config';
import { InvestigateAppServices } from './services/types';
import type { InvestigateAppStartDependencies } from './types';
@ -46,6 +46,21 @@ function Application({
[coreStart, pluginsStart, services]
);
const App = () => {
const routes = getRoutes();
return (
<Routes>
{Object.keys(routes).map((path) => {
const { handler, exact } = routes[path];
const Wrapper = () => {
return handler();
};
return <Route key={path} path={path} exact={exact} component={Wrapper} />;
})}
</Routes>
);
};
const queryClient = new QueryClient();
return (
@ -53,11 +68,11 @@ function Application({
<InvestigateAppContextProvider context={context}>
<RedirectAppLinks coreStart={coreStart}>
<coreStart.i18n.Context>
<RouterProvider history={history} router={investigateRouter as any}>
<Router history={history}>
<QueryClientProvider client={queryClient}>
<RouteRenderer />
<App />
</QueryClientProvider>
</RouterProvider>
</Router>
</coreStart.i18n.Context>
</RedirectAppLinks>
</InvestigateAppContextProvider>

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEffect, useRef } from 'react';
export function useAbortSignal() {
const controllerRef = useRef(new AbortController());
useEffect(() => {
const controller = controllerRef.current;
return () => {
controller.abort();
};
}, []);
return controllerRef.current.signal;
}

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { useMutation } from '@tanstack/react-query';
import {
CreateInvestigationNoteInput,
CreateInvestigationNoteResponse,
} from '@kbn/investigation-shared';
import { useKibana } from './use_kibana';
type ServerError = IHttpFetchError<ResponseErrorBody>;
export function useAddInvestigationNote() {
const {
core: {
http,
notifications: { toasts },
},
} = useKibana();
return useMutation<
CreateInvestigationNoteResponse,
ServerError,
{ investigationId: string; note: CreateInvestigationNoteInput },
{ investigationId: string }
>(
['addInvestigationNote'],
({ investigationId, note }) => {
const body = JSON.stringify(note);
return http.post<CreateInvestigationNoteResponse>(
`/api/observability/investigations/${investigationId}/notes`,
{ body, version: '2023-10-31' }
);
},
{
onSuccess: (response, {}) => {
// TODO: clear investigationNotes key from queryClient, and push new note to the internal store.
// console.log(response);
toasts.addSuccess('Note saved');
},
onError: (error, {}, context) => {
// console.log(error);
},
}
);
}

View file

@ -0,0 +1,58 @@
/*
* 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 { GetInvestigationResponse } from '@kbn/investigation-shared';
import { useQuery } from '@tanstack/react-query';
import { investigationKeys } from './query_key_factory';
import { useKibana } from './use_kibana';
export interface Params {
id: string;
}
export interface UseFetchInvestigationResponse {
isInitialLoading: boolean;
isLoading: boolean;
isRefetching: boolean;
isSuccess: boolean;
isError: boolean;
data: GetInvestigationResponse | undefined;
}
export function useFetchInvestigation({ id }: Params): UseFetchInvestigationResponse {
const {
core: {
http,
notifications: { toasts },
},
} = useKibana();
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
queryKey: investigationKeys.fetch({ id }),
queryFn: async ({ signal }) => {
return await http.get<GetInvestigationResponse>(`/api/observability/investigations/${id}`, {
version: '2023-10-31',
signal,
});
},
refetchOnWindowFocus: false,
onError: (error: Error) => {
toasts.addError(error, {
title: 'Something went wrong while fetching Investigations',
});
},
});
return {
data,
isInitialLoading,
isLoading,
isRefetching,
isSuccess,
isError,
};
}

View file

@ -6,7 +6,7 @@
*/
import { useQuery } from '@tanstack/react-query';
import { FindInvestigationsResponse } from '@kbn/investigate-plugin/common';
import { FindInvestigationsResponse } from '@kbn/investigation-shared';
import { investigationKeys } from './query_key_factory';
import { useKibana } from './use_kibana';

View file

@ -10,7 +10,7 @@ import { BASE_RAC_ALERTS_API_PATH, EcsFieldsResponse } from '@kbn/rule-registry-
import { useKibana } from './use_kibana';
export interface AlertParams {
id: string;
id?: string;
}
export interface UseFetchAlertResponse {
@ -40,15 +40,7 @@ export function useFetchAlert({ id }: AlertParams): UseFetchAlertResponse {
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',

View file

@ -6,7 +6,7 @@
*/
import { useQuery } from '@tanstack/react-query';
import { GetInvestigationResponse } from '@kbn/investigate-plugin/common/schema/get';
import { GetInvestigationResponse } from '@kbn/investigation-shared';
import { investigationKeys } from './query_key_factory';
import { useKibana } from './use_kibana';
@ -41,15 +41,7 @@ export function useFetchInvestigation({
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',

View file

@ -1,14 +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 { type PathsOf, type TypeOf, useParams } from '@kbn/typed-react-router-config';
import type { InvestigateRoutes } from '../routes/config';
export function useInvestigateParams<TPath extends PathsOf<InvestigateRoutes>>(
path: TPath
): TypeOf<InvestigateRoutes, TPath> {
return useParams(path)! as TypeOf<InvestigateRoutes, TPath>;
}

View file

@ -1,55 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { PathsOf, TypeAsArgs, TypeOf } from '@kbn/typed-react-router-config';
import { useMemo } from 'react';
import { InvestigateRouter, InvestigateRoutes } from '../routes/config';
import { investigateRouter } from '../routes/config';
import { useKibana } from './use_kibana';
interface StatefulInvestigateRouter extends InvestigateRouter {
push<T extends PathsOf<InvestigateRoutes>>(
path: T,
...params: TypeAsArgs<TypeOf<InvestigateRoutes, T>>
): void;
replace<T extends PathsOf<InvestigateRoutes>>(
path: T,
...params: TypeAsArgs<TypeOf<InvestigateRoutes, T>>
): void;
}
export function useInvestigateRouter(): StatefulInvestigateRouter {
const {
core: {
http,
application: { navigateToApp },
},
} = useKibana();
const link = (...args: any[]) => {
// @ts-expect-error
return investigateRouter.link(...args);
};
return useMemo<StatefulInvestigateRouter>(
() => ({
...investigateRouter,
push: (...args) => {
const next = link(...args);
navigateToApp('investigations', { path: next, replace: false });
},
replace: (path, ...args) => {
const next = link(path, ...args);
navigateToApp('investigations', { path: next, replace: true });
},
link: (path, ...args) => {
return http.basePath.prepend('/app/investigations' + link(path, ...args));
},
}),
[navigateToApp, http.basePath]
);
}

View file

@ -20,7 +20,7 @@ export default meta;
const defaultProps: ComponentStoryObj<typeof Component> = {
args: {},
render: (props) => <Component {...props} />,
render: (props) => <Component investigationId="123" />,
};
export const InvestigateViewStory: ComponentStoryObj<typeof Component> = {

View file

@ -6,27 +6,41 @@
*/
import datemath from '@elastic/datemath';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import type { InvestigateWidgetCreate } from '@kbn/investigate-plugin/public';
import { AuthenticatedUser } from '@kbn/security-plugin/common';
import { keyBy, noop } from 'lodash';
import React, { useEffect, useMemo, useRef } from 'react';
import React, { useMemo } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { AddObservationUI } from '../../../../components/add_observation_ui';
import { InvestigateSearchBar } from '../../../../components/investigate_search_bar';
import { InvestigateWidgetGrid } from '../../../../components/investigate_widget_grid';
import { useAddInvestigationNote } from '../../../../hooks/use_add_investigation_note';
import { useDateRange } from '../../../../hooks/use_date_range';
import { useFetchInvestigation } from '../../../../hooks/use_fetch_investigation';
import { useKibana } from '../../../../hooks/use_kibana';
import { InvestigationNotes } from '../investigation_notes/investigation_notes';
function InvestigationDetailsWithUser({ user }: { user: AuthenticatedUser }) {
function InvestigationDetailsWithUser({
user,
investigationId,
}: {
user: AuthenticatedUser;
investigationId: string;
}) {
const {
dependencies: {
start: { investigate },
},
} = useKibana();
const widgetDefinitions = useMemo(() => investigate.getWidgetDefinitions(), [investigate]);
const widgetDefinitions = investigate.getWidgetDefinitions();
const [range, setRange] = useDateRange();
const { data: investigationData } = useFetchInvestigation({ id: investigationId });
const { mutateAsync: addInvestigationNote } = useAddInvestigationNote();
const handleAddInvestigationNote = async (note: string) => {
await addInvestigationNote({ investigationId, note: { content: note } });
await addNote(note);
};
const {
addItem,
copyItem,
@ -42,33 +56,6 @@ function InvestigationDetailsWithUser({ user }: { user: AuthenticatedUser }) {
to: range.end.toISOString(),
});
const createWidget = (widgetCreate: InvestigateWidgetCreate) => {
return addItem(widgetCreate);
};
const createWidgetRef = useRef(createWidget);
createWidgetRef.current = createWidget;
useEffect(() => {
if (
renderableInvestigation?.parameters.timeRange.from &&
renderableInvestigation?.parameters.timeRange.to &&
range.start.toISOString() !== renderableInvestigation.parameters.timeRange.from &&
range.end.toISOString() !== renderableInvestigation.parameters.timeRange.to
) {
setRange({
from: renderableInvestigation.parameters.timeRange.from,
to: renderableInvestigation.parameters.timeRange.to,
});
}
}, [
renderableInvestigation?.parameters.timeRange.from,
renderableInvestigation?.parameters.timeRange.to,
range.start,
range.end,
setRange,
]);
const gridItems = useMemo(() => {
const widgetDefinitionsByType = keyBy(widgetDefinitions, 'type');
@ -88,7 +75,7 @@ function InvestigationDetailsWithUser({ user }: { user: AuthenticatedUser }) {
});
}, [renderableInvestigation, widgetDefinitions]);
if (!investigation || !renderableInvestigation || !gridItems) {
if (!investigation || !renderableInvestigation || !gridItems || !investigationData) {
return <EuiLoadingSpinner />;
}
@ -135,20 +122,24 @@ function InvestigationDetailsWithUser({ user }: { user: AuthenticatedUser }) {
<AddObservationUI
timeRange={renderableInvestigation.parameters.timeRange}
onWidgetAdd={(widget) => {
return createWidgetRef.current(widget);
return addItem(widget);
}}
/>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<InvestigationNotes notes={investigation.notes} addNote={addNote} deleteNote={deleteNote} />
<InvestigationNotes
notes={investigationData.notes}
addNote={handleAddInvestigationNote}
deleteNote={deleteNote}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
export function InvestigationDetails({}: {}) {
export function InvestigationDetails({ investigationId }: { investigationId: string }) {
const {
core: { security },
} = useKibana();
@ -157,5 +148,12 @@ export function InvestigationDetails({}: {}) {
return security.authc.getCurrentUser();
}, [security]);
return user.value ? <InvestigationDetailsWithUser user={user.value} /> : null;
if (investigationId == null) {
// TODO: return 404 page
return null;
}
return user.value ? (
<InvestigationDetailsWithUser user={user.value} investigationId={investigationId} />
) : null;
}

View file

@ -16,8 +16,8 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/css';
import { i18n } from '@kbn/i18n';
import { InvestigationNote } from '@kbn/investigate-plugin/common';
import React from 'react';
import { InvestigationNote } from '@kbn/investigate-plugin/common';
import { useTheme } from '../../../../hooks/use_theme';
import { ResizableTextInput } from './resizable_text_input';
import { TimelineMessage } from './timeline_message';
@ -69,7 +69,7 @@ export function InvestigationNotes({ notes, addNote, deleteNote }: Props) {
return (
<TimelineMessage
key={currNote.id}
icon={<EuiAvatar name={currNote.createdBy.username} size="s" />}
icon={<EuiAvatar name={currNote.createdBy} size="s" />}
note={currNote}
onDelete={() => deleteNote(currNote.id)}
/>

View file

@ -6,9 +6,9 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiMarkdownFormat, EuiText } from '@elastic/eui';
import { css } from '@emotion/css';
import { InvestigationNote } from '@kbn/investigate-plugin/common';
// eslint-disable-next-line import/no-extraneous-dependencies
import { format } from 'date-fns';
import { InvestigationNote } from '@kbn/investigate-plugin/common';
import React from 'react';
import { InvestigateTextButton } from '../../../../components/investigate_text_button';
import { useTheme } from '../../../../hooks/use_theme';

View file

@ -7,15 +7,16 @@
import { EuiButton, EuiButtonEmpty, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { alertOriginSchema } from '@kbn/investigation-shared';
import { ALERT_RULE_CATEGORY } from '@kbn/rule-data-utils/src/default_alerts_as_data';
import { AlertOrigin } from '@kbn/investigate-plugin/common/schema/origin';
import React from 'react';
import { useParams } from 'react-router-dom';
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 { useFetchInvestigation } from '../../hooks/use_get_investigation_details';
import { useKibana } from '../../hooks/use_kibana';
import { InvestigationDetails } from './components/investigation_details';
import { InvestigationDetailsPathParams } from './types';
export function InvestigationDetailsPage() {
const {
@ -27,9 +28,7 @@ export function InvestigationDetailsPage() {
},
} = useKibana();
const {
path: { id },
} = useInvestigateParams('/{id}');
const { investigationId } = useParams<InvestigationDetailsPathParams>();
const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate;
@ -37,17 +36,15 @@ export function InvestigationDetailsPage() {
data: investigationDetails,
isLoading: isFetchInvestigationLoading,
isError: isFetchInvestigationError,
} = useFetchInvestigation({ id });
} = useFetchInvestigation({ id: investigationId });
const alertId = investigationDetails ? (investigationDetails.origin as AlertOrigin).id : '';
const alertId = alertOriginSchema.is(investigationDetails?.origin)
? investigationDetails?.origin.id
: undefined;
const {
data: alertDetails,
isLoading: isFetchAlertLoading,
isError: isFetchAlertError,
} = useFetchAlert({ id: alertId });
const { data: alertDetails } = useFetchAlert({ id: alertId });
if (isFetchInvestigationLoading || isFetchAlertLoading) {
if (isFetchInvestigationLoading) {
return (
<h1>
{i18n.translate('xpack.investigateApp.fetchInvestigation.loadingLabel', {
@ -57,7 +54,7 @@ export function InvestigationDetailsPage() {
);
}
if (isFetchInvestigationError || isFetchAlertError) {
if (isFetchInvestigationError) {
return (
<h1>
{i18n.translate('xpack.investigateApp.fetchInvestigation.errorLabel', {
@ -109,7 +106,7 @@ export function InvestigationDetailsPage() {
],
}}
>
<InvestigationDetails />
<InvestigationDetails investigationId={investigationId} />
</ObservabilityPageTemplate>
);
}

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export interface InvestigationDetailsPathParams {
investigationId: string;
}

View file

@ -4,33 +4,31 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createRouter } from '@kbn/typed-react-router-config';
import * as t from 'io-ts';
import React from 'react';
import { InvestigationDetailsPage } from '../pages/details/investigation_details_page';
import { InvestigationListPage } from '../pages/list/investigation_list_page';
/**
* The array of route definitions to be used when the application
* creates the routes.
*/
const investigateRoutes = {
'/': {
element: <InvestigationListPage />,
},
'/new': {
element: <InvestigationDetailsPage />,
},
'/{id}': {
element: <InvestigationDetailsPage />,
params: t.type({
path: t.type({ id: t.string }),
}),
},
export const getRoutes = (): {
[path: string]: {
handler: () => React.ReactElement;
params: Record<string, string>;
exact: boolean;
};
} => {
return {
'/': {
handler: () => {
return <InvestigationListPage />;
},
params: {},
exact: true,
},
'/:investigationId': {
handler: () => {
return <InvestigationDetailsPage />;
},
params: {},
exact: true,
},
};
};
export type InvestigateRoutes = typeof investigateRoutes;
export const investigateRouter = createRouter(investigateRoutes);
export type InvestigateRouter = typeof investigateRouter;

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import { alertOriginSchema, blankOriginSchema } from '@kbn/investigate-plugin/common/schema/origin';
import { alertOriginSchema, blankOriginSchema } from '@kbn/investigation-shared';
import * as t from 'io-ts';
import { investigationNoteSchema } from './investigation_note';
export const investigationSchema = t.type({
id: t.string,
@ -18,6 +19,7 @@ export const investigationSchema = t.type({
}),
origin: t.union([alertOriginSchema, blankOriginSchema]),
status: t.union([t.literal('ongoing'), t.literal('closed')]),
notes: t.array(investigationNoteSchema),
});
export type Investigation = t.TypeOf<typeof investigationSchema>;

View file

@ -0,0 +1,18 @@
/*
* 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';
export const investigationNoteSchema = t.type({
id: t.string,
createdAt: t.number,
createdBy: t.string,
content: t.string,
});
export type InvestigationNote = t.TypeOf<typeof investigationNoteSchema>;
export type StoredInvestigationNote = t.OutputOf<typeof investigationNoteSchema>;

View file

@ -5,14 +5,22 @@
* 2.0.
*/
import { createInvestigationParamsSchema } from '@kbn/investigate-plugin/common';
import { findInvestigationsParamsSchema } from '@kbn/investigate-plugin/common';
import { getInvestigationParamsSchema } from '@kbn/investigate-plugin/common';
import {
createInvestigationNoteParamsSchema,
createInvestigationParamsSchema,
deleteInvestigationParamsSchema,
findInvestigationsParamsSchema,
getInvestigationNotesParamsSchema,
getInvestigationParamsSchema,
} from '@kbn/investigation-shared';
import { createInvestigation } from '../services/create_investigation';
import { investigationRepositoryFactory } from '../services/investigation_repository';
import { createInvestigateAppServerRoute } from './create_investigate_app_server_route';
import { createInvestigationNote } from '../services/create_investigation_note';
import { deleteInvestigation } from '../services/delete_investigation';
import { findInvestigations } from '../services/find_investigations';
import { getInvestigation } from '../services/get_investigation';
import { getInvestigationNotes } from '../services/get_investigation_notes';
import { investigationRepositoryFactory } from '../services/investigation_repository';
import { createInvestigateAppServerRoute } from './create_investigate_app_server_route';
const createInvestigationRoute = createInvestigateAppServerRoute({
endpoint: 'POST /api/observability/investigations 2023-10-31',
@ -56,11 +64,56 @@ const getInvestigationRoute = createInvestigateAppServerRoute({
},
});
const deleteInvestigationRoute = createInvestigateAppServerRoute({
endpoint: 'DELETE /api/observability/investigations/{id} 2023-10-31',
options: {
tags: [],
},
params: deleteInvestigationParamsSchema,
handler: async (params) => {
const soClient = (await params.context.core).savedObjects.client;
const repository = investigationRepositoryFactory({ soClient, logger: params.logger });
return await deleteInvestigation(params.params.path.id, repository);
},
});
const createInvestigationNoteRoute = createInvestigateAppServerRoute({
endpoint: 'POST /api/observability/investigations/{id}/notes 2023-10-31',
options: {
tags: [],
},
params: createInvestigationNoteParamsSchema,
handler: async (params) => {
const soClient = (await params.context.core).savedObjects.client;
const repository = investigationRepositoryFactory({ soClient, logger: params.logger });
return await createInvestigationNote(params.params.path.id, params.params.body, repository);
},
});
const getInvestigationNotesRoute = createInvestigateAppServerRoute({
endpoint: 'GET /api/observability/investigations/{id}/notes 2023-10-31',
options: {
tags: [],
},
params: getInvestigationNotesParamsSchema,
handler: async (params) => {
const soClient = (await params.context.core).savedObjects.client;
const repository = investigationRepositoryFactory({ soClient, logger: params.logger });
return await getInvestigationNotes(params.params.path.id, repository);
},
});
export function getGlobalInvestigateAppServerRouteRepository() {
return {
...createInvestigationRoute,
...findInvestigationsRoute,
...getInvestigationRoute,
...deleteInvestigationRoute,
...createInvestigationNoteRoute,
...getInvestigationNotesRoute,
};
}

View file

@ -5,10 +5,7 @@
* 2.0.
*/
import {
CreateInvestigationInput,
CreateInvestigationResponse,
} from '@kbn/investigate-plugin/common';
import { CreateInvestigationInput, CreateInvestigationResponse } from '@kbn/investigation-shared';
import { InvestigationRepository } from './investigation_repository';
enum InvestigationStatus {
@ -25,6 +22,7 @@ export async function createInvestigation(
createdAt: Date.now(),
createdBy: 'elastic',
status: InvestigationStatus.ongoing,
notes: [],
};
await repository.save(investigation);

View file

@ -0,0 +1,33 @@
/*
* 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 {
CreateInvestigationNoteInput,
CreateInvestigationNoteResponse,
} from '@kbn/investigation-shared';
import { v4 } from 'uuid';
import { InvestigationRepository } from './investigation_repository';
export async function createInvestigationNote(
investigationId: string,
params: CreateInvestigationNoteInput,
repository: InvestigationRepository
): Promise<CreateInvestigationNoteResponse> {
const investigation = await repository.findById(investigationId);
const investigationNote = {
id: v4(),
content: params.content,
createdBy: 'TODO: get user from request',
createdAt: Date.now(),
};
investigation.notes.push(investigationNote);
await repository.save(investigation);
return investigationNote;
}

View file

@ -0,0 +1,15 @@
/*
* 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 { InvestigationRepository } from './investigation_repository';
export async function deleteInvestigation(
id: string,
repository: InvestigationRepository
): Promise<void> {
await repository.deleteById(id);
}

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { FindInvestigationsResponse } from '@kbn/investigate-plugin/common';
import {
FindInvestigationsParams,
FindInvestigationsResponse,
findInvestigationsResponseSchema,
} from '@kbn/investigate-plugin/common/schema/find';
} from '@kbn/investigation-shared';
import { InvestigationRepository } from './investigation_repository';
export async function findInvestigations(

View file

@ -5,8 +5,11 @@
* 2.0.
*/
import { GetInvestigationParams } from '@kbn/investigate-plugin/common';
import { GetInvestigationResponse } from '@kbn/investigate-plugin/common/schema/get';
import {
GetInvestigationParams,
GetInvestigationResponse,
getInvestigationResponseSchema,
} from '@kbn/investigation-shared';
import { InvestigationRepository } from './investigation_repository';
export async function getInvestigation(
@ -15,5 +18,5 @@ export async function getInvestigation(
): Promise<GetInvestigationResponse> {
const investigation = await repository.findById(params.id);
return investigation;
return getInvestigationResponseSchema.encode(investigation);
}

View file

@ -0,0 +1,21 @@
/*
* 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 {
GetInvestigationNotesResponse,
getInvestigationNotesResponseSchema,
} from '@kbn/investigation-shared';
import { InvestigationRepository } from './investigation_repository';
export async function getInvestigationNotes(
investigationId: string,
repository: InvestigationRepository
): Promise<GetInvestigationNotesResponse> {
const investigation = await repository.findById(investigationId);
return getInvestigationNotesResponseSchema.encode(investigation.notes);
}

View file

@ -21,7 +21,6 @@
"@kbn/react-kibana-context-theme",
"@kbn/shared-ux-link-redirect-app",
"@kbn/kibana-react-plugin",
"@kbn/typed-react-router-config",
"@kbn/i18n",
"@kbn/embeddable-plugin",
"@kbn/observability-ai-assistant-plugin",
@ -54,5 +53,7 @@
"@kbn/core-saved-objects-server",
"@kbn/rule-registry-plugin",
"@kbn/rule-data-utils",
"@kbn/shared-ux-router",
"@kbn/investigation-shared",
],
}

View file

@ -6,13 +6,13 @@
*/
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { QueryKey, useMutation } from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import {
CreateInvestigationInput,
CreateInvestigationResponse,
} from '@kbn/investigate-plugin/common';
import { FindInvestigationsResponse } from '@kbn/investigate-plugin/common';
FindInvestigationsResponse,
} from '@kbn/investigation-shared';
import { QueryKey, useMutation } from '@tanstack/react-query';
import { useKibana } from '../../../utils/kibana_react';
type ServerError = IHttpFetchError<ResponseErrorBody>;

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { FindInvestigationsResponse } from '@kbn/investigation-shared';
import { useQuery } from '@tanstack/react-query';
import { FindInvestigationsResponse } from '@kbn/investigate-plugin/common';
import { useKibana } from '../../../utils/kibana_react';
export interface InvestigationsByAlertParams {

View file

@ -111,6 +111,7 @@
"@kbn/observability-alerting-rule-utils",
"@kbn/core-ui-settings-server-mocks",
"@kbn/investigate-plugin",
"@kbn/investigation-shared",
],
"exclude": [
"target/**/*"

View file

@ -5320,6 +5320,10 @@
version "0.0.0"
uid ""
"@kbn/investigation-shared@link:packages/kbn-investigation-shared":
version "0.0.0"
uid ""
"@kbn/io-ts-utils@link:packages/kbn-io-ts-utils":
version "0.0.0"
uid ""