mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Inventory] Inventory plugin (#191798)
## Description This PR adds an inventory plugin, which renders an inventory UI. Currently only data streams are rendered. This is part of the LogsAI initiative - basically we need a UI for tasks like structuring data, extracting entities, listing the results etc. This is mostly POC-level stuff. Eventually some of this code might be handed over to ECO but let's cross that bridge when we get to it. ## Notes for reviewers: @elastic/appex-ai-infra @elastic/security-generative-ai: added a `truncateList` utility function that takes the first n elements of an array and appends a `{l-n} more` string value if there are more values than n. Really simple but I expect will also be very often used because we cannot send a huge amount of items to the LLM. @elastic/kibana-core @elastic/kibana-operations: just boiler plate stuff for adding a new plugin (and thank you for enabling us to run `quick_checks` locally! @elastic/obs-knowledge-team: added support for streaming using an Observable. @elastic/obs-ux-management-team: added links to the Inventory UI in the Observability plugin @elastic/obs-entities: I've added an entity manager client to be able to fetch entity definitions on the server. Maybe there's a better way? LMK. @elastic/obs-ux-logs-team: added a deeplink to the Inventory UI. I've also moved CODEOWNERS for this package to @elastic/obs-ux-management-team as they own the Observability plugin where this is mostly used. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
4d5c25305b
commit
98aa1ab769
110 changed files with 3075 additions and 882 deletions
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/deeplinks-observability",
|
||||
"owner": "@elastic/obs-ux-logs-team"
|
||||
"owner": "@elastic/obs-ux-management-team"
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ export type SearchHit<
|
|||
? {
|
||||
fields: Partial<Record<ValueTypeOfField<TFields>, unknown[]>>;
|
||||
}
|
||||
: {}) &
|
||||
: { fields?: Record<string, unknown[]> }) &
|
||||
(TDocValueFields extends DocValueFields
|
||||
? {
|
||||
fields: Partial<Record<ValueTypeOfField<TDocValueFields>, unknown[]>>;
|
||||
|
|
|
@ -86,6 +86,7 @@ pageLoadAssetSize:
|
|||
inspector: 148711
|
||||
integrationAssistant: 19524
|
||||
interactiveSetup: 80000
|
||||
inventory: 27430
|
||||
investigate: 17970
|
||||
investigateApp: 91898
|
||||
kibanaOverview: 56279
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Uint8Array> | 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<string> {
|
||||
const rawResponse = response.response;
|
||||
|
||||
const body = rawResponse?.body;
|
||||
if (!body) {
|
||||
return throwError(() => {
|
||||
throw new NoReadableStreamError();
|
||||
});
|
||||
}
|
||||
|
||||
return new Observable<string>((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<Uint8Array>): Promise<void> => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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<string, any> = DefaultClientOptions
|
||||
>(core: CoreStart | CoreSetup) {
|
||||
TClientOptions extends HttpFetchOptions = {}
|
||||
>(core: CoreStart | CoreSetup): RouteRepositoryClient<TRepository, TClientOptions> {
|
||||
const fetch = (
|
||||
endpoint: string,
|
||||
params: { path?: Record<string, string>; 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<Record<string, any>>;
|
||||
};
|
||||
fetch: (endpoint, ...args) => {
|
||||
const allOptions = args[0] ?? {};
|
||||
const params = 'params' in allOptions ? (allOptions.params as Record<string, any>) : {};
|
||||
const otherOptions = omit(allOptions, 'params') as 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, params, otherOptions) as any;
|
||||
},
|
||||
} as { fetch: RouteRepositoryClient<TRepository, TClientOptions> };
|
||||
stream: (endpoint, ...args) => {
|
||||
const allOptions = args[0] ?? {};
|
||||
const params = 'params' in allOptions ? (allOptions.params as Record<string, any>) : {};
|
||||
const otherOptions = omit(allOptions, 'params') as TClientOptions;
|
||||
|
||||
return from(
|
||||
fetch(endpoint, params, {
|
||||
...otherOptions,
|
||||
asResponse: true,
|
||||
rawResponse: true,
|
||||
}) as Promise<HttpResponse>
|
||||
).pipe(httpResponseIntoObservable()) as any;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
|
@ -17,5 +17,6 @@
|
|||
"@kbn/server-route-repository-utils",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/core-http-browser",
|
||||
"@kbn/sse-utils-client",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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<T extends { path: Record<string, any> }> = RequiredKeys<
|
||||
T['path']
|
||||
> extends never
|
||||
? { path?: T['path'] }
|
||||
: { path: T['path'] };
|
||||
|
||||
type QueryMaybeOptional<T extends { query: Record<string, any> }> = RequiredKeys<
|
||||
T['query']
|
||||
> extends never
|
||||
? { query?: T['query'] }
|
||||
: { query: T['query'] };
|
||||
|
||||
type BodyMaybeOptional<T extends { body: Record<string, any> }> = RequiredKeys<
|
||||
T['body']
|
||||
> extends never
|
||||
? { body?: T['body'] }
|
||||
: { body: T['body'] };
|
||||
|
||||
type ParamsMaybeOptional<
|
||||
TPath extends Record<string, any>,
|
||||
TQuery extends Record<string, any>,
|
||||
TBody extends Record<string, any>
|
||||
> = PathMaybeOptional<{ path: TPath }> &
|
||||
QueryMaybeOptional<{ query: TQuery }> &
|
||||
BodyMaybeOptional<{ body: TBody }>;
|
||||
|
||||
type ZodMaybeOptional<T extends { path: any; query: any; body: any }> = ParamsMaybeOptional<
|
||||
T['path'],
|
||||
T['query'],
|
||||
T['body']
|
||||
>;
|
||||
|
||||
type MaybeOptional<T extends { params: Record<string, any> }> = RequiredKeys<
|
||||
type MaybeOptional<T extends { params?: Record<string, any> }> = RequiredKeys<
|
||||
T['params']
|
||||
> extends never
|
||||
? { params?: T['params'] }
|
||||
|
@ -64,19 +35,19 @@ type WithoutIncompatibleMethods<T extends t.Any> = Omit<T, 'encode' | 'asEncoder
|
|||
asEncoder: () => t.Encoder<any, any>;
|
||||
};
|
||||
|
||||
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<t.Type<RouteParams>>;
|
||||
|
||||
export type RouteParamsRT = IoTsParamsObject | ZodParamsObject;
|
||||
|
||||
|
@ -97,22 +68,98 @@ type ValidateEndpoint<TEndpoint extends string> = string extends TEndpoint
|
|||
: false
|
||||
: false;
|
||||
|
||||
type IsAny<T> = 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<ServerSentEvent> but
|
||||
// to disallow Observable<NotAServerSentEvent>.
|
||||
|
||||
type ValidateSerializableValue<T, TWalkRecursively extends boolean = true> = IsAny<T> extends true
|
||||
? 1
|
||||
: T extends Function
|
||||
? 0
|
||||
: T extends Record<string, any>
|
||||
? TWalkRecursively extends true
|
||||
? ValuesType<{
|
||||
[key in keyof T]: ValidateSerializableValue<T[key], false>;
|
||||
}>
|
||||
: 1
|
||||
: T extends string | number | boolean | null | undefined
|
||||
? 1
|
||||
: T extends any[]
|
||||
? 1
|
||||
: 0;
|
||||
|
||||
type GuardAgainstInvalidRecord<T> = 0 extends ValidateSerializableValue<T> ? never : T;
|
||||
|
||||
type ServerRouteHandlerReturnTypeWithoutRecord =
|
||||
| Observable<ServerSentEvent>
|
||||
| Readable
|
||||
| IKibanaResponse
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| void;
|
||||
|
||||
type ServerRouteHandlerReturnType = ServerRouteHandlerReturnTypeWithoutRecord | Record<string, any>;
|
||||
|
||||
type ServerRouteHandler<
|
||||
TRouteHandlerResources extends ServerRouteHandlerResources,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined,
|
||||
TReturnType extends ServerRouteHandlerReturnType
|
||||
> = (
|
||||
options: TRouteHandlerResources &
|
||||
(TRouteParamsRT extends RouteParamsRT ? DecodedRequestParamsOfType<TRouteParamsRT> : {})
|
||||
) => Promise<
|
||||
TReturnType extends ServerRouteHandlerReturnTypeWithoutRecord
|
||||
? TReturnType
|
||||
: GuardAgainstInvalidRecord<TReturnType>
|
||||
>;
|
||||
|
||||
export type CreateServerRouteFactory<
|
||||
TRouteHandlerResources extends ServerRouteHandlerResources,
|
||||
TRouteCreateOptions extends ServerRouteCreateOptions
|
||||
> = <
|
||||
TEndpoint extends string,
|
||||
TReturnType extends ServerRouteHandlerReturnType,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined = undefined
|
||||
>(
|
||||
options: {
|
||||
endpoint: ValidateEndpoint<TEndpoint> extends true ? TEndpoint : never;
|
||||
handler: ServerRouteHandler<TRouteHandlerResources, TRouteParamsRT, TReturnType>;
|
||||
params?: TRouteParamsRT;
|
||||
} & TRouteCreateOptions
|
||||
) => Record<
|
||||
TEndpoint,
|
||||
ServerRoute<
|
||||
TEndpoint,
|
||||
TRouteParamsRT,
|
||||
TRouteHandlerResources,
|
||||
Awaited<TReturnType>,
|
||||
TRouteCreateOptions
|
||||
>
|
||||
>;
|
||||
|
||||
export type ServerRoute<
|
||||
TEndpoint extends string,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined,
|
||||
TRouteHandlerResources extends ServerRouteHandlerResources,
|
||||
TReturnType,
|
||||
TReturnType extends ServerRouteHandlerReturnType,
|
||||
TRouteCreateOptions extends ServerRouteCreateOptions
|
||||
> = ValidateEndpoint<TEndpoint> extends true
|
||||
? {
|
||||
endpoint: TEndpoint;
|
||||
params?: TRouteParamsRT;
|
||||
handler: ({}: TRouteHandlerResources &
|
||||
(TRouteParamsRT extends RouteParamsRT
|
||||
? DecodedRequestParamsOfType<TRouteParamsRT>
|
||||
: {})) => Promise<TReturnType>;
|
||||
} & TRouteCreateOptions
|
||||
: never;
|
||||
> = {
|
||||
endpoint: TEndpoint;
|
||||
handler: ServerRouteHandler<TRouteHandlerResources, TRouteParamsRT, TReturnType>;
|
||||
} & TRouteCreateOptions &
|
||||
(TRouteParamsRT extends RouteParamsRT ? { params: TRouteParamsRT } : {});
|
||||
|
||||
export type ServerRouteRepository = Record<
|
||||
string,
|
||||
|
@ -124,22 +171,22 @@ type ClientRequestParamsOfType<TRouteParamsRT extends RouteParamsRT> =
|
|||
? MaybeOptional<{
|
||||
params: t.OutputOf<TRouteParamsRT>;
|
||||
}>
|
||||
: TRouteParamsRT extends z.Schema
|
||||
: TRouteParamsRT extends z.ZodSchema
|
||||
? MaybeOptional<{
|
||||
params: ZodMaybeOptional<z.input<TRouteParamsRT>>;
|
||||
params: z.input<TRouteParamsRT>;
|
||||
}>
|
||||
: {};
|
||||
: never;
|
||||
|
||||
type DecodedRequestParamsOfType<TRouteParamsRT extends RouteParamsRT> =
|
||||
TRouteParamsRT extends t.Mixed
|
||||
? MaybeOptional<{
|
||||
params: t.TypeOf<TRouteParamsRT>;
|
||||
}>
|
||||
: TRouteParamsRT extends z.Schema
|
||||
: TRouteParamsRT extends z.ZodSchema
|
||||
? MaybeOptional<{
|
||||
params: ZodMaybeOptional<z.output<TRouteParamsRT>>;
|
||||
params: z.output<TRouteParamsRT>;
|
||||
}>
|
||||
: {};
|
||||
: never;
|
||||
|
||||
export type EndpointOf<TServerRouteRepository extends ServerRouteRepository> =
|
||||
keyof TServerRouteRepository;
|
||||
|
@ -186,24 +233,36 @@ export type ClientRequestParamsOf<
|
|||
>
|
||||
? TRouteParamsRT extends RouteParamsRT
|
||||
? ClientRequestParamsOfType<TRouteParamsRT>
|
||||
: {}
|
||||
: TRouteParamsRT extends undefined
|
||||
? {}
|
||||
: never
|
||||
: never;
|
||||
|
||||
type MaybeOptionalArgs<T extends Record<string, any>> = RequiredKeys<T> extends never
|
||||
? [T] | []
|
||||
: [T];
|
||||
|
||||
export type RouteRepositoryClient<
|
||||
export interface RouteRepositoryClient<
|
||||
TServerRouteRepository extends ServerRouteRepository,
|
||||
TAdditionalClientOptions extends Record<string, any> = DefaultClientOptions
|
||||
> = <TEndpoint extends Extract<keyof TServerRouteRepository, string>>(
|
||||
endpoint: TEndpoint,
|
||||
...args: MaybeOptionalArgs<
|
||||
ClientRequestParamsOf<TServerRouteRepository, TEndpoint> & TAdditionalClientOptions
|
||||
>
|
||||
) => Promise<ReturnOf<TServerRouteRepository, TEndpoint>>;
|
||||
|
||||
export type DefaultClientOptions = HttpFetchOptions;
|
||||
TAdditionalClientOptions extends Record<string, any>
|
||||
> {
|
||||
fetch<TEndpoint extends Extract<keyof TServerRouteRepository, string>>(
|
||||
endpoint: TEndpoint,
|
||||
...args: MaybeOptionalArgs<
|
||||
ClientRequestParamsOf<TServerRouteRepository, TEndpoint> & TAdditionalClientOptions
|
||||
>
|
||||
): Promise<ReturnOf<TServerRouteRepository, TEndpoint>>;
|
||||
stream<TEndpoint extends Extract<keyof TServerRouteRepository, string>>(
|
||||
endpoint: TEndpoint,
|
||||
...args: MaybeOptionalArgs<
|
||||
ClientRequestParamsOf<TServerRouteRepository, TEndpoint> & TAdditionalClientOptions
|
||||
>
|
||||
): ReturnOf<TServerRouteRepository, TEndpoint> extends Observable<infer TReturnType>
|
||||
? TReturnType extends ServerSentEvent
|
||||
? Observable<TReturnType>
|
||||
: 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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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<typeof createRepositoryClient<MyPluginRouteRepository, DefaultClientOptions>>;
|
||||
|
||||
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<DefaultRouteHandlerResources & MyPluginRouteDependencies>();
|
||||
```
|
||||
|
||||
|
@ -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<MyPluginRouteDependencies>({
|
||||
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);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
|
|
@ -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<TEndpoint, TRouteParamsRT, TRouteHandlerResources, TReturnType, TRouteCreateOptions>
|
||||
> {
|
||||
>(): CreateServerRouteFactory<TRouteHandlerResources, TRouteCreateOptions> {
|
||||
return (route) => ({ [route.endpoint]: route } as any);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<TDependencies extends Record<string, any>>({
|
|||
|
||||
if (isKibanaResponse(result)) {
|
||||
return result;
|
||||
} else if (isObservable(result)) {
|
||||
return response.ok({
|
||||
body: observableIntoEventSourceStream(result as Observable<ServerSentEvent>),
|
||||
});
|
||||
} else {
|
||||
const body = result || {};
|
||||
return response.ok({ body });
|
||||
|
|
|
@ -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);
|
||||
timeout: 1,
|
||||
})
|
||||
.then((res) => {
|
||||
assertType<{
|
||||
noParamsForMe: boolean;
|
||||
// @ts-expect-error Property 'noParamsForMe' is missing in type
|
||||
}>(res);
|
||||
|
||||
assertType<{
|
||||
yesParamsForMe: boolean;
|
||||
}>(res);
|
||||
});
|
||||
assertType<{
|
||||
yesParamsForMe: boolean;
|
||||
}>(res);
|
||||
});
|
||||
|
||||
client('GET /internal/endpoint_with_params_zod', {
|
||||
params: {
|
||||
path: {
|
||||
serviceName: '',
|
||||
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);
|
||||
timeout: 1,
|
||||
})
|
||||
.then((res) => {
|
||||
assertType<{
|
||||
noParamsForMe: boolean;
|
||||
// @ts-expect-error Property 'noParamsForMe' is missing in type
|
||||
}>(res);
|
||||
|
||||
assertType<{
|
||||
yesParamsForMe: boolean;
|
||||
}>(res);
|
||||
});
|
||||
assertType<{
|
||||
yesParamsForMe: boolean;
|
||||
}>(res);
|
||||
});
|
||||
|
||||
client('GET /internal/endpoint_returning_result', {
|
||||
timeout: 1,
|
||||
}).then((res) => {
|
||||
assertType<{
|
||||
result: boolean;
|
||||
}>(res);
|
||||
});
|
||||
client
|
||||
.fetch('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);
|
||||
});
|
||||
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<Observable<{ type: 'foo'; streamed_response: boolean }>>(
|
||||
client.stream('POST /internal/endpoint_returning_observable', {
|
||||
timeout: 10,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
"@kbn/logging-mocks",
|
||||
"@kbn/server-route-repository-utils",
|
||||
"@kbn/zod",
|
||||
"@kbn/sse-utils-server",
|
||||
"@kbn/sse-utils",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
3
packages/kbn-sse-utils-client/README.md
Normal file
3
packages/kbn-sse-utils-client/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/sse-utils-client
|
||||
|
||||
See @kbn/sse-utils.
|
10
packages/kbn-sse-utils-client/index.ts
Normal file
10
packages/kbn-sse-utils-client/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", 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';
|
14
packages/kbn-sse-utils-client/jest.config.js
Normal file
14
packages/kbn-sse-utils-client/jest.config.js
Normal file
|
@ -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: ['<rootDir>/packages/kbn-sse-utils-client'],
|
||||
};
|
5
packages/kbn-sse-utils-client/kibana.jsonc
Normal file
5
packages/kbn-sse-utils-client/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/sse-utils-client",
|
||||
"owner": "@elastic/obs-knowledge-team"
|
||||
}
|
6
packages/kbn-sse-utils-client/package.json
Normal file
6
packages/kbn-sse-utils-client/package.json
Normal file
|
@ -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"
|
||||
}
|
|
@ -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<Uint8Array> | null | undefined };
|
||||
}
|
||||
|
||||
export function createObservableFromHttpResponse<T extends ServerSentEvent = never>(
|
||||
response: StreamedHttpResponse
|
||||
): Observable<T> {
|
||||
const rawResponse = response.response;
|
||||
|
||||
const body = rawResponse?.body;
|
||||
if (!body) {
|
||||
return throwError(() => {
|
||||
throw createSSEInternalError(`No readable stream found in response`);
|
||||
});
|
||||
}
|
||||
|
||||
return new Observable<T>((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<ServerSentErrorEvent, 'type'>;
|
||||
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<Uint8Array>): Promise<void> => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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<string, unknown>>) {
|
||||
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<StreamedHttpResponse>({
|
||||
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<StreamedHttpResponse>({
|
||||
response: {
|
||||
// @ts-expect-error
|
||||
body: ReadableStream.from(toSse(...events)),
|
||||
},
|
||||
}).pipe(httpResponseIntoObservable(), toArray())
|
||||
);
|
||||
}).rejects.toThrowError(`Internal error`);
|
||||
});
|
||||
});
|
|
@ -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<StreamedHttpResponse, T> {
|
||||
return switchMap((response) => createObservableFromHttpResponse<T>(response));
|
||||
}
|
21
packages/kbn-sse-utils-client/tsconfig.json
Normal file
21
packages/kbn-sse-utils-client/tsconfig.json
Normal file
|
@ -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",
|
||||
]
|
||||
}
|
3
packages/kbn-sse-utils-server/README.md
Normal file
3
packages/kbn-sse-utils-server/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/sse-utils-server
|
||||
|
||||
See @kbn/sse-utils.
|
10
packages/kbn-sse-utils-server/index.ts
Normal file
10
packages/kbn-sse-utils-server/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", 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';
|
14
packages/kbn-sse-utils-server/jest.config.js
Normal file
14
packages/kbn-sse-utils-server/jest.config.js
Normal file
|
@ -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: ['<rootDir>/packages/kbn-sse-utils-server'],
|
||||
};
|
5
packages/kbn-sse-utils-server/kibana.jsonc
Normal file
5
packages/kbn-sse-utils-server/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/sse-utils-server",
|
||||
"owner": "@elastic/obs-knowledge-team"
|
||||
}
|
6
packages/kbn-sse-utils-server/package.json
Normal file
6
packages/kbn-sse-utils-server/package.json
Normal file
|
@ -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"
|
||||
}
|
|
@ -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<ServerSentEvent>): 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;
|
||||
}
|
19
packages/kbn-sse-utils-server/tsconfig.json
Normal file
19
packages/kbn-sse-utils-server/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/sse-utils",
|
||||
]
|
||||
}
|
57
packages/kbn-sse-utils/README.md
Normal file
57
packages/kbn-sse-utils/README.md
Normal file
|
@ -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<string, any>;
|
||||
};
|
||||
```
|
||||
|
||||
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.
|
19
packages/kbn-sse-utils/index.ts
Normal file
19
packages/kbn-sse-utils/index.ts
Normal file
|
@ -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';
|
14
packages/kbn-sse-utils/jest.config.js
Normal file
14
packages/kbn-sse-utils/jest.config.js
Normal file
|
@ -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: ['<rootDir>/packages/kbn-sse-utils'],
|
||||
};
|
5
packages/kbn-sse-utils/kibana.jsonc
Normal file
5
packages/kbn-sse-utils/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/sse-utils",
|
||||
"owner": "@elastic/obs-knowledge-team"
|
||||
}
|
6
packages/kbn-sse-utils/package.json
Normal file
6
packages/kbn-sse-utils/package.json
Normal file
|
@ -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"
|
||||
}
|
88
packages/kbn-sse-utils/src/errors.ts
Normal file
88
packages/kbn-sse-utils/src/errors.ts
Normal file
|
@ -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<string, any> | 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<string, any>;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
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<string, Record<string, any> | 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;
|
||||
}
|
24
packages/kbn-sse-utils/src/events.ts
Normal file
24
packages/kbn-sse-utils/src/events.ts
Normal file
|
@ -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<string, any>
|
||||
> = keyof TData extends 'type'
|
||||
? never
|
||||
: TData & {
|
||||
type: TEventType;
|
||||
};
|
||||
|
||||
export enum ServerSentEventType {
|
||||
error = 'error',
|
||||
data = 'data',
|
||||
}
|
||||
|
||||
export type ServerSentEvent = ServerSentEventBase<string, Record<string, unknown>>;
|
19
packages/kbn-sse-utils/tsconfig.json
Normal file
19
packages/kbn-sse-utils/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/i18n",
|
||||
]
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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<string, any> | undefined | unknown;
|
||||
|
||||
export type OutputUpdateEvent<TId extends string = string> =
|
||||
InferenceTaskEventBase<OutputEventType.OutputUpdate> & {
|
||||
export type OutputUpdateEvent<TId extends string = string> = ServerSentEventBase<
|
||||
OutputEventType.OutputUpdate,
|
||||
{
|
||||
id: TId;
|
||||
content: string;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
export type OutputCompleteEvent<
|
||||
TId extends string = string,
|
||||
TOutput extends Output = Output
|
||||
> = InferenceTaskEventBase<OutputEventType.OutputComplete> & {
|
||||
id: TId;
|
||||
output: TOutput;
|
||||
content?: string;
|
||||
};
|
||||
> = ServerSentEventBase<
|
||||
OutputEventType.OutputComplete,
|
||||
{
|
||||
id: TId;
|
||||
output: TOutput;
|
||||
content: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export type OutputEvent<TId extends string = string, TOutput extends Output = Output> =
|
||||
| OutputUpdateEvent<TId>
|
||||
|
@ -67,9 +72,9 @@ export function createOutputCompleteEvent<TId extends string, TOutput extends Ou
|
|||
content?: string
|
||||
): OutputCompleteEvent<TId, TOutput> {
|
||||
return {
|
||||
id,
|
||||
type: OutputEventType.OutputComplete,
|
||||
id,
|
||||
output,
|
||||
content,
|
||||
content: content ?? '',
|
||||
};
|
||||
}
|
||||
|
|
16
x-pack/plugins/inference/common/util/truncate_list.ts
Normal file
16
x-pack/plugins/inference/common/util/truncate_list.ts
Normal file
|
@ -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<T>(values: T[], limit: number): Array<T | string> {
|
||||
if (values.length <= limit) {
|
||||
return values;
|
||||
}
|
||||
|
||||
return [...take(values, limit), `${values.length - limit} more values`];
|
||||
}
|
|
@ -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<StreamedHttpResponse, T> {
|
||||
return pipe(
|
||||
switchMap((response) => createObservableFromHttpResponse(response)),
|
||||
catchError((error) => {
|
||||
return throwError(() => createInferenceInternalError(error.message));
|
||||
}),
|
||||
map((line): T => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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<TToolOptions extends ToolOptions>({
|
|||
functions,
|
||||
},
|
||||
},
|
||||
toolCallId: v4().substring(0, 6),
|
||||
toolCallId: generateFakeToolCallId(),
|
||||
};
|
||||
|
||||
return merge(
|
||||
|
@ -113,6 +112,7 @@ export function naturalLanguageToEsql<TToolOptions extends ToolOptions>({
|
|||
keywords,
|
||||
requestedDocumentation,
|
||||
},
|
||||
content: '',
|
||||
}),
|
||||
client
|
||||
.chatComplete({
|
||||
|
|
|
@ -18,22 +18,23 @@
|
|||
".storybook/**/*.js"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/i18n",
|
||||
"@kbn/sse-utils",
|
||||
"@kbn/esql-ast",
|
||||
"@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/esql-validation-autocomplete",
|
||||
"@kbn/esql-ast",
|
||||
"@kbn/dev-cli-runner",
|
||||
"@kbn/babel-register",
|
||||
"@kbn/expect",
|
||||
"@kbn/tooling-log",
|
||||
"@kbn/es-types",
|
||||
"@kbn/field-types",
|
||||
"@kbn/expressions-plugin",
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/repo-info"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export function createSharedUseFetcher<TEndpoint extends APIEndpoint>(
|
|||
): SharedUseFetcher<TEndpoint> {
|
||||
const Context = createContext<APIClientRequestParamsOf<APIEndpoint> | undefined>(undefined);
|
||||
|
||||
const returnValue: SharedUseFetcher<TEndpoint> = {
|
||||
const returnValue: SharedUseFetcher<APIEndpoint> = {
|
||||
useFetcherResult: () => {
|
||||
const context = useContext(Context);
|
||||
|
||||
|
@ -34,16 +34,16 @@ export function createSharedUseFetcher<TEndpoint extends APIEndpoint>(
|
|||
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<typeof callApmApi>));
|
||||
return callApmApi(endpoint, ...((params ? [{ params }] : []) as any));
|
||||
},
|
||||
[params]
|
||||
);
|
||||
|
||||
return result as ReturnType<SharedUseFetcher<TEndpoint>['useFetcherResult']>;
|
||||
return result as ReturnType<SharedUseFetcher<APIEndpoint>['useFetcherResult']>;
|
||||
},
|
||||
Provider: (props) => {
|
||||
const { children } = props;
|
||||
|
|
|
@ -22,12 +22,12 @@ export type APMClientOptions = Omit<FetchOptions, 'query' | 'body' | 'pathname'
|
|||
signal: AbortSignal | null;
|
||||
};
|
||||
|
||||
export type APMClient = RouteRepositoryClient<APMServerRouteRepository, APMClientOptions>;
|
||||
export type APMClient = RouteRepositoryClient<APMServerRouteRepository, APMClientOptions>['fetch'];
|
||||
|
||||
export type AutoAbortedAPMClient = RouteRepositoryClient<
|
||||
APMServerRouteRepository,
|
||||
Omit<APMClientOptions, 'signal'>
|
||||
>;
|
||||
>['fetch'];
|
||||
|
||||
export type APIReturnType<TEndpoint extends APIEndpoint> = ReturnOf<
|
||||
APMServerRouteRepository,
|
||||
|
@ -43,7 +43,10 @@ export type APIClientRequestParamsOf<TEndpoint extends APIEndpoint> = ClientRequ
|
|||
|
||||
export type AbstractAPMRepository = ServerRouteRepository;
|
||||
|
||||
export type AbstractAPMClient = RouteRepositoryClient<AbstractAPMRepository, APMClientOptions>;
|
||||
export type AbstractAPMClient = RouteRepositoryClient<
|
||||
AbstractAPMRepository,
|
||||
APMClientOptions
|
||||
>['fetch'];
|
||||
|
||||
export let callApmApi: APMClient = () => {
|
||||
throw new Error('callApmApi has to be initialized before used. Call createCallApmApi first.');
|
||||
|
|
|
@ -68,7 +68,9 @@ const getRegisterRouteDependencies = () => {
|
|||
};
|
||||
|
||||
const initApi = (
|
||||
routes: Array<ServerRoute<any, t.Any, APMRouteHandlerResources, any, APMRouteCreateOptions>>
|
||||
routes: Array<
|
||||
ServerRoute<any, t.Any | undefined, APMRouteHandlerResources, any, APMRouteCreateOptions>
|
||||
>
|
||||
) => {
|
||||
const { mocks, dependencies } = getRegisterRouteDependencies();
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ export const debugTelemetryRoute = createApmServerRoute({
|
|||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
handler: async (resources): Promise<APMTelemetry | unknown> => {
|
||||
handler: async (resources): Promise<APMTelemetry> => {
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,12 +26,12 @@ export type DatasetQualityClientOptions = Omit<
|
|||
export type DatasetQualityClient = RouteRepositoryClient<
|
||||
DatasetQualityServerRouteRepository,
|
||||
DatasetQualityClientOptions
|
||||
>;
|
||||
>['fetch'];
|
||||
|
||||
export type AutoAbortedClient = RouteRepositoryClient<
|
||||
DatasetQualityServerRouteRepository,
|
||||
Omit<DatasetQualityClientOptions, 'signal'>
|
||||
>;
|
||||
>['fetch'];
|
||||
|
||||
export type APIReturnType<TEndpoint extends APIEndpoint> = ReturnOf<
|
||||
DatasetQualityServerRouteRepository,
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
import type { EntityManagerRouteRepository } from '../../server';
|
||||
import { EntityManagerUnauthorizedError } from './errors';
|
||||
|
||||
type EntityManagerRepositoryClient = RouteRepositoryClient<EntityManagerRouteRepository>;
|
||||
type EntityManagerRepositoryClient = RouteRepositoryClient<EntityManagerRouteRepository, {}>;
|
||||
|
||||
type QueryParamOf<T extends { params?: any }> = Exclude<T['params'], undefined>['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<EntityManagerRouteRepository>(core).fetch;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -30,8 +30,11 @@ import {
|
|||
EntityManagerServerSetup,
|
||||
} from './types';
|
||||
|
||||
export type EntityManagerServerPluginSetup = ReturnType<EntityManagerServerPlugin['setup']>;
|
||||
export type EntityManagerServerPluginStart = ReturnType<EntityManagerServerPlugin['start']>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface EntityManagerServerPluginSetup {}
|
||||
export interface EntityManagerServerPluginStart {
|
||||
getScopedClient: (options: { request: KibanaRequest }) => Promise<EntityClient>;
|
||||
}
|
||||
|
||||
export const config: PluginConfigDescriptor<EntityManagerConfig> = {
|
||||
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() {}
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
|
@ -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];
|
|
@ -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 (
|
||||
<InventoryContextProvider context={context}>
|
||||
<Story />
|
||||
</InventoryContextProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# Inventory
|
||||
|
||||
Home of the Inventory plugin, which renders the... _inventory_.
|
|
@ -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[];
|
||||
}
|
|
@ -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: [
|
||||
'<rootDir>/x-pack/plugins/observability_solution/inventory/public',
|
||||
'<rootDir>/x-pack/plugins/observability_solution/inventory/common',
|
||||
'<rootDir>/x-pack/plugins/observability_solution/inventory/server',
|
||||
],
|
||||
setupFiles: [
|
||||
'<rootDir>/x-pack/plugins/observability_solution/inventory/.storybook/jest_setup.js',
|
||||
],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/observability_solution/inventory/{public,common,server}/**/*.{js,ts,tsx}',
|
||||
],
|
||||
|
||||
coverageReporters: ['html'],
|
||||
};
|
22
x-pack/plugins/observability_solution/inventory/kibana.jsonc
Normal file
22
x-pack/plugins/observability_solution/inventory/kibana.jsonc
Normal file
|
@ -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": []
|
||||
}
|
||||
}
|
|
@ -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<HttpFetchOptions, 'body'> & {
|
||||
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<InventoryAPIClientOptions, 'signal'>
|
||||
>;
|
||||
|
||||
export type InventoryAPIEndpoint = keyof InventoryServerRouteRepository;
|
||||
|
||||
export type APIReturnType<TEndpoint extends InventoryAPIEndpoint> = ReturnOf<
|
||||
InventoryServerRouteRepository,
|
||||
TEndpoint
|
||||
>;
|
||||
|
||||
export type InventoryAPIClientRequestParamsOf<TEndpoint extends InventoryAPIEndpoint> =
|
||||
ClientRequestParamsOf<InventoryServerRouteRepository, TEndpoint>;
|
||||
|
||||
export function createCallInventoryAPI(core: CoreStart | CoreSetup): InventoryAPIClient {
|
||||
return createRepositoryClient(core);
|
||||
}
|
|
@ -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<CoreTheme>;
|
||||
services: InventoryServices;
|
||||
}) {
|
||||
const theme = useMemo(() => {
|
||||
return { theme$ };
|
||||
}, [theme$]);
|
||||
|
||||
const context: InventoryKibanaContext = useMemo(
|
||||
() => ({
|
||||
core: coreStart,
|
||||
dependencies: {
|
||||
start: pluginsStart,
|
||||
},
|
||||
services,
|
||||
}),
|
||||
[coreStart, pluginsStart, services]
|
||||
);
|
||||
|
||||
return (
|
||||
<KibanaRenderContextProvider
|
||||
theme={theme}
|
||||
i18n={coreStart.i18n}
|
||||
analytics={coreStart.analytics}
|
||||
>
|
||||
<InventoryContextProvider context={context}>
|
||||
<RedirectAppLinks coreStart={coreStart}>
|
||||
<coreStart.i18n.Context>
|
||||
<RouterProvider history={history} router={inventoryRouter as any}>
|
||||
<RouteRenderer />
|
||||
</RouterProvider>
|
||||
</coreStart.i18n.Context>
|
||||
</RedirectAppLinks>
|
||||
</InventoryContextProvider>
|
||||
</KibanaRenderContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export { Application };
|
|
@ -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<React.ComponentProps<typeof Component>, 'onLockAllClick' | 'onUnlockAllClick'>;
|
||||
}
|
||||
|
||||
type StoryMeta = Meta<Args>;
|
||||
type Story = StoryObj<Args>;
|
||||
|
||||
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 (
|
||||
<div style={{ width: 240 }}>
|
||||
<Component {...args.props} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
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',
|
||||
};
|
|
@ -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 (
|
||||
<EuiLink data-test-subj="inventoryEntityTypeListBaseLink" href={href}>
|
||||
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon size="s" type={icon} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<EuiText size="s">{label}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow">{count}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function EntityTypeListBase({
|
||||
definitions,
|
||||
loading,
|
||||
error,
|
||||
}: {
|
||||
loading?: boolean;
|
||||
definitions?: EntityTypeDefinition[];
|
||||
error?: Error;
|
||||
}) {
|
||||
const router = useInventoryRouter();
|
||||
if (loading) {
|
||||
return <EuiLoadingSpinner size="s" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{definitions?.map((definition) => {
|
||||
return (
|
||||
<EntityTypeListItem
|
||||
key={definition.type}
|
||||
href={router.link('/{type}', { path: { type: definition.type } })}
|
||||
icon={definition.icon}
|
||||
count={definition.count}
|
||||
label={definition.label}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export function EntityTypeList() {
|
||||
const {
|
||||
services: { inventoryAPIClient },
|
||||
} = useKibana();
|
||||
|
||||
const { value, loading, error } = useAbortableAsync(
|
||||
({ signal }) => {
|
||||
return inventoryAPIClient.fetch('GET /internal/inventory/entity_types', {
|
||||
signal,
|
||||
});
|
||||
},
|
||||
[inventoryAPIClient]
|
||||
);
|
||||
|
||||
return <EntityTypeListBase definitions={value?.definitions} error={error} loading={loading} />;
|
||||
}
|
|
@ -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 <KibanaContextProvider services={context}>{children}</KibanaContextProvider>;
|
||||
}
|
|
@ -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 (
|
||||
<PageTemplate
|
||||
pageSectionProps={{
|
||||
className: css`
|
||||
max-height: calc(100vh - var(--euiFixedHeadersOffset, 0));
|
||||
overflow: auto;
|
||||
padding-inline: 0px;
|
||||
`,
|
||||
contentProps: {
|
||||
className: css`
|
||||
padding-block: 0px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup direction="row" gutterSize="s" alignItems="stretch">
|
||||
<EuiPanel
|
||||
className={css`
|
||||
width: 288px;
|
||||
max-width: 288px;
|
||||
min-width: 288px;
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
border-right: 1px solid ${theme.colors.lightShade};
|
||||
`}
|
||||
hasBorder={false}
|
||||
hasShadow={false}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiTitle size="xs">
|
||||
<h1>
|
||||
{i18n.translate('xpack.inventory.inventoryPageHeaderLabel', {
|
||||
defaultMessage: 'Inventory',
|
||||
})}
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EntityTypeList />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="l">
|
||||
{children}
|
||||
</EuiPanel>
|
||||
</EuiFlexGroup>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
|
@ -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<TPath extends PathsOf<InventoryRoutes>>(
|
||||
path: TPath
|
||||
): TypeOf<InventoryRoutes, TPath> {
|
||||
return useParams(path)! as TypeOf<InventoryRoutes, TPath>;
|
||||
}
|
|
@ -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<InventoryRoutes>;
|
||||
}
|
|
@ -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<T extends PathsOf<InventoryRoutes>>(
|
||||
path: T,
|
||||
...params: TypeAsArgs<TypeOf<InventoryRoutes, T>>
|
||||
): void;
|
||||
replace<T extends PathsOf<InventoryRoutes>>(
|
||||
path: T,
|
||||
...params: TypeAsArgs<TypeOf<InventoryRoutes, T>>
|
||||
): void;
|
||||
}
|
||||
|
||||
export function useInventoryRouter(): StatefulInventoryRouter {
|
||||
const {
|
||||
core: {
|
||||
http,
|
||||
application: { navigateToApp },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const link = (...args: any[]) => {
|
||||
// @ts-expect-error
|
||||
return inventoryRouter.link(...args);
|
||||
};
|
||||
|
||||
return useMemo<StatefulInventoryRouter>(
|
||||
() => ({
|
||||
...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]
|
||||
);
|
||||
}
|
|
@ -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<InventoryKibanaContext>().services;
|
||||
};
|
||||
|
||||
export { useTypedKibana as useKibana };
|
|
@ -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<ConfigSchema>) =>
|
||||
new InventoryPlugin(pluginInitializerContext);
|
|
@ -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<ConfigSchema>) {
|
||||
this.logger = context.logger.get();
|
||||
}
|
||||
setup(
|
||||
coreSetup: CoreSetup<InventoryStartDependencies, InventoryPublicStart>,
|
||||
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<unknown>) => {
|
||||
// Load application bundle and Get start services
|
||||
const [{ Application }, [coreStart, pluginsStart]] = await Promise.all([
|
||||
import('./application'),
|
||||
coreSetup.getStartServices(),
|
||||
]);
|
||||
|
||||
const services: InventoryServices = {
|
||||
inventoryAPIClient,
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<Application
|
||||
coreStart={coreStart}
|
||||
history={appMountParameters.history}
|
||||
pluginsStart={pluginsStart}
|
||||
theme$={appMountParameters.theme$}
|
||||
services={services}
|
||||
/>,
|
||||
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 {};
|
||||
}
|
||||
}
|
|
@ -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: (
|
||||
<InventoryPageTemplate>
|
||||
<Outlet />
|
||||
</InventoryPageTemplate>
|
||||
),
|
||||
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;
|
|
@ -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;
|
||||
}
|
|
@ -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 {}
|
|
@ -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<typeof config>;
|
|
@ -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<InventoryConfig> = {
|
||||
schema: configSchema,
|
||||
};
|
||||
|
||||
export const plugin: PluginInitializer<
|
||||
InventoryServerSetup,
|
||||
InventoryServerStart,
|
||||
InventorySetupDependencies,
|
||||
InventoryStartDependencies
|
||||
> = async (pluginInitializerContext: PluginInitializerContext<InventoryConfig>) =>
|
||||
new InventoryPlugin(pluginInitializerContext);
|
|
@ -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<ConfigSchema>) {
|
||||
this.logger = context.logger.get();
|
||||
}
|
||||
setup(
|
||||
coreSetup: CoreSetup<InventoryStartDependencies, InventoryServerStart>,
|
||||
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 {};
|
||||
}
|
||||
}
|
|
@ -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
|
||||
>();
|
|
@ -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,
|
||||
};
|
|
@ -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
|
||||
>;
|
|
@ -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<InventoryRouteHandlerResources, 'request' | 'context' | 'logger' | 'params'>;
|
||||
}) {
|
||||
registerRoutes({
|
||||
core,
|
||||
logger,
|
||||
repository: getGlobalInventoryServerRouteRepository(),
|
||||
dependencies,
|
||||
});
|
||||
}
|
|
@ -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<LicensingApiRequestHandlerContext, 'license' | 'featureUsage'>;
|
||||
}>;
|
||||
|
||||
export interface InventoryRouteHandlerResources {
|
||||
request: KibanaRequest;
|
||||
context: InventoryRequestHandlerContext;
|
||||
logger: Logger;
|
||||
plugins: {
|
||||
[key in keyof InventorySetupDependencies]: {
|
||||
setup: Required<InventorySetupDependencies>[key];
|
||||
};
|
||||
} & {
|
||||
[key in keyof InventoryStartDependencies]: {
|
||||
start: () => Promise<Required<InventoryStartDependencies>[key]>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface InventoryRouteCreateOptions {
|
||||
options: {
|
||||
timeout?: {
|
||||
idleSocket?: number;
|
||||
};
|
||||
tags: Array<'access:inventory'>;
|
||||
};
|
||||
}
|
|
@ -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 {}
|
|
@ -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",
|
||||
]
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue