diff --git a/packages/kbn-investigation-shared/src/rest_specs/create_item.ts b/packages/kbn-investigation-shared/src/rest_specs/create_item.ts index c94673313a50c..a0696df4040d8 100644 --- a/packages/kbn-investigation-shared/src/rest_specs/create_item.ts +++ b/packages/kbn-investigation-shared/src/rest_specs/create_item.ts @@ -7,14 +7,14 @@ */ import * as t from 'io-ts'; -import { investigationItemsSchema } from '../schema'; +import { itemSchema } from '../schema'; import { investigationItemResponseSchema } from './investigation_item'; const createInvestigationItemParamsSchema = t.type({ path: t.type({ investigationId: t.string, }), - body: investigationItemsSchema, + body: itemSchema, }); const createInvestigationItemResponseSchema = investigationItemResponseSchema; diff --git a/packages/kbn-investigation-shared/src/rest_specs/index.ts b/packages/kbn-investigation-shared/src/rest_specs/index.ts index 50c1e300cd96a..cb13c11886481 100644 --- a/packages/kbn-investigation-shared/src/rest_specs/index.ts +++ b/packages/kbn-investigation-shared/src/rest_specs/index.ts @@ -17,6 +17,7 @@ export type * from './investigation_note'; export type * from './create_item'; export type * from './delete_item'; export type * from './get_items'; +export type * from './investigation_item'; export * from './create'; export * from './create_note'; @@ -29,3 +30,4 @@ export * from './investigation_note'; export * from './create_item'; export * from './delete_item'; export * from './get_items'; +export * from './investigation_item'; diff --git a/packages/kbn-investigation-shared/src/schema/investigation_item.ts b/packages/kbn-investigation-shared/src/schema/investigation_item.ts index 8689224960c52..717bf246e3590 100644 --- a/packages/kbn-investigation-shared/src/schema/investigation_item.ts +++ b/packages/kbn-investigation-shared/src/schema/investigation_item.ts @@ -8,20 +8,23 @@ import * as t from 'io-ts'; -const esqlItemSchema = t.type({ +const itemSchema = t.type({ title: t.string, - type: t.literal('esql'), - params: t.type({ - esql: t.string, - suggestion: t.any, - }), + type: t.string, + params: t.record(t.string, t.any), }); -const investigationItemsSchema = esqlItemSchema; // replace with union with various item types - const investigationItemSchema = t.intersection([ - t.type({ id: t.string, createdAt: t.number, createdBy: t.string }), - investigationItemsSchema, + t.type({ + id: t.string, + createdAt: t.number, + createdBy: t.string, + }), + itemSchema, ]); -export { investigationItemSchema, investigationItemsSchema, esqlItemSchema }; +type Item = t.TypeOf; +type InvestigationItem = t.TypeOf; + +export type { Item, InvestigationItem }; +export { investigationItemSchema, itemSchema }; diff --git a/x-pack/plugins/observability_solution/investigate/common/index.ts b/x-pack/plugins/observability_solution/investigate/common/index.ts index 541e2d7206bf3..47b5ad6ed2318 100644 --- a/x-pack/plugins/observability_solution/investigate/common/index.ts +++ b/x-pack/plugins/observability_solution/investigate/common/index.ts @@ -4,11 +4,5 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -export type { - Investigation, - InvestigateWidget, - InvestigateWidgetCreate, - InvestigationNote, -} from './types'; export { mergePlainObjects } from './utils/merge_plain_objects'; diff --git a/x-pack/plugins/observability_solution/investigate/common/types.ts b/x-pack/plugins/observability_solution/investigate/common/types.ts index 8a2bba966ed7e..55eab5836ce5b 100644 --- a/x-pack/plugins/observability_solution/investigate/common/types.ts +++ b/x-pack/plugins/observability_solution/investigate/common/types.ts @@ -5,47 +5,9 @@ * 2.0. */ -import type { DeepPartial } from 'utility-types'; - export interface GlobalWidgetParameters { timeRange: { from: string; to: string; }; } - -export interface Investigation { - id: string; - createdAt: number; - title: string; - items: InvestigateWidget[]; - notes: InvestigationNote[]; - parameters: GlobalWidgetParameters; -} - -export interface InvestigationNote { - id: string; - createdAt: number; - createdBy: string; - content: string; -} - -export interface InvestigateWidget< - TParameters extends Record = {}, - TData extends Record = {} -> { - id: string; - createdAt: number; - createdBy: string; - title: string; - type: string; - parameters: GlobalWidgetParameters & TParameters; - data: TData; -} - -export type InvestigateWidgetCreate = {}> = Pick< - InvestigateWidget, - 'title' | 'type' -> & { - parameters: DeepPartial & TParameters; -}; diff --git a/x-pack/plugins/observability_solution/investigate/public/create_widget.ts b/x-pack/plugins/observability_solution/investigate/public/create_widget.ts deleted file mode 100644 index 697202ac42d2b..0000000000000 --- a/x-pack/plugins/observability_solution/investigate/public/create_widget.ts +++ /dev/null @@ -1,35 +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 { DeepPartial } from 'utility-types'; -import { InvestigateWidgetCreate } from '../common'; -import { GlobalWidgetParameters } from '../common/types'; - -type MakePartial, K extends keyof T> = Omit & - DeepPartial>; - -type PredefinedKeys = 'type'; - -export type WidgetFactory> = < - T extends MakePartial, PredefinedKeys> ->( - widgetCreate: T -) => Pick, PredefinedKeys> & - Omit & { parameters: T['parameters'] & DeepPartial }; - -export function createWidgetFactory>( - type: string -): WidgetFactory { - const createWidget: WidgetFactory = (widgetCreate) => { - return { - type, - ...widgetCreate, - }; - }; - - return createWidget; -} diff --git a/x-pack/plugins/observability_solution/investigate/public/esql_widget/constants.ts b/x-pack/plugins/observability_solution/investigate/public/esql_widget/constants.ts deleted file mode 100644 index 068d4b49fcf80..0000000000000 --- a/x-pack/plugins/observability_solution/investigate/public/esql_widget/constants.ts +++ /dev/null @@ -1,8 +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. - */ - -export const ESQL_WIDGET_NAME = 'esql'; diff --git a/x-pack/plugins/observability_solution/investigate/public/esql_widget/create_esql_widget.ts b/x-pack/plugins/observability_solution/investigate/public/esql_widget/create_esql_widget.ts deleted file mode 100644 index f3226afd14198..0000000000000 --- a/x-pack/plugins/observability_solution/investigate/public/esql_widget/create_esql_widget.ts +++ /dev/null @@ -1,12 +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 { createWidgetFactory } from '../create_widget'; -import { ESQL_WIDGET_NAME } from './constants'; -import type { EsqlWidgetParameters } from './types'; - -export const createEsqlWidget = createWidgetFactory(ESQL_WIDGET_NAME); diff --git a/x-pack/plugins/observability_solution/investigate/public/esql_widget/types.ts b/x-pack/plugins/observability_solution/investigate/public/esql_widget/types.ts deleted file mode 100644 index 764daedc9c5ed..0000000000000 --- a/x-pack/plugins/observability_solution/investigate/public/esql_widget/types.ts +++ /dev/null @@ -1,34 +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 { IconType } from '@elastic/eui'; -import type { Ast } from '@kbn/interpreter'; - -// copied over from the Lens plugin to prevent dependency hell -type TableChangeType = 'initial' | 'unchanged' | 'reduced' | 'extended' | 'reorder' | 'layers'; - -interface Suggestion { - visualizationId: string; - datasourceState?: V; - datasourceId?: string; - columns: number; - score: number; - title: string; - visualizationState: T; - previewExpression?: Ast | string; - previewIcon: IconType; - hide?: boolean; - // flag to indicate if the visualization is incomplete - incomplete?: boolean; - changeType: TableChangeType; - keptLayerIds: string[]; -} - -export interface EsqlWidgetParameters { - esql: string; - suggestion?: Suggestion; -} diff --git a/x-pack/plugins/observability_solution/investigate/public/hooks/use_investigation/create_new_investigation.ts b/x-pack/plugins/observability_solution/investigate/public/hooks/use_investigation/create_new_investigation.ts deleted file mode 100644 index af6227e552115..0000000000000 --- a/x-pack/plugins/observability_solution/investigate/public/hooks/use_investigation/create_new_investigation.ts +++ /dev/null @@ -1,47 +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 { i18n } from '@kbn/i18n'; -import { GetInvestigationResponse } from '@kbn/investigation-shared'; -import { v4 } from 'uuid'; -import type { Investigation } from '../../../common'; - -export function createNewInvestigation(): Investigation { - return { - id: v4(), - createdAt: new Date().getTime(), - title: i18n.translate('xpack.investigate.newInvestigationTitle', { - defaultMessage: 'New investigation', - }), - items: [], - notes: [], - parameters: { - timeRange: { - from: new Date(Date.now() - 15 * 60 * 1000).toISOString(), - to: new Date().toISOString(), - }, - }, - }; -} - -export function fromInvestigationResponse( - investigationData: GetInvestigationResponse -): Investigation { - return { - id: investigationData.id, - createdAt: investigationData.createdAt, - title: investigationData.title, - items: [], - notes: investigationData.notes, - parameters: { - timeRange: { - from: new Date(investigationData.params.timeRange.from).toISOString(), - to: new Date(investigationData.params.timeRange.to).toISOString(), - }, - }, - }; -} diff --git a/x-pack/plugins/observability_solution/investigate/public/hooks/use_investigation/index.tsx b/x-pack/plugins/observability_solution/investigate/public/hooks/use_investigation/index.tsx deleted file mode 100644 index fd3c50cdb80ac..0000000000000 --- a/x-pack/plugins/observability_solution/investigate/public/hooks/use_investigation/index.tsx +++ /dev/null @@ -1,166 +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 { AuthenticatedUser, NotificationsStart } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import { GetInvestigationResponse } from '@kbn/investigation-shared'; -import { pull } from 'lodash'; -import React, { useMemo, useRef, useState } from 'react'; -import useObservable from 'react-use/lib/useObservable'; -import { v4 } from 'uuid'; -import type { GlobalWidgetParameters } from '../..'; -import type { InvestigateWidget, InvestigateWidgetCreate } from '../../../common'; -import type { WidgetDefinition } from '../../types'; -import { createNewInvestigation, fromInvestigationResponse } from './create_new_investigation'; -import { StatefulInvestigation, createInvestigationStore } from './investigation_store'; - -export type RenderableInvestigateWidget = InvestigateWidget & { - loading: boolean; - element: React.ReactNode; -}; - -export type RenderableInvestigation = Omit & { - items: RenderableInvestigateWidget[]; -}; - -export interface UseInvestigationApi { - investigation?: StatefulInvestigation; - renderableInvestigation?: RenderableInvestigation; - copyItem: (id: string) => Promise; - deleteItem: (id: string) => Promise; - addItem: (options: InvestigateWidgetCreate) => Promise; - setGlobalParameters: (parameters: GlobalWidgetParameters) => Promise; - setTitle: (title: string) => Promise; -} - -function useInvestigationWithoutContext({ - user, - notifications, - widgetDefinitions, - investigationData, -}: { - user: AuthenticatedUser; - notifications: NotificationsStart; - widgetDefinitions: WidgetDefinition[]; - investigationData?: GetInvestigationResponse; -}): UseInvestigationApi { - const [investigationStore, _] = useState(() => - createInvestigationStore({ - user, - widgetDefinitions, - investigation: investigationData - ? fromInvestigationResponse(investigationData) - : createNewInvestigation(), - }) - ); - - const investigation$ = investigationStore.asObservable(); - const investigation = useObservable(investigation$)?.investigation; - - const addItem = async (widget: InvestigateWidgetCreate) => { - try { - const id = v4(); - await investigationStore.addItem(id, widget); - } catch (error) { - notifications.showErrorDialog({ - title: i18n.translate('xpack.investigate.failedToAddWidget', { - defaultMessage: 'Failed to add widget', - }), - error, - }); - } - }; - - const deleteItem = async (id: string) => { - return investigationStore.deleteItem(id); - }; - - const widgetComponentsById = useRef< - Record> - >({}); - - const itemsWithContext = useMemo(() => { - const unusedComponentIds = Object.keys(widgetComponentsById); - - const nextItemsWithContext = - investigation?.items.map((item) => { - let Component = widgetComponentsById.current[item.id]; - if (!Component) { - const id = item.id; - const widgetDefinition = widgetDefinitions.find( - (definition) => definition.type === item.type - )!; - - Component = widgetComponentsById.current[id] = (props) => { - return <>{widgetDefinition?.render({ widget: props.widget })}; - }; - } - - pull(unusedComponentIds, item.id); - - return { - ...item, - Component, - }; - }) ?? []; - - unusedComponentIds.forEach((id) => { - delete widgetComponentsById.current[id]; - }); - - return nextItemsWithContext; - }, [investigation?.items, widgetDefinitions]); - - const renderableInvestigation = useMemo(() => { - return investigation - ? { - ...investigation, - items: itemsWithContext.map((item) => { - const { Component, ...rest } = item; - return { - ...rest, - element: , - }; - }), - } - : undefined; - }, [investigation, itemsWithContext]); - - const { copyItem, setGlobalParameters, setTitle } = investigationStore; - - return { - addItem, - copyItem, - deleteItem, - investigation, - renderableInvestigation, - setGlobalParameters, - setTitle, - }; -} - -export function createUseInvestigation({ - notifications, - widgetDefinitions, -}: { - notifications: NotificationsStart; - widgetDefinitions: WidgetDefinition[]; -}) { - return ({ - user, - investigationData, - }: { - user: AuthenticatedUser; - investigationData?: GetInvestigationResponse; - }) => { - return useInvestigationWithoutContext({ - user, - notifications, - widgetDefinitions, - investigationData, - }); - }; -} diff --git a/x-pack/plugins/observability_solution/investigate/public/hooks/use_investigation/investigation_store.ts b/x-pack/plugins/observability_solution/investigate/public/hooks/use_investigation/investigation_store.ts deleted file mode 100644 index be2da347f995a..0000000000000 --- a/x-pack/plugins/observability_solution/investigate/public/hooks/use_investigation/investigation_store.ts +++ /dev/null @@ -1,152 +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 { AuthenticatedUser } from '@kbn/security-plugin/common'; -import { MaybePromise } from '@kbn/utility-types'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { v4 } from 'uuid'; -import { InvestigateWidget } from '../../../common'; -import { - GlobalWidgetParameters, - InvestigateWidgetCreate, - Investigation, -} from '../../../common/types'; -import { WidgetDefinition } from '../../types'; -import { regenerateItem } from './regenerate_item'; - -export type StatefulInvestigateWidget = InvestigateWidget & { - loading: boolean; -}; - -export type StatefulInvestigation = Omit & { - items: StatefulInvestigateWidget[]; -}; - -interface InvestigationStore { - copyItem: (id: string) => Promise; - deleteItem: (id: string) => Promise; - addItem: (id: string, item: InvestigateWidgetCreate) => Promise; - asObservable: () => Observable<{ - investigation: StatefulInvestigation; - }>; - setGlobalParameters: (globalWidgetParameters: GlobalWidgetParameters) => Promise; - setTitle: (title: string) => Promise; - destroy: () => void; -} - -export function createInvestigationStore({ - investigation, - user, - widgetDefinitions, -}: { - investigation: Investigation; - user: AuthenticatedUser; - widgetDefinitions: WidgetDefinition[]; -}): InvestigationStore { - const controller = new AbortController(); - - const observable$ = new BehaviorSubject<{ investigation: StatefulInvestigation }>({ - investigation: { - ...investigation, - items: investigation.items.map((item) => ({ ...item, loading: false })), - }, - }); - - async function updateInvestigationInPlace( - cb: (prevInvestigation: StatefulInvestigation) => MaybePromise - ) { - observable$.next({ investigation: await cb(observable$.value.investigation) }); - } - - const asObservable = observable$.asObservable(); - - return { - addItem: (itemId, item) => { - return updateInvestigationInPlace(async (prevInvestigation) => { - return { - ...prevInvestigation, - items: prevInvestigation.items.concat({ - ...(await regenerateItem({ - user, - widgetDefinitions, - signal: controller.signal, - widget: { - ...item, - id: itemId, - }, - globalWidgetParameters: prevInvestigation.parameters, - })), - loading: false, - }), - }; - }); - }, - copyItem: (itemId) => { - return updateInvestigationInPlace((prevInvestigation) => { - const itemToCopy = prevInvestigation.items.find((item) => item.id === itemId); - if (!itemToCopy) { - throw new Error('Cannot find item for id ' + itemId); - } - return { - ...prevInvestigation, - items: prevInvestigation.items.concat({ - ...itemToCopy, - id: v4(), - }), - }; - }); - }, - deleteItem: (itemId) => { - return updateInvestigationInPlace((prevInvestigation) => { - return { - ...prevInvestigation, - items: prevInvestigation.items.filter((item) => item.id !== itemId), - }; - }); - }, - asObservable: () => asObservable, - destroy: () => { - return controller.abort(); - }, - setGlobalParameters: async (parameters) => { - await updateInvestigationInPlace((prevInvestigation) => { - return { - ...prevInvestigation, - items: prevInvestigation.items.map((item) => { - return { ...item, loading: true }; - }), - }; - }); - - await updateInvestigationInPlace(async (prevInvestigation) => { - return { - ...prevInvestigation, - parameters, - items: await Promise.all( - prevInvestigation.items.map(async (item) => { - return { - ...(await regenerateItem({ - widget: item, - globalWidgetParameters: parameters, - signal: controller.signal, - user, - widgetDefinitions, - })), - loading: false, - }; - }) - ), - }; - }); - }, - setTitle: async (title: string) => { - return updateInvestigationInPlace((prevInvestigation) => { - return { ...prevInvestigation, title }; - }); - }, - }; -} diff --git a/x-pack/plugins/observability_solution/investigate/public/hooks/use_investigation/regenerate_item.ts b/x-pack/plugins/observability_solution/investigate/public/hooks/use_investigation/regenerate_item.ts deleted file mode 100644 index 7f7d6208ed9eb..0000000000000 --- a/x-pack/plugins/observability_solution/investigate/public/hooks/use_investigation/regenerate_item.ts +++ /dev/null @@ -1,52 +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 { AuthenticatedUser } from '@kbn/core-security-common'; -import { v4 } from 'uuid'; -import { InvestigateWidget, InvestigateWidgetCreate, mergePlainObjects } from '../../../common'; -import { GlobalWidgetParameters } from '../../../common/types'; -import { WidgetDefinition } from '../../types'; - -export async function regenerateItem({ - user, - widgetDefinitions, - signal, - widget, - globalWidgetParameters, -}: { - user: AuthenticatedUser; - widgetDefinitions: WidgetDefinition[]; - widget: InvestigateWidgetCreate | InvestigateWidget; - signal: AbortSignal; - globalWidgetParameters: GlobalWidgetParameters; -}): Promise { - const now = Date.now(); - - const definition = widgetDefinitions.find( - (currentDefinition) => currentDefinition.type === widget.type - ); - - if (!definition) { - throw new Error(`Definition for widget ${widget.type} not found`); - } - - const nextParameters = mergePlainObjects(widget.parameters, globalWidgetParameters); - - const widgetData = await definition.generate({ - parameters: nextParameters, - signal, - }); - - return { - createdAt: now, - id: v4(), - ...widget, - parameters: nextParameters, - data: widgetData, - createdBy: user.username, - }; -} diff --git a/x-pack/plugins/observability_solution/investigate/public/index.ts b/x-pack/plugins/observability_solution/investigate/public/index.ts index 2f55a27ef1c27..07493d8360552 100644 --- a/x-pack/plugins/observability_solution/investigate/public/index.ts +++ b/x-pack/plugins/observability_solution/investigate/public/index.ts @@ -13,27 +13,16 @@ import type { InvestigateSetupDependencies, InvestigateStartDependencies, ConfigSchema, - OnWidgetAdd, } from './types'; -export type { InvestigatePublicSetup, InvestigatePublicStart, OnWidgetAdd }; +export type { InvestigatePublicSetup, InvestigatePublicStart }; -export { - type Investigation, - type InvestigateWidget, - type InvestigateWidgetCreate, - type GlobalWidgetParameters, -} from '../common/types'; +export { type GlobalWidgetParameters } from '../common/types'; export { mergePlainObjects } from '../common/utils/merge_plain_objects'; -export { createWidgetFactory } from './create_widget'; export { getEsFilterFromGlobalParameters } from './util/get_es_filters_from_global_parameters'; -export { ESQL_WIDGET_NAME } from './esql_widget/constants'; -export { createEsqlWidget } from './esql_widget/create_esql_widget'; -export type { EsqlWidgetParameters } from './esql_widget/types'; - export const plugin: PluginInitializer< InvestigatePublicSetup, InvestigatePublicStart, diff --git a/x-pack/plugins/observability_solution/investigate/public/investigation/item_definition_registry.ts b/x-pack/plugins/observability_solution/investigate/public/investigation/item_definition_registry.ts new file mode 100644 index 0000000000000..6fcb8308e7161 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate/public/investigation/item_definition_registry.ts @@ -0,0 +1,45 @@ +/* + * 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 { GlobalWidgetParameters } from '../../common/types'; + +export type ItemDefinitionData = Record; +export type ItemDefinitionParams = Record; + +export interface ItemDefinition< + Params extends ItemDefinitionParams = {}, + Data extends ItemDefinitionData = {} +> { + type: string; + generate: (option: { itemParams: Params; globalParams: GlobalWidgetParameters }) => Promise; + render: (option: { + data: Data; + itemParams: Params; + globalParams: GlobalWidgetParameters; + }) => React.ReactNode; +} + +export class ItemDefinitionRegistry { + private readonly definitions: ItemDefinition[] = []; + + constructor() {} + + public registerItem( + definition: ItemDefinition + ) { + // @ts-ignore TODO fix this type issue with generics + this.definitions.push(definition); + } + + public getItemDefinitions(): ItemDefinition[] { + return this.definitions; + } + + public getItemDefinitionByType(type: string): ItemDefinition | undefined { + return this.definitions.find((definition) => definition.type === type); + } +} diff --git a/x-pack/plugins/observability_solution/investigate/public/plugin.tsx b/x-pack/plugins/observability_solution/investigate/public/plugin.tsx index 887753446c4a9..9429383d851a8 100644 --- a/x-pack/plugins/observability_solution/investigate/public/plugin.tsx +++ b/x-pack/plugins/observability_solution/investigate/public/plugin.tsx @@ -4,17 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { - AuthenticatedUser, - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext, -} from '@kbn/core/public'; -import { GetInvestigationResponse } from '@kbn/investigation-shared'; -import type { Logger } from '@kbn/logging'; -import { useMemo } from 'react'; -import { createUseInvestigation } from './hooks/use_investigation'; +import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; +import { + ItemDefinition, + ItemDefinitionData, + ItemDefinitionParams, + ItemDefinitionRegistry, +} from './investigation/item_definition_registry'; import type { ConfigSchema, InvestigatePublicSetup, @@ -22,7 +18,6 @@ import type { InvestigateSetupDependencies, InvestigateStartDependencies, } from './types'; -import { WidgetRegistry } from './widget_registry'; export class InvestigatePlugin implements @@ -33,57 +28,28 @@ export class InvestigatePlugin InvestigateStartDependencies > { - logger: Logger; - - widgetRegistry: WidgetRegistry = new WidgetRegistry(); + private itemDefinitionRegistry: ItemDefinitionRegistry = new ItemDefinitionRegistry(); - registrationPromises: Array> = []; + constructor(context: PluginInitializerContext) {} - constructor(context: PluginInitializerContext) { - this.logger = context.logger.get(); - } setup(coreSetup: CoreSetup, pluginsSetup: InvestigateSetupDependencies): InvestigatePublicSetup { return { - register: (callback) => { - const registrationPromise = Promise.race([ - callback(this.widgetRegistry.registerWidget), - new Promise((resolve, reject) => { - setTimeout(() => { - reject(new Error('Timed out running registration function')); - }, 30000); - }), - ]).catch((error) => { - this.logger.error( - new Error('Encountered an error during widget registration', { cause: error }) - ); - return Promise.resolve(); - }); - - this.registrationPromises.push(registrationPromise); + registerItemDefinition: < + Params extends ItemDefinitionParams, + Data extends ItemDefinitionData + >( + definition: ItemDefinition + ) => { + this.itemDefinitionRegistry.registerItem(definition); }, }; } start(coreStart: CoreStart, pluginsStart: InvestigateStartDependencies): InvestigatePublicStart { return { - getWidgetDefinitions: this.widgetRegistry.getWidgetDefinitions, - useInvestigation: ({ - user, - investigationData, - }: { - user: AuthenticatedUser; - investigationData?: GetInvestigationResponse; - }) => { - const widgetDefinitions = useMemo(() => this.widgetRegistry.getWidgetDefinitions(), []); - - return createUseInvestigation({ - notifications: coreStart.notifications, - widgetDefinitions, - })({ - user, - investigationData, - }); - }, + getItemDefinitions: () => this.itemDefinitionRegistry.getItemDefinitions(), + getItemDefinitionByType: (type: string) => + this.itemDefinitionRegistry.getItemDefinitionByType(type), }; } } diff --git a/x-pack/plugins/observability_solution/investigate/public/types.ts b/x-pack/plugins/observability_solution/investigate/public/types.ts index dc6eb8b62021d..36a87759c2711 100644 --- a/x-pack/plugins/observability_solution/investigate/public/types.ts +++ b/x-pack/plugins/observability_solution/investigate/public/types.ts @@ -6,54 +6,11 @@ */ /* eslint-disable @typescript-eslint/no-empty-interface*/ -import type { AuthenticatedUser } from '@kbn/core/public'; -import type { CompatibleJSONSchema } from '@kbn/observability-ai-assistant-plugin/public'; -import type { GetInvestigationResponse } from '@kbn/investigation-shared'; -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'; - -export type OnWidgetAdd = (create: InvestigateWidgetCreate) => Promise; - -interface WidgetRenderOptions { - widget: TInvestigateWidget; -} - -export interface WidgetDefinition { - type: string; - description: string; - schema: CompatibleJSONSchema; - generate: (options: { - parameters: GlobalWidgetParameters; - signal: AbortSignal; - }) => Promise>; - render: (options: WidgetRenderOptions) => React.ReactNode; -} - -type RegisterWidgetOptions = Omit; - -type MaybeSchemaFrom = - {} & (TSchema extends CompatibleJSONSchema ? FromSchema : {}); - -type GenerateCallback< - TSchema extends CompatibleJSONSchema | undefined, - TData extends Record | undefined -> = (options: { - parameters: MaybeSchemaFrom & GlobalWidgetParameters; - signal: AbortSignal; -}) => Promise; - -export type RegisterWidget = < - TSchema extends CompatibleJSONSchema, - TData extends Record ->( - definition: Omit & { schema: TSchema }, - generateCallback: GenerateCallback, - renderCallback: ( - options: WidgetRenderOptions, TData>> - ) => React.ReactNode -) => void; +import { + ItemDefinition, + ItemDefinitionData, + ItemDefinitionParams, +} from './investigation/item_definition_registry'; export interface ConfigSchema {} @@ -62,13 +19,15 @@ export interface InvestigateSetupDependencies {} export interface InvestigateStartDependencies {} export interface InvestigatePublicSetup { - register: (callback: (registerWidget: RegisterWidget) => Promise) => void; + registerItemDefinition: < + Params extends ItemDefinitionParams = {}, + Data extends ItemDefinitionData = {} + >( + itemDefinition: ItemDefinition + ) => void; } export interface InvestigatePublicStart { - getWidgetDefinitions: () => WidgetDefinition[]; - useInvestigation: ({}: { - user: AuthenticatedUser; - investigationData?: GetInvestigationResponse; - }) => UseInvestigationApi; + getItemDefinitions: () => ItemDefinition[]; + getItemDefinitionByType: (type: string) => ItemDefinition | undefined; } diff --git a/x-pack/plugins/observability_solution/investigate/public/widget_registry.ts b/x-pack/plugins/observability_solution/investigate/public/widget_registry.ts deleted file mode 100644 index f76adf5a72506..0000000000000 --- a/x-pack/plugins/observability_solution/investigate/public/widget_registry.ts +++ /dev/null @@ -1,26 +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 { RegisterWidget, WidgetDefinition } from './types'; - -export class WidgetRegistry { - private readonly definitions: WidgetDefinition[] = []; - - constructor() {} - - registerWidget: RegisterWidget = (definition, generateCallback, renderCallback) => { - this.definitions.push({ - ...definition, - generate: generateCallback as WidgetDefinition['generate'], - render: renderCallback as WidgetDefinition['render'], - }); - }; - - getWidgetDefinitions = (): WidgetDefinition[] => { - return this.definitions; - }; -} diff --git a/x-pack/plugins/observability_solution/investigate/tsconfig.json b/x-pack/plugins/observability_solution/investigate/tsconfig.json index d48acf4a215ad..e2e39f527c2e1 100644 --- a/x-pack/plugins/observability_solution/investigate/tsconfig.json +++ b/x-pack/plugins/observability_solution/investigate/tsconfig.json @@ -14,14 +14,7 @@ "@kbn/core", "@kbn/logging", "@kbn/config-schema", - "@kbn/observability-ai-assistant-plugin", "@kbn/es-query", - "@kbn/interpreter", - "@kbn/security-plugin", - "@kbn/i18n", - "@kbn/utility-types", - "@kbn/core-security-common", - "@kbn/investigation-shared", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc b/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc index 5b31cbc3973a2..c7e860a047366 100644 --- a/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc +++ b/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc @@ -21,9 +21,9 @@ "security", ], "requiredBundles": [ + "esql", "kibanaReact", "kibanaUtils", - "esql", "esqlDataGrid", ], "optionalPlugins": [], diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.stories.tsx deleted file mode 100644 index 072d25b1e5526..0000000000000 --- a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.stories.tsx +++ /dev/null @@ -1,112 +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 { ComponentMeta, ComponentStoryObj } from '@storybook/react'; -import React, { useState } from 'react'; -import { v4 } from 'uuid'; -import { InvestigateWidgetGrid as Component, InvestigateWidgetGridItem } from '.'; -import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; - -const meta: ComponentMeta = { - component: Component, - title: 'app/Organisms/InvestigateWidgetGrid', - decorators: [KibanaReactStorybookDecorator], -}; - -export default meta; - -function WithPersistedChanges(props: React.ComponentProps) { - const [items, setItems] = useState(props.items); - - return ( - { - setItems((prevItems) => - prevItems.concat({ - ...item, - id: v4(), - }) - ); - }} - onItemDelete={async (item) => { - setItems((prevItems) => prevItems.filter((currentItem) => currentItem.id !== item.id)); - }} - items={items} - /> - ); -} - -const defaultProps: ComponentStoryObj = { - args: {}, - render: (props) => ( -
- -
- ), -}; - -function createItem>(overrides: T) { - return { - ...overrides, - id: v4(), - columns: 4, - rows: 2, - description: '', - loading: false, - overrides: [], - }; -} - -export const InvestigateWidgetGridStory: ComponentStoryObj = { - ...defaultProps, - args: { - ...defaultProps.args, - items: [ - createItem({ - title: '1', - element: ( -
- This should not overflow -
- ), - columns: 4, - rows: 12, - }), - - createItem({ - title: '2', - element: <>TODO, - columns: 2, - rows: 3, - overrides: [ - { - id: v4(), - label: '4 hours earlier', - }, - { - id: v4(), - label: 'service.name:opbeans-java AND service.enviroment:(production OR development)', - }, - ], - }), - createItem({ - title: '3', - element: <>TODO, - columns: 2, - rows: 3, - }), - createItem({ - title: '4', - element: <>TODO, - columns: 4, - rows: 3, - }), - ], - }, - name: 'default', -}; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/styles.scss b/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/styles.scss deleted file mode 100644 index 5528e053f84aa..0000000000000 --- a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/styles.scss +++ /dev/null @@ -1 +0,0 @@ -@import '../../../../../../../src/plugins/dashboard/public/dashboard_container/dashboard_container'; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts index d2f6bc9060e43..62078d7d55bca 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts @@ -13,6 +13,9 @@ export const investigationKeys = { notes: ['investigation', 'notes'] as const, fetchNotes: (params: { investigationId: string }) => [...investigationKeys.notes, 'fetch', params] as const, + items: ['investigation', 'items'] as const, + fetchItems: (params: { investigationId: string }) => + [...investigationKeys.items, 'fetch', params] as const, }; export type InvestigationKeys = typeof investigationKeys; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_add_investigation_item.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_add_investigation_item.ts new file mode 100644 index 0000000000000..a11b5d976fe00 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_add_investigation_item.ts @@ -0,0 +1,49 @@ +/* + * 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 { + CreateInvestigationItemParams, + CreateInvestigationItemResponse, +} from '@kbn/investigation-shared'; +import { useMutation } from '@tanstack/react-query'; +import { useKibana } from './use_kibana'; + +type ServerError = IHttpFetchError; + +export function useAddInvestigationItem() { + const { + core: { + http, + notifications: { toasts }, + }, + } = useKibana(); + + return useMutation< + CreateInvestigationItemResponse, + ServerError, + { investigationId: string; item: CreateInvestigationItemParams }, + { investigationId: string } + >( + ['addInvestigationItem'], + ({ investigationId, item }) => { + const body = JSON.stringify(item); + return http.post( + `/api/observability/investigations/${investigationId}/items`, + { body, version: '2023-10-31' } + ); + }, + { + onSuccess: (response, {}) => { + toasts.addSuccess('Item saved'); + }, + onError: (error, {}, context) => { + toasts.addError(new Error(error.body?.message ?? 'An error occurred'), { title: 'Error' }); + }, + } + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_delete_investigation_item.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_delete_investigation_item.ts new file mode 100644 index 0000000000000..41c19013e6b2d --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_delete_investigation_item.ts @@ -0,0 +1,44 @@ +/* + * 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 { useKibana } from './use_kibana'; + +type ServerError = IHttpFetchError; + +export function useDeleteInvestigationItem() { + const { + core: { + http, + notifications: { toasts }, + }, + } = useKibana(); + + return useMutation< + void, + ServerError, + { investigationId: string; itemId: string }, + { investigationId: string } + >( + ['deleteInvestigationItem'], + ({ investigationId, itemId }) => { + return http.delete( + `/api/observability/investigations/${investigationId}/items/${itemId}`, + { version: '2023-10-31' } + ); + }, + { + onSuccess: (response, {}) => { + toasts.addSuccess('Item deleted'); + }, + onError: (error, {}, context) => { + toasts.addError(new Error(error.body?.message ?? 'An error occurred'), { title: 'Error' }); + }, + } + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_delete_investigation_note.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_delete_investigation_note.ts index 136387372c581..aed3cc571ec92 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_delete_investigation_note.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_delete_investigation_note.ts @@ -25,7 +25,7 @@ export function useDeleteInvestigationNote() { { investigationId: string; noteId: string }, { investigationId: string } >( - ['addInvestigationNote'], + ['deleteInvestigationNote'], ({ investigationId, noteId }) => { return http.delete( `/api/observability/investigations/${investigationId}/notes/${noteId}`, diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_investigation_items.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_investigation_items.ts new file mode 100644 index 0000000000000..5cc253fc6c44d --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_investigation_items.ts @@ -0,0 +1,76 @@ +/* + * 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 { + GetInvestigationItemsResponse, + InvestigationItemResponse, +} from '@kbn/investigation-shared'; +import { + QueryObserverResult, + RefetchOptions, + RefetchQueryFilters, + useQuery, +} from '@tanstack/react-query'; +import { investigationKeys } from './query_key_factory'; +import { useKibana } from './use_kibana'; + +export interface Params { + investigationId: string; + initialItems?: InvestigationItemResponse[]; +} + +export interface Response { + isInitialLoading: boolean; + isLoading: boolean; + isRefetching: boolean; + isSuccess: boolean; + isError: boolean; + refetch: ( + options?: (RefetchOptions & RefetchQueryFilters) | undefined + ) => Promise>; + data: GetInvestigationItemsResponse | undefined; +} + +export function useFetchInvestigationItems({ investigationId, initialItems }: Params): Response { + const { + core: { + http, + notifications: { toasts }, + }, + } = useKibana(); + + const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery( + { + queryKey: investigationKeys.fetchItems({ investigationId }), + queryFn: async ({ signal }) => { + return await http.get( + `/api/observability/investigations/${investigationId}/items`, + { version: '2023-10-31', signal } + ); + }, + initialData: initialItems, + refetchOnWindowFocus: false, + refetchInterval: 10 * 1000, + refetchIntervalInBackground: true, + onError: (error: Error) => { + toasts.addError(error, { + title: 'Something went wrong while fetching investigation items', + }); + }, + } + ); + + return { + data, + isInitialLoading, + isLoading, + isRefetching, + isSuccess, + isError, + refetch, + }; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/items/README.md b/x-pack/plugins/observability_solution/investigate_app/public/items/README.md new file mode 100644 index 0000000000000..710c9fd3624ec --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/items/README.md @@ -0,0 +1 @@ +This folder replaces widgets/ diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/register_embeddable_widget.tsx b/x-pack/plugins/observability_solution/investigate_app/public/items/embeddable_item/register_embeddable_item.tsx similarity index 79% rename from x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/register_embeddable_widget.tsx rename to x-pack/plugins/observability_solution/investigate_app/public/items/embeddable_item/register_embeddable_item.tsx index 779fb9c5301b9..e07c940c8ca5f 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/register_embeddable_widget.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/items/embeddable_item/register_embeddable_item.tsx @@ -6,16 +6,14 @@ */ import { EuiLoadingSpinner } from '@elastic/eui'; import { css } from '@emotion/css'; +import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public'; import type { GlobalWidgetParameters } from '@kbn/investigate-plugin/public'; import { useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { v4 } from 'uuid'; -import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public'; -import { EMBEDDABLE_WIDGET_NAME } from '../../constants'; -import { useKibana } from '../../hooks/use_kibana'; -import { RegisterWidgetOptions } from '../register_widgets'; -import { EmbeddableWidgetParameters } from './types'; import { ErrorMessage } from '../../components/error_message'; +import { useKibana } from '../../hooks/use_kibana'; +import { Options } from '../register_items'; const embeddableClassName = css` height: 100%; @@ -24,7 +22,7 @@ const embeddableClassName = css` } `; -type Props = EmbeddableWidgetParameters & GlobalWidgetParameters; +type Props = EmbeddableItemParams & GlobalWidgetParameters; type ParentApi = ReturnType['getParentApi']>; @@ -155,40 +153,38 @@ function EmbeddableWidget(props: Props) { return ; } -export function registerEmbeddableWidget({ registerWidget }: RegisterWidgetOptions) { - registerWidget( - { - type: EMBEDDABLE_WIDGET_NAME, - description: 'Display a saved embeddable', - schema: { - type: 'object', - properties: { - type: { - type: 'string', - }, - config: { - type: 'object', - }, - savedObjectId: { - type: 'string', - }, - }, - required: ['type', 'config'], - } as const, - }, - async ({ parameters, signal }) => { +interface EmbeddableItemParams { + type: string; + config: Record; + savedObjectId?: string; +} + +export function registerEmbeddableItem({ + dependencies: { + setup: { investigate }, + }, + services, +}: Options) { + investigate.registerItemDefinition({ + type: 'esql', + generate: async (option: { + itemParams: EmbeddableItemParams; + globalParams: GlobalWidgetParameters; + }) => { return {}; }, - ({ widget }) => { + render: (option: { + itemParams: EmbeddableItemParams; + globalParams: GlobalWidgetParameters; + }) => { const parameters = { - type: widget.parameters.type, - config: widget.parameters.config, - savedObjectId: widget.parameters.savedObjectId, - timeRange: widget.parameters.timeRange, - query: widget.parameters.query, + type: option.itemParams.type, + config: option.itemParams.config, + savedObjectId: option.itemParams.savedObjectId, + timeRange: option.globalParams.timeRange, }; return ; - } - ); + }, + }); } diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/get_date_histogram_results.ts b/x-pack/plugins/observability_solution/investigate_app/public/items/esql_item/get_date_histogram_results.ts similarity index 100% rename from x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/get_date_histogram_results.ts rename to x-pack/plugins/observability_solution/investigate_app/public/items/esql_item/get_date_histogram_results.ts diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/register_esql_widget.tsx b/x-pack/plugins/observability_solution/investigate_app/public/items/esql_item/register_esql_item.tsx similarity index 80% rename from x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/register_esql_widget.tsx rename to x-pack/plugins/observability_solution/investigate_app/public/items/esql_item/register_esql_item.tsx index 84818c758ffe1..1b62b5476f021 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/widgets/esql_widget/register_esql_widget.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/items/esql_item/register_esql_item.tsx @@ -10,20 +10,16 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import type { ESQLSearchResponse } from '@kbn/es-types'; import { ESQLDataGrid } from '@kbn/esql-datagrid/public'; import { i18n } from '@kbn/i18n'; -import { - type EsqlWidgetParameters, - type GlobalWidgetParameters, -} from '@kbn/investigate-plugin/public'; +import { type GlobalWidgetParameters } from '@kbn/investigate-plugin/public'; import type { Suggestion } from '@kbn/lens-plugin/public'; import { useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public'; import React, { useMemo } from 'react'; import { ErrorMessage } from '../../components/error_message'; -import { ESQL_WIDGET_NAME } from '../../constants'; import { useKibana } from '../../hooks/use_kibana'; import { getDatatableFromEsqlResponse } from '../../utils/get_data_table_from_esql_response'; import { getEsFilterFromOverrides } from '../../utils/get_es_filter_from_overrides'; import { getLensAttrsForSuggestion } from '../../utils/get_lens_attrs_for_suggestion'; -import type { RegisterWidgetOptions } from '../register_widgets'; +import type { Options } from '../register_items'; import { getDateHistogramResults } from './get_date_histogram_results'; const lensClassName = css` @@ -45,6 +41,24 @@ interface Props { }; } +interface EsqlItemParams { + esql: string; + suggestion?: Suggestion; +} + +interface EsqlItemData { + dataView: DataView; + columns: ESQLSearchResponse['columns']; + values: ESQLSearchResponse['values']; + suggestion: Suggestion; + dateHistoResponse?: { + query: string; + columns: ESQLSearchResponse['columns']; + values: ESQLSearchResponse['values']; + groupingExpression: string; + }; +} + export function EsqlWidget({ suggestion, dataView, @@ -207,34 +221,21 @@ export function EsqlWidget({ ); } -export function registerEsqlWidget({ +export function registerEsqlItem({ dependencies: { setup: { investigate }, }, services, - registerWidget, -}: RegisterWidgetOptions) { - registerWidget( - { - type: ESQL_WIDGET_NAME, - description: 'Visualize an ES|QL query', - schema: { - type: 'object', - properties: { - esql: { - description: 'The ES|QL query', - type: 'string', - }, - }, - required: ['esql'], - } as const, - }, - async ({ parameters, signal }) => { - const { - esql: esqlQuery, - timeRange, - suggestion: suggestionFromParameters, - } = parameters as EsqlWidgetParameters & GlobalWidgetParameters; +}: Options) { + investigate.registerItemDefinition({ + type: 'esql', + generate: async (option: { + itemParams: EsqlItemParams; + globalParams: GlobalWidgetParameters; + }) => { + const controller = new AbortController(); + const { esql: esqlQuery, suggestion: suggestionFromParameters } = option.itemParams; + const { timeRange } = option.globalParams; const esql = await services.esql; @@ -252,7 +253,7 @@ export function registerEsqlWidget({ const mainResponse = await esql.queryWithMeta({ query: esqlQuery, - signal, + signal: controller.signal, filter: getFilter(), }); @@ -263,37 +264,36 @@ export function registerEsqlWidget({ columns: mainResponse.query.columns, esql, filter: getFilter(), - signal, + signal: controller.signal, suggestion, timeRange, }); return { - main: { - columns: mainResponse.query.columns, - values: mainResponse.query.values, - suggestion, - dataView: mainResponse.meta.dataView, - }, - dateHistogram: dateHistoResponse, + dataView: mainResponse.meta.dataView, + columns: mainResponse.query.columns, + values: mainResponse.query.values, + suggestion, + dateHistoResponse, }; }, - ({ widget }) => { - const { - main: { dataView, columns, values, suggestion }, - dateHistogram, - } = widget.data; + render: (option: { + itemParams: EsqlItemParams; + globalParams: GlobalWidgetParameters; + data: EsqlItemData; + }) => { + const { itemParams, data } = option; return ( ); - } - ); + }, + }); } diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/register_widgets.ts b/x-pack/plugins/observability_solution/investigate_app/public/items/register_items.ts similarity index 55% rename from x-pack/plugins/observability_solution/investigate_app/public/widgets/register_widgets.ts rename to x-pack/plugins/observability_solution/investigate_app/public/items/register_items.ts index fd2e2b9728176..ff0304105a0b0 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/widgets/register_widgets.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/items/register_items.ts @@ -5,22 +5,20 @@ * 2.0. */ -import type { RegisterWidget } from '@kbn/investigate-plugin/public/types'; import type { InvestigateAppServices } from '../services/types'; import type { InvestigateAppSetupDependencies, InvestigateAppStartDependencies } from '../types'; -import { registerEmbeddableWidget } from './embeddable_widget/register_embeddable_widget'; -import { registerEsqlWidget } from './esql_widget/register_esql_widget'; +import { registerEmbeddableItem } from './embeddable_item/register_embeddable_item'; +import { registerEsqlItem } from './esql_item/register_esql_item'; -export interface RegisterWidgetOptions { +export interface Options { dependencies: { setup: InvestigateAppSetupDependencies; start: InvestigateAppStartDependencies; }; services: InvestigateAppServices; - registerWidget: RegisterWidget; } -export function registerWidgets(options: RegisterWidgetOptions) { - registerEsqlWidget(options); - registerEmbeddableWidget(options); +export function registerItems(options: Options) { + registerEsqlItem(options); + registerEmbeddableItem(options); } diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/add_observation_ui/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/add_investigation_item/add_investigation_item.tsx similarity index 93% rename from x-pack/plugins/observability_solution/investigate_app/public/components/add_observation_ui/index.tsx rename to x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/add_investigation_item/add_investigation_item.tsx index 69f43ef515146..e4dcd2fe2000d 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/components/add_observation_ui/index.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/add_investigation_item/add_investigation_item.tsx @@ -9,19 +9,20 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiTitle } fro import { css } from '@emotion/css'; import { TextBasedLangEditor } from '@kbn/esql/public'; import { i18n } from '@kbn/i18n'; -import { GlobalWidgetParameters, OnWidgetAdd } from '@kbn/investigate-plugin/public'; +import { GlobalWidgetParameters } from '@kbn/investigate-plugin/public'; +import { Item } from '@kbn/investigation-shared'; import React from 'react'; import { EsqlWidgetPreview } from './esql_widget_preview'; type Props = { - onWidgetAdd: OnWidgetAdd; + onItemAdd: (item: Item) => void; } & GlobalWidgetParameters; const emptyPreview = css` padding: 36px 0px 36px 0px; `; -export function AddObservationUI({ onWidgetAdd, timeRange }: Props) { +export function AddInvestigationItem({ onItemAdd: onItemAdd, timeRange }: Props) { const [isOpen, setIsOpen] = React.useState(false); const [query, setQuery] = React.useState({ esql: '' }); @@ -113,9 +114,9 @@ export function AddObservationUI({ onWidgetAdd, timeRange }: Props) { { + onItemAdd={(item) => { resetState(); - return onWidgetAdd(widget); + return onItemAdd(item); }} /> )} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/add_observation_ui/esql_widget_preview.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/add_investigation_item/esql_widget_preview.tsx similarity index 85% rename from x-pack/plugins/observability_solution/investigate_app/public/components/add_observation_ui/esql_widget_preview.tsx rename to x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/add_investigation_item/esql_widget_preview.tsx index 2d1e1f1506797..c865dfcf91826 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/components/add_observation_ui/esql_widget_preview.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/add_investigation_item/esql_widget_preview.tsx @@ -8,38 +8,33 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import { css } from '@emotion/css'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { ESQLColumn, ESQLRow } from '@kbn/es-types'; -import { - ESQL_WIDGET_NAME, - GlobalWidgetParameters, - InvestigateWidgetCreate, - OnWidgetAdd, - createEsqlWidget, -} from '@kbn/investigate-plugin/public'; +import { GlobalWidgetParameters } from '@kbn/investigate-plugin/public'; +import { Item } from '@kbn/investigation-shared'; import type { Suggestion } from '@kbn/lens-plugin/public'; import { useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public'; import React, { useEffect, useMemo, useState } from 'react'; -import { useKibana } from '../../hooks/use_kibana'; -import { getEsFilterFromOverrides } from '../../utils/get_es_filter_from_overrides'; -import { getDateHistogramResults } from '../../widgets/esql_widget/get_date_histogram_results'; -import { EsqlWidget } from '../../widgets/esql_widget/register_esql_widget'; -import { ErrorMessage } from '../error_message'; -import { SuggestVisualizationList } from '../suggest_visualization_list'; +import { ErrorMessage } from '../../../../components/error_message'; +import { SuggestVisualizationList } from '../../../../components/suggest_visualization_list'; +import { useKibana } from '../../../../hooks/use_kibana'; +import { getDateHistogramResults } from '../../../../items/esql_item/get_date_histogram_results'; +import { EsqlWidget } from '../../../../items/esql_item/register_esql_item'; +import { getEsFilterFromOverrides } from '../../../../utils/get_es_filter_from_overrides'; -function getWidgetFromSuggestion({ +function getItemFromSuggestion({ query, suggestion, }: { query: string; suggestion: Suggestion; -}): InvestigateWidgetCreate { - return createEsqlWidget({ +}): Item { + return { title: suggestion.title, - type: ESQL_WIDGET_NAME, - parameters: { + type: 'esql', + params: { esql: query, suggestion, }, - }); + }; } function PreviewContainer({ children }: { children: React.ReactNode }) { @@ -64,11 +59,11 @@ function PreviewContainer({ children }: { children: React.ReactNode }) { export function EsqlWidgetPreview({ esqlQuery, - onWidgetAdd, + onItemAdd, timeRange, }: { esqlQuery: string; - onWidgetAdd: OnWidgetAdd; + onItemAdd: (item: Item) => void; } & GlobalWidgetParameters) { const { services: { esql }, @@ -199,7 +194,7 @@ export function EsqlWidgetPreview({ { - onWidgetAdd(getWidgetFromSuggestion({ query: esqlQuery, suggestion })); + onItemAdd(getItemFromSuggestion({ query: esqlQuery, suggestion })); }} loading={queryResult.loading} onMouseLeave={() => {}} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/grid_item/index.stories.tsx similarity index 90% rename from x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.stories.tsx rename to x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/grid_item/index.stories.tsx index 6111d0181ccbb..54b13f1eea29d 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.stories.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/grid_item/index.stories.tsx @@ -9,8 +9,8 @@ import { Meta, StoryObj } from '@storybook/react'; import React from 'react'; import { v4 } from 'uuid'; import { GridItem as Component } from '.'; -import { extendProps } from '../../../.storybook/extend_props'; -import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; +import { extendProps } from '../../../../../.storybook/extend_props'; +import { KibanaReactStorybookDecorator } from '../../../../../.storybook/storybook_decorator'; type Props = React.ComponentProps; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/grid_item/index.tsx similarity index 95% rename from x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.tsx rename to x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/grid_item/index.tsx index 465f6f803edd9..91f7a58b43b5e 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/components/grid_item/index.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/grid_item/index.tsx @@ -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 '../investigate_text_button'; +import { useTheme } from '../../../../hooks/use_theme'; +import { InvestigateTextButton } from '../../../../components/investigate_text_button'; export const GRID_ITEM_HEADER_HEIGHT = 40; diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_details/index.stories.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_details/index.stories.tsx index 8dfb18a753ac2..83b61c331843f 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_details/index.stories.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_details/index.stories.tsx @@ -5,10 +5,11 @@ * 2.0. */ +import { mockAuthenticatedUser } from '@kbn/core-security-common/mocks'; import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; import React from 'react'; -import { InvestigationDetails as Component } from '.'; import { KibanaReactStorybookDecorator } from '../../../../../.storybook/storybook_decorator'; +import { InvestigationDetails as Component } from './investigation_details'; const meta: ComponentMeta = { component: Component, @@ -20,7 +21,7 @@ export default meta; const defaultProps: ComponentStoryObj = { args: {}, - render: (props) => , + render: (props) => , }; export const InvestigateViewStory: ComponentStoryObj = { diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_details/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_details/index.tsx deleted file mode 100644 index c1c77ef564545..0000000000000 --- a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_details/index.tsx +++ /dev/null @@ -1,121 +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 datemath from '@elastic/datemath'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import { AuthenticatedUser } from '@kbn/security-plugin/common'; -import React 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 { useFetchInvestigation } from '../../../../hooks/use_fetch_investigation'; -import { useKibana } from '../../../../hooks/use_kibana'; -import { InvestigationNotes } from '../investigation_notes/investigation_notes'; - -function InvestigationDetailsWithUser({ - user, - investigationId, -}: { - user: AuthenticatedUser; - investigationId: string; -}) { - const { - dependencies: { - start: { investigate }, - }, - } = useKibana(); - // const widgetDefinitions = investigate.getWidgetDefinitions(); - const { data: investigationData } = useFetchInvestigation({ id: investigationId }); - - const { - addItem, - copyItem, - deleteItem, - investigation, - setGlobalParameters, - renderableInvestigation, - } = investigate.useInvestigation({ - user, - investigationData, - }); - - if (!investigation || !renderableInvestigation || !investigationData) { - return ; - } - - return ( - - - - - - { - const nextDateRange = { - from: datemath.parse(dateRange.from)!.toISOString(), - to: datemath.parse(dateRange.to)!.toISOString(), - }; - await setGlobalParameters({ - ...renderableInvestigation.parameters, - timeRange: nextDateRange, - }); - }} - /> - - - - { - return copyItem(copiedItem.id); - }} - onItemDelete={async (deletedItem) => { - return deleteItem(deletedItem.id); - }} - /> - - - - { - return addItem(widget); - }} - /> - - - - - - - - ); -} - -export function InvestigationDetails({ investigationId }: { investigationId: string }) { - const { - core: { security }, - } = useKibana(); - - const user = useAsync(() => { - return security.authc.getCurrentUser(); - }, [security]); - - return user.value ? ( - - ) : null; -} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_details/investigation_details.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_details/investigation_details.tsx new file mode 100644 index 0000000000000..5f6e1ca4515d3 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_details/investigation_details.tsx @@ -0,0 +1,38 @@ +/* + * 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, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { AuthenticatedUser } from '@kbn/security-plugin/common'; +import React from 'react'; +import { useFetchInvestigation } from '../../../../hooks/use_fetch_investigation'; +import { InvestigationItems } from '../investigation_items/investigation_items'; +import { InvestigationNotes } from '../investigation_notes/investigation_notes'; + +interface Props { + user: AuthenticatedUser; + investigationId: string; +} + +export function InvestigationDetails({ user, investigationId }: Props) { + const { data: investigation, isLoading } = useFetchInvestigation({ id: investigationId }); + + if (isLoading || !investigation) { + return ; + } + + return ( + + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_items/investigation_items.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_items/investigation_items.tsx new file mode 100644 index 0000000000000..dcc83eb968344 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_items/investigation_items.tsx @@ -0,0 +1,91 @@ +/* + * 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, EuiFlexItem } from '@elastic/eui'; +import { GetInvestigationResponse, Item } from '@kbn/investigation-shared'; +import { pick } from 'lodash'; +import React from 'react'; +import { useAddInvestigationItem } from '../../../../hooks/use_add_investigation_item'; +import { useDeleteInvestigationItem } from '../../../../hooks/use_delete_investigation_item'; +import { useFetchInvestigationItems } from '../../../../hooks/use_fetch_investigation_items'; +import { useRenderItems } from '../../hooks/use_render_items'; +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'; + +export interface Props { + investigationId: string; + investigation: GetInvestigationResponse; +} + +export function InvestigationItems({ investigationId, investigation }: Props) { + const { data: items, refetch } = useFetchInvestigationItems({ + investigationId, + initialItems: investigation.items, + }); + const renderableItems = useRenderItems({ items, params: investigation.params }); + + const { mutateAsync: addInvestigationItem, isLoading: isAdding } = useAddInvestigationItem(); + const { mutateAsync: deleteInvestigationItem, isLoading: isDeleting } = + useDeleteInvestigationItem(); + + const onAddItem = async (item: Item) => { + await addInvestigationItem({ investigationId, item }); + refetch(); + }; + + const onDeleteItem = async (itemId: string) => { + await deleteInvestigationItem({ investigationId, itemId }); + refetch(); + }; + + return ( + + + { + // const nextDateRange = { + // from: datemath.parse(dateRange.from)!.toISOString(), + // to: datemath.parse(dateRange.to)!.toISOString(), + // }; + // await setGlobalParameters({ + // ...renderableInvestigation.parameters, + // timeRange: nextDateRange, + // }); + }} + /> + + + { + await onAddItem(pick(copiedItem, ['title', 'type', 'params'])); + }} + onItemDelete={async (deletedItem) => { + await onDeleteItem(deletedItem.id); + }} + /> + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_items_list/investigation_items_list.tsx similarity index 75% rename from x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.tsx rename to x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_items_list/investigation_items_list.tsx index 41c3b3a3e8b66..8ec5bf3ffef98 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_widget_grid/index.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_items_list/investigation_items_list.tsx @@ -8,24 +8,19 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; +import { RenderedInvestigationItem } from '../../hooks/use_render_items'; import { GridItem } from '../grid_item'; -import './styles.scss'; - -export interface InvestigateWidgetGridItem { - id: string; - title: string; - element: React.ReactNode; - loading: boolean; -} interface InvestigateWidgetGridProps { - items: InvestigateWidgetGridItem[]; - onItemCopy: (item: InvestigateWidgetGridItem) => Promise; - onItemDelete: (item: InvestigateWidgetGridItem) => Promise; + items: RenderedInvestigationItem[]; + isLoading: boolean; + onItemCopy: (item: RenderedInvestigationItem) => Promise; + onItemDelete: (item: RenderedInvestigationItem) => Promise; } -export function InvestigateWidgetGrid({ +export function InvestigationItemsList({ items, + isLoading, onItemDelete, onItemCopy, }: InvestigateWidgetGridProps) { @@ -41,7 +36,7 @@ export function InvestigateWidgetGrid({ { return onItemCopy(item); }} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_notes/investigation_notes.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_notes/investigation_notes.tsx index 0f03a9f374ac3..8406ba8fe3f03 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_notes/investigation_notes.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_notes/investigation_notes.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { css } from '@emotion/css'; import { i18n } from '@kbn/i18n'; -import { InvestigationNote } from '@kbn/investigate-plugin/common'; +import { InvestigationNoteResponse, GetInvestigationResponse } from '@kbn/investigation-shared'; import React, { useState } from 'react'; import { useAddInvestigationNote } from '../../../../hooks/use_add_investigation_note'; import { useDeleteInvestigationNote } from '../../../../hooks/use_delete_investigation_note'; @@ -27,16 +27,16 @@ import { TimelineMessage } from './timeline_message'; export interface Props { investigationId: string; - initialNotes: InvestigationNote[]; + investigation: GetInvestigationResponse; } -export function InvestigationNotes({ investigationId, initialNotes }: Props) { +export function InvestigationNotes({ investigationId, investigation }: Props) { const theme = useTheme(); const [noteInput, setNoteInput] = useState(''); const { data: notes, refetch } = useFetchInvestigationNotes({ investigationId, - initialNotes, + initialNotes: investigation.notes, }); const { mutateAsync: addInvestigationNote, isLoading: isAdding } = useAddInvestigationNote(); const { mutateAsync: deleteInvestigationNote, isLoading: isDeleting } = @@ -70,7 +70,7 @@ export function InvestigationNotes({ investigationId, initialNotes }: Props) { - {notes?.map((currNote: InvestigationNote) => { + {notes?.map((currNote: InvestigationNoteResponse) => { return ( void; isDeleting: boolean; }) { diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_search_bar/index.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_search_bar/investigation_search_bar.tsx similarity index 93% rename from x-pack/plugins/observability_solution/investigate_app/public/components/investigate_search_bar/index.tsx rename to x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_search_bar/investigation_search_bar.tsx index 45519f2e799be..a6ad73bc67d0d 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/components/investigate_search_bar/index.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/investigation_search_bar/investigation_search_bar.tsx @@ -8,7 +8,7 @@ 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'; +import { useKibana } from '../../../../hooks/use_kibana'; const parentClassName = css` width: 100%; @@ -21,7 +21,7 @@ interface Props { onRefresh?: Required>['onRefresh']; } -export function InvestigateSearchBar({ +export function InvestigationSearchBar({ dateRangeFrom, dateRangeTo, onQuerySubmit, diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/hooks/use_render_items.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/hooks/use_render_items.tsx new file mode 100644 index 0000000000000..dc2c1c029bccd --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/hooks/use_render_items.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { GetInvestigationResponse, InvestigationItem } from '@kbn/investigation-shared'; +import React, { useEffect, useState } from 'react'; +import { useKibana } from '../../../hooks/use_kibana'; + +export type RenderedInvestigationItem = InvestigationItem & { + loading: boolean; + element: React.ReactNode; +}; + +export function useRenderItems({ + items, + params, +}: { + items?: InvestigationItem[]; + params: GetInvestigationResponse['params']; +}) { + const { + dependencies: { + start: { investigate }, + }, + } = useKibana(); + + const [renderableItems, setRenderableItems] = useState([]); + + useEffect(() => { + async function renderItems(currItems: InvestigationItem[]) { + return await Promise.all( + currItems.map(async (item) => { + const itemDefinition = investigate.getItemDefinitionByType(item.type); + if (!itemDefinition) { + return Promise.resolve({ + ...item, + loading: false, + element: ( +
+ {i18n.translate('xpack.investigateApp.renderableItems.div.notFoundLabel', { + defaultMessage: 'Not found for type {type}', + values: { type: item.type }, + })} +
+ ), + }); + } + + const globalParams = { + timeRange: { + from: new Date(params.timeRange.from).toISOString(), + to: new Date(params.timeRange.to).toISOString(), + }, + }; + + const data = await itemDefinition.generate({ + itemParams: item.params, + globalParams, + }); + + return Promise.resolve({ + ...item, + loading: false, + element: itemDefinition.render({ + data, + globalParams, + itemParams: item.params, + }), + }); + }) + ); + } + + if (items) { + renderItems(items).then((nextRenderableItems) => setRenderableItems(nextRenderableItems)); + } + }, [items, investigate, params]); + + return renderableItems; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/investigation_details_page.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/investigation_details_page.tsx index 90af6b4591c69..8bce69ea125af 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/investigation_details_page.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/investigation_details_page.tsx @@ -11,23 +11,29 @@ import { alertOriginSchema } from '@kbn/investigation-shared'; import { ALERT_RULE_CATEGORY } from '@kbn/rule-data-utils/src/default_alerts_as_data'; import React from 'react'; import { useParams } from 'react-router-dom'; +import useAsync from 'react-use/lib/useAsync'; import { paths } from '../../../common/paths'; 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 { InvestigationDetails } from './components/investigation_details/investigation_details'; import { InvestigationDetailsPathParams } from './types'; export function InvestigationDetailsPage() { const { core: { http: { basePath }, + security, }, dependencies: { start: { observabilityShared }, }, } = useKibana(); + const user = useAsync(() => { + return security.authc.getCurrentUser(); + }, [security]); + const { investigationId } = useParams(); const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate; @@ -44,6 +50,10 @@ export function InvestigationDetailsPage() { const { data: alertDetails } = useFetchAlert({ id: alertId }); + if (!user.value) { + return null; + } + if (isFetchInvestigationLoading) { return (

@@ -106,7 +116,7 @@ export function InvestigationDetailsPage() { ], }} > - + ); } diff --git a/x-pack/plugins/observability_solution/investigate_app/public/plugin.tsx b/x-pack/plugins/observability_solution/investigate_app/public/plugin.tsx index 75b07099cbb62..abbe762562541 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/plugin.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/plugin.tsx @@ -125,28 +125,26 @@ export class InvestigateAppPlugin .getStartServices() .then(([, pluginsStart]) => pluginsStart); - pluginsSetup.investigate.register((registerWidget) => - Promise.all([ - pluginsStartPromise, - import('./widgets/register_widgets').then((m) => m.registerWidgets), - getCreateEsqlService(), - ]).then(([pluginsStart, registerWidgets, createEsqlService]) => { - registerWidgets({ - dependencies: { - setup: pluginsSetup, - start: pluginsStart, - }, - services: { - esql: createEsqlService({ - data: pluginsStart.data, - dataViews: pluginsStart.dataViews, - lens: pluginsStart.lens, - }), - }, - registerWidget, - }); - }) - ); + // new + Promise.all([ + pluginsStartPromise, + import('./items/register_items').then((m) => m.registerItems), + getCreateEsqlService(), + ]).then(([pluginsStart, registerItems, createEsqlService]) => { + registerItems({ + dependencies: { + setup: pluginsSetup, + start: pluginsStart, + }, + services: { + esql: createEsqlService({ + data: pluginsStart.data, + dataViews: pluginsStart.dataViews, + lens: pluginsStart.lens, + }), + }, + }); + }); return {}; } diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/create_embeddable_widget.ts b/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/create_embeddable_widget.ts deleted file mode 100644 index 61e99df8b28b2..0000000000000 --- a/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/create_embeddable_widget.ts +++ /dev/null @@ -1,13 +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 { createWidgetFactory } from '@kbn/investigate-plugin/public'; -import { EMBEDDABLE_WIDGET_NAME } from '../../constants'; -import { EmbeddableWidgetParameters } from './types'; - -export const createEmbeddableWidget = - createWidgetFactory(EMBEDDABLE_WIDGET_NAME); diff --git a/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/types.ts b/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/types.ts deleted file mode 100644 index d3ecd0379c716..0000000000000 --- a/x-pack/plugins/observability_solution/investigate_app/public/widgets/embeddable_widget/types.ts +++ /dev/null @@ -1,18 +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 { InvestigateWidget, InvestigateWidgetCreate } from '@kbn/investigate-plugin/common'; - -export interface EmbeddableWidgetParameters { - type: string; - savedObjectId?: string; - config: Record; -} - -export type EmbeddableWidgetCreate = InvestigateWidgetCreate; - -export type EmbeddableWidget = InvestigateWidget;