diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2dec31b8135e..25423ed57cea 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -362,7 +362,7 @@ packages/deeplinks/devtools @elastic/kibana-management packages/deeplinks/fleet @elastic/fleet packages/deeplinks/management @elastic/kibana-management packages/deeplinks/ml @elastic/ml-ui -packages/deeplinks/observability @elastic/obs-ux-logs-team +packages/deeplinks/observability @elastic/obs-ux-management-team packages/deeplinks/search @elastic/search-kibana packages/deeplinks/security @elastic/security-solution packages/deeplinks/shared @elastic/appex-sharedux @@ -522,6 +522,7 @@ x-pack/plugins/integration_assistant @elastic/security-scalability src/plugins/interactive_setup @elastic/kibana-security test/interactive_setup_api_integration/plugins/test_endpoints @elastic/kibana-security packages/kbn-interpreter @elastic/kibana-visualizations +x-pack/plugins/observability_solution/inventory @elastic/obs-ux-infra_services-team 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 @@ -876,6 +877,9 @@ packages/kbn-sort-predicates @elastic/kibana-visualizations x-pack/plugins/spaces @elastic/kibana-security x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin @elastic/kibana-security packages/kbn-spec-to-console @elastic/kibana-management +packages/kbn-sse-utils @elastic/obs-knowledge-team +packages/kbn-sse-utils-client @elastic/obs-knowledge-team +packages/kbn-sse-utils-server @elastic/obs-knowledge-team x-pack/plugins/stack_alerts @elastic/response-ops x-pack/plugins/stack_connectors @elastic/response-ops x-pack/test/usage_collection/plugins/stack_management_usage_test @elastic/kibana-management diff --git a/.i18nrc.json b/.i18nrc.json index 7707bfdcde17..b65c71b1a0d4 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -116,6 +116,7 @@ "searchTypes": "packages/kbn-search-types", "securitySolutionPackages": "x-pack/packages/security-solution", "serverlessPackages": "packages/serverless", + "sse": [ "packages/kbn-sse-utils" ], "coloring": "packages/kbn-coloring/src", "languageDocumentationPopover": "packages/kbn-language-documentation-popover/src", "esql": "src/plugins/esql", diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 422625da5d1f..0010045ab3c9 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -648,6 +648,10 @@ the infrastructure monitoring use-case within Kibana. |Team owner: Security Integrations Scalability +|{kib-repo}blob/{branch}/x-pack/plugins/observability_solution/inventory/README.md[inventory] +|Home of the Inventory plugin, which renders the... inventory. + + |{kib-repo}blob/{branch}/x-pack/plugins/observability_solution/investigate/README.md[investigate] |undefined diff --git a/package.json b/package.json index 7af9353a892c..ff06d988f570 100644 --- a/package.json +++ b/package.json @@ -566,6 +566,7 @@ "@kbn/interactive-setup-plugin": "link:src/plugins/interactive_setup", "@kbn/interactive-setup-test-endpoints-plugin": "link:test/interactive_setup_api_integration/plugins/test_endpoints", "@kbn/interpreter": "link:packages/kbn-interpreter", + "@kbn/inventory-plugin": "link:x-pack/plugins/observability_solution/inventory", "@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", @@ -888,6 +889,9 @@ "@kbn/sort-predicates": "link:packages/kbn-sort-predicates", "@kbn/spaces-plugin": "link:x-pack/plugins/spaces", "@kbn/spaces-test-plugin": "link:x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin", + "@kbn/sse-utils": "link:packages/kbn-sse-utils", + "@kbn/sse-utils-client": "link:packages/kbn-sse-utils-client", + "@kbn/sse-utils-server": "link:packages/kbn-sse-utils-server", "@kbn/stack-alerts-plugin": "link:x-pack/plugins/stack_alerts", "@kbn/stack-connectors-plugin": "link:x-pack/plugins/stack_connectors", "@kbn/stack-management-usage-test-plugin": "link:x-pack/test/usage_collection/plugins/stack_management_usage_test", diff --git a/packages/deeplinks/observability/constants.ts b/packages/deeplinks/observability/constants.ts index 13d165b25d71..45868fa3a16b 100644 --- a/packages/deeplinks/observability/constants.ts +++ b/packages/deeplinks/observability/constants.ts @@ -30,3 +30,5 @@ export const INVESTIGATE_APP_ID = 'investigate'; export const OBLT_UX_APP_ID = 'ux'; export const OBLT_PROFILING_APP_ID = 'profiling'; + +export const INVENTORY_APP_ID = 'inventory'; diff --git a/packages/deeplinks/observability/deep_links.ts b/packages/deeplinks/observability/deep_links.ts index fab434aedc5c..088b9c866c03 100644 --- a/packages/deeplinks/observability/deep_links.ts +++ b/packages/deeplinks/observability/deep_links.ts @@ -19,6 +19,7 @@ import { AI_ASSISTANT_APP_ID, OBLT_UX_APP_ID, OBLT_PROFILING_APP_ID, + INVENTORY_APP_ID, } from './constants'; type LogsApp = typeof LOGS_APP_ID; @@ -32,6 +33,7 @@ type SloApp = typeof SLO_APP_ID; type AiAssistantApp = typeof AI_ASSISTANT_APP_ID; type ObltUxApp = typeof OBLT_UX_APP_ID; type ObltProfilingApp = typeof OBLT_PROFILING_APP_ID; +type InventoryApp = typeof INVENTORY_APP_ID; export type AppId = | LogsApp @@ -44,10 +46,13 @@ export type AppId = | SloApp | AiAssistantApp | ObltUxApp - | ObltProfilingApp; + | ObltProfilingApp + | InventoryApp; export type LogsLinkId = 'log-categories' | 'settings' | 'anomalies' | 'stream'; +export type InventoryLinkId = 'datastreams'; + export type ObservabilityOverviewLinkId = | 'alerts' | 'cases' @@ -90,4 +95,5 @@ export type DeepLinkId = | `${MetricsApp}:${MetricsLinkId}` | `${ApmApp}:${ApmLinkId}` | `${SyntheticsApp}:${SyntheticsLinkId}` - | `${ObltProfilingApp}:${ProfilingLinkId}`; + | `${ObltProfilingApp}:${ProfilingLinkId}` + | `${InventoryApp}:${InventoryLinkId}`; diff --git a/packages/deeplinks/observability/kibana.jsonc b/packages/deeplinks/observability/kibana.jsonc index bc014b05aa40..da2c0505737a 100644 --- a/packages/deeplinks/observability/kibana.jsonc +++ b/packages/deeplinks/observability/kibana.jsonc @@ -1,5 +1,5 @@ { "type": "shared-common", "id": "@kbn/deeplinks-observability", - "owner": "@elastic/obs-ux-logs-team" + "owner": "@elastic/obs-ux-management-team" } diff --git a/packages/kbn-es-types/src/search.ts b/packages/kbn-es-types/src/search.ts index f26ed8613b27..553401b19b98 100644 --- a/packages/kbn-es-types/src/search.ts +++ b/packages/kbn-es-types/src/search.ts @@ -89,7 +89,7 @@ export type SearchHit< ? { fields: Partial, unknown[]>>; } - : {}) & + : { fields?: Record }) & (TDocValueFields extends DocValueFields ? { fields: Partial, unknown[]>>; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index aea38badef49..2dbc5ed9acea 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -86,6 +86,7 @@ pageLoadAssetSize: inspector: 148711 integrationAssistant: 19524 interactiveSetup: 80000 + inventory: 27430 investigate: 17970 investigateApp: 91898 kibanaOverview: 56279 diff --git a/packages/kbn-server-route-repository-client/index.ts b/packages/kbn-server-route-repository-client/index.ts index 4b9042c58e79..db8f6cebfbc5 100644 --- a/packages/kbn-server-route-repository-client/index.ts +++ b/packages/kbn-server-route-repository-client/index.ts @@ -9,6 +9,7 @@ export { createRepositoryClient } from './src/create_repository_client'; export { isHttpFetchError } from './src/is_http_fetch_error'; +export { isRequestAbortedError } from './src/is_request_aborted_error'; export type { DefaultClientOptions, diff --git a/packages/kbn-server-route-repository-client/src/create_observable_from_http_response.ts b/packages/kbn-server-route-repository-client/src/create_observable_from_http_response.ts new file mode 100644 index 000000000000..1690244ca19a --- /dev/null +++ b/packages/kbn-server-route-repository-client/src/create_observable_from_http_response.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { createParser } from 'eventsource-parser'; +import { Observable, throwError } from 'rxjs'; + +export interface StreamedHttpResponse { + response?: { body: ReadableStream | null | undefined }; +} + +class NoReadableStreamError extends Error { + constructor() { + super(`No readable stream found in response`); + } +} + +export function isNoReadableStreamError(error: any): error is NoReadableStreamError { + return error instanceof NoReadableStreamError; +} + +export function createObservableFromHttpResponse( + response: StreamedHttpResponse +): Observable { + const rawResponse = response.response; + + const body = rawResponse?.body; + if (!body) { + return throwError(() => { + throw new NoReadableStreamError(); + }); + } + + return new Observable((subscriber) => { + const parser = createParser((event) => { + if (event.type === 'event') { + subscriber.next(event.data); + } + }); + + const readStream = async () => { + const reader = body.getReader(); + const decoder = new TextDecoder(); + + // Function to process each chunk + const processChunk = ({ + done, + value, + }: ReadableStreamReadResult): Promise => { + if (done) { + return Promise.resolve(); + } + + parser.feed(decoder.decode(value, { stream: true })); + + return reader.read().then(processChunk); + }; + + // Start reading the stream + return reader.read().then(processChunk); + }; + + readStream() + .then(() => { + subscriber.complete(); + }) + .catch((error) => { + subscriber.error(error); + }); + }); +} diff --git a/packages/kbn-server-route-repository-client/src/create_repository_client.ts b/packages/kbn-server-route-repository-client/src/create_repository_client.ts index d060480c262d..015db2b9948d 100644 --- a/packages/kbn-server-route-repository-client/src/create_repository_client.ts +++ b/packages/kbn-server-route-repository-client/src/create_repository_client.ts @@ -11,28 +11,52 @@ import type { CoreSetup, CoreStart } from '@kbn/core-lifecycle-browser'; import { RouteRepositoryClient, ServerRouteRepository, - DefaultClientOptions, formatRequest, } from '@kbn/server-route-repository-utils'; +import { httpResponseIntoObservable } from '@kbn/sse-utils-client'; +import { from } from 'rxjs'; +import { HttpFetchOptions, HttpFetchQuery, HttpResponse } from '@kbn/core-http-browser'; +import { omit } from 'lodash'; export function createRepositoryClient< TRepository extends ServerRouteRepository, - TClientOptions extends Record = DefaultClientOptions ->(core: CoreStart | CoreSetup) { + TClientOptions extends HttpFetchOptions = {} +>(core: CoreStart | CoreSetup): RouteRepositoryClient { + const fetch = ( + endpoint: string, + params: { path?: Record; body?: unknown; query?: HttpFetchQuery } | undefined, + options: TClientOptions + ) => { + const { method, pathname, version } = formatRequest(endpoint, params?.path); + + return core.http[method](pathname, { + ...options, + body: params && params.body ? JSON.stringify(params.body) : undefined, + query: params?.query, + version, + }); + }; + return { - fetch: (endpoint, optionsWithParams) => { - const { params, ...options } = (optionsWithParams ?? { params: {} }) as unknown as { - params?: Partial>; - }; + fetch: (endpoint, ...args) => { + const allOptions = args[0] ?? {}; + const params = 'params' in allOptions ? (allOptions.params as Record) : {}; + const otherOptions = omit(allOptions, 'params') as TClientOptions; - const { method, pathname, version } = formatRequest(endpoint, params?.path); + return fetch(endpoint, params, otherOptions) as any; + }, + stream: (endpoint, ...args) => { + const allOptions = args[0] ?? {}; + const params = 'params' in allOptions ? (allOptions.params as Record) : {}; + const otherOptions = omit(allOptions, 'params') as TClientOptions; - return core.http[method](pathname, { - ...options, - body: params && params.body ? JSON.stringify(params.body) : undefined, - query: params?.query, - version, - }); + return from( + fetch(endpoint, params, { + ...otherOptions, + asResponse: true, + rawResponse: true, + }) as Promise + ).pipe(httpResponseIntoObservable()) as any; }, - } as { fetch: RouteRepositoryClient }; + }; } diff --git a/packages/kbn-server-route-repository-client/src/is_request_aborted_error.ts b/packages/kbn-server-route-repository-client/src/is_request_aborted_error.ts new file mode 100644 index 000000000000..3ab33ebac821 --- /dev/null +++ b/packages/kbn-server-route-repository-client/src/is_request_aborted_error.ts @@ -0,0 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { get } from 'lodash'; + +export function isRequestAbortedError(error: unknown): error is Error { + return get(error, 'name') === 'AbortError'; +} diff --git a/packages/kbn-server-route-repository-client/tsconfig.json b/packages/kbn-server-route-repository-client/tsconfig.json index 8e70ce985175..e1ce8a6572b7 100644 --- a/packages/kbn-server-route-repository-client/tsconfig.json +++ b/packages/kbn-server-route-repository-client/tsconfig.json @@ -17,5 +17,6 @@ "@kbn/server-route-repository-utils", "@kbn/core-lifecycle-browser", "@kbn/core-http-browser", + "@kbn/sse-utils-client", ] } diff --git a/packages/kbn-server-route-repository-utils/src/typings.ts b/packages/kbn-server-route-repository-utils/src/typings.ts index 6974c27a3de7..35a2f41054c9 100644 --- a/packages/kbn-server-route-repository-utils/src/typings.ts +++ b/packages/kbn-server-route-repository-utils/src/typings.ts @@ -17,43 +17,14 @@ import type { RouteConfigOptions, RouteMethod, } from '@kbn/core/server'; +import type { ServerSentEvent } from '@kbn/sse-utils'; import { z } from '@kbn/zod'; import * as t from 'io-ts'; -import { RequiredKeys } from 'utility-types'; +import { Observable } from 'rxjs'; +import { Readable } from 'stream'; +import { RequiredKeys, ValuesType } from 'utility-types'; -type PathMaybeOptional }> = RequiredKeys< - T['path'] -> extends never - ? { path?: T['path'] } - : { path: T['path'] }; - -type QueryMaybeOptional }> = RequiredKeys< - T['query'] -> extends never - ? { query?: T['query'] } - : { query: T['query'] }; - -type BodyMaybeOptional }> = RequiredKeys< - T['body'] -> extends never - ? { body?: T['body'] } - : { body: T['body'] }; - -type ParamsMaybeOptional< - TPath extends Record, - TQuery extends Record, - TBody extends Record -> = PathMaybeOptional<{ path: TPath }> & - QueryMaybeOptional<{ query: TQuery }> & - BodyMaybeOptional<{ body: TBody }>; - -type ZodMaybeOptional = ParamsMaybeOptional< - T['path'], - T['query'], - T['body'] ->; - -type MaybeOptional }> = RequiredKeys< +type MaybeOptional }> = RequiredKeys< T['params'] > extends never ? { params?: T['params'] } @@ -64,19 +35,19 @@ type WithoutIncompatibleMethods = Omit t.Encoder; }; -export type ZodParamsObject = z.ZodObject<{ +export interface RouteParams { path?: any; query?: any; body?: any; +} + +export type ZodParamsObject = z.ZodObject<{ + path?: z.ZodSchema; + query?: z.ZodSchema; + body?: z.ZodSchema; }>; -export type IoTsParamsObject = WithoutIncompatibleMethods< - t.Type<{ - path?: any; - query?: any; - body?: any; - }> ->; +export type IoTsParamsObject = WithoutIncompatibleMethods>; export type RouteParamsRT = IoTsParamsObject | ZodParamsObject; @@ -97,22 +68,98 @@ type ValidateEndpoint = string extends TEndpoint : false : false; +type IsAny = 1 | 0 extends (T extends never ? 1 : 0) ? true : false; + +// this ensures only plain objects can be returned, if it's not one +// of the other allowed types. here's how it works: +// - if it's a function, it's invalid +// - if it's a primitive, it's valid +// - if it's an array, it's valid +// - if it's a record, walk it once and apply above principles +// we don't recursively walk because of circular references in object types +// we also don't check arrays, as the goal is to not be able to return +// things like classes and functions at the top level. specifically, +// this code is intended to allow for Observable but +// to disallow Observable. + +type ValidateSerializableValue = IsAny extends true + ? 1 + : T extends Function + ? 0 + : T extends Record + ? TWalkRecursively extends true + ? ValuesType<{ + [key in keyof T]: ValidateSerializableValue; + }> + : 1 + : T extends string | number | boolean | null | undefined + ? 1 + : T extends any[] + ? 1 + : 0; + +type GuardAgainstInvalidRecord = 0 extends ValidateSerializableValue ? never : T; + +type ServerRouteHandlerReturnTypeWithoutRecord = + | Observable + | Readable + | IKibanaResponse + | string + | number + | boolean + | null + | void; + +type ServerRouteHandlerReturnType = ServerRouteHandlerReturnTypeWithoutRecord | Record; + +type ServerRouteHandler< + TRouteHandlerResources extends ServerRouteHandlerResources, + TRouteParamsRT extends RouteParamsRT | undefined, + TReturnType extends ServerRouteHandlerReturnType +> = ( + options: TRouteHandlerResources & + (TRouteParamsRT extends RouteParamsRT ? DecodedRequestParamsOfType : {}) +) => Promise< + TReturnType extends ServerRouteHandlerReturnTypeWithoutRecord + ? TReturnType + : GuardAgainstInvalidRecord +>; + +export type CreateServerRouteFactory< + TRouteHandlerResources extends ServerRouteHandlerResources, + TRouteCreateOptions extends ServerRouteCreateOptions +> = < + TEndpoint extends string, + TReturnType extends ServerRouteHandlerReturnType, + TRouteParamsRT extends RouteParamsRT | undefined = undefined +>( + options: { + endpoint: ValidateEndpoint extends true ? TEndpoint : never; + handler: ServerRouteHandler; + params?: TRouteParamsRT; + } & TRouteCreateOptions +) => Record< + TEndpoint, + ServerRoute< + TEndpoint, + TRouteParamsRT, + TRouteHandlerResources, + Awaited, + TRouteCreateOptions + > +>; + export type ServerRoute< TEndpoint extends string, TRouteParamsRT extends RouteParamsRT | undefined, TRouteHandlerResources extends ServerRouteHandlerResources, - TReturnType, + TReturnType extends ServerRouteHandlerReturnType, TRouteCreateOptions extends ServerRouteCreateOptions -> = ValidateEndpoint extends true - ? { - endpoint: TEndpoint; - params?: TRouteParamsRT; - handler: ({}: TRouteHandlerResources & - (TRouteParamsRT extends RouteParamsRT - ? DecodedRequestParamsOfType - : {})) => Promise; - } & TRouteCreateOptions - : never; +> = { + endpoint: TEndpoint; + handler: ServerRouteHandler; +} & TRouteCreateOptions & + (TRouteParamsRT extends RouteParamsRT ? { params: TRouteParamsRT } : {}); export type ServerRouteRepository = Record< string, @@ -124,22 +171,22 @@ type ClientRequestParamsOfType = ? MaybeOptional<{ params: t.OutputOf; }> - : TRouteParamsRT extends z.Schema + : TRouteParamsRT extends z.ZodSchema ? MaybeOptional<{ - params: ZodMaybeOptional>; + params: z.input; }> - : {}; + : never; type DecodedRequestParamsOfType = TRouteParamsRT extends t.Mixed ? MaybeOptional<{ params: t.TypeOf; }> - : TRouteParamsRT extends z.Schema + : TRouteParamsRT extends z.ZodSchema ? MaybeOptional<{ - params: ZodMaybeOptional>; + params: z.output; }> - : {}; + : never; export type EndpointOf = keyof TServerRouteRepository; @@ -186,24 +233,36 @@ export type ClientRequestParamsOf< > ? TRouteParamsRT extends RouteParamsRT ? ClientRequestParamsOfType - : {} + : TRouteParamsRT extends undefined + ? {} + : never : never; type MaybeOptionalArgs> = RequiredKeys extends never ? [T] | [] : [T]; -export type RouteRepositoryClient< +export interface RouteRepositoryClient< TServerRouteRepository extends ServerRouteRepository, - TAdditionalClientOptions extends Record = DefaultClientOptions -> = >( - endpoint: TEndpoint, - ...args: MaybeOptionalArgs< - ClientRequestParamsOf & TAdditionalClientOptions - > -) => Promise>; - -export type DefaultClientOptions = HttpFetchOptions; + TAdditionalClientOptions extends Record +> { + fetch>( + endpoint: TEndpoint, + ...args: MaybeOptionalArgs< + ClientRequestParamsOf & TAdditionalClientOptions + > + ): Promise>; + stream>( + endpoint: TEndpoint, + ...args: MaybeOptionalArgs< + ClientRequestParamsOf & TAdditionalClientOptions + > + ): ReturnOf extends Observable + ? TReturnType extends ServerSentEvent + ? Observable + : never + : never; +} interface CoreRouteHandlerResources { request: KibanaRequest; @@ -211,6 +270,8 @@ interface CoreRouteHandlerResources { context: RequestHandlerContext; } +export type DefaultClientOptions = HttpFetchOptions; + export interface DefaultRouteHandlerResources extends CoreRouteHandlerResources { logger: Logger; } diff --git a/packages/kbn-server-route-repository-utils/tsconfig.json b/packages/kbn-server-route-repository-utils/tsconfig.json index cb5d9846f6cc..593d368856aa 100644 --- a/packages/kbn-server-route-repository-utils/tsconfig.json +++ b/packages/kbn-server-route-repository-utils/tsconfig.json @@ -16,9 +16,10 @@ "target/**/*" ], "kbn_references": [ - "@kbn/core-http-browser", "@kbn/core-http-server", "@kbn/core", "@kbn/zod", + "@kbn/core-http-browser", + "@kbn/sse-utils", ] } diff --git a/packages/kbn-server-route-repository/README.md b/packages/kbn-server-route-repository/README.md index c34cf6654ba4..5b8307f2d066 100644 --- a/packages/kbn-server-route-repository/README.md +++ b/packages/kbn-server-route-repository/README.md @@ -5,6 +5,7 @@ Utility functions for creating a typed server route repository, and a typed clie ## Overview There are three main functions that make up this package: + 1. `createServerRouteFactory` 2. `registerRoutes` 3. `createRepositoryClient` @@ -22,6 +23,7 @@ By exporting the type of the repository from the server to the browser (make sur In the server side, we'll start by creating the route factory, to make things easier it is recommended to keep this in its own file and export it. > server/create_my_plugin_server_route.ts + ```javascript import { createServerRouteFactory } from '@kbn/server-route-repository'; import { @@ -40,17 +42,18 @@ The two generic arguments are optional, this example shows a "default" setup whi Next, let's create a minimal route. > server/my_route.ts + ```javascript import { createMyPluginServerRoute } from './create_my_plugin_server_route'; export const myRoute = createMyPluginServerRoute({ - endpoint: 'GET /internal/my_plugin/route', - handler: async (resources) => { - const { request, context, response, logger } = resources; - return response.ok({ - body: 'Hello, my route!', - }); - }, + endpoint: 'GET /internal/my_plugin/route', + handler: async (resources) => { + const { request, context, response, logger } = resources; + return response.ok({ + body: 'Hello, my route!', + }); + }, }); ``` @@ -87,11 +90,12 @@ We also export the type of the repository, we'll need this for the client which The client can be created either in `setup` or `start`. > browser/plugin.ts + ```javascript import { createRepositoryClient, isHttpFetchError, DefaultClientOptions } from '@kbn/server-route-repository-client'; import type { MyPluginRouteRepository } from '../server/plugin'; -export type MyPluginRepositoryClient = +export type MyPluginRepositoryClient = ReturnType>; class MyPlugin implements Plugin { @@ -116,10 +120,10 @@ class MyPlugin implements Plugin { This example prints 'Hello, my route!' and the type of the response is **inferred** to this. We pass in the type of the repository that we (_type_) imported from the server. The second generic parameter for `createRepositoryClient` is optional. -We also export the type of the client itself so we can use it to type the client as we pass it around. +We also export the type of the client itself so we can use it to type the client as we pass it around. When using the client's `fetch` function, the first argument is the route to call and this is auto completed to only the available routes. -The second argument is optional in this case but allows you to send in any extra options. +The second argument is optional in this case but allows you to send in any extra options. The client translates the endpoint and the options (including request parameters) to the right Core HTTP request. @@ -159,19 +163,20 @@ The `params` object is added to the route resources. `path`, `query` and `body` are validated before your handler is called and the types are **inferred** inside of the handler. When calling this endpoint, it will look like this: + ```javascript client('POST /internal/my_plugin/route/{my_path_param}', { - params: { - path: { - my_path_param: 'some_path_value', - }, - query: { - my_query_param: 'some_query_value', - }, - body: { - my_body_param: 'some_body_value', - }, + params: { + path: { + my_path_param: 'some_path_value', + }, + query: { + my_query_param: 'some_query_value', }, + body: { + my_body_param: 'some_body_value', + }, + }, }).then(console.log); ``` @@ -213,9 +218,9 @@ const myRoute = createMyPluginServerRoute({ const result = coinFlip(); if (result === 'heads') { - throw teapot(); + throw teapot(); } else { - return 'Hello, my route!'; + return 'Hello, my route!'; } }, }); @@ -235,7 +240,7 @@ export interface MyPluginRouteDependencies { myDependency: MyDependency; } -export const createMyPluginServerRoute = +export const createMyPluginServerRoute = createServerRouteFactory(); ``` @@ -244,14 +249,16 @@ If you don't want your route to have access to the default resources, you could Then we use the same type when calling `registerRoutes` ```javascript -registerRoutes({ +registerRoutes < + MyPluginRouteDependencies > + { core, logger, repository, dependencies: { - myDependency: new MyDependency(), + myDependency: new MyDependency(), }, -}); + }; ``` This way, when creating a route, you will have `myDependency` available in the route resources. @@ -260,13 +267,13 @@ This way, when creating a route, you will have `myDependency` available in the r import { createMyPluginServerRoute } from './create_my_plugin_server_route'; export const myRoute = createMyPluginServerRoute({ - endpoint: 'GET /internal/my_plugin/route', - handler: async (resources) => { - const { request, context, response, logger, myDependency } = resources; - return response.ok({ - body: myDependency.sayHello(), - }); - }, + endpoint: 'GET /internal/my_plugin/route', + handler: async (resources) => { + const { request, context, response, logger, myDependency } = resources; + return response.ok({ + body: myDependency.sayHello(), + }); + }, }); ``` @@ -295,21 +302,22 @@ export const createMyPluginServerRoute = createServerRouteFactory< If you don't want your route to have access to the options provided by Core HTTP, you could pass in only `MyPluginRouteCreateOptions`. You can then specify this option when creating the route. + ```javascript import { createMyPluginServerRoute } from './create_my_plugin_server_route'; export const myRoute = createMyPluginServerRoute({ - options: { - access: 'internal', - }, - isDangerous: true, - endpoint: 'GET /internal/my_plugin/route', - handler: async (resources) => { - const { request, context, response, logger } = resources; - return response.ok({ - body: 'Hello, my route!', - }); - }, + options: { + access: 'internal', + }, + isDangerous: true, + endpoint: 'GET /internal/my_plugin/route', + handler: async (resources) => { + const { request, context, response, logger } = resources; + return response.ok({ + body: 'Hello, my route!', + }); + }, }); ``` @@ -346,3 +354,37 @@ class MyPlugin implements Plugin { ``` If you don't want your route to have access to the options provided by Core HTTP, you could pass in only `MyPluginClientOptions`. + +## Streaming + +@kbn/server-route-repository supports streaming events as well. It uses server-sent events (SSE) for this. To use it, simply return an Observable in the route handler: + +```javascript +import { createMyPluginServerRoute } from './create_my_plugin_server_route'; +import { ServerSentEvent } from '@kbn/sse-utils'; + +export const myRoute = createMyPluginServerRoute({ + endpoint: 'GET /internal/my_plugin/streaming_route', + handler: async (resources) => { + const { request, context, response, logger } = resources; + return of({ + type: 'my_event' as const, + data: { + myData: {} + } + }) + }, +}); +``` + +This will create a Node.js response stream where events are emitted as soon as the Observable emits them. Errors are automatically serialized, deserialized and thrown. See @kbn/sse-utils for more details. + +To parse the event stream in the browser, use the `stream` method on the repository client. It returns a typed Observable: + +```javascript +myPluginRepositoryClient.stream('GET /internal/my_plugin/streaming_route').subscribe({ + next: (value /*:{ type: 'my_event', data: { myData: {} }}*/) => { + console.log(value); + }, +}); +``` diff --git a/packages/kbn-server-route-repository/src/create_server_route_factory.ts b/packages/kbn-server-route-repository/src/create_server_route_factory.ts index 67f3f465f683..be375bd06948 100644 --- a/packages/kbn-server-route-repository/src/create_server_route_factory.ts +++ b/packages/kbn-server-route-repository/src/create_server_route_factory.ts @@ -7,33 +7,17 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { - RouteParamsRT, - ServerRoute, +import type { + DefaultRouteCreateOptions, + DefaultRouteHandlerResources, ServerRouteCreateOptions, ServerRouteHandlerResources, - DefaultRouteHandlerResources, - DefaultRouteCreateOptions, } from '@kbn/server-route-repository-utils'; +import type { CreateServerRouteFactory } from '@kbn/server-route-repository-utils/src/typings'; export function createServerRouteFactory< TRouteHandlerResources extends ServerRouteHandlerResources = DefaultRouteHandlerResources, TRouteCreateOptions extends ServerRouteCreateOptions = DefaultRouteCreateOptions ->(): < - TEndpoint extends string, - TReturnType, - TRouteParamsRT extends RouteParamsRT | undefined = undefined ->( - route: ServerRoute< - TEndpoint, - TRouteParamsRT, - TRouteHandlerResources, - TReturnType, - TRouteCreateOptions - > -) => Record< - TEndpoint, - ServerRoute -> { +>(): CreateServerRouteFactory { return (route) => ({ [route.endpoint]: route } as any); } diff --git a/packages/kbn-server-route-repository/src/make_zod_validation_object.ts b/packages/kbn-server-route-repository/src/make_zod_validation_object.ts index 3467fcb58d35..23d50e5bdb25 100644 --- a/packages/kbn-server-route-repository/src/make_zod_validation_object.ts +++ b/packages/kbn-server-route-repository/src/make_zod_validation_object.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ZodObject, ZodAny } from '@kbn/zod'; +import { z, ZodObject } from '@kbn/zod'; import { ZodParamsObject } from '@kbn/server-route-repository-utils'; import { noParamsValidationObject } from './validation_objects'; @@ -19,7 +19,7 @@ export function makeZodValidationObject(params: ZodParamsObject) { }; } -function asStrict(schema: ZodAny) { +function asStrict(schema: z.Schema) { if (schema instanceof ZodObject) { return schema.strict(); } else { diff --git a/packages/kbn-server-route-repository/src/register_routes.ts b/packages/kbn-server-route-repository/src/register_routes.ts index 4b55defa9c5d..5e0fa51a4544 100644 --- a/packages/kbn-server-route-repository/src/register_routes.ts +++ b/packages/kbn-server-route-repository/src/register_routes.ts @@ -20,8 +20,11 @@ import { ZodParamsObject, parseEndpoint, } from '@kbn/server-route-repository-utils'; +import { observableIntoEventSourceStream } from '@kbn/sse-utils-server'; import { isZod } from '@kbn/zod'; import { merge } from 'lodash'; +import { Observable, isObservable } from 'rxjs'; +import { ServerSentEvent } from '@kbn/sse-utils'; import { passThroughValidationObject, noParamsValidationObject } from './validation_objects'; import { validateAndDecodeParams } from './validate_and_decode_params'; import { makeZodValidationObject } from './make_zod_validation_object'; @@ -89,6 +92,10 @@ export function registerRoutes>({ if (isKibanaResponse(result)) { return result; + } else if (isObservable(result)) { + return response.ok({ + body: observableIntoEventSourceStream(result as Observable), + }); } else { const body = result || {}; return response.ok({ body }); diff --git a/packages/kbn-server-route-repository/src/test_types.ts b/packages/kbn-server-route-repository/src/test_types.ts index d35237651711..acef5a524eb6 100644 --- a/packages/kbn-server-route-repository/src/test_types.ts +++ b/packages/kbn-server-route-repository/src/test_types.ts @@ -11,6 +11,7 @@ import * as t from 'io-ts'; import { z } from '@kbn/zod'; import { kibanaResponseFactory } from '@kbn/core/server'; import { EndpointOf, ReturnOf, RouteRepositoryClient } from '@kbn/server-route-repository-utils'; +import { Observable, of } from 'rxjs'; import { createServerRouteFactory } from './create_server_route_factory'; import { decodeRequestParams } from './decode_request_params'; @@ -98,13 +99,12 @@ createServerRouteFactory<{}, { options: { tags: string[] } }>()({ }); // Public APIs should be versioned -// @ts-expect-error createServerRouteFactory<{}, { options: { tags: string[] } }>()({ + // @ts-expect-error endpoint: 'GET /api/endpoint_with_params', options: { tags: [], }, - // @ts-expect-error handler: async (resources) => {}, }); @@ -116,6 +116,15 @@ createServerRouteFactory<{}, { options: { tags: string[] } }>()({ handler: async (resources) => {}, }); +// cannot return observables that are not in the SSE structure +const route = createServerRouteFactory<{}, {}>()({ + endpoint: 'POST /internal/endpoint_returning_observable_without_sse_structure', + // @ts-expect-error + handler: async () => { + return of({ streamed_response: true }); + }, +}); + const createServerRoute = createServerRouteFactory<{}, {}>(); const repository = { @@ -201,6 +210,12 @@ const repository = { }); }, }), + ...createServerRoute({ + endpoint: 'POST /internal/endpoint_returning_observable', + handler: async () => { + return of({ type: 'foo' as const, streamed_response: true }); + }, + }), }; type TestRepository = typeof repository; @@ -248,21 +263,21 @@ const client: TestClient = {} as any; // It should respect any additional create options. // @ts-expect-error Property 'timeout' is missing -client('GET /internal/endpoint_without_params', {}); +client.fetch('GET /internal/endpoint_without_params', {}); -client('GET /internal/endpoint_without_params', { +client.fetch('GET /internal/endpoint_without_params', { timeout: 1, }); // It does not allow params for routes without a params codec -client('GET /internal/endpoint_without_params', { +client.fetch('GET /internal/endpoint_without_params', { // @ts-expect-error Object literal may only specify known properties, and 'params' does not exist in type params: {}, timeout: 1, }); // It requires params for routes with a params codec -client('GET /internal/endpoint_with_params', { +client.fetch('GET /internal/endpoint_with_params', { params: { // @ts-expect-error property 'serviceName' is missing in type '{}' path: {}, @@ -270,7 +285,7 @@ client('GET /internal/endpoint_with_params', { timeout: 1, }); -client('GET /internal/endpoint_with_params_zod', { +client.fetch('GET /internal/endpoint_with_params_zod', { params: { // @ts-expect-error property 'serviceName' is missing in type '{}' path: {}, @@ -279,16 +294,16 @@ client('GET /internal/endpoint_with_params_zod', { }); // Params are optional if the codec has no required keys -client('GET /internal/endpoint_with_optional_params', { +client.fetch('GET /internal/endpoint_with_optional_params', { timeout: 1, }); -client('GET /internal/endpoint_with_optional_params_zod', { +client.fetch('GET /internal/endpoint_with_optional_params_zod', { timeout: 1, }); // If optional, an error will still occur if the params do not match -client('GET /internal/endpoint_with_optional_params', { +client.fetch('GET /internal/endpoint_with_optional_params', { timeout: 1, params: { // @ts-expect-error Object literal may only specify known properties, and 'path' does not exist in type @@ -296,7 +311,7 @@ client('GET /internal/endpoint_with_optional_params', { }, }); -client('GET /internal/endpoint_with_optional_params_zod', { +client.fetch('GET /internal/endpoint_with_optional_params_zod', { timeout: 1, params: { // @ts-expect-error Object literal may only specify known properties, and 'path' does not exist in type @@ -305,57 +320,65 @@ client('GET /internal/endpoint_with_optional_params_zod', { }); // The return type is correctly inferred -client('GET /internal/endpoint_with_params', { - params: { - path: { - serviceName: '', +client + .fetch('GET /internal/endpoint_with_params', { + params: { + path: { + serviceName: '', + }, }, - }, - timeout: 1, -}).then((res) => { - assertType<{ - noParamsForMe: boolean; - // @ts-expect-error Property 'noParamsForMe' is missing in type - }>(res); - - assertType<{ - yesParamsForMe: boolean; - }>(res); -}); - -client('GET /internal/endpoint_with_params_zod', { - params: { - path: { - serviceName: '', + timeout: 1, + }) + .then((res) => { + assertType<{ + noParamsForMe: boolean; + // @ts-expect-error Property 'noParamsForMe' is missing in type + }>(res); + + assertType<{ + yesParamsForMe: boolean; + }>(res); + }); + +client + .fetch('GET /internal/endpoint_with_params_zod', { + params: { + path: { + serviceName: '', + }, }, - }, - timeout: 1, -}).then((res) => { - assertType<{ - noParamsForMe: boolean; - // @ts-expect-error Property 'noParamsForMe' is missing in type - }>(res); - - assertType<{ - yesParamsForMe: boolean; - }>(res); -}); - -client('GET /internal/endpoint_returning_result', { - timeout: 1, -}).then((res) => { - assertType<{ - result: boolean; - }>(res); -}); - -client('GET /internal/endpoint_returning_kibana_response', { - timeout: 1, -}).then((res) => { - assertType<{ - result: boolean; - }>(res); -}); + timeout: 1, + }) + .then((res) => { + assertType<{ + noParamsForMe: boolean; + // @ts-expect-error Property 'noParamsForMe' is missing in type + }>(res); + + assertType<{ + yesParamsForMe: boolean; + }>(res); + }); + +client + .fetch('GET /internal/endpoint_returning_result', { + timeout: 1, + }) + .then((res) => { + assertType<{ + result: boolean; + }>(res); + }); + +client + .fetch('GET /internal/endpoint_returning_kibana_response', { + timeout: 1, + }) + .then((res) => { + assertType<{ + result: boolean; + }>(res); + }); // decodeRequestParams should return the type of the codec that is passed assertType<{ path: { serviceName: string } }>( @@ -384,3 +407,9 @@ assertType<{ path: { serviceName: boolean } }>( t.type({ path: t.type({ serviceName: t.string }) }) ) ); + +assertType>( + client.stream('POST /internal/endpoint_returning_observable', { + timeout: 10, + }) +); diff --git a/packages/kbn-server-route-repository/tsconfig.json b/packages/kbn-server-route-repository/tsconfig.json index bb8a0847c39b..0e11a0f09589 100644 --- a/packages/kbn-server-route-repository/tsconfig.json +++ b/packages/kbn-server-route-repository/tsconfig.json @@ -21,6 +21,8 @@ "@kbn/logging-mocks", "@kbn/server-route-repository-utils", "@kbn/zod", + "@kbn/sse-utils-server", + "@kbn/sse-utils", ], "exclude": [ "target/**/*", diff --git a/packages/kbn-sse-utils-client/README.md b/packages/kbn-sse-utils-client/README.md new file mode 100644 index 000000000000..8c0c2ac37d48 --- /dev/null +++ b/packages/kbn-sse-utils-client/README.md @@ -0,0 +1,3 @@ +# @kbn/sse-utils-client + +See @kbn/sse-utils. diff --git a/packages/kbn-sse-utils-client/index.ts b/packages/kbn-sse-utils-client/index.ts new file mode 100644 index 000000000000..c074be46c525 --- /dev/null +++ b/packages/kbn-sse-utils-client/index.ts @@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { httpResponseIntoObservable } from './src/http_response_into_observable'; diff --git a/packages/kbn-sse-utils-client/jest.config.js b/packages/kbn-sse-utils-client/jest.config.js new file mode 100644 index 000000000000..29aa22cc6dc3 --- /dev/null +++ b/packages/kbn-sse-utils-client/jest.config.js @@ -0,0 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-sse-utils-client'], +}; diff --git a/packages/kbn-sse-utils-client/kibana.jsonc b/packages/kbn-sse-utils-client/kibana.jsonc new file mode 100644 index 000000000000..eda99336320f --- /dev/null +++ b/packages/kbn-sse-utils-client/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/sse-utils-client", + "owner": "@elastic/obs-knowledge-team" +} diff --git a/packages/kbn-sse-utils-client/package.json b/packages/kbn-sse-utils-client/package.json new file mode 100644 index 000000000000..b33f0b27d564 --- /dev/null +++ b/packages/kbn-sse-utils-client/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/sse-utils-client", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" +} \ No newline at end of file diff --git a/packages/kbn-sse-utils-client/src/create_observable_from_http_response.ts b/packages/kbn-sse-utils-client/src/create_observable_from_http_response.ts new file mode 100644 index 000000000000..814a528535c7 --- /dev/null +++ b/packages/kbn-sse-utils-client/src/create_observable_from_http_response.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { createParser } from 'eventsource-parser'; +import { Observable, throwError } from 'rxjs'; +import { createSSEInternalError, ServerSentEvent, ServerSentEventError } from '@kbn/sse-utils'; +import { ServerSentErrorEvent } from '@kbn/sse-utils/src/errors'; + +export interface StreamedHttpResponse { + response?: { body: ReadableStream | null | undefined }; +} + +export function createObservableFromHttpResponse( + response: StreamedHttpResponse +): Observable { + const rawResponse = response.response; + + const body = rawResponse?.body; + if (!body) { + return throwError(() => { + throw createSSEInternalError(`No readable stream found in response`); + }); + } + + return new Observable((subscriber) => { + const parser = createParser((event) => { + if (event.type === 'event') + try { + const data = JSON.parse(event.data); + if (event.event === 'error') { + const errorData = data as Omit; + subscriber.error( + new ServerSentEventError( + errorData.error.code, + errorData.error.message, + errorData.error.meta + ) + ); + } else { + subscriber.next({ type: event.event || 'event', ...data } as T); + } + } catch (error) { + subscriber.error(createSSEInternalError(`Failed to parse JSON`)); + } + }); + + const readStream = async () => { + const reader = body.getReader(); + const decoder = new TextDecoder(); + + // Function to process each chunk + const processChunk = ({ + done, + value, + }: ReadableStreamReadResult): Promise => { + if (done) { + return Promise.resolve(); + } + + parser.feed(decoder.decode(value, { stream: true })); + + return reader.read().then(processChunk); + }; + + // Start reading the stream + return reader.read().then(processChunk); + }; + + readStream() + .then(() => { + subscriber.complete(); + }) + .catch((error) => { + subscriber.error(error); + }); + }); +} diff --git a/packages/kbn-sse-utils-client/src/http_response_into_observable.test.ts b/packages/kbn-sse-utils-client/src/http_response_into_observable.test.ts new file mode 100644 index 000000000000..c4e3bd86efdd --- /dev/null +++ b/packages/kbn-sse-utils-client/src/http_response_into_observable.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { lastValueFrom, of, toArray } from 'rxjs'; +import { httpResponseIntoObservable } from './http_response_into_observable'; +import type { StreamedHttpResponse } from './create_observable_from_http_response'; +import { ServerSentEventErrorCode } from '@kbn/sse-utils/src/errors'; + +function toSse(...events: Array<{ type: string } & Record>) { + return events.map((event) => { + const { type, ...rest } = event; + return new TextEncoder().encode(`event: ${type}\ndata: ${JSON.stringify(rest)}\n\n`); + }); +} + +describe('httpResponseIntoObservable', () => { + it('parses SSE output', async () => { + const events = [ + { + type: 'chatCompletionChunk', + content: 'Hello', + }, + { + type: 'chatCompletionChunk', + content: 'Hello again', + }, + ]; + + const messages = await lastValueFrom( + of({ + response: { + // @ts-expect-error + body: ReadableStream.from(toSse(...events)), + }, + }).pipe(httpResponseIntoObservable(), toArray()) + ); + + expect(messages).toEqual(events); + }); + + it('throws serialized errors', async () => { + const events = [ + { + type: 'error', + error: { + code: ServerSentEventErrorCode.internalError, + message: 'Internal error', + }, + }, + ]; + + await expect(async () => { + await lastValueFrom( + of({ + response: { + // @ts-expect-error + body: ReadableStream.from(toSse(...events)), + }, + }).pipe(httpResponseIntoObservable(), toArray()) + ); + }).rejects.toThrowError(`Internal error`); + }); +}); diff --git a/packages/kbn-sse-utils-client/src/http_response_into_observable.ts b/packages/kbn-sse-utils-client/src/http_response_into_observable.ts new file mode 100644 index 000000000000..b72a0e3939e0 --- /dev/null +++ b/packages/kbn-sse-utils-client/src/http_response_into_observable.ts @@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { OperatorFunction, switchMap } from 'rxjs'; +import type { ServerSentEvent } from '@kbn/sse-utils/src/events'; +import { + createObservableFromHttpResponse, + StreamedHttpResponse, +} from './create_observable_from_http_response'; + +export function httpResponseIntoObservable< + T extends ServerSentEvent = ServerSentEvent +>(): OperatorFunction { + return switchMap((response) => createObservableFromHttpResponse(response)); +} diff --git a/packages/kbn-sse-utils-client/tsconfig.json b/packages/kbn-sse-utils-client/tsconfig.json new file mode 100644 index 000000000000..fa695dc1d0f5 --- /dev/null +++ b/packages/kbn-sse-utils-client/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/sse-utils", + ] +} diff --git a/packages/kbn-sse-utils-server/README.md b/packages/kbn-sse-utils-server/README.md new file mode 100644 index 000000000000..3c5aa4cc4e9d --- /dev/null +++ b/packages/kbn-sse-utils-server/README.md @@ -0,0 +1,3 @@ +# @kbn/sse-utils-server + +See @kbn/sse-utils. diff --git a/packages/kbn-sse-utils-server/index.ts b/packages/kbn-sse-utils-server/index.ts new file mode 100644 index 000000000000..ec2c60a2fe81 --- /dev/null +++ b/packages/kbn-sse-utils-server/index.ts @@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { observableIntoEventSourceStream } from './src/observable_into_event_source_stream'; diff --git a/packages/kbn-sse-utils-server/jest.config.js b/packages/kbn-sse-utils-server/jest.config.js new file mode 100644 index 000000000000..a2fe0d9caa7d --- /dev/null +++ b/packages/kbn-sse-utils-server/jest.config.js @@ -0,0 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-sse-utils-server'], +}; diff --git a/packages/kbn-sse-utils-server/kibana.jsonc b/packages/kbn-sse-utils-server/kibana.jsonc new file mode 100644 index 000000000000..9e06e575b798 --- /dev/null +++ b/packages/kbn-sse-utils-server/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/sse-utils-server", + "owner": "@elastic/obs-knowledge-team" +} diff --git a/packages/kbn-sse-utils-server/package.json b/packages/kbn-sse-utils-server/package.json new file mode 100644 index 000000000000..34c4f9c0a7c9 --- /dev/null +++ b/packages/kbn-sse-utils-server/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/sse-utils-server", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" +} \ No newline at end of file diff --git a/packages/kbn-sse-utils-server/src/observable_into_event_source_stream.ts b/packages/kbn-sse-utils-server/src/observable_into_event_source_stream.ts new file mode 100644 index 000000000000..e0d653e44dab --- /dev/null +++ b/packages/kbn-sse-utils-server/src/observable_into_event_source_stream.ts @@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { map, Observable } from 'rxjs'; +import { PassThrough } from 'stream'; +import { ServerSentEvent } from '@kbn/sse-utils'; + +export function observableIntoEventSourceStream(source$: Observable): PassThrough { + const withSerializedEvents$ = source$.pipe( + map((event) => { + const { type, ...rest } = event; + return `event: ${type}\ndata: ${JSON.stringify(rest)}\n\n`; + }) + ); + + const stream = new PassThrough(); + + withSerializedEvents$.subscribe({ + next: (line) => { + stream.write(line); + }, + complete: () => { + stream.end(); + }, + error: (error) => { + stream.write(`event: error\ndata: ${JSON.stringify(error)}\n\n`); + stream.end(); + }, + }); + + return stream; +} diff --git a/packages/kbn-sse-utils-server/tsconfig.json b/packages/kbn-sse-utils-server/tsconfig.json new file mode 100644 index 000000000000..9053749c5898 --- /dev/null +++ b/packages/kbn-sse-utils-server/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/sse-utils", + ] +} diff --git a/packages/kbn-sse-utils/README.md b/packages/kbn-sse-utils/README.md new file mode 100644 index 000000000000..ad6dbf8b67c0 --- /dev/null +++ b/packages/kbn-sse-utils/README.md @@ -0,0 +1,57 @@ +# @kbn/sse-utils + +This package exports utility functions that can be used to format and parse server-sent events(SSE). SSE is useful when streaming data back to the browser as part of a long-running process, such as LLM-based inference. It can convert an Observable that emits values of type `ServerSentEvent` into a response stream on the server, emitting lines in an SSE-compatible format, and it can convert an SSE response stream back into deserialized event. + +## Server + +On the server, you can use `observableIntoEventSourceStream` to convert an Observable that emits `ServerSentEvent` values into a Node.js response stream: + +```ts +import { observableIntoEventSourceStream } from '@kbn/sse-utils-server'; + +function myRequestHandler( + context: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory +) { + return response.ok({ + body: observableIntoEventSourceStream( + of({ + type: 'my_event_type', + data: { + anyData: {}, + }, + }) + ), + }); +} +``` + +All emitted values have to be of `ServerSentEvent` type: + +```ts +type ServerSentEvent = { + type: string; + data: Record; +}; +``` + +Any error that occurs in the Observable is written to the stream as an event, and the stream is closed. + +## Client + +On the client, you can use `http `@elastic/core-http-browser` to convert the stream of events back into an Observable: + +```ts +import { httpResponseIntoObservable } from '@kbn/sse-utils-client'; +function streamEvents(http: Http) { + from( + http.post('/internal/my_event_stream', { + asResponse: true, + rawResponse: true, + }) + ).pipe(httpResponseIntoObservable()); +} +``` + +Any serialized error events from the stream are de-serialized, and thrown as an error in the Observable. diff --git a/packages/kbn-sse-utils/index.ts b/packages/kbn-sse-utils/index.ts new file mode 100644 index 000000000000..0a9bf36fb000 --- /dev/null +++ b/packages/kbn-sse-utils/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { + createSSERequestError, + createSSEInternalError, + isSSEError, + isSSEInternalError, + isSSERequestError, + ServerSentEventError, +} from './src/errors'; + +export type { ServerSentEvent, ServerSentEventBase } from './src/events'; diff --git a/packages/kbn-sse-utils/jest.config.js b/packages/kbn-sse-utils/jest.config.js new file mode 100644 index 000000000000..73de8aa4d38b --- /dev/null +++ b/packages/kbn-sse-utils/jest.config.js @@ -0,0 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-sse-utils'], +}; diff --git a/packages/kbn-sse-utils/kibana.jsonc b/packages/kbn-sse-utils/kibana.jsonc new file mode 100644 index 000000000000..3bd583763b4d --- /dev/null +++ b/packages/kbn-sse-utils/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/sse-utils", + "owner": "@elastic/obs-knowledge-team" +} diff --git a/packages/kbn-sse-utils/package.json b/packages/kbn-sse-utils/package.json new file mode 100644 index 000000000000..b91a2c72afef --- /dev/null +++ b/packages/kbn-sse-utils/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/sse-utils", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" +} \ No newline at end of file diff --git a/packages/kbn-sse-utils/src/errors.ts b/packages/kbn-sse-utils/src/errors.ts new file mode 100644 index 000000000000..5c6c69ff0ef6 --- /dev/null +++ b/packages/kbn-sse-utils/src/errors.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; +import { ServerSentEventBase, ServerSentEventType } from './events'; + +export enum ServerSentEventErrorCode { + internalError = 'internalError', + requestError = 'requestError', +} + +export class ServerSentEventError< + TCode extends string, + TMeta extends Record | undefined +> extends Error { + constructor(public code: TCode, message: string, public meta: TMeta) { + super(message); + } + + toJSON(): ServerSentErrorEvent { + return { + type: ServerSentEventType.error, + error: { + code: this.code, + message: this.message, + meta: this.meta, + }, + }; + } +} + +export type ServerSentErrorEvent = ServerSentEventBase< + ServerSentEventType.error, + { + error: { + code: string; + message: string; + meta?: Record; + }; + } +>; + +export type ServerSentEventInternalError = ServerSentEventError< + ServerSentEventErrorCode.internalError, + {} +>; + +export type ServerSentEventRequestError = ServerSentEventError< + ServerSentEventErrorCode.requestError, + { status: number } +>; + +export function createSSEInternalError( + message: string = i18n.translate('sse.internalError', { + defaultMessage: 'An internal error occurred', + }) +): ServerSentEventInternalError { + return new ServerSentEventError(ServerSentEventErrorCode.internalError, message, {}); +} + +export function createSSERequestError( + message: string, + status: number +): ServerSentEventRequestError { + return new ServerSentEventError(ServerSentEventErrorCode.requestError, message, { + status, + }); +} + +export function isSSEError( + error: unknown +): error is ServerSentEventError | undefined> { + return error instanceof ServerSentEventError; +} + +export function isSSEInternalError(error: unknown): error is ServerSentEventInternalError { + return isSSEError(error) && error.code === ServerSentEventErrorCode.internalError; +} + +export function isSSERequestError(error: unknown): error is ServerSentEventRequestError { + return isSSEError(error) && error.code === ServerSentEventErrorCode.requestError; +} diff --git a/packages/kbn-sse-utils/src/events.ts b/packages/kbn-sse-utils/src/events.ts new file mode 100644 index 000000000000..26ed688a709b --- /dev/null +++ b/packages/kbn-sse-utils/src/events.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export type ServerSentEventBase< + TEventType extends string, + TData extends Record +> = keyof TData extends 'type' + ? never + : TData & { + type: TEventType; + }; + +export enum ServerSentEventType { + error = 'error', + data = 'data', +} + +export type ServerSentEvent = ServerSentEventBase>; diff --git a/packages/kbn-sse-utils/tsconfig.json b/packages/kbn-sse-utils/tsconfig.json new file mode 100644 index 000000000000..b38926e5f25b --- /dev/null +++ b/packages/kbn-sse-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/i18n", + ] +} diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 6dab85feb2fc..25aac790f08f 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -45,6 +45,7 @@ export const storybookAliases = { grouping: 'packages/kbn-grouping/.storybook', home: 'src/plugins/home/.storybook', infra: 'x-pack/plugins/observability_solution/infra/.storybook', + inventory: 'x-pack/plugins/observability_solution/inventory/.storybook', investigate: 'x-pack/plugins/observability_solution/investigate_app/.storybook', kibana_react: 'src/plugins/kibana_react/.storybook', lists: 'x-pack/plugins/lists/.storybook', diff --git a/tsconfig.base.json b/tsconfig.base.json index 44bb897bea1c..3c30cdbda22c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1038,6 +1038,8 @@ "@kbn/interactive-setup-test-endpoints-plugin/*": ["test/interactive_setup_api_integration/plugins/test_endpoints/*"], "@kbn/interpreter": ["packages/kbn-interpreter"], "@kbn/interpreter/*": ["packages/kbn-interpreter/*"], + "@kbn/inventory-plugin": ["x-pack/plugins/observability_solution/inventory"], + "@kbn/inventory-plugin/*": ["x-pack/plugins/observability_solution/inventory/*"], "@kbn/investigate-app-plugin": ["x-pack/plugins/observability_solution/investigate_app"], "@kbn/investigate-app-plugin/*": ["x-pack/plugins/observability_solution/investigate_app/*"], "@kbn/investigate-plugin": ["x-pack/plugins/observability_solution/investigate"], @@ -1746,6 +1748,12 @@ "@kbn/spaces-test-plugin/*": ["x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin/*"], "@kbn/spec-to-console": ["packages/kbn-spec-to-console"], "@kbn/spec-to-console/*": ["packages/kbn-spec-to-console/*"], + "@kbn/sse-utils": ["packages/kbn-sse-utils"], + "@kbn/sse-utils/*": ["packages/kbn-sse-utils/*"], + "@kbn/sse-utils-client": ["packages/kbn-sse-utils-client"], + "@kbn/sse-utils-client/*": ["packages/kbn-sse-utils-client/*"], + "@kbn/sse-utils-server": ["packages/kbn-sse-utils-server"], + "@kbn/sse-utils-server/*": ["packages/kbn-sse-utils-server/*"], "@kbn/stack-alerts-plugin": ["x-pack/plugins/stack_alerts"], "@kbn/stack-alerts-plugin/*": ["x-pack/plugins/stack_alerts/*"], "@kbn/stack-connectors-plugin": ["x-pack/plugins/stack_connectors"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index f2ab7a782915..a36cc693a6b7 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -59,6 +59,7 @@ "xpack.ingestPipelines": "plugins/ingest_pipelines", "xpack.integrationAssistant": "plugins/integration_assistant", "xpack.inference": "plugins/inference", + "xpack.inventory": "plugins/observability_solution/inventory", "xpack.investigate": "plugins/observability_solution/investigate", "xpack.investigateApp": "plugins/observability_solution/investigate_app", "xpack.kubernetesSecurity": "plugins/kubernetes_security", diff --git a/x-pack/packages/observability/observability_utils/es/queries/exclude_frozen_query.ts b/x-pack/packages/observability/observability_utils/es/queries/exclude_frozen_query.ts new file mode 100644 index 000000000000..f348d925c41c --- /dev/null +++ b/x-pack/packages/observability/observability_utils/es/queries/exclude_frozen_query.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { estypes } from '@elastic/elasticsearch'; + +export function excludeFrozenQuery(): estypes.QueryDslQueryContainer[] { + return [ + { + bool: { + must_not: [ + { + term: { + _tier: 'data_frozen', + }, + }, + ], + }, + }, + ]; +} diff --git a/x-pack/plugins/inference/common/output/create_output_api.ts b/x-pack/plugins/inference/common/output/create_output_api.ts index 0da2a84c53c5..35fc2b364700 100644 --- a/x-pack/plugins/inference/common/output/create_output_api.ts +++ b/x-pack/plugins/inference/common/output/create_output_api.ts @@ -47,12 +47,12 @@ export function createOutputApi(chatCompleteApi: ChatCompleteAPI): OutputAPI { return { id, - type: OutputEventType.OutputComplete, output: event.toolCalls.length && 'arguments' in event.toolCalls[0].function ? event.toolCalls[0].function.arguments : undefined, content: event.content, + type: OutputEventType.OutputComplete, }; }) ); diff --git a/x-pack/plugins/inference/common/output/index.ts b/x-pack/plugins/inference/common/output/index.ts index 12636498ab92..d7522f2cfa52 100644 --- a/x-pack/plugins/inference/common/output/index.ts +++ b/x-pack/plugins/inference/common/output/index.ts @@ -6,8 +6,8 @@ */ import { Observable } from 'rxjs'; +import { ServerSentEventBase } from '@kbn/sse-utils'; import { FromToolSchema, ToolSchema } from '../chat_complete/tool_schema'; -import { InferenceTaskEventBase } from '../inference_task'; import { Message } from '../chat_complete'; export enum OutputEventType { @@ -17,20 +17,25 @@ export enum OutputEventType { type Output = Record | undefined | unknown; -export type OutputUpdateEvent = - InferenceTaskEventBase & { +export type OutputUpdateEvent = ServerSentEventBase< + OutputEventType.OutputUpdate, + { id: TId; content: string; - }; + } +>; export type OutputCompleteEvent< TId extends string = string, TOutput extends Output = Output -> = InferenceTaskEventBase & { - id: TId; - output: TOutput; - content?: string; -}; +> = ServerSentEventBase< + OutputEventType.OutputComplete, + { + id: TId; + output: TOutput; + content: string; + } +>; export type OutputEvent = | OutputUpdateEvent @@ -67,9 +72,9 @@ export function createOutputCompleteEvent { return { - id, type: OutputEventType.OutputComplete, + id, output, - content, + content: content ?? '', }; } diff --git a/x-pack/plugins/inference/common/util/truncate_list.ts b/x-pack/plugins/inference/common/util/truncate_list.ts new file mode 100644 index 000000000000..59b5b1699a3b --- /dev/null +++ b/x-pack/plugins/inference/common/util/truncate_list.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { take } from 'lodash'; + +export function truncateList(values: T[], limit: number): Array { + if (values.length <= limit) { + return values; + } + + return [...take(values, limit), `${values.length - limit} more values`]; +} diff --git a/x-pack/plugins/inference/public/util/http_response_into_observable.ts b/x-pack/plugins/inference/public/util/http_response_into_observable.ts index 53fdf302076a..c63a7bcb3cd1 100644 --- a/x-pack/plugins/inference/public/util/http_response_into_observable.ts +++ b/x-pack/plugins/inference/public/util/http_response_into_observable.ts @@ -5,23 +5,26 @@ * 2.0. */ -import { map, OperatorFunction, pipe, switchMap, tap } from 'rxjs'; -import { InferenceTaskEvent, InferenceTaskEventType } from '../../common/inference_task'; -import { - createObservableFromHttpResponse, - StreamedHttpResponse, -} from './create_observable_from_http_response'; +import { catchError, map, OperatorFunction, pipe, switchMap, tap, throwError } from 'rxjs'; import { createInferenceInternalError, InferenceTaskError, InferenceTaskErrorEvent, } from '../../common/errors'; +import { InferenceTaskEvent, InferenceTaskEventType } from '../../common/inference_task'; +import { + createObservableFromHttpResponse, + StreamedHttpResponse, +} from './create_observable_from_http_response'; export function httpResponseIntoObservable< T extends InferenceTaskEvent = never >(): OperatorFunction { return pipe( switchMap((response) => createObservableFromHttpResponse(response)), + catchError((error) => { + return throwError(() => createInferenceInternalError(error.message)); + }), map((line): T => { try { return JSON.parse(line); diff --git a/x-pack/plugins/inference/server/index.ts b/x-pack/plugins/inference/server/index.ts index e45ae303d283..d02dfec73394 100644 --- a/x-pack/plugins/inference/server/index.ts +++ b/x-pack/plugins/inference/server/index.ts @@ -18,6 +18,7 @@ export { withoutTokenCountEvents } from '../common/chat_complete/without_token_c export { withoutChunkEvents } from '../common/chat_complete/without_chunk_events'; export { withoutOutputUpdateEvents } from '../common/output/without_output_update_events'; +export type { InferenceClient } from './types'; export { naturalLanguageToEsql } from './tasks/nl_to_esql'; export type { InferenceServerSetup, InferenceServerStart }; diff --git a/x-pack/plugins/inference/server/tasks/nl_to_esql/index.ts b/x-pack/plugins/inference/server/tasks/nl_to_esql/index.ts index 2d6fe4ca7dac..92f36c1ccef8 100644 --- a/x-pack/plugins/inference/server/tasks/nl_to_esql/index.ts +++ b/x-pack/plugins/inference/server/tasks/nl_to_esql/index.ts @@ -8,8 +8,7 @@ import type { Logger } from '@kbn/logging'; import { isEmpty, mapValues, pick } from 'lodash'; import { Observable, from, map, merge, of, switchMap } from 'rxjs'; -import { v4 } from 'uuid'; -import { ToolSchema, isChatCompletionMessageEvent } from '../../../common'; +import { ToolSchema, generateFakeToolCallId, isChatCompletionMessageEvent } from '../../../common'; import { ChatCompletionChunkEvent, ChatCompletionMessageEvent, @@ -97,7 +96,7 @@ export function naturalLanguageToEsql({ functions, }, }, - toolCallId: v4().substring(0, 6), + toolCallId: generateFakeToolCallId(), }; return merge( @@ -113,6 +112,7 @@ export function naturalLanguageToEsql({ keywords, requestedDocumentation, }, + content: '', }), client .chatComplete({ diff --git a/x-pack/plugins/inference/tsconfig.json b/x-pack/plugins/inference/tsconfig.json index 34c393d37583..cc81eec1da96 100644 --- a/x-pack/plugins/inference/tsconfig.json +++ b/x-pack/plugins/inference/tsconfig.json @@ -18,22 +18,23 @@ ".storybook/**/*.js" ], "kbn_references": [ - "@kbn/core", "@kbn/i18n", - "@kbn/logging", - "@kbn/core-http-server", - "@kbn/actions-plugin", - "@kbn/config-schema", - "@kbn/esql-validation-autocomplete", + "@kbn/sse-utils", "@kbn/esql-ast", - "@kbn/dev-cli-runner", + "@kbn/esql-validation-autocomplete", + "@kbn/core", + "@kbn/logging", "@kbn/babel-register", + "@kbn/dev-cli-runner", "@kbn/expect", "@kbn/tooling-log", + "@kbn/repo-info", + "@kbn/logging-mocks", + "@kbn/core-http-server", + "@kbn/actions-plugin", + "@kbn/config-schema", "@kbn/es-types", "@kbn/field-types", "@kbn/expressions-plugin", - "@kbn/logging-mocks", - "@kbn/repo-info" ] } diff --git a/x-pack/plugins/observability_solution/apm/public/hooks/create_shared_use_fetcher.tsx b/x-pack/plugins/observability_solution/apm/public/hooks/create_shared_use_fetcher.tsx index 7d7c40830e36..b215ae39f67f 100644 --- a/x-pack/plugins/observability_solution/apm/public/hooks/create_shared_use_fetcher.tsx +++ b/x-pack/plugins/observability_solution/apm/public/hooks/create_shared_use_fetcher.tsx @@ -26,7 +26,7 @@ export function createSharedUseFetcher( ): SharedUseFetcher { const Context = createContext | undefined>(undefined); - const returnValue: SharedUseFetcher = { + const returnValue: SharedUseFetcher = { useFetcherResult: () => { const context = useContext(Context); @@ -34,16 +34,16 @@ export function createSharedUseFetcher( throw new Error('Context was not found'); } - const params = context.params; + const params = 'params' in context ? context.params : undefined; const result = useFetcher( (callApmApi) => { - return callApmApi(...([endpoint, { params }] as Parameters)); + return callApmApi(endpoint, ...((params ? [{ params }] : []) as any)); }, [params] ); - return result as ReturnType['useFetcherResult']>; + return result as ReturnType['useFetcherResult']>; }, Provider: (props) => { const { children } = props; diff --git a/x-pack/plugins/observability_solution/apm/public/services/rest/create_call_apm_api.ts b/x-pack/plugins/observability_solution/apm/public/services/rest/create_call_apm_api.ts index 0ea4b965a1d7..0964517ef18b 100644 --- a/x-pack/plugins/observability_solution/apm/public/services/rest/create_call_apm_api.ts +++ b/x-pack/plugins/observability_solution/apm/public/services/rest/create_call_apm_api.ts @@ -22,12 +22,12 @@ export type APMClientOptions = Omit; +export type APMClient = RouteRepositoryClient['fetch']; export type AutoAbortedAPMClient = RouteRepositoryClient< APMServerRouteRepository, Omit ->; +>['fetch']; export type APIReturnType = ReturnOf< APMServerRouteRepository, @@ -43,7 +43,10 @@ export type APIClientRequestParamsOf = ClientRequ export type AbstractAPMRepository = ServerRouteRepository; -export type AbstractAPMClient = RouteRepositoryClient; +export type AbstractAPMClient = RouteRepositoryClient< + AbstractAPMRepository, + APMClientOptions +>['fetch']; export let callApmApi: APMClient = () => { throw new Error('callApmApi has to be initialized before used. Call createCallApmApi first.'); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.test.ts b/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.test.ts index c5e652fa910d..59a3b0c3fa9e 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.test.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.test.ts @@ -68,7 +68,9 @@ const getRegisterRouteDependencies = () => { }; const initApi = ( - routes: Array> + routes: Array< + ServerRoute + > ) => { const { mocks, dependencies } = getRegisterRouteDependencies(); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/debug_telemetry/route.ts b/x-pack/plugins/observability_solution/apm/server/routes/debug_telemetry/route.ts index ed18f6849fcb..c3ab67150f39 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/debug_telemetry/route.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/debug_telemetry/route.ts @@ -17,7 +17,7 @@ export const debugTelemetryRoute = createApmServerRoute({ options: { tags: ['access:apm', 'access:apm_write'], }, - handler: async (resources): Promise => { + handler: async (resources): Promise => { const { plugins, context } = resources; const coreContext = await context.core; const taskManagerStart = await plugins.taskManager?.start(); @@ -30,6 +30,6 @@ export const debugTelemetryRoute = createApmServerRoute({ APM_TELEMETRY_SAVED_OBJECT_ID ); - return apmTelemetryObject.attributes; + return apmTelemetryObject.attributes as APMTelemetry; }, }); diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/rest/create_call_dataset_quality_api.ts b/x-pack/plugins/observability_solution/dataset_quality/common/rest/create_call_dataset_quality_api.ts index 39e8f8afa0ea..96da7a234d66 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/rest/create_call_dataset_quality_api.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/rest/create_call_dataset_quality_api.ts @@ -26,12 +26,12 @@ export type DatasetQualityClientOptions = Omit< export type DatasetQualityClient = RouteRepositoryClient< DatasetQualityServerRouteRepository, DatasetQualityClientOptions ->; +>['fetch']; export type AutoAbortedClient = RouteRepositoryClient< DatasetQualityServerRouteRepository, Omit ->; +>['fetch']; export type APIReturnType = ReturnOf< DatasetQualityServerRouteRepository, diff --git a/x-pack/plugins/observability_solution/entity_manager/public/lib/entity_client.ts b/x-pack/plugins/observability_solution/entity_manager/public/lib/entity_client.ts index c4a2897cbc47..dc22a0b991b0 100644 --- a/x-pack/plugins/observability_solution/entity_manager/public/lib/entity_client.ts +++ b/x-pack/plugins/observability_solution/entity_manager/public/lib/entity_client.ts @@ -20,7 +20,7 @@ import { import type { EntityManagerRouteRepository } from '../../server'; import { EntityManagerUnauthorizedError } from './errors'; -type EntityManagerRepositoryClient = RouteRepositoryClient; +type EntityManagerRepositoryClient = RouteRepositoryClient; type QueryParamOf = Exclude['query']; @@ -36,7 +36,7 @@ type CreateEntityDefinitionQuery = QueryParamOf< >; export class EntityClient { - public readonly repositoryClient: EntityManagerRepositoryClient; + public readonly repositoryClient: EntityManagerRepositoryClient['fetch']; constructor(core: CoreStart | CoreSetup) { this.repositoryClient = createRepositoryClient(core).fetch; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/client/index.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/client/index.ts new file mode 100644 index 000000000000..a2578e1ee09e --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/client/index.ts @@ -0,0 +1,28 @@ +/* + * 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 { IScopedClusterClient, SavedObjectsClientContract } from '@kbn/core/server'; +import { findEntityDefinitions } from '../entities/find_entity_definition'; +import type { EntityDefinitionWithState } from '../entities/types'; + +export class EntityManagerClient { + constructor( + private readonly esClient: IScopedClusterClient, + private readonly soClient: SavedObjectsClientContract + ) {} + + findEntityDefinitions({ page, perPage }: { page?: number; perPage?: number } = {}): Promise< + EntityDefinitionWithState[] + > { + return findEntityDefinitions({ + esClient: this.esClient.asCurrentUser, + soClient: this.soClient, + page, + perPage, + }); + } +} diff --git a/x-pack/plugins/observability_solution/entity_manager/server/plugin.ts b/x-pack/plugins/observability_solution/entity_manager/server/plugin.ts index 3adfe5b9167e..101fdde95c9d 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/plugin.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/plugin.ts @@ -30,8 +30,11 @@ import { EntityManagerServerSetup, } from './types'; -export type EntityManagerServerPluginSetup = ReturnType; -export type EntityManagerServerPluginStart = ReturnType; +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface EntityManagerServerPluginSetup {} +export interface EntityManagerServerPluginStart { + getScopedClient: (options: { request: KibanaRequest }) => Promise; +} export const config: PluginConfigDescriptor = { schema: configSchema, @@ -56,7 +59,10 @@ export class EntityManagerServerPlugin this.logger = context.logger.get(); } - public setup(core: CoreSetup, plugins: EntityManagerPluginSetupDependencies) { + public setup( + core: CoreSetup, + plugins: EntityManagerPluginSetupDependencies + ): EntityManagerServerPluginSetup { core.savedObjects.registerType(entityDefinition); core.savedObjects.registerType(EntityDiscoveryApiKeyType); plugins.encryptedSavedObjects.registerType({ @@ -76,9 +82,7 @@ export class EntityManagerServerPlugin server: this.server, getScopedClient: async ({ request }: { request: KibanaRequest }) => { const [coreStart] = await core.getStartServices(); - const esClient = coreStart.elasticsearch.client.asScoped(request).asCurrentUser; - const soClient = coreStart.savedObjects.getScopedClient(request); - return new EntityClient({ esClient, soClient, logger: this.logger }); + return this.getScopedClient({ request, coreStart }); }, }, core, @@ -88,7 +92,22 @@ export class EntityManagerServerPlugin return {}; } - public start(core: CoreStart, plugins: EntityManagerPluginStartDependencies) { + private async getScopedClient({ + request, + coreStart, + }: { + request: KibanaRequest; + coreStart: CoreStart; + }) { + const esClient = coreStart.elasticsearch.client.asScoped(request).asCurrentUser; + const soClient = coreStart.savedObjects.getScopedClient(request); + return new EntityClient({ esClient, soClient, logger: this.logger }); + } + + public start( + core: CoreStart, + plugins: EntityManagerPluginStartDependencies + ): EntityManagerServerPluginStart { if (this.server) { this.server.core = core; this.server.isServerless = core.elasticsearch.getCapabilities().serverless; @@ -114,7 +133,11 @@ export class EntityManagerServerPlugin }) .catch((err) => this.logger.error(err)); - return {}; + return { + getScopedClient: async ({ request }: { request: KibanaRequest }) => { + return this.getScopedClient({ request, coreStart: core }); + }, + }; } public stop() {} diff --git a/x-pack/plugins/observability_solution/entity_manager/tsconfig.json b/x-pack/plugins/observability_solution/entity_manager/tsconfig.json index 537e31e9bda9..7d1c0f378854 100644 --- a/x-pack/plugins/observability_solution/entity_manager/tsconfig.json +++ b/x-pack/plugins/observability_solution/entity_manager/tsconfig.json @@ -4,7 +4,7 @@ "outDir": "target/types" }, "include": [ - "../../../../typings/**/*", + "../../../typings/**/*", "common/**/*", "server/**/*", "public/**/*", @@ -12,25 +12,25 @@ ], "exclude": ["target/**/*"], "kbn_references": [ - "@kbn/core-plugins-server", - "@kbn/core", "@kbn/config-schema", - "@kbn/core-http-server", - "@kbn/core-elasticsearch-client-server-mocks", - "@kbn/datemath", + "@kbn/entities-schema", + "@kbn/core", + "@kbn/core-plugins-server", + "@kbn/server-route-repository-client", "@kbn/logging", + "@kbn/core-http-server", + "@kbn/security-plugin", + "@kbn/es-query", "@kbn/core-elasticsearch-server", "@kbn/core-saved-objects-api-server", + "@kbn/core-elasticsearch-client-server-mocks", "@kbn/core-saved-objects-api-server-mocks", - "@kbn/entities-schema", - "@kbn/es-query", - "@kbn/security-plugin", - "@kbn/encrypted-saved-objects-plugin", "@kbn/logging-mocks", - "@kbn/licensing-plugin", - "@kbn/server-route-repository-client", + "@kbn/datemath", "@kbn/server-route-repository", "@kbn/zod", "@kbn/zod-helpers", + "@kbn/encrypted-saved-objects-plugin", + "@kbn/licensing-plugin", ] } diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx b/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx new file mode 100644 index 000000000000..1c5e2fd1f205 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx @@ -0,0 +1,31 @@ +/* + * 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 { coreMock } from '@kbn/core/public/mocks'; +import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; +import type { InferencePublicStart } from '@kbn/inference-plugin/public'; +import type { InventoryKibanaContext } from '../public/hooks/use_kibana'; + +export function getMockInventoryContext(): InventoryKibanaContext { + const core = coreMock.createStart(); + + return { + core, + dependencies: { + start: { + observabilityShared: {} as unknown as ObservabilitySharedPluginStart, + inference: {} as unknown as InferencePublicStart, + }, + }, + services: { + inventoryAPIClient: { + fetch: jest.fn(), + stream: jest.fn(), + }, + }, + }; +} diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/jest_setup.js b/x-pack/plugins/observability_solution/inventory/.storybook/jest_setup.js new file mode 100644 index 000000000000..32071b8aa3f6 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/.storybook/jest_setup.js @@ -0,0 +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. + */ + +import { setGlobalConfig } from '@storybook/testing-react'; +import * as globalStorybookConfig from './preview'; + +setGlobalConfig(globalStorybookConfig); diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/main.js b/x-pack/plugins/observability_solution/inventory/.storybook/main.js new file mode 100644 index 000000000000..86b48c32f103 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/.storybook/main.js @@ -0,0 +1,8 @@ +/* + * 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. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/preview.js b/x-pack/plugins/observability_solution/inventory/.storybook/preview.js new file mode 100644 index 000000000000..c8155e9c3d92 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/.storybook/preview.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiThemeProviderDecorator } from '@kbn/kibana-react-plugin/common'; +import * as jest from 'jest-mock'; + +window.jest = jest; + +export const decorators = [EuiThemeProviderDecorator]; diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/storybook_decorator.tsx b/x-pack/plugins/observability_solution/inventory/.storybook/storybook_decorator.tsx new file mode 100644 index 000000000000..87589988492d --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/.storybook/storybook_decorator.tsx @@ -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 React, { ComponentType, useMemo } from 'react'; +import { InventoryContextProvider } from '../public/components/inventory_context_provider'; +import { getMockInventoryContext } from './get_mock_inventory_context'; + +export function KibanaReactStorybookDecorator(Story: ComponentType) { + const context = useMemo(() => getMockInventoryContext(), []); + return ( + + + + ); +} diff --git a/x-pack/plugins/observability_solution/inventory/README.md b/x-pack/plugins/observability_solution/inventory/README.md new file mode 100644 index 000000000000..446b85483402 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/README.md @@ -0,0 +1,3 @@ +# Inventory + +Home of the Inventory plugin, which renders the... _inventory_. diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts new file mode 100644 index 000000000000..af0e5c82b978 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface EntityTypeDefinition { + type: string; + label: string; + icon: string; + count: number; +} + +export interface EntityDefinition { + type: string; + field: string; + filter?: string; + index: string[]; +} diff --git a/x-pack/plugins/observability_solution/inventory/jest.config.js b/x-pack/plugins/observability_solution/inventory/jest.config.js new file mode 100644 index 000000000000..4e4450567243 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/jest.config.js @@ -0,0 +1,25 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: [ + '/x-pack/plugins/observability_solution/inventory/public', + '/x-pack/plugins/observability_solution/inventory/common', + '/x-pack/plugins/observability_solution/inventory/server', + ], + setupFiles: [ + '/x-pack/plugins/observability_solution/inventory/.storybook/jest_setup.js', + ], + collectCoverage: true, + collectCoverageFrom: [ + '/x-pack/plugins/observability_solution/inventory/{public,common,server}/**/*.{js,ts,tsx}', + ], + + coverageReporters: ['html'], +}; diff --git a/x-pack/plugins/observability_solution/inventory/kibana.jsonc b/x-pack/plugins/observability_solution/inventory/kibana.jsonc new file mode 100644 index 000000000000..ced0f412ab93 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/kibana.jsonc @@ -0,0 +1,22 @@ +{ + "type": "plugin", + "id": "@kbn/inventory-plugin", + "owner": "@elastic/obs-ux-infra_services-team", + "plugin": { + "id": "inventory", + "server": true, + "browser": true, + "configPath": ["xpack", "inventory"], + "requiredPlugins": [ + "observabilityShared", + "entityManager", + "inference", + "dataViews" + ], + "requiredBundles": [ + "kibanaReact" + ], + "optionalPlugins": [], + "extraPublicDirs": [] + } +} diff --git a/x-pack/plugins/observability_solution/inventory/public/api/index.tsx b/x-pack/plugins/observability_solution/inventory/public/api/index.tsx new file mode 100644 index 000000000000..4600813a1f54 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/api/index.tsx @@ -0,0 +1,50 @@ +/* + * 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 { CoreSetup, CoreStart, HttpFetchOptions } from '@kbn/core/public'; +import type { + ClientRequestParamsOf, + ReturnOf, + RouteRepositoryClient, +} from '@kbn/server-route-repository'; +import { createRepositoryClient } from '@kbn/server-route-repository-client'; +import type { InventoryServerRouteRepository } from '../../server'; + +type FetchOptions = Omit & { + body?: any; +}; + +export type InventoryAPIClientOptions = Omit< + FetchOptions, + 'query' | 'body' | 'pathname' | 'signal' +> & { + signal: AbortSignal | null; +}; + +export type InventoryAPIClient = RouteRepositoryClient< + InventoryServerRouteRepository, + InventoryAPIClientOptions +>; + +export type AutoAbortedInventoryAPIClient = RouteRepositoryClient< + InventoryServerRouteRepository, + Omit +>; + +export type InventoryAPIEndpoint = keyof InventoryServerRouteRepository; + +export type APIReturnType = ReturnOf< + InventoryServerRouteRepository, + TEndpoint +>; + +export type InventoryAPIClientRequestParamsOf = + ClientRequestParamsOf; + +export function createCallInventoryAPI(core: CoreStart | CoreSetup): InventoryAPIClient { + return createRepositoryClient(core); +} diff --git a/x-pack/plugins/observability_solution/inventory/public/application.tsx b/x-pack/plugins/observability_solution/inventory/public/application.tsx new file mode 100644 index 000000000000..5b235c15e7c4 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/application.tsx @@ -0,0 +1,67 @@ +/* + * 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 { CoreStart, CoreTheme } from '@kbn/core/public'; +import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import type { History } from 'history'; +import React, { useMemo } from 'react'; +import type { Observable } from 'rxjs'; +import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import type { InventoryStartDependencies } from './types'; +import { inventoryRouter } from './routes/config'; +import { InventoryKibanaContext } from './hooks/use_kibana'; +import { InventoryServices } from './services/types'; +import { InventoryContextProvider } from './components/inventory_context_provider'; + +function Application({ + coreStart, + history, + pluginsStart, + theme$, + services, +}: { + coreStart: CoreStart; + history: History; + pluginsStart: InventoryStartDependencies; + theme$: Observable; + services: InventoryServices; +}) { + const theme = useMemo(() => { + return { theme$ }; + }, [theme$]); + + const context: InventoryKibanaContext = useMemo( + () => ({ + core: coreStart, + dependencies: { + start: pluginsStart, + }, + services, + }), + [coreStart, pluginsStart, services] + ); + + return ( + + + + + + + + + + + + ); +} + +export { Application }; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_type_list/index.stories.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_type_list/index.stories.tsx new file mode 100644 index 000000000000..570622406c9a --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_type_list/index.stories.tsx @@ -0,0 +1,88 @@ +/* + * 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 { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { mergePlainObjects } from '@kbn/investigate-plugin/common'; +import { EntityTypeListBase as Component } from '.'; +import { KibanaReactStorybookDecorator } from '../../../.storybook/storybook_decorator'; + +interface Args { + props: Omit, 'onLockAllClick' | 'onUnlockAllClick'>; +} + +type StoryMeta = Meta; +type Story = StoryObj; + +const meta: StoryMeta = { + component: Component, + title: 'app/Molecules/EntityTypeList', + decorators: [KibanaReactStorybookDecorator], +}; + +export default meta; + +const defaultStory: Story = { + args: { + props: { + definitions: [], + loading: true, + }, + }, + render: function Render(args) { + return ( +
+ +
+ ); + }, +}; + +export const Default: Story = { + ...defaultStory, + args: { + props: mergePlainObjects(defaultStory.args!.props!, { + loading: false, + definitions: [ + { + icon: 'node', + label: 'Services', + type: 'service', + count: 9, + }, + { + icon: 'pipeNoBreaks', + label: 'Datasets', + type: 'dataset', + count: 11, + }, + ], + }), + }, + name: 'default', +}; + +export const Empty: Story = { + ...defaultStory, + args: { + props: mergePlainObjects(defaultStory.args!.props!, { + definitions: [], + loading: false, + }), + }, + name: 'empty', +}; + +export const Loading: Story = { + ...defaultStory, + args: { + props: mergePlainObjects(defaultStory.args!.props!, { + loading: true, + }), + }, + name: 'loading', +}; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_type_list/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_type_list/index.tsx new file mode 100644 index 000000000000..47488f23f325 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_type_list/index.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { useAbortableAsync } from '@kbn/observability-utils/hooks/use_abortable_async'; +import { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiLoadingSpinner, + EuiText, +} from '@elastic/eui'; +import { useKibana } from '../../hooks/use_kibana'; +import { EntityTypeDefinition } from '../../../common/entities'; +import { useInventoryRouter } from '../../hooks/use_inventory_router'; + +export function EntityTypeListItem({ + href, + icon, + label, + count, +}: { + href: string; + icon: string; + label: string; + count: number; +}) { + return ( + + + + + + + {label} + + + {count} + + + + ); +} + +export function EntityTypeListBase({ + definitions, + loading, + error, +}: { + loading?: boolean; + definitions?: EntityTypeDefinition[]; + error?: Error; +}) { + const router = useInventoryRouter(); + if (loading) { + return ; + } + + return ( + + {definitions?.map((definition) => { + return ( + + ); + })} + + ); +} + +export function EntityTypeList() { + const { + services: { inventoryAPIClient }, + } = useKibana(); + + const { value, loading, error } = useAbortableAsync( + ({ signal }) => { + return inventoryAPIClient.fetch('GET /internal/inventory/entity_types', { + signal, + }); + }, + [inventoryAPIClient] + ); + + return ; +} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/inventory_context_provider/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/inventory_context_provider/index.tsx new file mode 100644 index 000000000000..068086dd17cc --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/inventory_context_provider/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import type { InventoryKibanaContext } from '../../hooks/use_kibana'; + +export function InventoryContextProvider({ + context, + children, +}: { + context: InventoryKibanaContext; + children: React.ReactNode; +}) { + return {children}; +} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/index.tsx new file mode 100644 index 000000000000..386df9a51cae --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/index.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiFlexGroup, EuiPanel, EuiTitle } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { i18n } from '@kbn/i18n'; +import { useTheme } from '@kbn/observability-utils/hooks/use_theme'; +import React from 'react'; +import { useKibana } from '../../hooks/use_kibana'; +import { EntityTypeList } from '../entity_type_list'; + +export function InventoryPageTemplate({ children }: { children: React.ReactNode }) { + const { + dependencies: { + start: { observabilityShared }, + }, + } = useKibana(); + + const { PageTemplate } = observabilityShared.navigation; + + const theme = useTheme(); + + return ( + + + + + +

+ {i18n.translate('xpack.inventory.inventoryPageHeaderLabel', { + defaultMessage: 'Inventory', + })} +

+
+ + + +
+
+ + + {children} + +
+
+ ); +} diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_inventory_params.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_inventory_params.ts new file mode 100644 index 000000000000..e39506a6fabe --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_inventory_params.ts @@ -0,0 +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. + */ +import { type PathsOf, type TypeOf, useParams } from '@kbn/typed-react-router-config'; +import type { InventoryRoutes } from '../routes/config'; + +export function useInventoryParams>( + path: TPath +): TypeOf { + return useParams(path)! as TypeOf; +} diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_inventory_route_path.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_inventory_route_path.ts new file mode 100644 index 000000000000..9edb2a7da75d --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_inventory_route_path.ts @@ -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 { PathsOf, useRoutePath } from '@kbn/typed-react-router-config'; +import type { InventoryRoutes } from '../routes/config'; + +export function useInventoryRoutePath() { + const path = useRoutePath(); + + return path as PathsOf; +} diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_inventory_router.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_inventory_router.ts new file mode 100644 index 000000000000..5c968eaf852e --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_inventory_router.ts @@ -0,0 +1,55 @@ +/* + * 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 type { InventoryRouter, InventoryRoutes } from '../routes/config'; +import { inventoryRouter } from '../routes/config'; +import { useKibana } from './use_kibana'; + +interface StatefulInventoryRouter extends InventoryRouter { + push>( + path: T, + ...params: TypeAsArgs> + ): void; + replace>( + path: T, + ...params: TypeAsArgs> + ): void; +} + +export function useInventoryRouter(): StatefulInventoryRouter { + const { + core: { + http, + application: { navigateToApp }, + }, + } = useKibana(); + + const link = (...args: any[]) => { + // @ts-expect-error + return inventoryRouter.link(...args); + }; + + return useMemo( + () => ({ + ...inventoryRouter, + push: (...args) => { + const next = link(...args); + navigateToApp('inventory', { path: next, replace: false }); + }, + replace: (path, ...args) => { + const next = link(path, ...args); + navigateToApp('inventory', { path: next, replace: true }); + }, + link: (path, ...args) => { + return http.basePath.prepend('/app/observability/inventory' + link(path, ...args)); + }, + }), + [navigateToApp, http.basePath] + ); +} diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_kibana.tsx b/x-pack/plugins/observability_solution/inventory/public/hooks/use_kibana.tsx new file mode 100644 index 000000000000..2b75cc513b24 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_kibana.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { CoreStart } from '@kbn/core/public'; +import type { InventoryStartDependencies } from '../types'; +import type { InventoryServices } from '../services/types'; + +export interface InventoryKibanaContext { + core: CoreStart; + dependencies: { start: InventoryStartDependencies }; + services: InventoryServices; +} + +const useTypedKibana = () => { + return useKibana().services; +}; + +export { useTypedKibana as useKibana }; diff --git a/x-pack/plugins/observability_solution/inventory/public/index.ts b/x-pack/plugins/observability_solution/inventory/public/index.ts new file mode 100644 index 000000000000..620d853e3475 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/index.ts @@ -0,0 +1,26 @@ +/* + * 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 { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; + +import { InventoryPlugin } from './plugin'; +import type { + InventoryPublicSetup, + InventoryPublicStart, + InventorySetupDependencies, + InventoryStartDependencies, + ConfigSchema, +} from './types'; + +export type { InventoryPublicSetup, InventoryPublicStart }; + +export const plugin: PluginInitializer< + InventoryPublicSetup, + InventoryPublicStart, + InventorySetupDependencies, + InventoryStartDependencies +> = (pluginInitializerContext: PluginInitializerContext) => + new InventoryPlugin(pluginInitializerContext); diff --git a/x-pack/plugins/observability_solution/inventory/public/plugin.tsx b/x-pack/plugins/observability_solution/inventory/public/plugin.tsx new file mode 100644 index 000000000000..355309939ea6 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/plugin.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { + AppMountParameters, + APP_WRAPPER_CLASS, + CoreSetup, + CoreStart, + DEFAULT_APP_CATEGORIES, + Plugin, + PluginInitializerContext, +} from '@kbn/core/public'; +import type { Logger } from '@kbn/logging'; +import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; +import { css } from '@emotion/css'; +import type { + ConfigSchema, + InventoryPublicSetup, + InventoryPublicStart, + InventorySetupDependencies, + InventoryStartDependencies, +} from './types'; +import { InventoryServices } from './services/types'; +import { createCallInventoryAPI } from './api'; + +export class InventoryPlugin + implements + Plugin< + InventoryPublicSetup, + InventoryPublicStart, + InventorySetupDependencies, + InventoryStartDependencies + > +{ + logger: Logger; + + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + setup( + coreSetup: CoreSetup, + pluginsSetup: InventorySetupDependencies + ): InventoryPublicSetup { + const inventoryAPIClient = createCallInventoryAPI(coreSetup); + + coreSetup.application.register({ + id: INVENTORY_APP_ID, + title: i18n.translate('xpack.inventory.appTitle', { + defaultMessage: 'Inventory', + }), + euiIconType: 'logoObservability', + appRoute: '/app/observability/inventory', + category: DEFAULT_APP_CATEGORIES.observability, + visibleIn: ['sideNav'], + order: 8001, + deepLinks: [ + { + id: 'inventory', + title: i18n.translate('xpack.inventory.inventoryDeepLinkTitle', { + defaultMessage: 'Inventory', + }), + path: '/', + }, + ], + mount: async (appMountParameters: AppMountParameters) => { + // Load application bundle and Get start services + const [{ Application }, [coreStart, pluginsStart]] = await Promise.all([ + import('./application'), + coreSetup.getStartServices(), + ]); + + const services: InventoryServices = { + inventoryAPIClient, + }; + + ReactDOM.render( + , + appMountParameters.element + ); + + const appWrapperClassName = css` + overflow: auto; + `; + + const appWrapperElement = document.getElementsByClassName(APP_WRAPPER_CLASS)[1]; + + appWrapperElement.classList.add(appWrapperClassName); + + return () => { + ReactDOM.unmountComponentAtNode(appMountParameters.element); + appWrapperElement.classList.remove(appWrapperClassName); + }; + }, + }); + + return {}; + } + + start(coreStart: CoreStart, pluginsStart: InventoryStartDependencies): InventoryPublicStart { + return {}; + } +} diff --git a/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx b/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx new file mode 100644 index 000000000000..11d9d4836d98 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx @@ -0,0 +1,41 @@ +/* + * 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'; +import { createRouter, Outlet } from '@kbn/typed-react-router-config'; +import React from 'react'; +import { InventoryPageTemplate } from '../components/inventory_page_template'; + +/** + * The array of route definitions to be used when the application + * creates the routes. + */ +const inventoryRoutes = { + '/': { + element: ( + + + + ), + children: { + '/{type}': { + element: <>, + params: t.type({ + path: t.type({ type: t.string }), + }), + }, + '/': { + element: <>, + }, + }, + }, +}; + +export type InventoryRoutes = typeof inventoryRoutes; + +export const inventoryRouter = createRouter(inventoryRoutes); + +export type InventoryRouter = typeof inventoryRouter; diff --git a/x-pack/plugins/observability_solution/inventory/public/services/types.ts b/x-pack/plugins/observability_solution/inventory/public/services/types.ts new file mode 100644 index 000000000000..008437fbf889 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/services/types.ts @@ -0,0 +1,12 @@ +/* + * 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 { InventoryAPIClient } from '../api'; + +export interface InventoryServices { + inventoryAPIClient: InventoryAPIClient; +} diff --git a/x-pack/plugins/observability_solution/inventory/public/types.ts b/x-pack/plugins/observability_solution/inventory/public/types.ts new file mode 100644 index 000000000000..66c0789650a0 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/types.ts @@ -0,0 +1,29 @@ +/* + * 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 { + ObservabilitySharedPluginStart, + ObservabilitySharedPluginSetup, +} from '@kbn/observability-shared-plugin/public'; +import type { InferencePublicStart, InferencePublicSetup } from '@kbn/inference-plugin/public'; + +/* eslint-disable @typescript-eslint/no-empty-interface*/ + +export interface ConfigSchema {} + +export interface InventorySetupDependencies { + observabilityShared: ObservabilitySharedPluginSetup; + inference: InferencePublicSetup; +} + +export interface InventoryStartDependencies { + observabilityShared: ObservabilitySharedPluginStart; + inference: InferencePublicStart; +} + +export interface InventoryPublicSetup {} + +export interface InventoryPublicStart {} diff --git a/x-pack/plugins/observability_solution/inventory/server/config.ts b/x-pack/plugins/observability_solution/inventory/server/config.ts new file mode 100644 index 000000000000..2d6d7604b40e --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/config.ts @@ -0,0 +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. + */ + +import { schema, type TypeOf } from '@kbn/config-schema'; + +export const config = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); + +export type InventoryConfig = TypeOf; diff --git a/x-pack/plugins/observability_solution/inventory/server/index.ts b/x-pack/plugins/observability_solution/inventory/server/index.ts new file mode 100644 index 000000000000..ad878918bad4 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/index.ts @@ -0,0 +1,37 @@ +/* + * 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 { + PluginConfigDescriptor, + PluginInitializer, + PluginInitializerContext, +} from '@kbn/core/server'; +import type { InventoryConfig } from './config'; +import { InventoryPlugin } from './plugin'; +import type { + InventoryServerSetup, + InventoryServerStart, + InventorySetupDependencies, + InventoryStartDependencies, +} from './types'; + +export type { InventoryServerRouteRepository } from './routes/get_global_inventory_route_repository'; + +export type { InventoryServerSetup, InventoryServerStart }; + +import { config as configSchema } from './config'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, +}; + +export const plugin: PluginInitializer< + InventoryServerSetup, + InventoryServerStart, + InventorySetupDependencies, + InventoryStartDependencies +> = async (pluginInitializerContext: PluginInitializerContext) => + new InventoryPlugin(pluginInitializerContext); diff --git a/x-pack/plugins/observability_solution/inventory/server/plugin.ts b/x-pack/plugins/observability_solution/inventory/server/plugin.ts new file mode 100644 index 000000000000..1ac928b72cdb --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/plugin.ts @@ -0,0 +1,64 @@ +/* + * 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 { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; +import { mapValues } from 'lodash'; +import { registerServerRoutes } from './routes/register_routes'; +import { InventoryRouteHandlerResources } from './routes/types'; +import type { + ConfigSchema, + InventoryServerSetup, + InventoryServerStart, + InventorySetupDependencies, + InventoryStartDependencies, +} from './types'; + +export class InventoryPlugin + implements + Plugin< + InventoryServerSetup, + InventoryServerStart, + InventorySetupDependencies, + InventoryStartDependencies + > +{ + logger: Logger; + + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + setup( + coreSetup: CoreSetup, + pluginsSetup: InventorySetupDependencies + ): InventoryServerSetup { + const startServicesPromise = coreSetup + .getStartServices() + .then(([_coreStart, pluginsStart]) => pluginsStart); + + registerServerRoutes({ + core: coreSetup, + logger: this.logger, + dependencies: { + plugins: mapValues(pluginsSetup, (value, key) => { + return { + start: () => + startServicesPromise.then( + (startServices) => startServices[key as keyof typeof startServices] + ), + setup: () => value, + }; + }) as unknown as InventoryRouteHandlerResources['plugins'], + }, + }); + return {}; + } + + start(core: CoreStart, pluginsStart: InventoryStartDependencies): InventoryServerStart { + return {}; + } +} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/create_inventory_server_route.ts b/x-pack/plugins/observability_solution/inventory/server/routes/create_inventory_server_route.ts new file mode 100644 index 000000000000..61c00e3b417a --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/create_inventory_server_route.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createServerRouteFactory } from '@kbn/server-route-repository'; +import type { InventoryRouteCreateOptions, InventoryRouteHandlerResources } from './types'; + +export const createInventoryServerRoute = createServerRouteFactory< + InventoryRouteHandlerResources, + InventoryRouteCreateOptions +>(); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts new file mode 100644 index 000000000000..0622ed32ac9d --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import type { EntityTypeDefinition } from '../../../common/entities'; +import { createInventoryServerRoute } from '../create_inventory_server_route'; + +export const listEntityTypesRoute = createInventoryServerRoute({ + endpoint: 'GET /internal/inventory/entity_types', + options: { + tags: ['access:inventory'], + }, + handler: async ({ plugins, request }): Promise<{ definitions: EntityTypeDefinition[] }> => { + return { + definitions: [ + { + label: i18n.translate('xpack.inventory.entityTypeLabels.datasets', { + defaultMessage: 'Datasets', + }), + icon: 'pipeNoBreaks', + type: 'dataset', + count: 0, + }, + ], + }; + }, +}); + +export const entitiesRoutes = { + ...listEntityTypesRoute, +}; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/get_global_inventory_route_repository.ts b/x-pack/plugins/observability_solution/inventory/server/routes/get_global_inventory_route_repository.ts new file mode 100644 index 000000000000..190178cb25a9 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/get_global_inventory_route_repository.ts @@ -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 { entitiesRoutes } from './entities/route'; + +export function getGlobalInventoryServerRouteRepository() { + return { + ...entitiesRoutes, + }; +} + +export type InventoryServerRouteRepository = ReturnType< + typeof getGlobalInventoryServerRouteRepository +>; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/inventory/server/routes/register_routes.ts new file mode 100644 index 000000000000..d6b8223c8ca7 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/register_routes.ts @@ -0,0 +1,28 @@ +/* + * 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 { CoreSetup } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; +import { registerRoutes } from '@kbn/server-route-repository'; +import { getGlobalInventoryServerRouteRepository } from './get_global_inventory_route_repository'; +import type { InventoryRouteHandlerResources } from './types'; + +export function registerServerRoutes({ + core, + logger, + dependencies, +}: { + core: CoreSetup; + logger: Logger; + dependencies: Omit; +}) { + registerRoutes({ + core, + logger, + repository: getGlobalInventoryServerRouteRepository(), + dependencies, + }); +} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/types.ts b/x-pack/plugins/observability_solution/inventory/server/routes/types.ts new file mode 100644 index 000000000000..397710509dab --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/types.ts @@ -0,0 +1,39 @@ +/* + * 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 { CustomRequestHandlerContext, KibanaRequest } from '@kbn/core/server'; +import type { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server/types'; +import type { Logger } from '@kbn/logging'; +import type { InventorySetupDependencies, InventoryStartDependencies } from '../types'; + +export type InventoryRequestHandlerContext = CustomRequestHandlerContext<{ + licensing: Pick; +}>; + +export interface InventoryRouteHandlerResources { + request: KibanaRequest; + context: InventoryRequestHandlerContext; + logger: Logger; + plugins: { + [key in keyof InventorySetupDependencies]: { + setup: Required[key]; + }; + } & { + [key in keyof InventoryStartDependencies]: { + start: () => Promise[key]>; + }; + }; +} + +export interface InventoryRouteCreateOptions { + options: { + timeout?: { + idleSocket?: number; + }; + tags: Array<'access:inventory'>; + }; +} diff --git a/x-pack/plugins/observability_solution/inventory/server/types.ts b/x-pack/plugins/observability_solution/inventory/server/types.ts new file mode 100644 index 000000000000..a58bd62fe57a --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/types.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + EntityManagerServerPluginStart, + EntityManagerServerPluginSetup, +} from '@kbn/entityManager-plugin/server'; +import type { InferenceServerSetup, InferenceServerStart } from '@kbn/inference-plugin/server'; +import type { + DataViewsServerPluginSetup, + DataViewsServerPluginStart, +} from '@kbn/data-views-plugin/server'; +/* eslint-disable @typescript-eslint/no-empty-interface*/ + +export interface ConfigSchema {} + +export interface InventorySetupDependencies { + entityManager: EntityManagerServerPluginSetup; + inference: InferenceServerSetup; + dataViews: DataViewsServerPluginSetup; +} + +export interface InventoryStartDependencies { + entityManager: EntityManagerServerPluginStart; + inference: InferenceServerStart; + dataViews: DataViewsServerPluginStart; +} + +export interface InventoryServerSetup {} + +export interface InventoryClient {} + +export interface InventoryServerStart {} diff --git a/x-pack/plugins/observability_solution/inventory/tsconfig.json b/x-pack/plugins/observability_solution/inventory/tsconfig.json new file mode 100644 index 000000000000..89fdd2e8fdf0 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "../../../typings/**/*", + "common/**/*", + "public/**/*", + "typings/**/*", + "public/**/*.json", + "server/**/*", + ".storybook/**/*" + ], + "exclude": [ + "target/**/*", + ".storybook/**/*.js" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/logging", + "@kbn/config-schema", + "@kbn/observability-shared-plugin", + "@kbn/server-route-repository", + "@kbn/shared-ux-link-redirect-app", + "@kbn/typed-react-router-config", + "@kbn/investigate-plugin", + "@kbn/observability-utils", + "@kbn/kibana-react-plugin", + "@kbn/i18n", + "@kbn/deeplinks-observability", + "@kbn/entityManager-plugin", + "@kbn/licensing-plugin", + "@kbn/inference-plugin", + "@kbn/data-views-plugin", + "@kbn/server-route-repository-client", + "@kbn/react-kibana-context-render", + ] +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/api/index.ts b/x-pack/plugins/observability_solution/investigate_app/public/api/index.ts index 8421a212ac1a..af02f4a15e74 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/api/index.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/api/index.ts @@ -28,12 +28,12 @@ export type InvestigateAppAPIClientOptions = Omit< export type InvestigateAppAPIClient = RouteRepositoryClient< InvestigateAppServerRouteRepository, InvestigateAppAPIClientOptions ->; +>['fetch']; export type AutoAbortedInvestigateAppAPIClient = RouteRepositoryClient< InvestigateAppServerRouteRepository, Omit ->; +>['fetch']; export type InvestigateAppAPIEndpoint = keyof InvestigateAppServerRouteRepository; diff --git a/x-pack/plugins/observability_solution/observability/kibana.jsonc b/x-pack/plugins/observability_solution/observability/kibana.jsonc index 0a2fa073c123..3697fa0ff628 100644 --- a/x-pack/plugins/observability_solution/observability/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability/kibana.jsonc @@ -52,7 +52,8 @@ "serverless", "guidedOnboarding", "observabilityAIAssistant", - "investigate" + "investigate", + "inventory" ], "requiredBundles": [ "data", diff --git a/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts b/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts index 9a064af35d3e..67062d223023 100644 --- a/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts +++ b/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import type { NavigationTreeDefinition } from '@kbn/core-chrome-browser'; import type { AddSolutionNavigationArg } from '@kbn/navigation-plugin/public'; import { of } from 'rxjs'; +import type { ObservabilityPublicPluginsStart } from './plugin'; const title = i18n.translate( 'xpack.observability.obltNav.headerSolutionSwitcher.obltSolutionTitle', @@ -17,474 +18,501 @@ const title = i18n.translate( ); const icon = 'logoObservability'; -const navTree: NavigationTreeDefinition = { - body: [ - { - type: 'navGroup', - id: 'observability_project_nav', - title, - icon, - defaultIsCollapsed: false, - isCollapsible: false, - breadcrumbStatus: 'hidden', - children: [ - { - link: 'observability-overview', - }, - { - link: 'discover', - renderAs: 'item', - children: [ - { - // This is to show "observability-log-explorer" breadcrumbs when navigating from "discover" to "log explorer" - link: 'observability-logs-explorer', - }, - ], - }, - { - link: 'dashboards', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/dashboards')); +export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { + const navTree: NavigationTreeDefinition = { + body: [ + { + type: 'navGroup', + id: 'observability_project_nav', + title, + icon, + defaultIsCollapsed: false, + isCollapsible: false, + breadcrumbStatus: 'hidden', + children: [ + { + link: 'observability-overview', }, - }, - { - link: 'observability-overview:alerts', - }, - { - link: 'observability-overview:cases', - renderAs: 'item', - children: [ - { - link: 'observability-overview:cases_configure', - }, - { - link: 'observability-overview:cases_create', - }, - ], - }, - { - link: 'slo', - }, - { - id: 'aiMl', - title: i18n.translate('xpack.observability.obltNav.ml.aiAndMlGroupTitle', { - defaultMessage: 'AI & ML', - }), - renderAs: 'accordion', - children: [ - { - link: 'observabilityAIAssistant', - title: i18n.translate('xpack.observability.obltNav.aiMl.aiAssistant', { - defaultMessage: 'AI Assistant', - }), + { + link: 'discover', + renderAs: 'item', + children: [ + { + // This is to show "observability-log-explorer" breadcrumbs when navigating from "discover" to "log explorer" + link: 'observability-logs-explorer', + }, + ], + }, + { + link: 'dashboards', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith(prepend('/app/dashboards')); }, - { - link: 'ml:anomalyDetection', - renderAs: 'item', - children: [ - { - link: 'ml:singleMetricViewer', - }, - { - link: 'ml:anomalyExplorer', - }, + }, + ...(pluginsStart.inventory + ? [ { - link: 'ml:settings', + link: 'inventory' as const, + getIsActive: ({ + pathNameSerialized, + prepend, + }: { + pathNameSerialized: string; + prepend: (path: string) => string; + }) => { + return pathNameSerialized.startsWith(prepend('/app/observability/inventory')); + }, }, - ], - }, - { - title: i18n.translate('xpack.observability.obltNav.ml.logRateAnalysis', { - defaultMessage: 'Log rate analysis', - }), - link: 'ml:logRateAnalysis', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.includes(prepend('/app/ml/aiops/log_rate_analysis')); - }, - }, - { - link: 'logs:anomalies', - }, - { - link: 'logs:log-categories', - }, - { - title: i18n.translate('xpack.observability.obltNav.ml.changePointDetection', { - defaultMessage: 'Change point detection', - }), - link: 'ml:changePointDetections', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.includes(prepend('/app/ml/aiops/change_point_detection')); + ] + : []), + { + link: 'observability-overview:alerts', + }, + { + link: 'observability-overview:cases', + renderAs: 'item', + children: [ + { + link: 'observability-overview:cases_configure', }, - }, - { - title: i18n.translate('xpack.observability.obltNav.ml.job.notifications', { - defaultMessage: 'Job notifications', - }), - link: 'ml:notifications', - }, - ], - }, - { - id: 'apm', - title: i18n.translate('xpack.observability.obltNav.applications', { - defaultMessage: 'Applications', - }), - renderAs: 'accordion', - children: [ - { - link: 'apm:services', - getIsActive: ({ pathNameSerialized }) => { - const regex = /app\/apm\/.*service.*/; - return regex.test(pathNameSerialized); + { + link: 'observability-overview:cases_create', }, - }, - { - link: 'apm:traces', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/apm/traces')); + ], + }, + { + link: 'slo', + }, + { + id: 'aiMl', + title: i18n.translate('xpack.observability.obltNav.ml.aiAndMlGroupTitle', { + defaultMessage: 'AI & ML', + }), + renderAs: 'accordion', + children: [ + { + link: 'observabilityAIAssistant', + title: i18n.translate('xpack.observability.obltNav.aiMl.aiAssistant', { + defaultMessage: 'AI Assistant', + }), }, - }, - { - link: 'apm:dependencies', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/apm/dependencies')); + { + link: 'ml:anomalyDetection', + renderAs: 'item', + children: [ + { + link: 'ml:singleMetricViewer', + }, + { + link: 'ml:anomalyExplorer', + }, + { + link: 'ml:settings', + }, + ], }, - }, - { - id: 'synthetics', - title: i18n.translate('xpack.observability.obltNav.apm.syntheticsGroupTitle', { - defaultMessage: 'Synthetics', - }), - renderAs: 'accordion', - children: [ - { - link: 'synthetics', - title: i18n.translate('xpack.observability.obltNav.apm.synthetics.monitors', { - defaultMessage: 'Monitors', - }), + { + title: i18n.translate('xpack.observability.obltNav.ml.logRateAnalysis', { + defaultMessage: 'Log rate analysis', + }), + link: 'ml:logRateAnalysis', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.includes(prepend('/app/ml/aiops/log_rate_analysis')); }, - { link: 'synthetics:certificates' }, - ], - }, - { link: 'ux' }, - ], - }, - { - id: 'metrics', - title: i18n.translate('xpack.observability.obltNav.infrastructure', { - defaultMessage: 'Infrastructure', - }), - renderAs: 'accordion', - children: [ - { - link: 'metrics:inventory', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/metrics/inventory')); }, - }, - { - link: 'metrics:hosts', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/metrics/hosts')); + { + link: 'logs:anomalies', }, - }, - { - link: 'metrics:metrics-explorer', - }, - { - id: 'profiling', - title: i18n.translate( - 'xpack.observability.obltNav.infrastructure.universalProfiling', - { - defaultMessage: 'Universal Profiling', - } - ), - renderAs: 'accordion', - children: [ - { - link: 'profiling:stacktraces', - }, - { - link: 'profiling:flamegraphs', - }, - { - link: 'profiling:functions', - }, - ], - }, - ], - }, - { - id: 'otherTools', - title: i18n.translate('xpack.observability.obltNav.otherTools', { - defaultMessage: 'Other tools', - }), - renderAs: 'accordion', - children: [ - { - link: 'logs:stream', - title: i18n.translate('xpack.observability.obltNav.otherTools.logsStream', { - defaultMessage: 'Logs stream', - }), - }, - { link: 'maps' }, - { link: 'canvas' }, - { link: 'graph' }, - ], - }, - ], - }, - ], - footer: [ - { type: 'recentlyAccessed' }, - { - type: 'navItem', - title: i18n.translate('xpack.observability.obltNav.getStarted', { - defaultMessage: 'Get started', - }), - link: 'observabilityOnboarding', - icon: 'launch', - }, - { - type: 'navItem', - id: 'devTools', - title: i18n.translate('xpack.observability.obltNav.devTools', { - defaultMessage: 'Developer tools', - }), - link: 'dev_tools', - icon: 'editorCodeBlock', - }, - { - type: 'navGroup', - id: 'project_settings_project_nav', - title: i18n.translate('xpack.observability.obltNav.management', { - defaultMessage: 'Management', - }), - icon: 'gear', - breadcrumbStatus: 'hidden', - children: [ - { - link: 'management', - title: i18n.translate('xpack.observability.obltNav.stackManagement', { - defaultMessage: 'Stack Management', - }), - renderAs: 'panelOpener', - spaceBefore: null, - children: [ - { - title: 'Ingest', - children: [{ link: 'management:ingest_pipelines' }, { link: 'management:pipelines' }], - }, - { - title: 'Data', - children: [ - { link: 'management:index_management' }, - { link: 'management:index_lifecycle_management' }, - { link: 'management:snapshot_restore' }, - { link: 'management:rollup_jobs' }, - { link: 'management:transform' }, - { link: 'management:cross_cluster_replication' }, - { link: 'management:remote_clusters' }, - { link: 'management:migrate_data' }, - ], - }, - { - title: 'Alerts and Insights', - children: [ - { link: 'management:triggersActions' }, - { link: 'management:cases' }, - { link: 'management:triggersActionsConnectors' }, - { link: 'management:reporting' }, - { link: 'management:jobsListLink' }, - { link: 'management:watcher' }, - { link: 'management:maintenanceWindows' }, - ], - }, - { - title: 'Security', - children: [ - { link: 'management:users' }, - { link: 'management:roles' }, - { link: 'management:api_keys' }, - { link: 'management:role_mappings' }, - ], - }, - { - title: 'Kibana', - children: [ - { link: 'management:dataViews' }, - { link: 'management:filesManagement' }, - { link: 'management:objects' }, - { link: 'management:tags' }, - { link: 'management:search_sessions' }, - { link: 'management:aiAssistantManagementSelection' }, - { link: 'management:spaces' }, - { link: 'management:settings' }, - ], - }, - { - title: 'Stack', - children: [ - { link: 'management:license_management' }, - { link: 'management:upgrade_assistant' }, - ], - }, - ], - }, - { - link: 'integrations', - }, - { - link: 'fleet', - }, - { - id: 'machine_learning-landing', - link: 'securitySolutionUI:machine_learning-landing', - renderAs: 'panelOpener', - spaceBefore: null, - children: [ - { - children: [ - { - link: 'ml:overview', - }, - { - link: 'ml:notifications', + { + link: 'logs:log-categories', + }, + { + title: i18n.translate('xpack.observability.obltNav.ml.changePointDetection', { + defaultMessage: 'Change point detection', + }), + link: 'ml:changePointDetections', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.includes( + prepend('/app/ml/aiops/change_point_detection') + ); }, - { - link: 'ml:memoryUsage', + }, + { + title: i18n.translate('xpack.observability.obltNav.ml.job.notifications', { + defaultMessage: 'Job notifications', + }), + link: 'ml:notifications', + }, + ], + }, + { + id: 'apm', + title: i18n.translate('xpack.observability.obltNav.applications', { + defaultMessage: 'Applications', + }), + renderAs: 'accordion', + children: [ + { + link: 'apm:services', + getIsActive: ({ pathNameSerialized }) => { + const regex = /app\/apm\/.*service.*/; + return regex.test(pathNameSerialized); }, - ], - }, - { - id: 'category-anomaly_detection', - title: i18n.translate('xpack.observability.obltNav.ml.anomaly_detection', { - defaultMessage: 'Anomaly detection', - }), - breadcrumbStatus: 'hidden', - children: [ - { - link: 'ml:anomalyDetection', - title: i18n.translate('xpack.observability.obltNav.ml.anomaly_detection.jobs', { - defaultMessage: 'Jobs', - }), + }, + { + link: 'apm:traces', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith(prepend('/app/apm/traces')); }, - { - link: 'ml:anomalyExplorer', + }, + { + link: 'apm:dependencies', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith(prepend('/app/apm/dependencies')); }, - { - link: 'ml:singleMetricViewer', + }, + { + id: 'synthetics', + title: i18n.translate('xpack.observability.obltNav.apm.syntheticsGroupTitle', { + defaultMessage: 'Synthetics', + }), + renderAs: 'accordion', + children: [ + { + link: 'synthetics', + title: i18n.translate('xpack.observability.obltNav.apm.synthetics.monitors', { + defaultMessage: 'Monitors', + }), + }, + { link: 'synthetics:certificates' }, + ], + }, + { link: 'ux' }, + ], + }, + { + id: 'metrics', + title: i18n.translate('xpack.observability.obltNav.infrastructure', { + defaultMessage: 'Infrastructure', + }), + renderAs: 'accordion', + children: [ + { + link: 'metrics:inventory', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith(prepend('/app/metrics/inventory')); }, - { - link: 'ml:settings', + }, + { + link: 'metrics:hosts', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith(prepend('/app/metrics/hosts')); }, - ], - }, - { - id: 'category-data_frame analytics', - title: i18n.translate('xpack.observability.obltNav.ml.data_frame_analytics', { - defaultMessage: 'Data frame analytics', - }), - breadcrumbStatus: 'hidden', - children: [ - { - link: 'ml:dataFrameAnalytics', - title: i18n.translate( - 'xpack.observability.obltNav.ml.data_frame_analytics.jobs', - { + }, + { + link: 'metrics:metrics-explorer', + }, + { + id: 'profiling', + title: i18n.translate( + 'xpack.observability.obltNav.infrastructure.universalProfiling', + { + defaultMessage: 'Universal Profiling', + } + ), + renderAs: 'accordion', + children: [ + { + link: 'profiling:stacktraces', + }, + { + link: 'profiling:flamegraphs', + }, + { + link: 'profiling:functions', + }, + ], + }, + ], + }, + { + id: 'otherTools', + title: i18n.translate('xpack.observability.obltNav.otherTools', { + defaultMessage: 'Other tools', + }), + renderAs: 'accordion', + children: [ + { + link: 'logs:stream', + title: i18n.translate('xpack.observability.obltNav.otherTools.logsStream', { + defaultMessage: 'Logs stream', + }), + }, + { link: 'maps' }, + { link: 'canvas' }, + { link: 'graph' }, + ], + }, + ], + }, + ], + footer: [ + { type: 'recentlyAccessed' }, + { + type: 'navItem', + title: i18n.translate('xpack.observability.obltNav.getStarted', { + defaultMessage: 'Get started', + }), + link: 'observabilityOnboarding', + icon: 'launch', + }, + { + type: 'navItem', + id: 'devTools', + title: i18n.translate('xpack.observability.obltNav.devTools', { + defaultMessage: 'Developer tools', + }), + link: 'dev_tools', + icon: 'editorCodeBlock', + }, + { + type: 'navGroup', + id: 'project_settings_project_nav', + title: i18n.translate('xpack.observability.obltNav.management', { + defaultMessage: 'Management', + }), + icon: 'gear', + breadcrumbStatus: 'hidden', + children: [ + { + link: 'management', + title: i18n.translate('xpack.observability.obltNav.stackManagement', { + defaultMessage: 'Stack Management', + }), + renderAs: 'panelOpener', + spaceBefore: null, + children: [ + { + title: 'Ingest', + children: [ + { link: 'management:ingest_pipelines' }, + { link: 'management:pipelines' }, + ], + }, + { + title: 'Data', + children: [ + { link: 'management:index_management' }, + { link: 'management:index_lifecycle_management' }, + { link: 'management:snapshot_restore' }, + { link: 'management:rollup_jobs' }, + { link: 'management:transform' }, + { link: 'management:cross_cluster_replication' }, + { link: 'management:remote_clusters' }, + { link: 'management:migrate_data' }, + ], + }, + { + title: 'Alerts and Insights', + children: [ + { link: 'management:triggersActions' }, + { link: 'management:cases' }, + { link: 'management:triggersActionsConnectors' }, + { link: 'management:reporting' }, + { link: 'management:jobsListLink' }, + { link: 'management:watcher' }, + { link: 'management:maintenanceWindows' }, + ], + }, + { + title: 'Security', + children: [ + { link: 'management:users' }, + { link: 'management:roles' }, + { link: 'management:api_keys' }, + { link: 'management:role_mappings' }, + ], + }, + { + title: 'Kibana', + children: [ + { link: 'management:dataViews' }, + { link: 'management:filesManagement' }, + { link: 'management:objects' }, + { link: 'management:tags' }, + { link: 'management:search_sessions' }, + { link: 'management:aiAssistantManagementSelection' }, + { link: 'management:spaces' }, + { link: 'management:settings' }, + ], + }, + { + title: 'Stack', + children: [ + { link: 'management:license_management' }, + { link: 'management:upgrade_assistant' }, + ], + }, + ], + }, + { + link: 'integrations', + }, + { + link: 'fleet', + }, + { + id: 'machine_learning-landing', + link: 'securitySolutionUI:machine_learning-landing', + renderAs: 'panelOpener', + spaceBefore: null, + children: [ + { + children: [ + { + link: 'ml:overview', + }, + { + link: 'ml:notifications', + }, + { + link: 'ml:memoryUsage', + }, + ], + }, + { + id: 'category-anomaly_detection', + title: i18n.translate('xpack.observability.obltNav.ml.anomaly_detection', { + defaultMessage: 'Anomaly detection', + }), + breadcrumbStatus: 'hidden', + children: [ + { + link: 'ml:anomalyDetection', + title: i18n.translate('xpack.observability.obltNav.ml.anomaly_detection.jobs', { defaultMessage: 'Jobs', - } - ), - }, - { - link: 'ml:resultExplorer', - }, - { - link: 'ml:analyticsMap', - }, - ], - }, - { - id: 'category-model_management', - title: i18n.translate('xpack.observability.obltNav.ml.model_management', { - defaultMessage: 'Model management', - }), - breadcrumbStatus: 'hidden', - children: [ - { - link: 'ml:nodesOverview', - }, - ], - }, - { - id: 'category-data_visualizer', - title: i18n.translate('xpack.observability.obltNav.ml.data_visualizer', { - defaultMessage: 'Data visualizer', - }), - breadcrumbStatus: 'hidden', - children: [ - { - link: 'ml:fileUpload', - title: i18n.translate( - 'xpack.observability.obltNav.ml.data_visualizer.file_data_visualizer', - { - defaultMessage: 'File data visualizer', - } - ), - }, - { - link: 'ml:indexDataVisualizer', - title: i18n.translate( - 'xpack.observability.obltNav.ml.data_visualizer.file_data_visualizer', - { - defaultMessage: 'Data view data visualizer', - } - ), - }, - { - link: 'ml:dataDrift', - }, - ], - }, - { - id: 'category-aiops_labs', - title: i18n.translate('xpack.observability.obltNav.ml.aiops_labs', { - defaultMessage: 'Aiops labs', - }), - breadcrumbStatus: 'hidden', - children: [ - { - link: 'ml:logRateAnalysis', - }, - { - link: 'ml:logPatternAnalysis', - }, - { - link: 'ml:changePointDetections', - }, - ], - }, - ], - }, - { - id: 'cloudLinkUserAndRoles', - cloudLink: 'userAndRoles', - }, - { - id: 'cloudLinkBilling', - cloudLink: 'billingAndSub', - }, - ], - }, - ], -}; + }), + }, + { + link: 'ml:anomalyExplorer', + }, + { + link: 'ml:singleMetricViewer', + }, + { + link: 'ml:settings', + }, + ], + }, + { + id: 'category-data_frame analytics', + title: i18n.translate('xpack.observability.obltNav.ml.data_frame_analytics', { + defaultMessage: 'Data frame analytics', + }), + breadcrumbStatus: 'hidden', + children: [ + { + link: 'ml:dataFrameAnalytics', + title: i18n.translate( + 'xpack.observability.obltNav.ml.data_frame_analytics.jobs', + { + defaultMessage: 'Jobs', + } + ), + }, + { + link: 'ml:resultExplorer', + }, + { + link: 'ml:analyticsMap', + }, + ], + }, + { + id: 'category-model_management', + title: i18n.translate('xpack.observability.obltNav.ml.model_management', { + defaultMessage: 'Model management', + }), + breadcrumbStatus: 'hidden', + children: [ + { + link: 'ml:nodesOverview', + }, + ], + }, + { + id: 'category-data_visualizer', + title: i18n.translate('xpack.observability.obltNav.ml.data_visualizer', { + defaultMessage: 'Data visualizer', + }), + breadcrumbStatus: 'hidden', + children: [ + { + link: 'ml:fileUpload', + title: i18n.translate( + 'xpack.observability.obltNav.ml.data_visualizer.file_data_visualizer', + { + defaultMessage: 'File data visualizer', + } + ), + }, + { + link: 'ml:indexDataVisualizer', + title: i18n.translate( + 'xpack.observability.obltNav.ml.data_visualizer.file_data_visualizer', + { + defaultMessage: 'Data view data visualizer', + } + ), + }, + { + link: 'ml:dataDrift', + }, + ], + }, + { + id: 'category-aiops_labs', + title: i18n.translate('xpack.observability.obltNav.ml.aiops_labs', { + defaultMessage: 'Aiops labs', + }), + breadcrumbStatus: 'hidden', + children: [ + { + link: 'ml:logRateAnalysis', + }, + { + link: 'ml:logPatternAnalysis', + }, + { + link: 'ml:changePointDetections', + }, + ], + }, + ], + }, + { + id: 'cloudLinkUserAndRoles', + cloudLink: 'userAndRoles', + }, + { + id: 'cloudLinkBilling', + cloudLink: 'billingAndSub', + }, + ], + }, + ], + }; + + return navTree; +} -export const definition: AddSolutionNavigationArg = { +export const createDefinition = ( + pluginsStart: ObservabilityPublicPluginsStart +): AddSolutionNavigationArg => ({ id: 'oblt', title, icon: 'logoObservability', homePage: 'observabilityOnboarding', - navigationTree$: of(navTree), + navigationTree$: of(createNavTree(pluginsStart)), dataTestSubj: 'observabilitySideNav', -}; +}); diff --git a/x-pack/plugins/observability_solution/observability/public/plugin.ts b/x-pack/plugins/observability_solution/observability/public/plugin.ts index 43967f1339c5..a22638213adb 100644 --- a/x-pack/plugins/observability_solution/observability/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability/public/plugin.ts @@ -8,8 +8,8 @@ import { CasesDeepLinkId, CasesPublicStart, getCasesDeepLinks } from '@kbn/cases-plugin/public'; import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; import type { CloudStart } from '@kbn/cloud-plugin/public'; -import type { IUiSettingsClient } from '@kbn/core/public'; import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; +import type { IUiSettingsClient } from '@kbn/core/public'; import { App, AppDeepLink, @@ -23,7 +23,7 @@ import { ToastsStart, } from '@kbn/core/public'; import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; +import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { LOGS_EXPLORER_LOCATOR_ID, LogsExplorerLocatorParams } from '@kbn/deeplinks-observability'; import type { DiscoverStart } from '@kbn/discover-plugin/public'; @@ -32,45 +32,46 @@ import type { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-pl import type { HomePublicPluginSetup, HomePublicPluginStart } from '@kbn/home-plugin/public'; import { i18n } from '@kbn/i18n'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; +import type { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; import type { NavigationEntry, ObservabilitySharedPluginSetup, ObservabilitySharedPluginStart, } from '@kbn/observability-shared-plugin/public'; -import type { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; -import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; -import { +import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; +import type { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '@kbn/triggers-actions-ui-plugin/public'; -import { BehaviorSubject, from } from 'rxjs'; -import { map, mergeMap } from 'rxjs'; +import { BehaviorSubject, from, map, mergeMap } from 'rxjs'; -import { AiopsPluginStart } from '@kbn/aiops-plugin/public/types'; +import type { AiopsPluginStart } from '@kbn/aiops-plugin/public/types'; +import type { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; +import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public'; -import { ExploratoryViewPublicStart } from '@kbn/exploratory-view-plugin/public'; -import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public'; -import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; -import { +import type { ExploratoryViewPublicStart } from '@kbn/exploratory-view-plugin/public'; +import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public'; +import type { InventoryPublicSetup, InventoryPublicStart } from '@kbn/inventory-plugin/public'; +import type { InvestigatePublicStart } from '@kbn/investigate-plugin/public'; +import type { LicenseManagementUIPluginSetup } from '@kbn/license-management-plugin/public'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; +import type { ObservabilityAIAssistantPublicSetup, ObservabilityAIAssistantPublicStart, } from '@kbn/observability-ai-assistant-plugin/public'; -import { SecurityPluginStart } from '@kbn/security-plugin/public'; -import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; -import { +import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; +import type { SecurityPluginStart } from '@kbn/security-plugin/public'; +import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; +import type { ActionTypeRegistryContract, RuleTypeRegistryContract, } from '@kbn/triggers-actions-ui-plugin/public'; -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; -import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; -import type { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public'; -import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; -import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; -import { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; -import { LicenseManagementUIPluginSetup } from '@kbn/license-management-plugin/public'; -import { InvestigatePublicStart } from '@kbn/investigate-plugin/public'; +import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { observabilityAppId, observabilityFeatureId } from '../common'; import { ALERTS_PATH, @@ -81,11 +82,11 @@ import { } from '../common/locators/paths'; import { registerDataHandler } from './context/has_data_context/data_handler'; import { createUseRulesLink } from './hooks/create_use_rules_link'; -import { RulesLocatorDefinition } from './locators/rules'; import { RuleDetailsLocatorDefinition } from './locators/rule_details'; +import { RulesLocatorDefinition } from './locators/rules'; import { - createObservabilityRuleTypeRegistry, ObservabilityRuleTypeRegistry, + createObservabilityRuleTypeRegistry, } from './rules/create_observability_rule_type_registry'; import { registerObservabilityRuleTypes } from './rules/register_observability_rule_types'; @@ -125,6 +126,7 @@ export interface ObservabilityPublicPluginsSetup { licensing: LicensingPluginSetup; serverless?: ServerlessPluginSetup; presentationUtil?: PresentationUtilPluginStart; + inventory?: InventoryPublicSetup; } export interface ObservabilityPublicPluginsStart { actionTypeRegistry: ActionTypeRegistryContract; @@ -163,6 +165,7 @@ export interface ObservabilityPublicPluginsStart { dataViewFieldEditor: DataViewFieldEditorStart; toastNotifications: ToastsStart; investigate?: InvestigatePublicStart; + inventory?: InventoryPublicStart; } export type ObservabilityPublicStart = ReturnType; @@ -358,6 +361,18 @@ export class Plugin ] : []; + const inventoryLink = pluginsSetup.inventory + ? [ + { + label: i18n.translate('xpack.observability.inventoryLinkTitle', { + defaultMessage: 'Inventory', + }), + app: INVENTORY_APP_ID, + path: '', + }, + ] + : []; + const isAiAssistantEnabled = pluginsStart.observabilityAIAssistant?.service.isEnabled(); @@ -421,6 +436,7 @@ export class Plugin sortKey: 100, entries: [ ...overviewLink, + ...inventoryLink, ...alertsLink, ...sloLink, ...casesLink, @@ -466,8 +482,8 @@ export class Plugin updater$: this.appUpdater$, }); - import('./navigation_tree').then(({ definition }) => { - return pluginsStart.navigation.addSolutionNavigation(definition); + import('./navigation_tree').then(({ createDefinition }) => { + return pluginsStart.navigation.addSolutionNavigation(createDefinition(pluginsStart)); }); return { diff --git a/x-pack/plugins/observability_solution/observability/tsconfig.json b/x-pack/plugins/observability_solution/observability/tsconfig.json index 2ee43e3b2683..873a87c11d1a 100644 --- a/x-pack/plugins/observability_solution/observability/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability/tsconfig.json @@ -12,110 +12,109 @@ "../../../../typings/**/*", ], "kbn_references": [ - "@kbn/core", + "@kbn/rule-data-utils", + "@kbn/triggers-actions-ui-plugin", + "@kbn/i18n", + "@kbn/deeplinks-observability", + "@kbn/es-query", + "@kbn/observability-get-padded-alert-time-range-util", + "@kbn/share-plugin", "@kbn/data-plugin", - "@kbn/home-plugin", - "@kbn/kibana-react-plugin", + "@kbn/alerting-comparators", + "@kbn/guided-onboarding", + "@kbn/rison", "@kbn/kibana-utils-plugin", + "@kbn/spaces-plugin", + "@kbn/utility-types", + "@kbn/core-http-server", + "@kbn/core", + "@kbn/inspector-plugin", + "@kbn/shared-ux-page-kibana-template", + "@kbn/observability-ai-assistant-plugin", + "@kbn/shared-ux-router", + "@kbn/kibana-react-plugin", + "@kbn/react-kibana-context-render", + "@kbn/observability-shared-plugin", + "@kbn/react-kibana-context-theme", + "@kbn/shared-ux-link-redirect-app", "@kbn/usage-collection-plugin", - "@kbn/alerting-plugin", - "@kbn/licensing-plugin", + "@kbn/cases-components", "@kbn/cases-plugin", - "@kbn/lens-plugin", + "@kbn/stack-alerts-plugin", + "@kbn/core-ui-settings-browser-mocks", + "@kbn/alerts-ui-shared", + "@kbn/core-notifications-browser", + "@kbn/core-ui-settings-browser", "@kbn/rule-registry-plugin", - "@kbn/spaces-plugin", + "@kbn/alerts-grouping", + "@kbn/grouping", + "@kbn/data-views-plugin", + "@kbn/core-http-browser", "@kbn/timelines-plugin", - "@kbn/translations-plugin", - "@kbn/unified-search-plugin", - "@kbn/guided-onboarding-plugin", + "@kbn/visualization-ui-components", + "@kbn/event-annotation-components", + "@kbn/slo-schema", + "@kbn/event-annotation-common", + "@kbn/react-kibana-mount", + "@kbn/i18n-react", + "@kbn/expression-metric-vis-plugin", + "@kbn/charts-plugin", + "@kbn/alerting-plugin", + "@kbn/aiops-log-rate-analysis", + "@kbn/aiops-plugin", + "@kbn/field-types", + "@kbn/test-jest-helpers", "@kbn/discover-plugin", - "@kbn/i18n", - "@kbn/rule-data-utils", - "@kbn/inspector-plugin", - "@kbn/data-views-plugin", "@kbn/embeddable-plugin", - "@kbn/triggers-actions-ui-plugin", - "@kbn/security-plugin", - "@kbn/shared-ux-page-kibana-template", + "@kbn/lens-plugin", + "@kbn/osquery-plugin", + "@kbn/ui-actions-plugin", + "@kbn/unified-search-plugin", + "@kbn/lens-embeddable-utils", "@kbn/std", - "@kbn/i18n-react", - "@kbn/utility-types", - "@kbn/datemath", - "@kbn/core-ui-settings-browser", - "@kbn/es-query", - "@kbn/server-route-repository", + "@kbn/actions-plugin", + "@kbn/licensing-plugin", + "@kbn/core-chrome-browser", + "@kbn/navigation-plugin", + "@kbn/observability-alert-details", + "@kbn/investigation-shared", + "@kbn/observability-alerting-rule-utils", "@kbn/ui-theme", - "@kbn/test-jest-helpers", - "@kbn/config-schema", - "@kbn/features-plugin", - "@kbn/logging-mocks", - "@kbn/logging", - "@kbn/share-plugin", - "@kbn/core-notifications-browser", - "@kbn/guided-onboarding", - "@kbn/charts-plugin", + "@kbn/core-application-common", "@kbn/securitysolution-ecs", - "@kbn/shared-ux-router", - "@kbn/alerts-ui-shared", "@kbn/alerts-as-data-utils", - "@kbn/core-application-browser", - "@kbn/files-plugin", - "@kbn/core-theme-browser", - "@kbn/core-elasticsearch-server", - "@kbn/observability-shared-plugin", + "@kbn/datemath", + "@kbn/logs-shared-plugin", "@kbn/exploratory-view-plugin", - "@kbn/rison", - "@kbn/io-ts-utils", - "@kbn/observability-alert-details", - "@kbn/observability-get-padded-alert-time-range-util", - "@kbn/ui-actions-plugin", - "@kbn/field-types", - "@kbn/safer-lodash-set", - "@kbn/core-http-server", - "@kbn/cloud-plugin", - "@kbn/stack-alerts-plugin", - "@kbn/data-view-editor-plugin", - "@kbn/actions-plugin", "@kbn/core-capabilities-common", - "@kbn/observability-ai-assistant-plugin", - "@kbn/osquery-plugin", "@kbn/content-management-plugin", - "@kbn/embeddable-plugin", - "@kbn/aiops-plugin", - "@kbn/content-management-plugin", - "@kbn/deeplinks-observability", - "@kbn/core-application-common", - "@kbn/react-kibana-context-theme", - "@kbn/shared-ux-link-redirect-app", - "@kbn/lens-embeddable-utils", - "@kbn/serverless", - "@kbn/presentation-util-plugin", - "@kbn/es-types", - "@kbn/core-ui-settings-browser-mocks", + "@kbn/cloud-plugin", + "@kbn/data-view-editor-plugin", "@kbn/field-formats-plugin", - "@kbn/event-annotation-common", + "@kbn/home-plugin", "@kbn/data-view-field-editor-plugin", - "@kbn/cases-components", - "@kbn/aiops-log-rate-analysis", - "@kbn/alerting-comparators", - "@kbn/react-kibana-context-render", - "@kbn/react-kibana-mount", - "@kbn/core-chrome-browser", - "@kbn/navigation-plugin", - "@kbn/visualization-ui-components", - "@kbn/expression-metric-vis-plugin", - "@kbn/securitysolution-io-ts-utils", - "@kbn/event-annotation-components", - "@kbn/slo-schema", + "@kbn/guided-onboarding-plugin", + "@kbn/inventory-plugin", + "@kbn/investigate-plugin", "@kbn/license-management-plugin", - "@kbn/observability-alerting-rule-utils", + "@kbn/presentation-util-plugin", + "@kbn/security-plugin", + "@kbn/serverless", + "@kbn/core-application-browser", + "@kbn/core-theme-browser", + "@kbn/translations-plugin", + "@kbn/config-schema", + "@kbn/securitysolution-io-ts-utils", + "@kbn/core-elasticsearch-server", + "@kbn/logging", + "@kbn/safer-lodash-set", + "@kbn/features-plugin", + "@kbn/files-plugin", + "@kbn/server-route-repository", + "@kbn/io-ts-utils", "@kbn/core-ui-settings-server-mocks", - "@kbn/investigate-plugin", - "@kbn/investigation-shared", - "@kbn/grouping", - "@kbn/alerts-grouping", - "@kbn/core-http-browser", - "@kbn/logs-shared-plugin", + "@kbn/es-types", + "@kbn/logging-mocks", ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/api/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/api/index.ts index bf7ec4731bae..9530252e6611 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/api/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/api/index.ts @@ -11,7 +11,7 @@ import type { ReturnOf, RouteRepositoryClient, } from '@kbn/server-route-repository'; -import { formatRequest } from '@kbn/server-route-repository-utils'; +import { createRepositoryClient } from '@kbn/server-route-repository-client'; import type { ObservabilityAIAssistantServerRouteRepository } from '../../server'; type FetchOptions = Omit & { @@ -28,12 +28,12 @@ export type ObservabilityAIAssistantAPIClientOptions = Omit< export type ObservabilityAIAssistantAPIClient = RouteRepositoryClient< ObservabilityAIAssistantServerRouteRepository, ObservabilityAIAssistantAPIClientOptions ->; +>['fetch']; export type AutoAbortedObservabilityAIAssistantAPIClient = RouteRepositoryClient< ObservabilityAIAssistantServerRouteRepository, Omit ->; +>['fetch']; export type ObservabilityAIAssistantAPIEndpoint = keyof ObservabilityAIAssistantServerRouteRepository; @@ -47,19 +47,11 @@ export type ObservabilityAIAssistantAPIClientRequestParamsOf< TEndpoint extends ObservabilityAIAssistantAPIEndpoint > = ClientRequestParamsOf; -export function createCallObservabilityAIAssistantAPI(core: CoreStart | CoreSetup) { - return ((endpoint, options) => { - const { params } = options as unknown as { - params?: Partial>; - }; - - const { method, pathname, version } = formatRequest(endpoint, params?.path); - - return core.http[method](pathname, { - ...options, - body: params && params.body ? JSON.stringify(params.body) : undefined, - query: params?.query, - version, - }); - }) as ObservabilityAIAssistantAPIClient; +export function createCallObservabilityAIAssistantAPI( + core: CoreStart | CoreSetup +): ObservabilityAIAssistantAPIClient { + return createRepositoryClient< + ObservabilityAIAssistantServerRouteRepository, + ObservabilityAIAssistantAPIClientOptions + >(core).fetch; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json index cd314e65fec9..aeb103e041ca 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json @@ -14,34 +14,34 @@ ], "kbn_references": [ "@kbn/i18n", - "@kbn/core-analytics-browser", + "@kbn/inference-plugin", "@kbn/logging", + "@kbn/kibana-utils-plugin", + "@kbn/core-analytics-browser", "@kbn/core", "@kbn/server-route-repository", - "@kbn/licensing-plugin", + "@kbn/server-route-repository-client", "@kbn/actions-plugin", + "@kbn/licensing-plugin", "@kbn/std", - "@kbn/kibana-utils-plugin", + "@kbn/utility-types-jest", "@kbn/kibana-react-plugin", "@kbn/shared-ux-utility", "@kbn/security-plugin", - "@kbn/utility-types-jest", "@kbn/config-schema", - "@kbn/io-ts-utils", "@kbn/utility-types", "@kbn/data-views-plugin", + "@kbn/io-ts-utils", "@kbn/rule-registry-plugin", "@kbn/alerting-plugin", "@kbn/spaces-plugin", "@kbn/task-manager-plugin", + "@kbn/core-elasticsearch-server", + "@kbn/core-ui-settings-server", "@kbn/apm-utils", "@kbn/features-plugin", "@kbn/cloud-plugin", "@kbn/serverless", - "@kbn/core-elasticsearch-server", - "@kbn/core-ui-settings-server", - "@kbn/server-route-repository-utils", - "@kbn/inference-plugin", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/services/rest/create_call_api.ts b/x-pack/plugins/observability_solution/observability_onboarding/public/services/rest/create_call_api.ts index ec19a8ef7691..3f3175aeb251 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/services/rest/create_call_api.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/services/rest/create_call_api.ts @@ -29,12 +29,12 @@ export type ObservabilityOnboardingClientOptions = Omit< export type ObservabilityOnboardingClient = RouteRepositoryClient< ObservabilityOnboardingServerRouteRepository, ObservabilityOnboardingClientOptions ->; +>['fetch']; export type AutoAbortedObservabilityClient = RouteRepositoryClient< ObservabilityOnboardingServerRouteRepository, Omit ->; +>['fetch']; export type APIReturnType = ReturnOf< ObservabilityOnboardingServerRouteRepository, diff --git a/x-pack/plugins/observability_solution/ux/public/services/rest/create_call_apm_api.ts b/x-pack/plugins/observability_solution/ux/public/services/rest/create_call_apm_api.ts index 1f9fd6704932..0fe003ece56a 100644 --- a/x-pack/plugins/observability_solution/ux/public/services/rest/create_call_apm_api.ts +++ b/x-pack/plugins/observability_solution/ux/public/services/rest/create_call_apm_api.ts @@ -22,12 +22,12 @@ export type APMClientOptions = Omit; +export type APMClient = RouteRepositoryClient['fetch']; export type AutoAbortedAPMClient = RouteRepositoryClient< APMServerRouteRepository, Omit ->; +>['fetch']; export type APIReturnType = ReturnOf< APMServerRouteRepository, @@ -43,7 +43,10 @@ export type APIClientRequestParamsOf = ClientRequ export type AbstractAPMRepository = ServerRouteRepository; -export type AbstractAPMClient = RouteRepositoryClient; +export type AbstractAPMClient = RouteRepositoryClient< + AbstractAPMRepository, + APMClientOptions +>['fetch']; export let callApmApi: APMClient = () => { throw new Error('callApmApi has to be initialized before used. Call createCallApmApi first.'); diff --git a/yarn.lock b/yarn.lock index 81df55ed7d42..59765d837552 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5354,6 +5354,10 @@ version "0.0.0" uid "" +"@kbn/inventory-plugin@link:x-pack/plugins/observability_solution/inventory": + version "0.0.0" + uid "" + "@kbn/investigate-app-plugin@link:x-pack/plugins/observability_solution/investigate_app": version "0.0.0" uid "" @@ -6770,6 +6774,18 @@ version "0.0.0" uid "" +"@kbn/sse-utils-client@link:packages/kbn-sse-utils-client": + version "0.0.0" + uid "" + +"@kbn/sse-utils-server@link:packages/kbn-sse-utils-server": + version "0.0.0" + uid "" + +"@kbn/sse-utils@link:packages/kbn-sse-utils": + version "0.0.0" + uid "" + "@kbn/stack-alerts-plugin@link:x-pack/plugins/stack_alerts": version "0.0.0" uid ""