;
let response: KibanaResponseFactory;
diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts
index 59611e51d9a15..570f4f973dd53 100644
--- a/src/core/server/http/router/router.ts
+++ b/src/core/server/http/router/router.ts
@@ -24,7 +24,7 @@ import {
} from './response';
import { RouteConfig, RouteConfigOptions, RouteMethod, validBodyOutput } from './route';
import { HapiResponseAdapter } from './response_adapter';
-import { RequestHandlerContext } from '../..';
+import { RequestHandlerContextBase } from '../..';
import { wrapErrors } from './error_wrapper';
import { RouteValidator } from './validator';
@@ -46,7 +46,7 @@ export interface RouterRoute {
*/
export type RouteRegistrar<
Method extends RouteMethod,
- Context extends RequestHandlerContext = RequestHandlerContext
+ Context extends RequestHandlerContextBase = RequestHandlerContextBase
> = (
route: RouteConfig
,
handler: RequestHandler
@@ -58,7 +58,7 @@ export type RouteRegistrar<
*
* @public
*/
-export interface IRouter {
+export interface IRouter {
/**
* Resulted path
*/
@@ -118,7 +118,7 @@ export type ContextEnhancer<
Q,
B,
Method extends RouteMethod,
- Context extends RequestHandlerContext
+ Context extends RequestHandlerContextBase
> = (handler: RequestHandler) => RequestHandlerEnhanced
;
function getRouteFullPath(routerPath: string, routePath: string) {
@@ -202,7 +202,7 @@ function validOptions(
/**
* @internal
*/
-export class Router
+export class Router
implements IRouter
{
public routes: Array> = [];
@@ -307,7 +307,7 @@ type WithoutHeadArgument = T extends (first: any, ...rest: infer Params) => i
: never;
type RequestHandlerEnhanced = WithoutHeadArgument<
- RequestHandler
+ RequestHandler
>;
/**
@@ -350,7 +350,7 @@ export type RequestHandler<
P = unknown,
Q = unknown,
B = unknown,
- Context extends RequestHandlerContext = RequestHandlerContext,
+ Context extends RequestHandlerContextBase = RequestHandlerContextBase,
Method extends RouteMethod = any,
ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory
> = (
@@ -376,7 +376,7 @@ export type RequestHandlerWrapper = <
P,
Q,
B,
- Context extends RequestHandlerContext = RequestHandlerContext,
+ Context extends RequestHandlerContextBase = RequestHandlerContextBase,
Method extends RouteMethod = any,
ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory
>(
diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts
index 55bef12fd0f8b..46b74deec2bc2 100644
--- a/src/core/server/http/types.ts
+++ b/src/core/server/http/types.ts
@@ -21,7 +21,7 @@ import { OnPostAuthHandler } from './lifecycle/on_post_auth';
import { OnPreResponseHandler } from './lifecycle/on_pre_response';
import { IBasePath } from './base_path_service';
import { ExternalUrlConfig } from './external_url';
-import type { PluginOpaqueId, RequestHandlerContext } from '..';
+import type { PluginOpaqueId, RequestHandlerContextBase } from '..';
/**
* An object that handles registration of http request context providers.
@@ -36,7 +36,7 @@ export type RequestHandlerContextContainer = IContextContainer;
* @public
*/
export type RequestHandlerContextProvider<
- Context extends RequestHandlerContext,
+ Context extends RequestHandlerContextBase,
ContextName extends keyof Context
> = IContextProvider;
@@ -117,7 +117,9 @@ export interface HttpAuth {
* ```
* @public
*/
-export interface HttpServicePreboot {
+export interface HttpServicePreboot<
+ DefaultRequestHandlerType extends RequestHandlerContextBase = RequestHandlerContextBase
+> {
/**
* Provides ability to acquire `preboot` {@link IRouter} instance for a particular top-level path and register handler
* functions for any number of nested routes.
@@ -135,7 +137,10 @@ export interface HttpServicePreboot {
* ```
* @public
*/
- registerRoutes(path: string, callback: (router: IRouter) => void): void;
+ registerRoutes(
+ path: string,
+ callback: (router: IRouter) => void
+ ): void;
/**
* Access or manipulate the Kibana base path
@@ -162,7 +167,12 @@ export interface InternalHttpServicePreboot
| 'server'
| 'getServerInfo'
> {
- registerRoutes(path: string, callback: (router: IRouter) => void): void;
+ registerRoutes<
+ DefaultRequestHandlerType extends RequestHandlerContextBase = RequestHandlerContextBase
+ >(
+ path: string,
+ callback: (router: IRouter) => void
+ ): void;
}
/**
@@ -237,7 +247,9 @@ export interface InternalHttpServicePreboot
* ```
* @public
*/
-export interface HttpServiceSetup {
+export interface HttpServiceSetup<
+ DefaultRequestHandlerType extends RequestHandlerContextBase = RequestHandlerContextBase
+> {
/**
* Creates cookie based session storage factory {@link SessionStorageFactory}
* @param cookieOptions {@link SessionStorageCookieOptions} - options to configure created cookie session storage.
@@ -333,7 +345,7 @@ export interface HttpServiceSetup {
* @public
*/
createRouter: <
- Context extends RequestHandlerContext = RequestHandlerContext
+ Context extends DefaultRequestHandlerType = DefaultRequestHandlerType
>() => IRouter;
/**
@@ -365,7 +377,7 @@ export interface HttpServiceSetup {
* @public
*/
registerRouteHandlerContext: <
- Context extends RequestHandlerContext,
+ Context extends DefaultRequestHandlerType,
ContextName extends keyof Omit
>(
contextName: ContextName,
@@ -384,7 +396,7 @@ export interface InternalHttpServiceSetup
auth: HttpServerSetup['auth'];
server: HttpServerSetup['server'];
externalUrl: ExternalUrlConfig;
- createRouter: (
+ createRouter: (
path: string,
plugin?: PluginOpaqueId
) => IRouter;
@@ -392,7 +404,7 @@ export interface InternalHttpServiceSetup
registerStaticDir: (path: string, dirPath: string) => void;
authRequestHeaders: IAuthHeadersStorage;
registerRouteHandlerContext: <
- Context extends RequestHandlerContext,
+ Context extends RequestHandlerContextBase,
ContextName extends keyof Omit
>(
pluginOpaqueId: PluginOpaqueId,
diff --git a/src/core/server/http_resources/http_resources_service.test.ts b/src/core/server/http_resources/http_resources_service.test.ts
index 91500737123c7..e626d496c8dec 100644
--- a/src/core/server/http_resources/http_resources_service.test.ts
+++ b/src/core/server/http_resources/http_resources_service.test.ts
@@ -8,7 +8,7 @@
import { getApmConfigMock } from './http_resources_service.test.mocks';
-import { IRouter, RouteConfig } from '../http';
+import { RouteConfig } from '../http';
import { mockCoreContext } from '@kbn/core-base-server-mocks';
import { coreMock } from '../mocks';
@@ -25,7 +25,7 @@ describe('HttpResources service', () => {
let service: HttpResourcesService;
let prebootDeps: PrebootDeps;
let setupDeps: SetupDeps;
- let router: jest.Mocked;
+ let router: ReturnType;
const kibanaRequest = httpServerMock.createKibanaRequest();
const context = coreMock.createCustomRequestHandlerContext({});
const apmConfig = { mockApmConfig: true };
diff --git a/src/core/server/http_resources/http_resources_service.ts b/src/core/server/http_resources/http_resources_service.ts
index 978ad8e72621b..2fa90b4346965 100644
--- a/src/core/server/http_resources/http_resources_service.ts
+++ b/src/core/server/http_resources/http_resources_service.ts
@@ -63,7 +63,10 @@ export class HttpResourcesService implements CoreService
+ ): HttpResources {
return {
register: (
route: RouteConfig
,
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 25dd89acab6e6..4198fe04789ae 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -46,29 +46,26 @@ import {
ElasticsearchServiceSetup,
configSchema as elasticsearchConfigSchema,
ElasticsearchServiceStart,
- IScopedClusterClient,
ElasticsearchServicePreboot,
} from './elasticsearch';
-import { HttpServicePreboot, HttpServiceSetup, HttpServiceStart } from './http';
+import type {
+ HttpServicePreboot,
+ HttpServiceSetup,
+ HttpServiceStart,
+ IRouter,
+ RequestHandler,
+} from './http';
import { HttpResources } from './http_resources';
import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins';
-import { IUiSettingsClient, UiSettingsServiceSetup, UiSettingsServiceStart } from './ui_settings';
-import { SavedObjectsClientContract } from './saved_objects/types';
-import {
- ISavedObjectTypeRegistry,
- SavedObjectsServiceSetup,
- SavedObjectsServiceStart,
- ISavedObjectsExporter,
- ISavedObjectsImporter,
- SavedObjectsClientProviderOptions,
-} from './saved_objects';
+import { UiSettingsServiceSetup, UiSettingsServiceStart } from './ui_settings';
+import { SavedObjectsServiceSetup, SavedObjectsServiceStart } from './saved_objects';
import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
import { MetricsServiceSetup, MetricsServiceStart } from './metrics';
import { StatusServiceSetup } from './status';
import { CoreUsageDataStart, CoreUsageDataSetup } from './core_usage_data';
import { I18nServiceSetup } from './i18n';
-import { DeprecationsServiceSetup, DeprecationsClient } from './deprecations';
+import { DeprecationsServiceSetup } from './deprecations';
// Because of #79265 we need to explicitly import, then export these types for
// scripts/telemetry_check.js to work as expected
import {
@@ -80,6 +77,9 @@ import {
CoreServicesUsageData,
} from './core_usage_data';
import { PrebootServicePreboot } from './preboot';
+import type { CoreRequestHandlerContext } from './core_route_handler_context';
+import type { PrebootCoreRequestHandlerContext } from './preboot_core_route_handler_context';
+import { KibanaResponseFactory, RouteMethod } from './http';
export type { PrebootServicePreboot } from './preboot';
@@ -151,6 +151,7 @@ export type {
UnauthorizedErrorHandlerResult,
UnauthorizedErrorHandlerToolkit,
UnauthorizedErrorHandler,
+ ElasticsearchRequestHandlerContext,
} from './elasticsearch';
export type {
@@ -176,7 +177,6 @@ export type {
HttpResponsePayload,
HttpServerInfo,
HttpServicePreboot,
- HttpServiceSetup,
HttpServiceStart,
ErrorHttpResponseOptions,
IKibanaSocket,
@@ -199,7 +199,6 @@ export type {
OnPreResponseExtensions,
OnPreResponseInfo,
RedirectResponseOptions,
- RequestHandler,
RequestHandlerWrapper,
RequestHandlerContextContainer,
RequestHandlerContextProvider,
@@ -208,7 +207,6 @@ export type {
ResponseHeaders,
KibanaResponseFactory,
RouteConfig,
- IRouter,
RouteRegistrar,
RouteMethod,
RouteConfigOptions,
@@ -387,6 +385,7 @@ export type {
SavedObjectsImportWarning,
SavedObjectsValidationMap,
SavedObjectsValidationSpec,
+ SavedObjectsRequestHandlerContext,
} from './saved_objects';
export type {
@@ -398,6 +397,7 @@ export type {
UiSettingsServiceStart,
UserProvidedValues,
DeprecationSettings,
+ UiSettingsRequestHandlerContext,
} from './ui_settings';
export type {
@@ -421,6 +421,7 @@ export type {
GetDeprecationsContext,
DeprecationsServiceSetup,
DeprecationsClient,
+ DeprecationsRequestHandlerContext,
} from './deprecations';
export type { AppCategory } from '../types';
@@ -473,7 +474,12 @@ export type {
AnalyticsServiceStart,
} from '@kbn/core-analytics-server';
-/** @public **/
+export type { CoreRequestHandlerContext } from './core_route_handler_context';
+
+/**
+ * Base, abstract type for request handler contexts.
+ * @public
+ **/
export interface RequestHandlerContextBase {
/**
* Await all the specified context parts and return them.
@@ -491,7 +497,7 @@ export interface RequestHandlerContextBase {
}
/**
- * Base context passed to a route handler.
+ * Base context passed to a route handler, containing the `core` context part.
*
* @public
*/
@@ -499,43 +505,21 @@ export interface RequestHandlerContext extends RequestHandlerContextBase {
core: Promise;
}
-/** @public */
-export type CustomRequestHandlerContext = RequestHandlerContext & {
- [Key in keyof T]: T[Key] extends Promise ? T[Key] : Promise;
-};
+/**
+ * @internal
+ */
+export interface PrebootRequestHandlerContext extends RequestHandlerContextBase {
+ core: Promise;
+}
/**
- * The `core` context provided to route handler.
+ * Mixin allowing plugins to define their own request handler contexts.
*
- * Provides the following clients and services:
- * - {@link SavedObjectsClient | savedObjects.client} - Saved Objects client
- * which uses the credentials of the incoming request
- * - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing
- * all the registered types.
- * - {@link IScopedClusterClient | elasticsearch.client} - Elasticsearch
- * data client which uses the credentials of the incoming request
- * - {@link IUiSettingsClient | uiSettings.client} - uiSettings client
- * which uses the credentials of the incoming request
* @public
*/
-export interface CoreRequestHandlerContext {
- savedObjects: {
- client: SavedObjectsClientContract;
- typeRegistry: ISavedObjectTypeRegistry;
- getClient: (options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract;
- getExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter;
- getImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter;
- };
- elasticsearch: {
- client: IScopedClusterClient;
- };
- uiSettings: {
- client: IUiSettingsClient;
- };
- deprecations: {
- client: DeprecationsClient;
- };
-}
+export type CustomRequestHandlerContext = RequestHandlerContext & {
+ [Key in keyof T]: T[Key] extends Promise ? T[Key] : Promise;
+};
/**
* Context passed to the `setup` method of `preboot` plugins.
@@ -547,7 +531,7 @@ export interface CorePreboot {
/** {@link ElasticsearchServicePreboot} */
elasticsearch: ElasticsearchServicePreboot;
/** {@link HttpServicePreboot} */
- http: HttpServicePreboot;
+ http: HttpServicePreboot;
/** {@link PrebootServicePreboot} */
preboot: PrebootServicePreboot;
}
@@ -573,7 +557,7 @@ export interface CoreSetup & {
/** {@link HttpResources} */
resources: HttpResources;
};
@@ -662,3 +646,42 @@ export const config = {
appenders: appendersSchema as Type,
},
};
+
+/**
+ * Public version of RequestHandler, default-scoped to {@link RequestHandlerContext}
+ * See [@link RequestHandler}
+ * @public
+ */
+type PublicRequestHandler<
+ P = unknown,
+ Q = unknown,
+ B = unknown,
+ Context extends RequestHandlerContext = RequestHandlerContext,
+ Method extends RouteMethod = any,
+ ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory
+> = RequestHandler;
+
+export type { PublicRequestHandler as RequestHandler, RequestHandler as BaseRequestHandler };
+
+/**
+ * Public version of IRouter, default-scoped to {@link RequestHandlerContext}
+ * See [@link IRouter}
+ * @public
+ */
+type PublicRouter =
+ IRouter;
+
+export type { PublicRouter as IRouter, IRouter as IBaseRouter };
+
+/**
+ * Public version of RequestHandlerContext, default-scoped to {@link RequestHandlerContext}
+ * See [@link RequestHandlerContext}
+ * @public
+ */
+type PublicHttpServiceSetup =
+ HttpServiceSetup;
+
+export type {
+ PublicHttpServiceSetup as HttpServiceSetup,
+ HttpServiceSetup as BaseHttpServiceSetup,
+};
diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts
index 0382d9f56ca1d..f224ff76418ab 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -23,6 +23,7 @@ import type {
CoreStart,
StartServicesAccessor,
CorePreboot,
+ RequestHandlerContext,
} from '.';
import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
import { httpServiceMock } from './http/http_service.mock';
@@ -139,7 +140,7 @@ function createCorePrebootMock() {
const mock: CorePrebootMockType = {
analytics: analyticsServiceMock.createAnalyticsServicePreboot(),
elasticsearch: elasticsearchServiceMock.createPreboot(),
- http: httpServiceMock.createPrebootContract(),
+ http: httpServiceMock.createPrebootContract() as CorePrebootMockType['http'],
preboot: prebootServiceMock.createPrebootContract(),
};
@@ -159,7 +160,7 @@ function createCoreSetupMock({
pluginStartContract?: any;
} = {}) {
const httpMock: jest.Mocked = {
- ...httpServiceMock.createSetupContract(),
+ ...httpServiceMock.createSetupContract(),
resources: httpResourcesMock.createRegistrar(),
};
diff --git a/src/core/server/preboot_core_route_handler_context.ts b/src/core/server/preboot_core_route_handler_context.ts
index 63378046e8050..ba3cc445f6958 100644
--- a/src/core/server/preboot_core_route_handler_context.ts
+++ b/src/core/server/preboot_core_route_handler_context.ts
@@ -10,12 +10,34 @@
import { InternalCorePreboot } from './internal_types';
import { IUiSettingsClient } from './ui_settings';
-class PrebootCoreUiSettingsRouteHandlerContext {
+/**
+ * @public
+ */
+export interface PrebootUiSettingsRequestHandlerContext {
+ client: IUiSettingsClient;
+}
+
+/**
+ * Implementation of {@link PrebootUiSettingsRequestHandlerContext}
+ * @internal
+ */
+class PrebootCoreUiSettingsRouteHandlerContext implements PrebootUiSettingsRequestHandlerContext {
constructor(public readonly client: IUiSettingsClient) {}
}
-export class PrebootCoreRouteHandlerContext {
- readonly uiSettings: PrebootCoreUiSettingsRouteHandlerContext;
+/**
+ * @public
+ */
+export interface PrebootCoreRequestHandlerContext {
+ uiSettings: PrebootUiSettingsRequestHandlerContext;
+}
+
+/**
+ * Implementation of {@link PrebootCoreRequestHandlerContext}.
+ * @internal
+ */
+export class PrebootCoreRouteHandlerContext implements PrebootCoreRequestHandlerContext {
+ readonly uiSettings: PrebootUiSettingsRequestHandlerContext;
constructor(private readonly corePreboot: InternalCorePreboot) {
this.uiSettings = new PrebootCoreUiSettingsRouteHandlerContext(
diff --git a/src/core/server/rendering/bootstrap/register_bootstrap_route.ts b/src/core/server/rendering/bootstrap/register_bootstrap_route.ts
index 06f4100a01576..c22f7f56d70f2 100644
--- a/src/core/server/rendering/bootstrap/register_bootstrap_route.ts
+++ b/src/core/server/rendering/bootstrap/register_bootstrap_route.ts
@@ -6,14 +6,14 @@
* Side Public License, v 1.
*/
-import { IRouter } from '../../http';
+import type { InternalRenderingRouter } from '../internal_types';
import type { BootstrapRenderer } from './bootstrap_renderer';
export const registerBootstrapRoute = ({
router,
renderer,
}: {
- router: IRouter;
+ router: InternalRenderingRouter;
renderer: BootstrapRenderer;
}) => {
router.get(
diff --git a/src/core/server/rendering/internal_types.ts b/src/core/server/rendering/internal_types.ts
new file mode 100644
index 0000000000000..6ad3fd086f819
--- /dev/null
+++ b/src/core/server/rendering/internal_types.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { RequestHandlerContextBase } from '..';
+import type { IRouter } from '../http';
+import type { UiSettingsRequestHandlerContext } from '../ui_settings';
+
+/**
+ * Request handler context used by core's rendering routes.
+ * @internal
+ */
+export interface InternalRenderingRequestHandlerContext extends RequestHandlerContextBase {
+ core: Promise<{
+ uiSettings: UiSettingsRequestHandlerContext;
+ }>;
+}
+
+/**
+ * Router bound to the {@link InternalRenderingRequestHandlerContext}.
+ * Used by core's rendering routes.
+ * @internal
+ */
+export type InternalRenderingRouter = IRouter;
diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx
index 2ce22b731b5e5..ac6ff9ee2f2b8 100644
--- a/src/core/server/rendering/rendering_service.tsx
+++ b/src/core/server/rendering/rendering_service.tsx
@@ -29,6 +29,7 @@ import { getSettingValue, getStylesheetPaths } from './render_utils';
import type { HttpAuth, KibanaRequest } from '../http';
import { IUiSettingsClient } from '../ui_settings';
import { filterUiPlugins } from './filter_ui_plugins';
+import type { InternalRenderingRequestHandlerContext } from './internal_types';
type RenderOptions =
| (RenderingPrebootDeps & { status?: never; elasticsearch?: never })
@@ -42,7 +43,7 @@ export class RenderingService {
http,
uiPlugins,
}: RenderingPrebootDeps): Promise {
- http.registerRoutes('', (router) => {
+ http.registerRoutes('', (router) => {
registerBootstrapRoute({
router,
renderer: bootstrapRendererFactory({
@@ -66,7 +67,7 @@ export class RenderingService {
uiPlugins,
}: RenderingSetupDeps): Promise {
registerBootstrapRoute({
- router: http.createRouter(''),
+ router: http.createRouter(''),
renderer: bootstrapRendererFactory({
uiPlugins,
serverBasePath: http.basePath.serverBasePath,
diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts
index 73a20c0402bbe..434096193ff9f 100644
--- a/src/core/server/saved_objects/index.ts
+++ b/src/core/server/saved_objects/index.ts
@@ -99,3 +99,5 @@ export type { SavedObjectsValidationMap, SavedObjectsValidationSpec } from './va
export { savedObjectsConfig, savedObjectsMigrationConfig } from './saved_objects_config';
export { SavedObjectTypeRegistry } from './saved_objects_type_registry';
export type { ISavedObjectTypeRegistry } from './saved_objects_type_registry';
+export { CoreSavedObjectsRouteHandlerContext } from './saved_objects_route_handler_context';
+export type { SavedObjectsRequestHandlerContext } from './saved_objects_route_handler_context';
diff --git a/src/core/server/saved_objects/internal_types.ts b/src/core/server/saved_objects/internal_types.ts
new file mode 100644
index 0000000000000..6d3851fca120b
--- /dev/null
+++ b/src/core/server/saved_objects/internal_types.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { RequestHandlerContextBase } from '..';
+import type { IRouter } from '../http';
+import type { ElasticsearchRequestHandlerContext } from '../elasticsearch';
+import type { SavedObjectsRequestHandlerContext } from './saved_objects_route_handler_context';
+
+/**
+ * Request handler context used by core's savedObjects routes.
+ * @internal
+ */
+export interface InternalSavedObjectsRequestHandlerContext extends RequestHandlerContextBase {
+ core: Promise<{
+ savedObjects: SavedObjectsRequestHandlerContext;
+ elasticsearch: ElasticsearchRequestHandlerContext;
+ }>;
+}
+
+/**
+ * Router bound to the {@link InternalSavedObjectsRequestHandlerContext}.
+ * Used by core's savedObjects routes.
+ * @internal
+ */
+export type InternalSavedObjectRouter = IRouter;
diff --git a/src/core/server/saved_objects/migrations/actions/es_errors.test.ts b/src/core/server/saved_objects/migrations/actions/es_errors.test.ts
index b34366b7386d2..f0004851b1bf9 100644
--- a/src/core/server/saved_objects/migrations/actions/es_errors.test.ts
+++ b/src/core/server/saved_objects/migrations/actions/es_errors.test.ts
@@ -9,6 +9,7 @@
import {
isClusterShardLimitExceeded,
isIncompatibleMappingException,
+ isIndexNotFoundException,
isWriteBlockException,
} from './es_errors';
@@ -37,6 +38,9 @@ describe('isWriteBlockError', () => {
})
).toEqual(false);
});
+ it('returns false undefined', () => {
+ expect(isWriteBlockException(undefined)).toEqual(false);
+ });
});
describe('isIncompatibleMappingExceptionError', () => {
@@ -57,6 +61,31 @@ describe('isIncompatibleMappingExceptionError', () => {
})
).toEqual(true);
});
+ it('returns false undefined', () => {
+ expect(isIncompatibleMappingException(undefined)).toEqual(false);
+ });
+});
+
+describe('isIndexNotFoundException', () => {
+ it('returns true with index_not_found_exception errors', () => {
+ expect(
+ isIndexNotFoundException({
+ type: 'index_not_found_exception',
+ reason: 'idk',
+ })
+ ).toEqual(true);
+ });
+ it('returns false for other errors', () => {
+ expect(
+ isIndexNotFoundException({
+ type: 'validation_exception',
+ reason: 'idk',
+ })
+ ).toEqual(false);
+ });
+ it('returns false undefined', () => {
+ expect(isIndexNotFoundException(undefined)).toEqual(false);
+ });
});
describe('isClusterShardLimitExceeded', () => {
@@ -77,4 +106,7 @@ describe('isClusterShardLimitExceeded', () => {
})
).toEqual(false);
});
+ it('returns false undefined', () => {
+ expect(isClusterShardLimitExceeded(undefined)).toEqual(false);
+ });
});
diff --git a/src/core/server/saved_objects/migrations/actions/es_errors.ts b/src/core/server/saved_objects/migrations/actions/es_errors.ts
index 9f571d38ffd85..2bf432de032a6 100644
--- a/src/core/server/saved_objects/migrations/actions/es_errors.ts
+++ b/src/core/server/saved_objects/migrations/actions/es_errors.ts
@@ -7,25 +7,28 @@
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-export const isWriteBlockException = ({ type, reason }: estypes.ErrorCause): boolean => {
+export const isWriteBlockException = (errorCause?: estypes.ErrorCause): boolean => {
return (
- type === 'cluster_block_exception' &&
- reason.match(/index \[.+] blocked by: \[FORBIDDEN\/8\/.+ \(api\)\]/) !== null
+ errorCause?.type === 'cluster_block_exception' &&
+ errorCause?.reason.match(/index \[.+] blocked by: \[FORBIDDEN\/8\/.+ \(api\)\]/) !== null
);
};
-export const isIncompatibleMappingException = ({ type }: estypes.ErrorCause): boolean => {
- return type === 'strict_dynamic_mapping_exception' || type === 'mapper_parsing_exception';
+export const isIncompatibleMappingException = (errorCause?: estypes.ErrorCause): boolean => {
+ return (
+ errorCause?.type === 'strict_dynamic_mapping_exception' ||
+ errorCause?.type === 'mapper_parsing_exception'
+ );
};
-export const isIndexNotFoundException = ({ type }: estypes.ErrorCause): boolean => {
- return type === 'index_not_found_exception';
+export const isIndexNotFoundException = (errorCause?: estypes.ErrorCause): boolean => {
+ return errorCause?.type === 'index_not_found_exception';
};
-export const isClusterShardLimitExceeded = ({ type, reason }: estypes.ErrorCause): boolean => {
+export const isClusterShardLimitExceeded = (errorCause?: estypes.ErrorCause): boolean => {
return (
- type === 'validation_exception' &&
- reason.match(
+ errorCause?.type === 'validation_exception' &&
+ errorCause?.reason.match(
/this action would add .* shards, but this cluster currently has .* maximum normal shards open/
) !== null
);
diff --git a/src/core/server/saved_objects/routes/bulk_create.ts b/src/core/server/saved_objects/routes/bulk_create.ts
index b72cd6715fa10..6a671c830ca69 100644
--- a/src/core/server/saved_objects/routes/bulk_create.ts
+++ b/src/core/server/saved_objects/routes/bulk_create.ts
@@ -7,15 +7,18 @@
*/
import { schema } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: InternalCoreUsageDataSetup;
}
-export const registerBulkCreateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {
+export const registerBulkCreateRoute = (
+ router: InternalSavedObjectRouter,
+ { coreUsageData }: RouteDependencies
+) => {
router.post(
{
path: '/_bulk_create',
diff --git a/src/core/server/saved_objects/routes/bulk_get.ts b/src/core/server/saved_objects/routes/bulk_get.ts
index 87b6a604ac6c4..ba485c832ca65 100644
--- a/src/core/server/saved_objects/routes/bulk_get.ts
+++ b/src/core/server/saved_objects/routes/bulk_get.ts
@@ -7,15 +7,18 @@
*/
import { schema } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: InternalCoreUsageDataSetup;
}
-export const registerBulkGetRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {
+export const registerBulkGetRoute = (
+ router: InternalSavedObjectRouter,
+ { coreUsageData }: RouteDependencies
+) => {
router.post(
{
path: '/_bulk_get',
diff --git a/src/core/server/saved_objects/routes/bulk_resolve.ts b/src/core/server/saved_objects/routes/bulk_resolve.ts
index 5754ec180541c..e689e243ebc95 100644
--- a/src/core/server/saved_objects/routes/bulk_resolve.ts
+++ b/src/core/server/saved_objects/routes/bulk_resolve.ts
@@ -7,15 +7,18 @@
*/
import { schema } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: InternalCoreUsageDataSetup;
}
-export const registerBulkResolveRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {
+export const registerBulkResolveRoute = (
+ router: InternalSavedObjectRouter,
+ { coreUsageData }: RouteDependencies
+) => {
router.post(
{
path: '/_bulk_resolve',
diff --git a/src/core/server/saved_objects/routes/bulk_update.ts b/src/core/server/saved_objects/routes/bulk_update.ts
index 961b8349d1745..2edfa23ed8786 100644
--- a/src/core/server/saved_objects/routes/bulk_update.ts
+++ b/src/core/server/saved_objects/routes/bulk_update.ts
@@ -7,15 +7,18 @@
*/
import { schema } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: InternalCoreUsageDataSetup;
}
-export const registerBulkUpdateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {
+export const registerBulkUpdateRoute = (
+ router: InternalSavedObjectRouter,
+ { coreUsageData }: RouteDependencies
+) => {
router.put(
{
path: '/_bulk_update',
diff --git a/src/core/server/saved_objects/routes/create.ts b/src/core/server/saved_objects/routes/create.ts
index eb3938db0a9c7..282cbafc0b2f5 100644
--- a/src/core/server/saved_objects/routes/create.ts
+++ b/src/core/server/saved_objects/routes/create.ts
@@ -7,15 +7,18 @@
*/
import { schema } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: InternalCoreUsageDataSetup;
}
-export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {
+export const registerCreateRoute = (
+ router: InternalSavedObjectRouter,
+ { coreUsageData }: RouteDependencies
+) => {
router.post(
{
path: '/{type}/{id?}',
diff --git a/src/core/server/saved_objects/routes/delete.ts b/src/core/server/saved_objects/routes/delete.ts
index 5c239a55a6923..7410a237d39f7 100644
--- a/src/core/server/saved_objects/routes/delete.ts
+++ b/src/core/server/saved_objects/routes/delete.ts
@@ -7,15 +7,18 @@
*/
import { schema } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: InternalCoreUsageDataSetup;
}
-export const registerDeleteRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {
+export const registerDeleteRoute = (
+ router: InternalSavedObjectRouter,
+ { coreUsageData }: RouteDependencies
+) => {
router.delete(
{
path: '/{type}/{id}',
diff --git a/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts b/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts
index a30da9723b916..5d36994928363 100644
--- a/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts
+++ b/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts
@@ -6,8 +6,8 @@
* Side Public License, v 1.
*/
-import { IRouter } from '../../../http';
import { catchAndReturnBoomErrors } from '../utils';
+import type { InternalSavedObjectRouter } from '../../internal_types';
import { deleteUnknownTypeObjects } from '../../deprecations';
interface RouteDependencies {
@@ -16,7 +16,7 @@ interface RouteDependencies {
}
export const registerDeleteUnknownTypesRoute = (
- router: IRouter,
+ router: InternalSavedObjectRouter,
{ kibanaIndex, kibanaVersion }: RouteDependencies
) => {
router.post(
diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts
index 16547970369c2..e011c71575080 100644
--- a/src/core/server/saved_objects/routes/export.ts
+++ b/src/core/server/saved_objects/routes/export.ts
@@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema';
import stringify from 'json-stable-stringify';
import { createPromiseFromStreams, createMapStream, createConcatStream } from '@kbn/utils';
-import { IRouter, KibanaRequest } from '../../http';
+import { KibanaRequest } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { SavedObjectConfig } from '../saved_objects_config';
import {
@@ -18,6 +18,7 @@ import {
SavedObjectsExportByObjectOptions,
SavedObjectsExportError,
} from '../export';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { validateTypes, validateObjects, catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
@@ -129,7 +130,7 @@ const validateOptions = (
};
export const registerExportRoute = (
- router: IRouter,
+ router: InternalSavedObjectRouter,
{ config, coreUsageData }: RouteDependencies
) => {
const { maxImportExportSize } = config;
diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts
index 01ac9ae9025f4..7ebfef5be7c39 100644
--- a/src/core/server/saved_objects/routes/find.ts
+++ b/src/core/server/saved_objects/routes/find.ts
@@ -7,15 +7,18 @@
*/
import { schema } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: InternalCoreUsageDataSetup;
}
-export const registerFindRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {
+export const registerFindRoute = (
+ router: InternalSavedObjectRouter,
+ { coreUsageData }: RouteDependencies
+) => {
const referenceSchema = schema.object({
type: schema.string(),
id: schema.string(),
diff --git a/src/core/server/saved_objects/routes/get.ts b/src/core/server/saved_objects/routes/get.ts
index 2ea9a13bbce64..e7a2a779c4037 100644
--- a/src/core/server/saved_objects/routes/get.ts
+++ b/src/core/server/saved_objects/routes/get.ts
@@ -7,15 +7,18 @@
*/
import { schema } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: InternalCoreUsageDataSetup;
}
-export const registerGetRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {
+export const registerGetRoute = (
+ router: InternalSavedObjectRouter,
+ { coreUsageData }: RouteDependencies
+) => {
router.get(
{
path: '/{type}/{id}',
diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts
index 545d01b454741..0c56acf9e2d68 100644
--- a/src/core/server/saved_objects/routes/import.ts
+++ b/src/core/server/saved_objects/routes/import.ts
@@ -9,10 +9,10 @@
import { Readable } from 'stream';
import { extname } from 'path';
import { schema } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { SavedObjectConfig } from '../saved_objects_config';
import { SavedObjectsImportError } from '../import';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils';
interface RouteDependencies {
@@ -27,7 +27,7 @@ interface FileStream extends Readable {
}
export const registerImportRoute = (
- router: IRouter,
+ router: InternalSavedObjectRouter,
{ config, coreUsageData }: RouteDependencies
) => {
const { maxImportPayloadBytes } = config;
diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts
index f76f802928190..34083e0d6ddf8 100644
--- a/src/core/server/saved_objects/routes/index.ts
+++ b/src/core/server/saved_objects/routes/index.ts
@@ -11,6 +11,7 @@ import { InternalHttpServiceSetup } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { SavedObjectConfig } from '../saved_objects_config';
import { IKibanaMigrator } from '../migrations';
+import type { InternalSavedObjectsRequestHandlerContext } from '../internal_types';
import { registerGetRoute } from './get';
import { registerResolveRoute } from './resolve';
import { registerCreateRoute } from './create';
@@ -46,7 +47,8 @@ export function registerRoutes({
kibanaVersion: string;
kibanaIndex: string;
}) {
- const router = http.createRouter('/api/saved_objects/');
+ const router =
+ http.createRouter('/api/saved_objects/');
registerGetRoute(router, { coreUsageData });
registerResolveRoute(router, { coreUsageData });
@@ -62,7 +64,7 @@ export function registerRoutes({
registerImportRoute(router, { config, coreUsageData });
registerResolveImportErrorsRoute(router, { config, coreUsageData });
- const legacyRouter = http.createRouter('');
+ const legacyRouter = http.createRouter('');
registerLegacyImportRoute(legacyRouter, {
maxImportPayloadBytes: config.maxImportPayloadBytes,
coreUsageData,
@@ -70,7 +72,9 @@ export function registerRoutes({
});
registerLegacyExportRoute(legacyRouter, { kibanaVersion, coreUsageData, logger });
- const internalRouter = http.createRouter('/internal/saved_objects/');
+ const internalRouter = http.createRouter(
+ '/internal/saved_objects/'
+ );
registerMigrateRoute(internalRouter, migratorPromise);
registerDeleteUnknownTypesRoute(internalRouter, { kibanaIndex, kibanaVersion });
diff --git a/src/core/server/saved_objects/routes/integration_tests/bulk_create.test.ts b/src/core/server/saved_objects/routes/integration_tests/bulk_create.test.ts
index eeec6bd6b0be0..c34ea099d0ec8 100644
--- a/src/core/server/saved_objects/routes/integration_tests/bulk_create.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/bulk_create.test.ts
@@ -13,6 +13,7 @@ import { CoreUsageStatsClient } from '../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { setupServer } from '../test_utils';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
type SetupServerReturn = Awaited>;
@@ -28,7 +29,8 @@ describe('POST /api/saved_objects/_bulk_create', () => {
savedObjectsClient = handlerContext.savedObjects.client;
savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: [] });
- const router = httpSetup.createRouter('/api/saved_objects/');
+ const router =
+ httpSetup.createRouter('/api/saved_objects/');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsBulkCreate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
diff --git a/src/core/server/saved_objects/routes/integration_tests/bulk_get.test.ts b/src/core/server/saved_objects/routes/integration_tests/bulk_get.test.ts
index f389307d45c41..a82a5351e9949 100644
--- a/src/core/server/saved_objects/routes/integration_tests/bulk_get.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/bulk_get.test.ts
@@ -13,6 +13,7 @@ import { CoreUsageStatsClient } from '../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { setupServer } from '../test_utils';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
type SetupServerReturn = Awaited>;
@@ -30,7 +31,8 @@ describe('POST /api/saved_objects/_bulk_get', () => {
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [],
});
- const router = httpSetup.createRouter('/api/saved_objects/');
+ const router =
+ httpSetup.createRouter('/api/saved_objects/');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsBulkGet.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
diff --git a/src/core/server/saved_objects/routes/integration_tests/bulk_resolve.test.ts b/src/core/server/saved_objects/routes/integration_tests/bulk_resolve.test.ts
index bb5067a13ba3e..90f06368468be 100644
--- a/src/core/server/saved_objects/routes/integration_tests/bulk_resolve.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/bulk_resolve.test.ts
@@ -13,6 +13,7 @@ import { CoreUsageStatsClient } from '../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { setupServer } from '../test_utils';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
type SetupServerReturn = Awaited>;
@@ -30,7 +31,8 @@ describe('POST /api/saved_objects/_bulk_resolve', () => {
savedObjectsClient.bulkResolve.mockResolvedValue({
resolved_objects: [],
});
- const router = httpSetup.createRouter('/api/saved_objects/');
+ const router =
+ httpSetup.createRouter('/api/saved_objects/');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsBulkResolve.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
diff --git a/src/core/server/saved_objects/routes/integration_tests/bulk_update.test.ts b/src/core/server/saved_objects/routes/integration_tests/bulk_update.test.ts
index 8c55686b2fd0f..d92041bce3c72 100644
--- a/src/core/server/saved_objects/routes/integration_tests/bulk_update.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/bulk_update.test.ts
@@ -13,6 +13,7 @@ import { CoreUsageStatsClient } from '../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { setupServer } from '../test_utils';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
type SetupServerReturn = Awaited>;
@@ -27,7 +28,8 @@ describe('PUT /api/saved_objects/_bulk_update', () => {
({ server, httpSetup, handlerContext } = await setupServer());
savedObjectsClient = handlerContext.savedObjects.client;
- const router = httpSetup.createRouter('/api/saved_objects/');
+ const router =
+ httpSetup.createRouter('/api/saved_objects/');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsBulkUpdate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
diff --git a/src/core/server/saved_objects/routes/integration_tests/create.test.ts b/src/core/server/saved_objects/routes/integration_tests/create.test.ts
index 9d65843271cc0..e4f566c769234 100644
--- a/src/core/server/saved_objects/routes/integration_tests/create.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/create.test.ts
@@ -13,6 +13,7 @@ import { CoreUsageStatsClient } from '../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { setupServer } from '../test_utils';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
type SetupServerReturn = Awaited>;
@@ -37,7 +38,8 @@ describe('POST /api/saved_objects/{type}', () => {
savedObjectsClient = handlerContext.savedObjects.client;
savedObjectsClient.create.mockImplementation(() => Promise.resolve(clientResponse));
- const router = httpSetup.createRouter('/api/saved_objects/');
+ const router =
+ httpSetup.createRouter('/api/saved_objects/');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsCreate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
diff --git a/src/core/server/saved_objects/routes/integration_tests/delete.test.ts b/src/core/server/saved_objects/routes/integration_tests/delete.test.ts
index 84a2db60b587a..849b99b49fbc0 100644
--- a/src/core/server/saved_objects/routes/integration_tests/delete.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/delete.test.ts
@@ -13,6 +13,7 @@ import { CoreUsageStatsClient } from '../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { setupServer } from '../test_utils';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
type SetupServerReturn = Awaited>;
@@ -28,7 +29,8 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => {
savedObjectsClient = handlerContext.savedObjects.getClient();
handlerContext.savedObjects.getClient = jest.fn().mockImplementation(() => savedObjectsClient);
- const router = httpSetup.createRouter('/api/saved_objects/');
+ const router =
+ httpSetup.createRouter('/api/saved_objects/');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsDelete.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
diff --git a/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts b/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts
index 60d916744babc..87376de206810 100644
--- a/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts
@@ -12,6 +12,7 @@ import { elasticsearchServiceMock } from '../../../elasticsearch/elasticsearch_s
import { typeRegistryMock } from '../../saved_objects_type_registry.mock';
import { setupServer } from '../test_utils';
import { SavedObjectsType } from '../../..';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
type SetupServerReturn = Awaited>;
@@ -37,7 +38,9 @@ describe('POST /internal/saved_objects/deprecations/_delete_unknown_types', () =
handlerContext.elasticsearch.client.asCurrentUser = elasticsearchClient.asCurrentUser;
handlerContext.elasticsearch.client.asInternalUser = elasticsearchClient.asInternalUser;
- const router = httpSetup.createRouter('/internal/saved_objects/');
+ const router = httpSetup.createRouter(
+ '/internal/saved_objects/'
+ );
registerDeleteUnknownTypesRoute(router, {
kibanaVersion,
kibanaIndex,
diff --git a/src/core/server/saved_objects/routes/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/integration_tests/export.test.ts
index 1227d2636c555..9bdd6f39abbd0 100644
--- a/src/core/server/saved_objects/routes/integration_tests/export.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/export.test.ts
@@ -19,6 +19,7 @@ import { savedObjectsExporterMock } from '../../export/saved_objects_exporter.mo
import { SavedObjectConfig } from '../../saved_objects_config';
import { registerExportRoute } from '../export';
import { setupServer, createExportableType } from '../test_utils';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
type SetupServerReturn = Awaited>;
const allowedTypes = ['index-pattern', 'search'];
@@ -41,7 +42,8 @@ describe('POST /api/saved_objects/_export', () => {
);
exporter = handlerContext.savedObjects.getExporter();
- const router = httpSetup.createRouter('/api/saved_objects/');
+ const router =
+ httpSetup.createRouter('/api/saved_objects/');
handlerContext.savedObjects.getExporter = jest
.fn()
.mockImplementation(() => exporter as ReturnType);
diff --git a/src/core/server/saved_objects/routes/integration_tests/find.test.ts b/src/core/server/saved_objects/routes/integration_tests/find.test.ts
index c18044f8973b8..8758666bc487d 100644
--- a/src/core/server/saved_objects/routes/integration_tests/find.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/find.test.ts
@@ -15,6 +15,7 @@ import { CoreUsageStatsClient } from '../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { setupServer } from '../test_utils';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
type SetupServerReturn = Awaited>;
@@ -38,7 +39,8 @@ describe('GET /api/saved_objects/_find', () => {
savedObjectsClient.find.mockResolvedValue(clientResponse);
- const router = httpSetup.createRouter('/api/saved_objects/');
+ const router =
+ httpSetup.createRouter('/api/saved_objects/');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsFind.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
diff --git a/src/core/server/saved_objects/routes/integration_tests/get.test.ts b/src/core/server/saved_objects/routes/integration_tests/get.test.ts
index 98d9d9170e5eb..8b34c9f2ef958 100644
--- a/src/core/server/saved_objects/routes/integration_tests/get.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/get.test.ts
@@ -17,6 +17,7 @@ import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_da
import { HttpService, InternalHttpServiceSetup } from '../../../http';
import { createHttpServer, createCoreContext } from '../../../http/test_utils';
import { contextServiceMock, coreMock } from '../../../mocks';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
const coreId = Symbol('core');
@@ -41,11 +42,16 @@ describe('GET /api/saved_objects/{type}/{id}', () => {
handlerContext = coreMock.createRequestHandlerContext();
savedObjectsClient = handlerContext.savedObjects.client;
- httpSetup.registerRouteHandlerContext(coreId, 'core', async (ctx, req, res) => {
- return handlerContext;
- });
+ httpSetup.registerRouteHandlerContext(
+ coreId,
+ 'core',
+ (ctx, req, res) => {
+ return handlerContext;
+ }
+ );
- const router = httpSetup.createRouter('/api/saved_objects/');
+ const router =
+ httpSetup.createRouter('/api/saved_objects/');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsGet.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts
index 2f6f206366766..9e6e52977b2e1 100644
--- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts
@@ -17,6 +17,7 @@ import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_da
import { SavedObjectConfig } from '../../saved_objects_config';
import { setupServer, createExportableType } from '../test_utils';
import { SavedObjectsErrorHelpers, SavedObjectsImporter } from '../..';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
type SetupServerReturn = Awaited>;
@@ -69,7 +70,9 @@ describe(`POST ${URL}`, () => {
.fn()
.mockImplementation(() => importer as jest.Mocked);
- const router = httpSetup.createRouter('/internal/saved_objects/');
+ const router = httpSetup.createRouter(
+ '/internal/saved_objects/'
+ );
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsImport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve.test.ts
index f551abda188ef..78397ca00cb60 100644
--- a/src/core/server/saved_objects/routes/integration_tests/resolve.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/resolve.test.ts
@@ -17,6 +17,7 @@ import { executionContextServiceMock } from '@kbn/core-execution-context-server-
import { HttpService, InternalHttpServiceSetup } from '../../../http';
import { createHttpServer, createCoreContext } from '../../../http/test_utils';
import { contextServiceMock, coreMock } from '../../../mocks';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
const coreId = Symbol('core');
@@ -41,11 +42,16 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => {
handlerContext = coreMock.createRequestHandlerContext();
savedObjectsClient = handlerContext.savedObjects.client;
- httpSetup.registerRouteHandlerContext(coreId, 'core', async (ctx, req, res) => {
- return handlerContext;
- });
+ httpSetup.registerRouteHandlerContext(
+ coreId,
+ 'core',
+ (ctx, req, res) => {
+ return handlerContext;
+ }
+ );
- const router = httpSetup.createRouter('/api/saved_objects/');
+ const router =
+ httpSetup.createRouter('/api/saved_objects/');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsResolve.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts
index dfc69edaff420..101cf05032618 100644
--- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts
@@ -17,6 +17,7 @@ import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_da
import { setupServer, createExportableType } from '../test_utils';
import { SavedObjectConfig } from '../../saved_objects_config';
import { SavedObjectsImporter } from '../..';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
type SetupServerReturn = Awaited>;
@@ -76,7 +77,8 @@ describe(`POST ${URL}`, () => {
.fn()
.mockImplementation(() => importer as jest.Mocked);
- const router = httpSetup.createRouter('/api/saved_objects/');
+ const router =
+ httpSetup.createRouter('/api/saved_objects/');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsResolveImportErrors.mockRejectedValue(
new Error('Oh no!') // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
diff --git a/src/core/server/saved_objects/routes/integration_tests/update.test.ts b/src/core/server/saved_objects/routes/integration_tests/update.test.ts
index 78e7b9c1e93d5..1a41d94b0febe 100644
--- a/src/core/server/saved_objects/routes/integration_tests/update.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/update.test.ts
@@ -13,6 +13,7 @@ import { CoreUsageStatsClient } from '../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { setupServer } from '../test_utils';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../internal_types';
type SetupServerReturn = Awaited>;
@@ -38,7 +39,8 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => {
savedObjectsClient = handlerContext.savedObjects.client;
savedObjectsClient.update.mockResolvedValue(clientResponse);
- const router = httpSetup.createRouter('/api/saved_objects/');
+ const router =
+ httpSetup.createRouter('/api/saved_objects/');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsUpdate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
diff --git a/src/core/server/saved_objects/routes/legacy_import_export/export.ts b/src/core/server/saved_objects/routes/legacy_import_export/export.ts
index 7141d74b71904..9073090de8fe7 100644
--- a/src/core/server/saved_objects/routes/legacy_import_export/export.ts
+++ b/src/core/server/saved_objects/routes/legacy_import_export/export.ts
@@ -8,12 +8,13 @@
import moment from 'moment';
import { schema } from '@kbn/config-schema';
+import type { Logger } from '@kbn/logging';
import { InternalCoreUsageDataSetup } from '../../../core_usage_data';
-import { IRouter, Logger } from '../../..';
+import type { InternalSavedObjectRouter } from '../../internal_types';
import { exportDashboards } from './lib';
export const registerLegacyExportRoute = (
- router: IRouter,
+ router: InternalSavedObjectRouter,
{
kibanaVersion,
coreUsageData,
diff --git a/src/core/server/saved_objects/routes/legacy_import_export/import.ts b/src/core/server/saved_objects/routes/legacy_import_export/import.ts
index d98c14f9b620d..d4a13b2973964 100644
--- a/src/core/server/saved_objects/routes/legacy_import_export/import.ts
+++ b/src/core/server/saved_objects/routes/legacy_import_export/import.ts
@@ -7,12 +7,14 @@
*/
import { schema } from '@kbn/config-schema';
-import { IRouter, Logger, SavedObject } from '../../..';
+import type { Logger } from '@kbn/logging';
+import type { SavedObject } from '../../..';
import { InternalCoreUsageDataSetup } from '../../../core_usage_data';
+import type { InternalSavedObjectRouter } from '../../internal_types';
import { importDashboards } from './lib';
export const registerLegacyImportRoute = (
- router: IRouter,
+ router: InternalSavedObjectRouter,
{
maxImportPayloadBytes,
coreUsageData,
diff --git a/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/export.test.ts
index 6ae4a74aee014..7d0a8607fb6ed 100644
--- a/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/export.test.ts
+++ b/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/export.test.ts
@@ -38,6 +38,7 @@ import { coreUsageDataServiceMock } from '../../../../core_usage_data/core_usage
import { registerLegacyExportRoute } from '../export';
import { setupServer } from '../../test_utils';
import { loggerMock } from '@kbn/logging-mocks';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../../internal_types';
type SetupServerReturn = Awaited>;
let coreUsageStatsClient: jest.Mocked;
@@ -49,7 +50,7 @@ describe('POST /api/dashboards/export', () => {
beforeEach(async () => {
({ server, httpSetup } = await setupServer());
- const router = httpSetup.createRouter('');
+ const router = httpSetup.createRouter('');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementLegacyDashboardsExport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
diff --git a/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/import.test.ts
index 13d5638440547..37f82aa47ece5 100644
--- a/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/import.test.ts
+++ b/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/import.test.ts
@@ -38,6 +38,7 @@ import { coreUsageDataServiceMock } from '../../../../core_usage_data/core_usage
import { registerLegacyImportRoute } from '../import';
import { setupServer } from '../../test_utils';
import { loggerMock } from '@kbn/logging-mocks';
+import type { InternalSavedObjectsRequestHandlerContext } from '../../../internal_types';
type SetupServerReturn = Awaited>;
let coreUsageStatsClient: jest.Mocked;
@@ -49,7 +50,7 @@ describe('POST /api/dashboards/import', () => {
beforeEach(async () => {
({ server, httpSetup } = await setupServer());
- const router = httpSetup.createRouter('');
+ const router = httpSetup.createRouter('');
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementLegacyDashboardsImport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
diff --git a/src/core/server/saved_objects/routes/migrate.ts b/src/core/server/saved_objects/routes/migrate.ts
index 404074124c92b..05579855cbfc3 100644
--- a/src/core/server/saved_objects/routes/migrate.ts
+++ b/src/core/server/saved_objects/routes/migrate.ts
@@ -6,12 +6,12 @@
* Side Public License, v 1.
*/
-import { IRouter } from '../../http';
import { IKibanaMigrator } from '../migrations';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { catchAndReturnBoomErrors } from './utils';
export const registerMigrateRoute = (
- router: IRouter,
+ router: InternalSavedObjectRouter,
migratorPromise: Promise
) => {
router.post(
diff --git a/src/core/server/saved_objects/routes/resolve.ts b/src/core/server/saved_objects/routes/resolve.ts
index ae09f6526baa3..b0a7153e8d226 100644
--- a/src/core/server/saved_objects/routes/resolve.ts
+++ b/src/core/server/saved_objects/routes/resolve.ts
@@ -7,14 +7,17 @@
*/
import { schema } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
+import type { InternalSavedObjectRouter } from '../internal_types';
interface RouteDependencies {
coreUsageData: InternalCoreUsageDataSetup;
}
-export const registerResolveRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {
+export const registerResolveRoute = (
+ router: InternalSavedObjectRouter,
+ { coreUsageData }: RouteDependencies
+) => {
router.get(
{
path: '/resolve/{type}/{id}',
diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts
index bf536e906d7da..4bedec1715a4f 100644
--- a/src/core/server/saved_objects/routes/resolve_import_errors.ts
+++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts
@@ -10,11 +10,12 @@ import { extname } from 'path';
import { Readable } from 'stream';
import { schema } from '@kbn/config-schema';
import { chain } from 'lodash';
-import { IRouter } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { SavedObjectConfig } from '../saved_objects_config';
import { SavedObjectsImportError } from '../import';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils';
+
interface RouteDependencies {
config: SavedObjectConfig;
coreUsageData: InternalCoreUsageDataSetup;
@@ -27,7 +28,7 @@ interface FileStream extends Readable {
}
export const registerResolveImportErrorsRoute = (
- router: IRouter,
+ router: InternalSavedObjectRouter,
{ config, coreUsageData }: RouteDependencies
) => {
const { maxImportPayloadBytes } = config;
diff --git a/src/core/server/saved_objects/routes/test_utils.ts b/src/core/server/saved_objects/routes/test_utils.ts
index 0d5e7588ad6ce..5783ecb9e5e27 100644
--- a/src/core/server/saved_objects/routes/test_utils.ts
+++ b/src/core/server/saved_objects/routes/test_utils.ts
@@ -11,6 +11,7 @@ import { ContextService } from '../../context';
import { createHttpServer, createCoreContext } from '../../http/test_utils';
import { contextServiceMock, coreMock } from '../../mocks';
import { SavedObjectsType } from '../types';
+import { InternalSavedObjectsRequestHandlerContext } from '../internal_types';
const defaultCoreId = Symbol('core');
@@ -26,9 +27,13 @@ export const setupServer = async (coreId: symbol = defaultCoreId) => {
});
const handlerContext = coreMock.createRequestHandlerContext();
- httpSetup.registerRouteHandlerContext(coreId, 'core', async (ctx, req, res) => {
- return handlerContext;
- });
+ httpSetup.registerRouteHandlerContext(
+ coreId,
+ 'core',
+ (ctx, req, res) => {
+ return handlerContext;
+ }
+ );
return {
server,
diff --git a/src/core/server/saved_objects/routes/update.ts b/src/core/server/saved_objects/routes/update.ts
index 5383ab76a1e4d..3d68c6a698a04 100644
--- a/src/core/server/saved_objects/routes/update.ts
+++ b/src/core/server/saved_objects/routes/update.ts
@@ -7,16 +7,19 @@
*/
import { schema } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import type { SavedObjectsUpdateOptions } from '../service/saved_objects_client';
+import type { InternalSavedObjectRouter } from '../internal_types';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: InternalCoreUsageDataSetup;
}
-export const registerUpdateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {
+export const registerUpdateRoute = (
+ router: InternalSavedObjectRouter,
+ { coreUsageData }: RouteDependencies
+) => {
router.put(
{
path: '/{type}/{id}',
diff --git a/src/core/server/saved_objects/saved_objects_route_handler_context.ts b/src/core/server/saved_objects/saved_objects_route_handler_context.ts
new file mode 100644
index 0000000000000..bda45de389c98
--- /dev/null
+++ b/src/core/server/saved_objects/saved_objects_route_handler_context.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { KibanaRequest } from '../http';
+import type { InternalSavedObjectsServiceStart } from './saved_objects_service';
+import type { ISavedObjectTypeRegistry } from './saved_objects_type_registry';
+import type { SavedObjectsClientContract } from './types';
+import type { SavedObjectsClientProviderOptions } from './service';
+import type { ISavedObjectsExporter } from './export';
+import type { ISavedObjectsImporter } from './import';
+
+/**
+ * Core's `savedObjects` request handler context.
+ * @public
+ */
+export interface SavedObjectsRequestHandlerContext {
+ client: SavedObjectsClientContract;
+ typeRegistry: ISavedObjectTypeRegistry;
+ getClient: (options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract;
+ getExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter;
+ getImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter;
+}
+
+/**
+ * The {@link SavedObjectsRequestHandlerContext} implementation.
+ * @internal
+ */
+export class CoreSavedObjectsRouteHandlerContext implements SavedObjectsRequestHandlerContext {
+ constructor(
+ private readonly savedObjectsStart: InternalSavedObjectsServiceStart,
+ private readonly request: KibanaRequest
+ ) {}
+
+ #scopedSavedObjectsClient?: SavedObjectsClientContract;
+ #typeRegistry?: ISavedObjectTypeRegistry;
+
+ public get client() {
+ if (this.#scopedSavedObjectsClient == null) {
+ this.#scopedSavedObjectsClient = this.savedObjectsStart.getScopedClient(this.request);
+ }
+ return this.#scopedSavedObjectsClient;
+ }
+
+ public get typeRegistry() {
+ if (this.#typeRegistry == null) {
+ this.#typeRegistry = this.savedObjectsStart.getTypeRegistry();
+ }
+ return this.#typeRegistry;
+ }
+
+ public getClient = (options?: SavedObjectsClientProviderOptions) => {
+ if (!options) return this.client;
+ return this.savedObjectsStart.getScopedClient(this.request, options);
+ };
+
+ public getExporter = (client: SavedObjectsClientContract) => {
+ return this.savedObjectsStart.createExporter(client);
+ };
+
+ public getImporter = (client: SavedObjectsClientContract) => {
+ return this.savedObjectsStart.createImporter(client);
+ };
+}
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index 23586396bf62b..3f877d31eec3a 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -57,6 +57,7 @@ import { CoreRouteHandlerContext } from './core_route_handler_context';
import { PrebootCoreRouteHandlerContext } from './preboot_core_route_handler_context';
import { PrebootService } from './preboot';
import { DiscoveredPlugins } from './plugins';
+import type { RequestHandlerContext, PrebootRequestHandlerContext } from '.';
const coreId = Symbol('core');
const rootConfigPath = '';
@@ -208,9 +209,13 @@ export class Server {
await this.plugins.preboot(corePreboot);
- httpPreboot.registerRouteHandlerContext(coreId, 'core', (() => {
- return new PrebootCoreRouteHandlerContext(corePreboot);
- }) as any);
+ httpPreboot.registerRouteHandlerContext(
+ coreId,
+ 'core',
+ () => {
+ return new PrebootCoreRouteHandlerContext(corePreboot);
+ }
+ );
this.coreApp.preboot(corePreboot, uiPlugins);
@@ -413,9 +418,13 @@ export class Server {
}
private registerCoreContext(coreSetup: InternalCoreSetup) {
- coreSetup.http.registerRouteHandlerContext(coreId, 'core', async (context, req, res) => {
- return new CoreRouteHandlerContext(this.coreStart!, req);
- });
+ coreSetup.http.registerRouteHandlerContext(
+ coreId,
+ 'core',
+ (context, req) => {
+ return new CoreRouteHandlerContext(this.coreStart!, req);
+ }
+ );
}
public setupCoreConfig() {
diff --git a/src/core/server/ui_settings/index.ts b/src/core/server/ui_settings/index.ts
index d83d9c4358621..ee8cd7181fe8d 100644
--- a/src/core/server/ui_settings/index.ts
+++ b/src/core/server/ui_settings/index.ts
@@ -10,6 +10,8 @@ export type { UiSettingsClient, UiSettingsServiceOptions } from './ui_settings_c
export { config } from './ui_settings_config';
export { UiSettingsService } from './ui_settings_service';
+export { CoreUiSettingsRouteHandlerContext } from './ui_settings_route_handler_context';
+export type { UiSettingsRequestHandlerContext } from './ui_settings_route_handler_context';
export type {
UiSettingsServiceSetup,
diff --git a/src/core/server/ui_settings/internal_types.ts b/src/core/server/ui_settings/internal_types.ts
new file mode 100644
index 0000000000000..4292cca94ffbd
--- /dev/null
+++ b/src/core/server/ui_settings/internal_types.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { RequestHandlerContextBase } from '..';
+import type { IRouter } from '../http';
+import type { UiSettingsRequestHandlerContext } from './ui_settings_route_handler_context';
+
+/**
+ * Request handler context used by core's uiSetting routes.
+ * @internal
+ */
+export interface InternalUiSettingsRequestHandlerContext extends RequestHandlerContextBase {
+ core: Promise<{
+ uiSettings: UiSettingsRequestHandlerContext;
+ }>;
+}
+
+/**
+ * Router bound to the {@link InternalUiSettingsRequestHandlerContext}.
+ * Used by core's uiSetting routes.
+ * @internal
+ */
+export type InternalUiSettingsRouter = IRouter;
diff --git a/src/core/server/ui_settings/routes/delete.ts b/src/core/server/ui_settings/routes/delete.ts
index 87c6edf386428..705a5319c1cf5 100644
--- a/src/core/server/ui_settings/routes/delete.ts
+++ b/src/core/server/ui_settings/routes/delete.ts
@@ -8,8 +8,8 @@
import { schema } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { SavedObjectsErrorHelpers } from '../../saved_objects';
+import type { InternalUiSettingsRouter } from '../internal_types';
import { CannotOverrideError } from '../ui_settings_errors';
const validate = {
@@ -18,7 +18,7 @@ const validate = {
}),
};
-export function registerDeleteRoute(router: IRouter) {
+export function registerDeleteRoute(router: InternalUiSettingsRouter) {
router.delete(
{ path: '/api/kibana/settings/{key}', validate },
async (context, request, response) => {
diff --git a/src/core/server/ui_settings/routes/get.ts b/src/core/server/ui_settings/routes/get.ts
index 0929330cf0238..c940c2e1fe71e 100644
--- a/src/core/server/ui_settings/routes/get.ts
+++ b/src/core/server/ui_settings/routes/get.ts
@@ -6,10 +6,10 @@
* Side Public License, v 1.
*/
-import { IRouter } from '../../http';
+import type { InternalUiSettingsRouter } from '../internal_types';
import { SavedObjectsErrorHelpers } from '../../saved_objects';
-export function registerGetRoute(router: IRouter) {
+export function registerGetRoute(router: InternalUiSettingsRouter) {
router.get(
{ path: '/api/kibana/settings', validate: false },
async (context, request, response) => {
diff --git a/src/core/server/ui_settings/routes/index.ts b/src/core/server/ui_settings/routes/index.ts
index 0cf7233b8af19..22ca2ae38cde5 100644
--- a/src/core/server/ui_settings/routes/index.ts
+++ b/src/core/server/ui_settings/routes/index.ts
@@ -6,14 +6,13 @@
* Side Public License, v 1.
*/
-import { IRouter } from '../..';
-
+import type { InternalUiSettingsRouter } from '../internal_types';
import { registerDeleteRoute } from './delete';
import { registerGetRoute } from './get';
import { registerSetManyRoute } from './set_many';
import { registerSetRoute } from './set';
-export function registerRoutes(router: IRouter) {
+export function registerRoutes(router: InternalUiSettingsRouter) {
registerGetRoute(router);
registerDeleteRoute(router);
registerSetRoute(router);
diff --git a/src/core/server/ui_settings/routes/set.ts b/src/core/server/ui_settings/routes/set.ts
index 91518fb6f3476..af62fda0144b6 100644
--- a/src/core/server/ui_settings/routes/set.ts
+++ b/src/core/server/ui_settings/routes/set.ts
@@ -8,8 +8,8 @@
import { schema, ValidationError } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { SavedObjectsErrorHelpers } from '../../saved_objects';
+import type { InternalUiSettingsRouter } from '../internal_types';
import { CannotOverrideError } from '../ui_settings_errors';
const validate = {
@@ -21,7 +21,7 @@ const validate = {
}),
};
-export function registerSetRoute(router: IRouter) {
+export function registerSetRoute(router: InternalUiSettingsRouter) {
router.post(
{ path: '/api/kibana/settings/{key}', validate },
async (context, request, response) => {
diff --git a/src/core/server/ui_settings/routes/set_many.ts b/src/core/server/ui_settings/routes/set_many.ts
index f4f3f509bf920..fe0ee1a0a721f 100644
--- a/src/core/server/ui_settings/routes/set_many.ts
+++ b/src/core/server/ui_settings/routes/set_many.ts
@@ -8,8 +8,8 @@
import { schema, ValidationError } from '@kbn/config-schema';
-import { IRouter } from '../../http';
import { SavedObjectsErrorHelpers } from '../../saved_objects';
+import type { InternalUiSettingsRouter } from '../internal_types';
import { CannotOverrideError } from '../ui_settings_errors';
const validate = {
@@ -18,7 +18,7 @@ const validate = {
}),
};
-export function registerSetManyRoute(router: IRouter) {
+export function registerSetManyRoute(router: InternalUiSettingsRouter) {
router.post({ path: '/api/kibana/settings', validate }, async (context, request, response) => {
try {
const uiSettingsClient = (await context.core).uiSettings.client;
diff --git a/src/core/server/ui_settings/ui_settings_route_handler_context.ts b/src/core/server/ui_settings/ui_settings_route_handler_context.ts
new file mode 100644
index 0000000000000..a975f0faad6ef
--- /dev/null
+++ b/src/core/server/ui_settings/ui_settings_route_handler_context.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { CoreSavedObjectsRouteHandlerContext } from '../saved_objects';
+import type { IUiSettingsClient, InternalUiSettingsServiceStart } from './types';
+
+/**
+ * Core's `uiSettings` request handler context.
+ * @public
+ */
+export interface UiSettingsRequestHandlerContext {
+ client: IUiSettingsClient;
+}
+
+/**
+ * The {@link UiSettingsRequestHandlerContext} implementation.
+ * @internal
+ */
+export class CoreUiSettingsRouteHandlerContext implements UiSettingsRequestHandlerContext {
+ #client?: IUiSettingsClient;
+
+ constructor(
+ private readonly uiSettingsStart: InternalUiSettingsServiceStart,
+ private readonly savedObjectsRouterHandlerContext: CoreSavedObjectsRouteHandlerContext
+ ) {}
+
+ public get client() {
+ if (this.#client == null) {
+ this.#client = this.uiSettingsStart.asScopedToClient(
+ this.savedObjectsRouterHandlerContext.client
+ );
+ }
+ return this.#client;
+ }
+}
diff --git a/src/core/server/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_service.ts
index d303060d55595..342514660e8d1 100644
--- a/src/core/server/ui_settings/ui_settings_service.ts
+++ b/src/core/server/ui_settings/ui_settings_service.ts
@@ -26,6 +26,7 @@ import { uiSettingsType } from './saved_objects';
import { registerRoutes } from './routes';
import { getCoreSettings } from './settings';
import { UiSettingsDefaultsClient } from './ui_settings_defaults_client';
+import type { InternalUiSettingsRequestHandlerContext } from './internal_types';
export interface SetupDeps {
http: InternalHttpServiceSetup;
@@ -70,7 +71,7 @@ export class UiSettingsService
this.log.debug('Setting up ui settings service');
savedObjects.registerType(uiSettingsType);
- registerRoutes(http.createRouter(''));
+ registerRoutes(http.createRouter(''));
const config = await firstValueFrom(this.config$);
this.overrides = config.overrides;
diff --git a/src/plugins/console/public/application/containers/main/main.tsx b/src/plugins/console/public/application/containers/main/main.tsx
index 5895b919f9842..2833bf1fbebc3 100644
--- a/src/plugins/console/public/application/containers/main/main.tsx
+++ b/src/plugins/console/public/application/containers/main/main.tsx
@@ -26,6 +26,7 @@ import { useDataInit } from '../../hooks';
import { getTopNavConfig } from './get_top_nav';
import type { SenseEditor } from '../../models/sense_editor';
+import { getResponseWithMostSevereStatusCode } from '../../../lib/utils';
export function Main() {
const {
@@ -62,7 +63,7 @@ export function Main() {
);
}
- const lastDatum = requestData?.[requestData.length - 1] ?? requestError;
+ const data = getResponseWithMostSevereStatusCode(requestData) ?? requestError;
return (
@@ -95,13 +96,13 @@ export function Main() {
{
+ if (requestData) {
+ return requestData
+ .slice()
+ .sort((a, b) => a.response.statusCode - b.response.statusCode)
+ .pop();
+ }
+};
diff --git a/src/plugins/console/public/lib/utils/utils.test.js b/src/plugins/console/public/lib/utils/utils.test.js
index 738aa5b9bf5c3..8cdd93c3b6ed8 100644
--- a/src/plugins/console/public/lib/utils/utils.test.js
+++ b/src/plugins/console/public/lib/utils/utils.test.js
@@ -173,4 +173,24 @@ describe('Utils class', () => {
]);
});
});
+
+ test('get response with most severe status code', () => {
+ expect(
+ utils.getResponseWithMostSevereStatusCode([
+ { response: { statusCode: 500 } },
+ { response: { statusCode: 400 } },
+ { response: { statusCode: 200 } },
+ ])
+ ).toEqual({ response: { statusCode: 500 } });
+
+ expect(
+ utils.getResponseWithMostSevereStatusCode([
+ { response: { statusCode: 0 } },
+ { response: { statusCode: 100 } },
+ { response: { statusCode: 201 } },
+ ])
+ ).toEqual({ response: { statusCode: 201 } });
+
+ expect(utils.getResponseWithMostSevereStatusCode(undefined)).toBe(undefined);
+ });
});
diff --git a/src/plugins/console/server/lib/proxy_request.test.ts b/src/plugins/console/server/lib/proxy_request.test.ts
index 2bb5e481fbb26..98c63d9685c87 100644
--- a/src/plugins/console/server/lib/proxy_request.test.ts
+++ b/src/plugins/console/server/lib/proxy_request.test.ts
@@ -31,7 +31,7 @@ describe(`Console's send request`, () => {
it('correctly implements timeout and abort mechanism', async () => {
fakeRequest = {
- abort: sinon.stub(),
+ destroy: sinon.stub(),
on() {},
once() {},
} as any;
@@ -47,7 +47,7 @@ describe(`Console's send request`, () => {
fail('Should not reach here!');
} catch (e) {
expect(e.message).toEqual('Client request timeout');
- expect((fakeRequest.abort as sinon.SinonStub).calledOnce).toBe(true);
+ expect((fakeRequest.destroy as sinon.SinonStub).calledOnce).toBe(true);
}
});
diff --git a/src/plugins/console/server/lib/proxy_request.ts b/src/plugins/console/server/lib/proxy_request.ts
index c4fbfd315da4e..4a8839d1d8583 100644
--- a/src/plugins/console/server/lib/proxy_request.ts
+++ b/src/plugins/console/server/lib/proxy_request.ts
@@ -113,7 +113,10 @@ export const proxyRequest = ({
const timeoutPromise = new Promise((timeoutResolve, timeoutReject) => {
setTimeout(() => {
- if (!req.aborted && !req.socket) req.abort();
+ // Destroy the stream on timeout and close the connection.
+ if (!req.destroyed) {
+ req.destroy();
+ }
if (!resolved) {
timeoutReject(Boom.gatewayTimeout('Client request timeout'));
} else {
diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts
index e47fc12eca446..5c13747b238c7 100644
--- a/src/plugins/data/common/search/aggs/agg_config.ts
+++ b/src/plugins/data/common/search/aggs/agg_config.ts
@@ -152,7 +152,7 @@ export class AggConfig {
const isDeserialized = isType || isObject;
if (!isDeserialized) {
- val = aggParam.deserialize(val, this);
+ val = aggParam.deserialize(_.cloneDeep(val), this);
}
to[aggParam.name] = val;
diff --git a/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts b/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts
index 4412eeb78c428..98531c2fec879 100644
--- a/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts
+++ b/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts
@@ -8,7 +8,12 @@
import { BehaviorSubject } from 'rxjs';
-import { MetricsServiceSetup, ServiceStatus, ServiceStatusLevels } from '@kbn/core/server';
+import {
+ MetricsServiceSetup,
+ RequestHandlerContext,
+ ServiceStatus,
+ ServiceStatusLevels,
+} from '@kbn/core/server';
import {
contextServiceMock,
loggingSystemMock,
@@ -42,7 +47,7 @@ describe('/api/stats', () => {
});
metrics = metricsServiceMock.createSetupContract();
- const router = httpSetup.createRouter('');
+ const router = httpSetup.createRouter('');
registerStatsRoute({
router,
collectorSet: new CollectorSet({
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index 47890ec013046..c15d92ff57985 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -38,7 +38,7 @@
"xpack.main": "legacy/plugins/xpack_main",
"xpack.maps": ["plugins/maps"],
"xpack.aiops": [
- "packages/ml/aiops_utils",
+ "packages/ml/aiops_components",
"plugins/aiops"
],
"xpack.ml": ["plugins/ml"],
diff --git a/x-pack/packages/ml/aiops_components/BUILD.bazel b/x-pack/packages/ml/aiops_components/BUILD.bazel
new file mode 100644
index 0000000000000..f08ccff0d2893
--- /dev/null
+++ b/x-pack/packages/ml/aiops_components/BUILD.bazel
@@ -0,0 +1,145 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_config")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
+load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
+
+PKG_DIRNAME = "aiops_components"
+PKG_REQUIRE_NAME = "@kbn/aiops-components"
+
+SOURCE_FILES = glob(
+ [
+ "src/**/*.scss",
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ ],
+ exclude = [
+ "**/*.test.*",
+ ],
+)
+
+SRCS = SOURCE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "package.json",
+]
+
+# In this array place runtime dependencies, including other packages and NPM packages
+# which must be available for this code to run.
+#
+# To reference other packages use:
+# "//repo/relative/path/to/package"
+# eg. "//packages/kbn-utils"
+#
+# To reference a NPM package use:
+# "@npm//name-of-package"
+# eg. "@npm//lodash"
+RUNTIME_DEPS = [
+ "@npm//d3-brush",
+ "@npm//d3-scale",
+ "@npm//d3-selection",
+ "@npm//d3-transition",
+ "@npm//react",
+ "@npm//@elastic/charts",
+ "@npm//@elastic/eui",
+ "//packages/kbn-i18n-react",
+ "//x-pack/packages/ml/aiops_utils",
+]
+
+# In this array place dependencies necessary to build the types, which will include the
+# :npm_module_types target of other packages and packages from NPM, including @types/*
+# packages.
+#
+# To reference the types for another package use:
+# "//repo/relative/path/to/package:npm_module_types"
+# eg. "//packages/kbn-utils:npm_module_types"
+#
+# References to NPM packages work the same as RUNTIME_DEPS
+TYPES_DEPS = [
+ "@npm//@types/d3-brush",
+ "@npm//@types/d3-scale",
+ "@npm//@types/d3-selection",
+ "@npm//@types/d3-transition",
+ "@npm//@types/node",
+ "@npm//@types/jest",
+ "@npm//@types/react",
+ "@npm//@elastic/charts",
+ "@npm//@elastic/eui",
+ "//packages/kbn-i18n-react:npm_module_types",
+ "//x-pack/packages/ml/aiops_utils:npm_module_types",
+]
+
+jsts_transpiler(
+ name = "target_node",
+ srcs = SRCS,
+ build_pkg_name = package_name(),
+)
+
+jsts_transpiler(
+ name = "target_web",
+ srcs = SRCS,
+ build_pkg_name = package_name(),
+ web = True,
+ additional_args = [
+ "--copy-files",
+ "--quiet"
+ ],
+)
+
+ts_config(
+ name = "tsconfig",
+ src = "tsconfig.json",
+ deps = [
+ "//:tsconfig.base.json",
+ "//:tsconfig.bazel.json",
+ ],
+)
+
+ts_project(
+ name = "tsc_types",
+ args = ['--pretty'],
+ srcs = SRCS,
+ deps = TYPES_DEPS,
+ declaration = True,
+ emit_declaration_only = True,
+ out_dir = "target_types",
+ root_dir = "src",
+ tsconfig = ":tsconfig",
+)
+
+js_library(
+ name = PKG_DIRNAME,
+ srcs = NPM_MODULE_EXTRA_FILES,
+ deps = RUNTIME_DEPS + [":target_node", ":target_web"],
+ package_name = PKG_REQUIRE_NAME,
+ visibility = ["//visibility:public"],
+)
+
+pkg_npm(
+ name = "npm_module",
+ deps = [":" + PKG_DIRNAME],
+)
+
+filegroup(
+ name = "build",
+ srcs = [":npm_module"],
+ visibility = ["//visibility:public"],
+)
+
+pkg_npm_types(
+ name = "npm_module_types",
+ srcs = SRCS,
+ deps = [":tsc_types"],
+ package_name = PKG_REQUIRE_NAME,
+ tsconfig = ":tsconfig",
+ visibility = ["//visibility:public"],
+)
+
+filegroup(
+ name = "build_types",
+ srcs = [":npm_module_types"],
+ visibility = ["//visibility:public"],
+)
diff --git a/x-pack/packages/ml/aiops_components/README.md b/x-pack/packages/ml/aiops_components/README.md
new file mode 100644
index 0000000000000..36b36805d2872
--- /dev/null
+++ b/x-pack/packages/ml/aiops_components/README.md
@@ -0,0 +1,5 @@
+# @kbn/aiops-components
+
+React components for AIOps related efforts.
+
+https://docs.elastic.dev/kibana-dev-docs/api/kbn-aiops-components
diff --git a/x-pack/packages/ml/aiops_components/jest.config.js b/x-pack/packages/ml/aiops_components/jest.config.js
new file mode 100644
index 0000000000000..cadc9733723e9
--- /dev/null
+++ b/x-pack/packages/ml/aiops_components/jest.config.js
@@ -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.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../../..',
+ roots: ['/x-pack/packages/ml/aiops_components'],
+};
diff --git a/x-pack/packages/ml/aiops_components/package.json b/x-pack/packages/ml/aiops_components/package.json
new file mode 100644
index 0000000000000..f3cb901271998
--- /dev/null
+++ b/x-pack/packages/ml/aiops_components/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@kbn/aiops-components",
+ "description": "React components for AIOps related efforts.",
+ "author": "Machine Learning UI",
+ "homepage": "https://docs.elastic.dev/kibana-dev-docs/api/kbn-aiops-components",
+ "private": true,
+ "version": "1.0.0",
+ "main": "./target_node/index.js",
+ "browser": "./target_web/index.js",
+ "license": "SSPL-1.0 OR Elastic License 2.0"
+}
diff --git a/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush.scss b/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush.scss
new file mode 100644
index 0000000000000..a97dec29ecd62
--- /dev/null
+++ b/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush.scss
@@ -0,0 +1,11 @@
+.aiops-dual-brush {
+ .handle {
+ fill: $euiColorDarkShade;
+ }
+
+ .brush .selection {
+ stroke: none;
+ fill: $euiColorDarkShade !important;
+ opacity: .5 !important;
+ }
+}
diff --git a/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush.tsx b/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush.tsx
new file mode 100644
index 0000000000000..bb44f4ac16a89
--- /dev/null
+++ b/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush.tsx
@@ -0,0 +1,264 @@
+/*
+ * 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, { useEffect, useRef } from 'react';
+
+import * as d3Brush from 'd3-brush';
+import * as d3Scale from 'd3-scale';
+import * as d3Selection from 'd3-selection';
+import * as d3Transition from 'd3-transition';
+
+import type { WindowParameters } from '@kbn/aiops-utils';
+
+import './dual_brush.scss';
+
+const { brush, brushSelection, brushX } = d3Brush;
+const { scaleLinear } = d3Scale;
+const { select: d3Select } = d3Selection;
+// Import fix to apply correct types for the use of d3.select(this).transition()
+d3Select.prototype.transition = d3Transition.transition;
+
+const d3 = {
+ brush,
+ brushSelection,
+ brushX,
+ scaleLinear,
+ select: d3Select,
+ transition: d3Transition,
+};
+
+const isBrushXSelection = (arg: unknown): arg is [number, number] => {
+ return (
+ Array.isArray(arg) &&
+ arg.length === 2 &&
+ typeof arg[0] === 'number' &&
+ typeof arg[1] === 'number'
+ );
+};
+
+interface DualBrush {
+ id: string;
+ brush: d3Brush.BrushBehavior;
+ start: number;
+ end: number;
+}
+
+const BRUSH_HEIGHT = 20;
+const BRUSH_MARGIN = 4;
+const BRUSH_HANDLE_SIZE = 4;
+const BRUSH_HANDLE_ROUNDED_CORNER = 2;
+
+interface DualBrushProps {
+ windowParameters: WindowParameters;
+ min: number;
+ max: number;
+ onChange?: (windowParameters: WindowParameters) => void;
+ marginLeft: number;
+ width: number;
+}
+
+export function DualBrush({
+ windowParameters,
+ min,
+ max,
+ onChange,
+ marginLeft,
+ width,
+}: DualBrushProps) {
+ const d3BrushContainer = useRef(null);
+ const brushes = useRef([]);
+ const widthRef = useRef(width);
+
+ const { baselineMin, baselineMax, deviationMin, deviationMax } = windowParameters;
+
+ useEffect(() => {
+ if (d3BrushContainer.current && width > 0) {
+ const gBrushes = d3.select(d3BrushContainer.current);
+
+ function newBrush(id: string, start: number, end: number) {
+ brushes.current.push({
+ id,
+ brush: d3.brushX().handleSize(BRUSH_HANDLE_SIZE).on('end', brushend),
+ start,
+ end,
+ });
+
+ function brushend(this: d3Selection.BaseType) {
+ const currentWidth = widthRef.current;
+
+ const x = d3.scaleLinear().domain([min, max]).rangeRound([0, currentWidth]);
+
+ const px2ts = (px: number) => Math.round(x.invert(px));
+ const xMin = x(min) ?? 0;
+ const xMax = x(max) ?? 0;
+ const minExtentPx = Math.round((xMax - xMin) / 100);
+
+ const baselineBrush = d3.select('#brush-baseline');
+ const baselineSelection = d3.brushSelection(baselineBrush.node() as SVGGElement);
+
+ const deviationBrush = d3.select('#brush-deviation');
+ const deviationSelection = d3.brushSelection(deviationBrush.node() as SVGGElement);
+
+ if (!isBrushXSelection(deviationSelection) || !isBrushXSelection(baselineSelection)) {
+ return;
+ }
+
+ const baselineOverlay = baselineBrush.selectAll('.overlay');
+ const deviationOverlay = deviationBrush.selectAll('.overlay');
+
+ let baselineWidth;
+ let deviationWidth;
+ baselineOverlay.each((d, i, n) => {
+ baselineWidth = d3.select(n[i]).attr('width');
+ });
+ deviationOverlay.each((d, i, n) => {
+ deviationWidth = d3.select(n[i]).attr('width');
+ });
+
+ if (baselineWidth !== deviationWidth) {
+ return;
+ }
+
+ const newWindowParameters = {
+ baselineMin: px2ts(baselineSelection[0]),
+ baselineMax: px2ts(baselineSelection[1]),
+ deviationMin: px2ts(deviationSelection[0]),
+ deviationMax: px2ts(deviationSelection[1]),
+ };
+
+ if (
+ id === 'deviation' &&
+ deviationSelection &&
+ baselineSelection &&
+ deviationSelection[0] - minExtentPx < baselineSelection[1]
+ ) {
+ const newDeviationMin = baselineSelection[1] + minExtentPx;
+ const newDeviationMax = Math.max(deviationSelection[1], newDeviationMin + minExtentPx);
+
+ newWindowParameters.deviationMin = px2ts(newDeviationMin);
+ newWindowParameters.deviationMax = px2ts(newDeviationMax);
+
+ d3.select(this)
+ .transition()
+ .duration(200)
+ // @ts-expect-error call doesn't allow the brush move function
+ .call(brushes.current[1].brush.move, [newDeviationMin, newDeviationMax]);
+ } else if (
+ id === 'baseline' &&
+ deviationSelection &&
+ baselineSelection &&
+ deviationSelection[0] < baselineSelection[1] + minExtentPx
+ ) {
+ const newBaselineMax = deviationSelection[0] - minExtentPx;
+ const newBaselineMin = Math.min(baselineSelection[0], newBaselineMax - minExtentPx);
+
+ newWindowParameters.baselineMin = px2ts(newBaselineMin);
+ newWindowParameters.baselineMax = px2ts(newBaselineMax);
+
+ d3.select(this)
+ .transition()
+ .duration(200)
+ // @ts-expect-error call doesn't allow the brush move function
+ .call(brushes.current[0].brush.move, [newBaselineMin, newBaselineMax]);
+ }
+
+ brushes.current[0].start = newWindowParameters.baselineMin;
+ brushes.current[0].end = newWindowParameters.baselineMax;
+ brushes.current[1].start = newWindowParameters.deviationMin;
+ brushes.current[1].end = newWindowParameters.deviationMax;
+
+ if (onChange) {
+ onChange(newWindowParameters);
+ }
+ drawBrushes();
+ }
+ }
+
+ function drawBrushes() {
+ const mlBrushSelection = gBrushes
+ .selectAll('.brush')
+ .data(brushes.current, (d) => (d as DualBrush).id);
+
+ // Set up new brushes
+ mlBrushSelection
+ .enter()
+ .insert('g', '.brush')
+ .attr('class', 'brush')
+ .attr('id', (b: DualBrush) => {
+ return 'brush-' + b.id;
+ })
+ .each((brushObject: DualBrush, i, n) => {
+ const x = d3.scaleLinear().domain([min, max]).rangeRound([0, widthRef.current]);
+ brushObject.brush(d3.select(n[i]));
+ const xStart = x(brushObject.start) ?? 0;
+ const xEnd = x(brushObject.end) ?? 0;
+ brushObject.brush.move(d3.select(n[i]), [xStart, xEnd]);
+ });
+
+ // disable drag-select to reset a brush's selection
+ mlBrushSelection
+ .attr('class', 'brush')
+ .selectAll('.overlay')
+ .attr('width', width)
+ .style('pointer-events', 'none');
+
+ mlBrushSelection
+ .selectAll('.handle')
+ .attr('rx', BRUSH_HANDLE_ROUNDED_CORNER)
+ .attr('ry', BRUSH_HANDLE_ROUNDED_CORNER);
+
+ mlBrushSelection.exit().remove();
+ }
+
+ function updateBrushes() {
+ const mlBrushSelection = gBrushes
+ .selectAll('.brush')
+ .data(brushes.current, (d) => (d as DualBrush).id);
+
+ mlBrushSelection.each(function (brushObject, i, n) {
+ const x = d3.scaleLinear().domain([min, max]).rangeRound([0, widthRef.current]);
+ brushObject.brush.extent([
+ [0, BRUSH_MARGIN],
+ [width, BRUSH_HEIGHT - BRUSH_MARGIN],
+ ]);
+ brushObject.brush(d3.select(n[i] as SVGGElement));
+ const xStart = x(brushObject.start) ?? 0;
+ const xEnd = x(brushObject.end) ?? 0;
+ brushObject.brush.move(d3.select(n[i] as SVGGElement), [xStart, xEnd]);
+ });
+ }
+
+ if (brushes.current.length !== 2) {
+ widthRef.current = width;
+ newBrush('baseline', baselineMin, baselineMax);
+ newBrush('deviation', deviationMin, deviationMax);
+ } else {
+ if (widthRef.current !== width) {
+ widthRef.current = width;
+ updateBrushes();
+ }
+ }
+
+ drawBrushes();
+ }
+ }, [min, max, width, baselineMin, baselineMax, deviationMin, deviationMax, onChange]);
+
+ return (
+ <>
+ {width > 0 && (
+
+ )}
+ >
+ );
+}
diff --git a/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush_annotation.tsx b/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush_annotation.tsx
new file mode 100644
index 0000000000000..54e2c204acfa9
--- /dev/null
+++ b/x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush_annotation.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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, { FC } from 'react';
+
+import { RectAnnotation } from '@elastic/charts';
+import { useEuiTheme } from '@elastic/eui';
+
+interface BrushAnnotationProps {
+ id: string;
+ min: number;
+ max: number;
+}
+
+export const DualBrushAnnotation: FC = ({ id, min, max }) => {
+ const { euiTheme } = useEuiTheme();
+ const { colors } = euiTheme;
+
+ return (
+
+ );
+};
diff --git a/x-pack/packages/ml/aiops_components/src/dual_brush/index.ts b/x-pack/packages/ml/aiops_components/src/dual_brush/index.ts
new file mode 100644
index 0000000000000..c72973a19871f
--- /dev/null
+++ b/x-pack/packages/ml/aiops_components/src/dual_brush/index.ts
@@ -0,0 +1,9 @@
+/*
+ * 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 { DualBrushAnnotation } from './dual_brush_annotation';
+export { DualBrush } from './dual_brush';
diff --git a/x-pack/packages/ml/aiops_components/src/index.ts b/x-pack/packages/ml/aiops_components/src/index.ts
new file mode 100644
index 0000000000000..d468ffcc4bba5
--- /dev/null
+++ b/x-pack/packages/ml/aiops_components/src/index.ts
@@ -0,0 +1,9 @@
+/*
+ * 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 { DualBrush, DualBrushAnnotation } from './dual_brush';
+export { ProgressControls } from './progress_controls';
diff --git a/x-pack/packages/ml/aiops_components/src/progress_controls/index.ts b/x-pack/packages/ml/aiops_components/src/progress_controls/index.ts
new file mode 100644
index 0000000000000..64cbbe174fd11
--- /dev/null
+++ b/x-pack/packages/ml/aiops_components/src/progress_controls/index.ts
@@ -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.
+ */
+
+export { ProgressControls } from './progress_controls';
diff --git a/x-pack/packages/ml/aiops_utils/src/components/progress_controls.tsx b/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx
similarity index 100%
rename from x-pack/packages/ml/aiops_utils/src/components/progress_controls.tsx
rename to x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx
diff --git a/x-pack/packages/ml/aiops_components/tsconfig.json b/x-pack/packages/ml/aiops_components/tsconfig.json
new file mode 100644
index 0000000000000..ebe2e9eb5d0e4
--- /dev/null
+++ b/x-pack/packages/ml/aiops_components/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "extends": "../../../../tsconfig.bazel.json",
+ "compilerOptions": {
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "outDir": "target_types",
+ "rootDir": "src",
+ "stripInternal": false,
+ "types": [
+ "@types/d3-brush",
+ "@types/d3-scale",
+ "@types/d3-selection",
+ "@types/d3-transition",
+ "jest",
+ "node",
+ "react"
+ ]
+ },
+ "include": [
+ "src/**/*"
+ ]
+}
diff --git a/x-pack/packages/ml/aiops_utils/BUILD.bazel b/x-pack/packages/ml/aiops_utils/BUILD.bazel
index 755c152d62e34..f54af47470a3b 100644
--- a/x-pack/packages/ml/aiops_utils/BUILD.bazel
+++ b/x-pack/packages/ml/aiops_utils/BUILD.bazel
@@ -38,8 +38,6 @@ NPM_MODULE_EXTRA_FILES = [
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"@npm//react",
- "@npm//@elastic/eui",
- "//packages/kbn-i18n-react",
"//packages/kbn-logging"
]
@@ -56,8 +54,6 @@ TYPES_DEPS = [
"@npm//@types/node",
"@npm//@types/jest",
"@npm//@types/react",
- "@npm//@elastic/eui",
- "//packages/kbn-i18n-react:npm_module_types",
"//packages/kbn-logging:npm_module_types"
]
diff --git a/x-pack/packages/ml/aiops_utils/src/lib/accept_compression.test.ts b/x-pack/packages/ml/aiops_utils/src/accept_compression.test.ts
similarity index 100%
rename from x-pack/packages/ml/aiops_utils/src/lib/accept_compression.test.ts
rename to x-pack/packages/ml/aiops_utils/src/accept_compression.test.ts
diff --git a/x-pack/packages/ml/aiops_utils/src/lib/accept_compression.ts b/x-pack/packages/ml/aiops_utils/src/accept_compression.ts
similarity index 100%
rename from x-pack/packages/ml/aiops_utils/src/lib/accept_compression.ts
rename to x-pack/packages/ml/aiops_utils/src/accept_compression.ts
diff --git a/x-pack/packages/ml/aiops_utils/src/lib/fetch_stream.ts b/x-pack/packages/ml/aiops_utils/src/fetch_stream.ts
similarity index 100%
rename from x-pack/packages/ml/aiops_utils/src/lib/fetch_stream.ts
rename to x-pack/packages/ml/aiops_utils/src/fetch_stream.ts
diff --git a/x-pack/packages/ml/aiops_utils/src/lib/get_window_parameters.ts b/x-pack/packages/ml/aiops_utils/src/get_window_parameters.ts
similarity index 100%
rename from x-pack/packages/ml/aiops_utils/src/lib/get_window_parameters.ts
rename to x-pack/packages/ml/aiops_utils/src/get_window_parameters.ts
diff --git a/x-pack/packages/ml/aiops_utils/src/index.ts b/x-pack/packages/ml/aiops_utils/src/index.ts
index 1ffbde324470f..a02ecc2d41958 100644
--- a/x-pack/packages/ml/aiops_utils/src/index.ts
+++ b/x-pack/packages/ml/aiops_utils/src/index.ts
@@ -5,12 +5,11 @@
* 2.0.
*/
-export { ProgressControls } from './components/progress_controls';
-export { getWindowParameters } from './lib/get_window_parameters';
-export type { WindowParameters } from './lib/get_window_parameters';
-export { streamFactory } from './lib/stream_factory';
-export { useFetchStream } from './lib/use_fetch_stream';
+export { getWindowParameters } from './get_window_parameters';
+export type { WindowParameters } from './get_window_parameters';
+export { streamFactory } from './stream_factory';
+export { useFetchStream } from './use_fetch_stream';
export type {
UseFetchStreamCustomReducerParams,
UseFetchStreamParamsDefault,
-} from './lib/use_fetch_stream';
+} from './use_fetch_stream';
diff --git a/x-pack/packages/ml/aiops_utils/src/lib/stream_factory.test.ts b/x-pack/packages/ml/aiops_utils/src/stream_factory.test.ts
similarity index 100%
rename from x-pack/packages/ml/aiops_utils/src/lib/stream_factory.test.ts
rename to x-pack/packages/ml/aiops_utils/src/stream_factory.test.ts
diff --git a/x-pack/packages/ml/aiops_utils/src/lib/stream_factory.ts b/x-pack/packages/ml/aiops_utils/src/stream_factory.ts
similarity index 100%
rename from x-pack/packages/ml/aiops_utils/src/lib/stream_factory.ts
rename to x-pack/packages/ml/aiops_utils/src/stream_factory.ts
diff --git a/x-pack/packages/ml/aiops_utils/src/lib/string_reducer.ts b/x-pack/packages/ml/aiops_utils/src/string_reducer.ts
similarity index 100%
rename from x-pack/packages/ml/aiops_utils/src/lib/string_reducer.ts
rename to x-pack/packages/ml/aiops_utils/src/string_reducer.ts
diff --git a/x-pack/packages/ml/aiops_utils/src/lib/use_fetch_stream.ts b/x-pack/packages/ml/aiops_utils/src/use_fetch_stream.ts
similarity index 100%
rename from x-pack/packages/ml/aiops_utils/src/lib/use_fetch_stream.ts
rename to x-pack/packages/ml/aiops_utils/src/use_fetch_stream.ts
diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes.tsx
index b23a3a7b82a79..982f085b15415 100644
--- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes.tsx
+++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes.tsx
@@ -10,7 +10,8 @@ import React, { useEffect, FC } from 'react';
import { EuiCodeBlock, EuiSpacer, EuiText } from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/public';
-import { useFetchStream, ProgressControls } from '@kbn/aiops-utils';
+import { ProgressControls } from '@kbn/aiops-components';
+import { useFetchStream } from '@kbn/aiops-utils';
import type { WindowParameters } from '@kbn/aiops-utils';
import { useKibana } from '@kbn/kibana-react-plugin/public';
diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md
index 98eb504ff7094..62892c143146f 100644
--- a/x-pack/plugins/cases/README.md
+++ b/x-pack/plugins/cases/README.md
@@ -48,7 +48,7 @@ To initialize the `CasesContext` you can use this code:
// somewhere high on your plugin render tree
{/* or something similar */}
@@ -57,11 +57,11 @@ To initialize the `CasesContext` you can use this code:
props:
-| prop | type | description |
-| --------------------- | --------------- | -------------------------------------------------------------- |
-| PLUGIN_CASES_OWNER_ID | `string` | The owner string for your plugin. e.g: securitySolution |
-| CASES_USER_CAN_CRUD | `boolean` | Defines if the user has access to cases to CRUD |
-| CASES_FEATURES | `CasesFeatures` | `CasesFeatures` object defining the features to enable/disable |
+| prop | type | description |
+| --------------------- | ------------------- | -------------------------------------------------------------- |
+| PLUGIN_CASES_OWNER_ID | `string` | The owner string for your plugin. e.g: securitySolution |
+| CASES_PERMISSIONS | `CasesPermissions` | `CasesPermissions` object defining the user's permissions |
+| CASES_FEATURES | `CasesFeatures` | `CasesFeatures` object defining the features to enable/disable |
### Cases UI client
@@ -83,7 +83,10 @@ const { cases } = useKibana().services;
// call in the return as you would any component
cases.getCases({
basePath: '/investigate/cases',
- userCanCrud: true,
+ permissions: {
+ all: true,
+ read: true,
+ },
owner: ['securitySolution'],
features: { alerts: { sync: false }, metrics: ['alerts.count', 'lifespan'] }
timelineIntegration: {
@@ -206,7 +209,7 @@ Arguments:
| Property | Description |
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| userCanCrud | `boolean;` user permissions to crud |
+| permissions | `CasesPermissions` object defining the user's permissions |
| owner | `string[];` owner ids of the cases |
| basePath | `string;` path to mount the Cases router on top of |
| useFetchAlertData | `(alertIds: string[]) => [boolean, Record];` fetch alerts |
@@ -236,7 +239,7 @@ Arguments:
| Property | Description |
| --------------- | ---------------------------------------------------------------------------------- |
-| userCanCrud | `boolean;` user permissions to crud |
+| permissions | `CasesPermissions` object defining the user's permissions |
| owner | `string[];` owner ids of the cases |
| alertData? | `Omit;` alert data to post to case |
| hiddenStatuses? | `CaseStatuses[];` array of hidden statuses |
@@ -253,7 +256,7 @@ Arguments:
| Property | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------ |
-| userCanCrud | `boolean;` user permissions to crud |
+| permissions | `CasesPermissions` object defining the user's permissions |
| owner | `string[];` owner ids of the cases |
| onClose | `() => void;` callback when create case is canceled |
| onSuccess | `(theCase: Case) => Promise;` callback passing newly created case after pushCaseToExternalService is called |
@@ -267,11 +270,11 @@ UI component:
Arguments:
-| Property | Description |
-| -------------- | ------------------------------------------- |
-| userCanCrud | `boolean;` user permissions to crud |
-| owner | `string[];` owner ids of the cases |
-| maxCasesToShow | `number;` number of cases to show in widget |
+| Property | Description |
+| -------------- | ---------------------------------------------------------- |
+| permissions | `CasesPermissions` object defining the user's permissions |
+| owner | `string[];` owner ids of the cases |
+| maxCasesToShow | `number;` number of cases to show in widget |
UI component:
![Recent Cases Component][recent-cases-img]
@@ -289,7 +292,7 @@ Arguments:
| Property | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------ |
-| userCanCrud | `boolean;` user permissions to crud |
+| permissions | `CasesPermissions` object defining the user's permissions |
| onClose | `() => void;` callback when create case is canceled |
| onSuccess | `(theCase: Case) => Promise;` callback passing newly created case after pushCaseToExternalService is called |
| afterCaseCreated? | `(theCase: Case) => Promise;` callback passing newly created case before pushCaseToExternalService is called |
diff --git a/x-pack/plugins/cases/public/client/helpers/capabilities.ts b/x-pack/plugins/cases/public/client/helpers/capabilities.ts
new file mode 100644
index 0000000000000..652678d0add28
--- /dev/null
+++ b/x-pack/plugins/cases/public/client/helpers/capabilities.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface CasesPermissions {
+ all: boolean;
+ read: boolean;
+}
+
+export const getUICapabilities = (
+ featureCapabilities: Partial>>
+): CasesPermissions => {
+ const read = !!featureCapabilities?.read_cases;
+ const all = !!featureCapabilities?.crud_cases;
+
+ return {
+ all,
+ read,
+ };
+};
diff --git a/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx
index a40c92643ec34..37145c59b94ad 100644
--- a/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx
+++ b/x-pack/plugins/cases/public/client/ui/get_all_cases_selector_modal.tsx
@@ -22,12 +22,12 @@ const AllCasesSelectorModalLazy: React.FC = lazy(
export const getAllCasesSelectorModalLazy = ({
externalReferenceAttachmentTypeRegistry,
owner,
- userCanCrud,
+ permissions,
hiddenStatuses,
onRowClick,
onClose,
}: GetAllCasesSelectorModalPropsInternal) => (
-
+
}>
= lazy(() => import('../../component
export const getCasesLazy = ({
externalReferenceAttachmentTypeRegistry,
owner,
- userCanCrud,
+ permissions,
basePath,
onComponentInitialized,
actionsNavigation,
@@ -34,7 +34,7 @@ export const getCasesLazy = ({
value={{
externalReferenceAttachmentTypeRegistry,
owner,
- userCanCrud,
+ permissions,
basePath,
features,
releasePhase,
diff --git a/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx b/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx
index 699b2ea4c515c..48367bb7672c2 100644
--- a/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx
+++ b/x-pack/plugins/cases/public/client/ui/get_cases_context.tsx
@@ -22,7 +22,7 @@ const CasesProviderLazy: React.FC<{ value: GetCasesContextPropsInternal }> = laz
const CasesProviderLazyWrapper = ({
externalReferenceAttachmentTypeRegistry,
owner,
- userCanCrud,
+ permissions,
features,
children,
releasePhase,
@@ -33,7 +33,7 @@ const CasesProviderLazyWrapper = ({
value={{
externalReferenceAttachmentTypeRegistry,
owner,
- userCanCrud,
+ permissions,
features,
releasePhase,
}}
diff --git a/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx b/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx
index a6eb07a09aaf4..5149b71d19dd4 100644
--- a/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx
+++ b/x-pack/plugins/cases/public/client/ui/get_create_case_flyout.tsx
@@ -22,14 +22,14 @@ export const CreateCaseFlyoutLazy: React.FC = lazy(
export const getCreateCaseFlyoutLazy = ({
externalReferenceAttachmentTypeRegistry,
owner,
- userCanCrud,
+ permissions,
features,
afterCaseCreated,
onClose,
onSuccess,
attachments,
}: GetCreateCaseFlyoutPropsInternal) => (
-
+
}>
= lazy(
export const getRecentCasesLazy = ({
externalReferenceAttachmentTypeRegistry,
owner,
- userCanCrud,
+ permissions,
maxCasesToShow,
}: GetRecentCasesPropsInternal) => (
-
+
}>
diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.test.tsx b/x-pack/plugins/cases/public/common/lib/kibana/hooks.test.tsx
index 0f6a1e0035e5c..3fb2ccf2ddc6b 100644
--- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.test.tsx
+++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.test.tsx
@@ -23,7 +23,7 @@ describe('hooks', () => {
expect(result.current).toEqual({
actions: { crud: true, read: true },
- generalCases: { crud: true, read: true },
+ generalCases: { all: true, read: true },
visualize: { crud: true, read: true },
dashboard: { crud: true, read: true },
});
diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts
index a53a1e9ea452f..b144fcf9e24d2 100644
--- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts
+++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts
@@ -12,6 +12,7 @@ import { i18n } from '@kbn/i18n';
import { AuthenticatedUser } from '@kbn/security-plugin/common/model';
import { NavigateToAppOptions } from '@kbn/core/public';
+import { CasesPermissions, getUICapabilities } from '../../../client/helpers/capabilities';
import { convertToCamelCase } from '../../../api/utils';
import {
FEATURE_ID,
@@ -166,7 +167,7 @@ interface Capabilities {
}
interface UseApplicationCapabilities {
actions: Capabilities;
- generalCases: Capabilities;
+ generalCases: CasesPermissions;
visualize: Capabilities;
dashboard: Capabilities;
}
@@ -179,13 +180,14 @@ interface UseApplicationCapabilities {
export const useApplicationCapabilities = (): UseApplicationCapabilities => {
const capabilities = useKibana().services?.application?.capabilities;
const casesCapabilities = capabilities[FEATURE_ID];
+ const permissions = getUICapabilities(casesCapabilities);
return useMemo(
() => ({
actions: { crud: !!capabilities.actions?.save, read: !!capabilities.actions?.show },
generalCases: {
- crud: !!casesCapabilities?.crud_cases,
- read: !!casesCapabilities?.read_cases,
+ all: permissions.all,
+ read: permissions.read,
},
visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show },
dashboard: {
@@ -200,8 +202,8 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
capabilities.dashboard?.show,
capabilities.visualize?.save,
capabilities.visualize?.show,
- casesCapabilities?.crud_cases,
- casesCapabilities?.read_cases,
+ permissions.all,
+ permissions.read,
]
);
};
diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx
index 07ea1694b38e8..14ce43fc580af 100644
--- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx
+++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx
@@ -23,13 +23,14 @@ import {
import { FieldHook } from '../shared_imports';
import { StartServices } from '../../types';
import { ReleasePhase } from '../../components/types';
+import { CasesPermissions } from '../../client/helpers/capabilities';
import { AttachmentTypeRegistry } from '../../client/attachment_framework/registry';
import { ExternalReferenceAttachmentType } from '../../client/attachment_framework/types';
import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry';
interface TestProviderProps {
children: React.ReactNode;
- userCanCrud?: boolean;
+ permissions?: CasesPermissions;
features?: CasesFeatures;
owner?: string[];
releasePhase?: ReleasePhase;
@@ -45,7 +46,7 @@ const TestProvidersComponent: React.FC = ({
children,
features,
owner = [SECURITY_SOLUTION_OWNER],
- userCanCrud = true,
+ permissions = allCasesPermissions(),
releasePhase = 'ga',
externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(),
}) => {
@@ -63,7 +64,7 @@ const TestProvidersComponent: React.FC = ({
({ eui: euiDarkVars, darkMode: true })}>
{children}
@@ -92,10 +93,24 @@ export const testQueryClient = new QueryClient({
},
});
+export const buildCasesPermissions = (overrides: Partial = {}) => {
+ const read = overrides.read ?? true;
+ const all = overrides.all ?? true;
+
+ return {
+ all,
+ read,
+ };
+};
+
+export const allCasesPermissions = () => buildCasesPermissions();
+export const noCasesPermissions = () => buildCasesPermissions({ read: false, all: false });
+export const readCasesPermissions = () => buildCasesPermissions({ all: false });
+
export const createAppMockRenderer = ({
features,
owner = [SECURITY_SOLUTION_OWNER],
- userCanCrud = true,
+ permissions = allCasesPermissions(),
releasePhase = 'ga',
externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(),
}: Omit = {}): AppMockRenderer => {
@@ -118,7 +133,7 @@ export const createAppMockRenderer = ({
externalReferenceAttachmentTypeRegistry,
features,
owner,
- userCanCrud,
+ permissions,
releasePhase,
}}
>
diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx
index b9a253adc76f5..18e02ff098314 100644
--- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx
@@ -29,7 +29,6 @@ const createAttachments = jest.fn();
const addCommentProps: AddCommentProps = {
id: 'newComment',
caseId: '1234',
- userCanCrud: true,
onCommentSaving,
onCommentPosted,
showLoading: false,
@@ -120,8 +119,8 @@ describe('AddComment ', () => {
isLoading: true,
}));
const wrapper = mount(
-
-
+
+
);
diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx
index f3630d16cda79..235917a504e2e 100644
--- a/x-pack/plugins/cases/public/components/add_comment/index.tsx
+++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx
@@ -47,7 +47,6 @@ export interface AddCommentRefObject {
export interface AddCommentProps {
id: string;
caseId: string;
- userCanCrud?: boolean;
onCommentSaving?: () => void;
onCommentPosted: (newCase: Case) => void;
showLoading?: boolean;
@@ -57,20 +56,12 @@ export interface AddCommentProps {
export const AddComment = React.memo(
forwardRef(
(
- {
- id,
- caseId,
- userCanCrud,
- onCommentPosted,
- onCommentSaving,
- showLoading = true,
- statusActionButton,
- },
+ { id, caseId, onCommentPosted, onCommentSaving, showLoading = true, statusActionButton },
ref
) => {
const editorRef = useRef(null);
const [focusOnContext, setFocusOnContext] = useState(false);
- const { owner } = useCasesContext();
+ const { permissions, owner } = useCasesContext();
const { isLoading, createAttachments } = useCreateAttachments();
const { form } = useForm({
@@ -156,7 +147,7 @@ export const AddComment = React.memo(
return (
- {userCanCrud && (
+ {permissions.all && (
(
showAlertDetails,
useFetchAlertData,
}) => {
- const { userCanCrud, features } = useCasesContext();
+ const { features } = useCasesContext();
const { navigateToCaseView } = useCaseViewNavigation();
const { urlParams } = useUrlParams();
const refreshCaseViewPage = useRefreshCaseViewPage();
@@ -171,7 +171,6 @@ export const CaseViewPage = React.memo(
data-test-subj="case-view-title"
titleNode={
(
>
diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx
index 03fb726e1125c..ccf0bd9b475b3 100644
--- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx
+++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx
@@ -39,7 +39,7 @@ export const CaseViewActivity = ({
showAlertDetails?: (alertId: string, index: string) => void;
useFetchAlertData: UseFetchAlertData;
}) => {
- const { userCanCrud } = useCasesContext();
+ const { permissions } = useCasesContext();
const { getCaseViewUrl } = useCaseViewNavigation();
const { data: userActionsData, isLoading: isLoadingUserActions } = useGetCaseUserActions(
@@ -133,7 +133,7 @@ export const CaseViewActivity = ({
onShowAlertDetails={onShowAlertDetails}
onUpdateField={onUpdateField}
statusActionButton={
- userCanCrud ? (
+ permissions.all ? (
@@ -150,7 +149,7 @@ export const CaseViewActivity = ({
) : null}
) : null}
diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx
index 4990174b08dd7..b9213a8eb887f 100644
--- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx
@@ -189,7 +189,6 @@ describe('CaseView', () => {
onComponentInitialized: jest.fn(),
showAlertDetails: jest.fn(),
useFetchAlertData: jest.fn().mockReturnValue([false, alertsHit[0]]),
- userCanCrud: true,
}}
/>
);
diff --git a/x-pack/plugins/cases/public/components/cases_context/index.tsx b/x-pack/plugins/cases/public/components/cases_context/index.tsx
index ebcdebc012709..de63eefb79cc5 100644
--- a/x-pack/plugins/cases/public/components/cases_context/index.tsx
+++ b/x-pack/plugins/cases/public/components/cases_context/index.tsx
@@ -7,6 +7,7 @@
import React, { useState, useEffect, useReducer, Dispatch } from 'react';
import { merge } from 'lodash';
+import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect';
import { DEFAULT_FEATURES } from '../../../common/constants';
import { DEFAULT_BASE_PATH } from '../../common/navigation';
import { useApplication } from './use_application';
@@ -27,7 +28,10 @@ export interface CasesContextValue {
owner: string[];
appId: string;
appTitle: string;
- userCanCrud: boolean;
+ permissions: {
+ all: boolean;
+ read: boolean;
+ };
basePath: string;
features: CasesFeaturesAllRequired;
releasePhase: ReleasePhase;
@@ -37,7 +41,7 @@ export interface CasesContextValue {
export interface CasesContextProps
extends Pick<
CasesContextValue,
- 'owner' | 'userCanCrud' | 'externalReferenceAttachmentTypeRegistry'
+ 'owner' | 'permissions' | 'externalReferenceAttachmentTypeRegistry'
> {
basePath?: string;
features?: CasesFeatures;
@@ -56,7 +60,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({
value: {
externalReferenceAttachmentTypeRegistry,
owner,
- userCanCrud,
+ permissions,
basePath = DEFAULT_BASE_PATH,
features = {},
releasePhase = 'ga',
@@ -67,7 +71,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({
const [value, setValue] = useState(() => ({
externalReferenceAttachmentTypeRegistry,
owner,
- userCanCrud,
+ permissions,
basePath,
/**
* The empty object at the beginning avoids the mutation
@@ -83,7 +87,14 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({
}));
/**
- * `userCanCrud` prop may change by the parent plugin.
+ * Only update the context if the nested permissions fields changed, this avoids a rerender when the object's reference
+ * changes.
+ */
+ useDeepCompareEffect(() => {
+ setValue((prev) => ({ ...prev, permissions }));
+ }, [permissions]);
+
+ /**
* `appId` and `appTitle` are dynamically retrieved from kibana context.
* We need to update the state if any of these values change, the rest of props are never updated.
*/
@@ -93,10 +104,9 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({
...prev,
appId,
appTitle,
- userCanCrud,
}));
}
- }, [appTitle, appId, userCanCrud]);
+ }, [appTitle, appId]);
return isCasesContextValue(value) ? (
@@ -108,7 +118,7 @@ export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({
CasesProvider.displayName = 'CasesProvider';
function isCasesContextValue(value: CasesContextStateValue): value is CasesContextValue {
- return value.appId != null && value.appTitle != null && value.userCanCrud != null;
+ return value.appId != null && value.appTitle != null && value.permissions != null;
}
// eslint-disable-next-line import/no-default-export
diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
index 890b8683ae6a5..b6c46cbd91731 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
@@ -10,7 +10,7 @@ import { ReactWrapper, mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { ConfigureCases } from '.';
-import { TestProviders } from '../../common/mock';
+import { noCasesPermissions, TestProviders } from '../../common/mock';
import { Connectors } from './connectors';
import { ClosureOptions } from './closure_options';
@@ -191,7 +191,7 @@ describe('ConfigureCases', () => {
test('it disables correctly when the user cannot crud', () => {
const newWrapper = mount(, {
wrappingComponent: TestProviders,
- wrappingComponentProps: { userCanCrud: false },
+ wrappingComponentProps: { permissions: noCasesPermissions() },
});
expect(newWrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled')).toBe(
diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx
index e7542ba39f382..bf75dd5a828cd 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx
@@ -51,7 +51,7 @@ const FormWrapper = styled.div`
`;
export const ConfigureCases: React.FC = React.memo(() => {
- const { userCanCrud } = useCasesContext();
+ const { permissions } = useCasesContext();
const { triggersActionsUi } = useKibana().services;
useCasesBreadcrumbs(CasesDeepLinkId.casesConfigure);
@@ -225,7 +225,7 @@ export const ConfigureCases: React.FC = React.memo(() => {
@@ -233,13 +233,13 @@ export const ConfigureCases: React.FC = React.memo(() => {
{ConnectorAddFlyout}
diff --git a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx
index f701a3e647add..d553295eaef98 100644
--- a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx
+++ b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx
@@ -13,6 +13,7 @@ import React from 'react';
import { CasesContext } from '../../cases_context';
import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer';
import { useCasesAddToNewCaseFlyout } from './use_cases_add_to_new_case_flyout';
+import { allCasesPermissions } from '../../../common/mock';
import { ExternalReferenceAttachmentTypeRegistry } from '../../../client/attachment_framework/external_reference_registry';
jest.mock('../../../common/use_cases_toast');
@@ -30,7 +31,7 @@ describe('use cases add to new case flyout hook', () => {
value={{
externalReferenceAttachmentTypeRegistry,
owner: ['test'],
- userCanCrud: true,
+ permissions: allCasesPermissions(),
appId: 'test',
appTitle: 'jest',
basePath: '/jest',
diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx
index 6309ce0ebd832..ee8c34faff078 100644
--- a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx
@@ -11,7 +11,12 @@ import { render, waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { EditConnector, EditConnectorProps } from '.';
-import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock';
+import {
+ AppMockRenderer,
+ createAppMockRenderer,
+ readCasesPermissions,
+ TestProviders,
+} from '../../common/mock';
import { basicCase, basicPush, caseUserActions, connectorsMock } from '../../containers/mock';
import { CaseConnector } from '../../containers/configure/types';
@@ -36,7 +41,6 @@ const getDefaultProps = (): EditConnectorProps => {
isValidConnector: true,
onSubmit,
userActions: caseUserActions,
- userCanCrud: true,
};
};
@@ -201,11 +205,9 @@ describe('EditConnector ', () => {
});
it('does not allow the connector to be edited when the user does not have write permissions', async () => {
- const defaultProps = getDefaultProps();
- const props = { ...defaultProps, userCanCrud: false };
const wrapper = mount(
-
-
+
+
);
await waitFor(() =>
diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx
index 29ab523764b47..0c691460ba007 100644
--- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx
+++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx
@@ -32,6 +32,7 @@ import { getConnectorById, getConnectorsFormValidators } from '../utils';
import { usePushToService } from '../use_push_to_service';
import { CaseServices } from '../../containers/use_get_case_user_actions';
import { useApplicationCapabilities } from '../../common/lib/kibana';
+import { useCasesContext } from '../cases_context/use_cases_context';
export interface EditConnectorProps {
caseData: Case;
@@ -48,7 +49,6 @@ export interface EditConnectorProps {
onSuccess: () => void
) => void;
userActions: CaseUserActions[];
- userCanCrud?: boolean;
}
const MyFlexGroup = styled(EuiFlexGroup)`
@@ -119,8 +119,8 @@ export const EditConnector = React.memo(
isValidConnector,
onSubmit,
userActions,
- userCanCrud = true,
}: EditConnectorProps) => {
+ const { permissions } = useCasesContext();
const caseFields = caseData.connector.fields;
const selectedConnector = caseData.connector.id;
@@ -273,7 +273,6 @@ export const EditConnector = React.memo(
connectors,
hasDataToPush,
onEditClick,
- userCanCrud,
isValidConnector,
});
@@ -289,7 +288,7 @@ export const EditConnector = React.memo(
{i18n.CONNECTORS}
{isLoading && }
- {!isLoading && !editConnector && userCanCrud && actionsReadCapabilities && (
+ {!isLoading && !editConnector && permissions.all && actionsReadCapabilities && (
{pushButton}
diff --git a/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap b/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap
index a4103a3e61fe5..41cc919b50200 100644
--- a/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap
+++ b/x-pack/plugins/cases/public/components/header_page/__snapshots__/editable_title.test.tsx.snap
@@ -41,7 +41,10 @@ exports[`EditableTitle renders 1`] = `
"owner": Array [
"securitySolution",
],
- "userCanCrud": true,
+ "permissions": Object {
+ "all": true,
+ "read": true,
+ },
}
}
>
@@ -49,7 +52,6 @@ exports[`EditableTitle renders 1`] = `
isLoading={false}
onSubmit={[MockFunction]}
title="Test title"
- userCanCrud={true}
/>
diff --git a/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap
index 3848a6db31098..fb8c6d854317a 100644
--- a/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/cases/public/components/header_page/__snapshots__/index.test.tsx.snap
@@ -41,7 +41,10 @@ exports[`HeaderPage it renders 1`] = `
"owner": Array [
"securitySolution",
],
- "userCanCrud": true,
+ "permissions": Object {
+ "all": true,
+ "read": true,
+ },
}
}
>
diff --git a/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx b/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx
index 111cc4940ac59..f36996c013471 100644
--- a/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx
+++ b/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx
@@ -9,7 +9,12 @@ import { shallow } from 'enzyme';
import React from 'react';
import '../../common/mock/match_media';
-import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock';
+import {
+ AppMockRenderer,
+ createAppMockRenderer,
+ readCasesPermissions,
+ TestProviders,
+} from '../../common/mock';
import { EditableTitle, EditableTitleProps } from './editable_title';
import { useMountAppended } from '../../utils/use_mount_appended';
@@ -20,7 +25,6 @@ describe('EditableTitle', () => {
title: 'Test title',
onSubmit: submitTitle,
isLoading: false,
- userCanCrud: true,
};
beforeEach(() => {
@@ -39,8 +43,8 @@ describe('EditableTitle', () => {
it('does not show the edit icon when the user does not have edit permissions', () => {
const wrapper = mount(
-
-
+
+
);
diff --git a/x-pack/plugins/cases/public/components/header_page/editable_title.tsx b/x-pack/plugins/cases/public/components/header_page/editable_title.tsx
index 0b142ca40a548..c92e1122c53e8 100644
--- a/x-pack/plugins/cases/public/components/header_page/editable_title.tsx
+++ b/x-pack/plugins/cases/public/components/header_page/editable_title.tsx
@@ -37,19 +37,13 @@ const MySpinner = styled(EuiLoadingSpinner)`
`;
export interface EditableTitleProps {
- userCanCrud: boolean;
isLoading: boolean;
title: string;
onSubmit: (title: string) => void;
}
-const EditableTitleComponent: React.FC = ({
- userCanCrud = false,
- onSubmit,
- isLoading,
- title,
-}) => {
- const { releasePhase } = useCasesContext();
+const EditableTitleComponent: React.FC = ({ onSubmit, isLoading, title }) => {
+ const { releasePhase, permissions } = useCasesContext();
const [editMode, setEditMode] = useState(false);
const [errors, setErrors] = useState([]);
const [newTitle, setNewTitle] = useState(title);
@@ -124,7 +118,7 @@ const EditableTitleComponent: React.FC = ({
) : (
{isLoading && }
- {!isLoading && userCanCrud && (
+ {!isLoading && permissions.all && (
{
it('displays a message without a link to create a case when the user does not have write permissions', () => {
const wrapper = mount(
-
+
);
diff --git a/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx
index dce685248c4c2..d39dfdbb2c50b 100644
--- a/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx
+++ b/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx
@@ -13,7 +13,7 @@ import { useCasesContext } from '../../cases_context/use_cases_context';
import { useCreateCaseNavigation } from '../../../common/navigation';
const NoCasesComponent = () => {
- const { userCanCrud } = useCasesContext();
+ const { permissions } = useCasesContext();
const { getCreateCaseUrl, navigateToCreateCase } = useCreateCaseNavigation();
const navigateToCreateCaseClick = useCallback(
@@ -24,7 +24,7 @@ const NoCasesComponent = () => {
[navigateToCreateCase]
);
- return userCanCrud ? (
+ return permissions.all ? (
<>
{i18n.NO_CASES}
{
});
const onSubmit = jest.fn();
const defaultProps: TagListProps = {
- userCanCrud: true,
isLoading: false,
onSubmit,
tags: [],
@@ -109,10 +108,9 @@ describe('TagList ', () => {
});
it('does not render when the user does not have write permissions', () => {
- const props = { ...defaultProps, userCanCrud: false };
const wrapper = mount(
-
-
+
+
);
expect(wrapper.find(`[data-test-subj="tag-list-edit"]`).exists()).toBeFalsy();
diff --git a/x-pack/plugins/cases/public/components/tag_list/index.tsx b/x-pack/plugins/cases/public/components/tag_list/index.tsx
index 2e2f9e783c011..74fb2efcb4fad 100644
--- a/x-pack/plugins/cases/public/components/tag_list/index.tsx
+++ b/x-pack/plugins/cases/public/components/tag_list/index.tsx
@@ -24,11 +24,11 @@ import { schema } from './schema';
import { useGetTags } from '../../containers/use_get_tags';
import { Tags } from './tags';
+import { useCasesContext } from '../cases_context/use_cases_context';
const CommonUseField = getUseField({ component: Field });
export interface TagListProps {
- userCanCrud?: boolean;
isLoading: boolean;
onSubmit: (a: string[]) => void;
tags: string[];
@@ -55,144 +55,143 @@ const ColumnFlexGroup = styled(EuiFlexGroup)`
`}
`;
-export const TagList = React.memo(
- ({ userCanCrud = true, isLoading, onSubmit, tags }: TagListProps) => {
- const initialState = { tags };
- const { form } = useForm({
- defaultValue: initialState,
- options: { stripEmptyFields: false },
- schema,
- });
- const { submit } = form;
- const [isEditTags, setIsEditTags] = useState(false);
+export const TagList = React.memo(({ isLoading, onSubmit, tags }: TagListProps) => {
+ const { permissions } = useCasesContext();
+ const initialState = { tags };
+ const { form } = useForm({
+ defaultValue: initialState,
+ options: { stripEmptyFields: false },
+ schema,
+ });
+ const { submit } = form;
+ const [isEditTags, setIsEditTags] = useState(false);
- const onSubmitTags = useCallback(async () => {
- const { isValid, data: newData } = await submit();
- if (isValid && newData.tags) {
- onSubmit(newData.tags);
- form.reset({ defaultValue: newData });
- setIsEditTags(false);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [onSubmit, submit]);
+ const onSubmitTags = useCallback(async () => {
+ const { isValid, data: newData } = await submit();
+ if (isValid && newData.tags) {
+ onSubmit(newData.tags);
+ form.reset({ defaultValue: newData });
+ setIsEditTags(false);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [onSubmit, submit]);
- const { data: tagOptions = [] } = useGetTags();
- const [options, setOptions] = useState(
- tagOptions.map((label) => ({
- label,
- }))
- );
+ const { data: tagOptions = [] } = useGetTags();
+ const [options, setOptions] = useState(
+ tagOptions.map((label) => ({
+ label,
+ }))
+ );
- useEffect(
- () =>
- setOptions(
- tagOptions.map((label) => ({
- label,
- }))
- ),
- [tagOptions]
- );
- return (
-
-
-
- {i18n.TAGS}
+ useEffect(
+ () =>
+ setOptions(
+ tagOptions.map((label) => ({
+ label,
+ }))
+ ),
+ [tagOptions]
+ );
+ return (
+
+
+
+ {i18n.TAGS}
+
+ {isLoading && }
+ {!isLoading && permissions.all && (
+
+
+
+ )}
+
+
+
+ {tags.length === 0 && !isEditTags && {i18n.NO_TAGS}
}
+ {!isEditTags && (
+
+
- {isLoading && }
- {!isLoading && userCanCrud && (
-
-
+ )}
+ {isEditTags && (
+
+
+
- )}
-
-
-
- {tags.length === 0 && !isEditTags && {i18n.NO_TAGS}
}
- {!isEditTags && (
-
+
+
+
+ {i18n.SAVE}
+
+
+
+
+ {i18n.CANCEL}
+
+
+
- )}
- {isEditTags && (
-
-
-
-
-
-
-
-
- {i18n.SAVE}
-
-
-
-
- {i18n.CANCEL}
-
-
-
-
-
- )}
-
-
- );
- }
-);
+
+ )}
+
+
+ );
+});
TagList.displayName = 'TagList';
diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx
index 15cfefd57ac57..c00ebc7b48045 100644
--- a/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx
@@ -11,7 +11,7 @@ import { render, screen } from '@testing-library/react';
import '../../common/mock/match_media';
import { usePushToService, ReturnUsePushToService, UsePushToService } from '.';
-import { TestProviders } from '../../common/mock';
+import { readCasesPermissions, TestProviders } from '../../common/mock';
import { CaseStatuses, ConnectorTypes } from '../../../common/api';
import { usePostPushToService } from '../../containers/use_post_push_to_service';
import { basicPush, actionLicenses, connectorsMock } from '../../containers/mock';
@@ -70,7 +70,6 @@ describe('usePushToService', () => {
hasDataToPush: true,
onEditClick,
isValidConnector: true,
- userCanCrud: true,
};
beforeEach(() => {
@@ -281,8 +280,6 @@ describe('usePushToService', () => {
});
describe('user does not have write permissions', () => {
- const noWriteProps = { ...defaultArgs, userCanCrud: false };
-
it('does not display a message when user does not have a premium license', async () => {
useFetchActionLicenseMock.mockImplementation(() => ({
isLoading: false,
@@ -293,9 +290,11 @@ describe('usePushToService', () => {
}));
await act(async () => {
const { result, waitForNextUpdate } = renderHook(
- () => usePushToService(noWriteProps),
+ () => usePushToService(defaultArgs),
{
- wrapper: ({ children }) => {children},
+ wrapper: ({ children }) => (
+ {children}
+ ),
}
);
await waitForNextUpdate();
@@ -313,9 +312,11 @@ describe('usePushToService', () => {
}));
await act(async () => {
const { result, waitForNextUpdate } = renderHook(
- () => usePushToService(noWriteProps),
+ () => usePushToService(defaultArgs),
{
- wrapper: ({ children }) => {children},
+ wrapper: ({ children }) => (
+ {children}
+ ),
}
);
await waitForNextUpdate();
@@ -328,7 +329,7 @@ describe('usePushToService', () => {
const { result, waitForNextUpdate } = renderHook(
() =>
usePushToService({
- ...noWriteProps,
+ ...defaultArgs,
connectors: [],
connector: {
id: 'none',
@@ -338,7 +339,9 @@ describe('usePushToService', () => {
},
}),
{
- wrapper: ({ children }) => {children},
+ wrapper: ({ children }) => (
+ {children}
+ ),
}
);
await waitForNextUpdate();
@@ -351,7 +354,7 @@ describe('usePushToService', () => {
const { result, waitForNextUpdate } = renderHook(
() =>
usePushToService({
- ...noWriteProps,
+ ...defaultArgs,
connector: {
id: 'none',
name: 'none',
@@ -360,7 +363,9 @@ describe('usePushToService', () => {
},
}),
{
- wrapper: ({ children }) => {children},
+ wrapper: ({ children }) => (
+ {children}
+ ),
}
);
await waitForNextUpdate();
@@ -373,7 +378,7 @@ describe('usePushToService', () => {
const { result, waitForNextUpdate } = renderHook(
() =>
usePushToService({
- ...noWriteProps,
+ ...defaultArgs,
connector: {
id: 'not-exist',
name: 'not-exist',
@@ -383,7 +388,9 @@ describe('usePushToService', () => {
isValidConnector: false,
}),
{
- wrapper: ({ children }) => {children},
+ wrapper: ({ children }) => (
+ {children}
+ ),
}
);
await waitForNextUpdate();
@@ -396,7 +403,7 @@ describe('usePushToService', () => {
const { result, waitForNextUpdate } = renderHook(
() =>
usePushToService({
- ...noWriteProps,
+ ...defaultArgs,
connectors: [],
connector: {
id: 'not-exist',
@@ -407,7 +414,9 @@ describe('usePushToService', () => {
isValidConnector: false,
}),
{
- wrapper: ({ children }) => {children},
+ wrapper: ({ children }) => (
+ {children}
+ ),
}
);
await waitForNextUpdate();
@@ -420,11 +429,13 @@ describe('usePushToService', () => {
const { result, waitForNextUpdate } = renderHook(
() =>
usePushToService({
- ...noWriteProps,
+ ...defaultArgs,
caseStatus: CaseStatuses.closed,
}),
{
- wrapper: ({ children }) => {children},
+ wrapper: ({ children }) => (
+ {children}
+ ),
}
);
await waitForNextUpdate();
diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx
index b2c4e79a35596..253170fdd955c 100644
--- a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx
+++ b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx
@@ -23,6 +23,7 @@ import { CaseServices } from '../../containers/use_get_case_user_actions';
import { ErrorMessage } from './callout/types';
import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page';
import { useGetActionLicense } from '../../containers/use_get_action_license';
+import { useCasesContext } from '../cases_context/use_cases_context';
export interface UsePushToService {
caseId: string;
@@ -33,7 +34,6 @@ export interface UsePushToService {
hasDataToPush: boolean;
isValidConnector: boolean;
onEditClick: () => void;
- userCanCrud: boolean;
}
export interface ReturnUsePushToService {
@@ -50,8 +50,8 @@ export const usePushToService = ({
hasDataToPush,
isValidConnector,
onEditClick,
- userCanCrud,
}: UsePushToService): ReturnUsePushToService => {
+ const { permissions } = useCasesContext();
const { isLoading, pushCaseToExternalService } = usePostPushToService();
const { isLoading: loadingLicense, data: actionLicense = null } = useGetActionLicense();
@@ -76,7 +76,7 @@ export const usePushToService = ({
// these message require that the user do some sort of write action as a result of the message, readonly users won't
// be able to perform such an action so let's not display the error to the user in that situation
- if (!userCanCrud) {
+ if (!permissions.all) {
return errors;
}
@@ -114,7 +114,7 @@ export const usePushToService = ({
return errors;
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [actionLicense, caseStatus, connectors.length, connector, loadingLicense, userCanCrud]);
+ }, [actionLicense, caseStatus, connectors.length, connector, loadingLicense, permissions.all]);
const pushToServiceButton = useMemo(
() => (
@@ -126,7 +126,7 @@ export const usePushToService = ({
isLoading ||
loadingLicense ||
errorsMsg.length > 0 ||
- !userCanCrud ||
+ !permissions.all ||
!isValidConnector ||
!hasDataToPush
}
@@ -146,29 +146,26 @@ export const usePushToService = ({
hasDataToPush,
isLoading,
loadingLicense,
- userCanCrud,
+ permissions.all,
isValidConnector,
]
);
- const objToReturn = useMemo(
- () => ({
- pushButton:
- errorsMsg.length > 0 || !hasDataToPush ? (
- 0 ? errorsMsg[0].title : i18n.PUSH_LOCKED_TITLE(connector.name)
- }
- content={
- {errorsMsg.length > 0 ? errorsMsg[0].description : i18n.PUSH_LOCKED_DESC}
- }
- >
- {pushToServiceButton}
-
- ) : (
- <>{pushToServiceButton}>
- ),
+ const objToReturn = useMemo(() => {
+ const hidePushButton = errorsMsg.length > 0 || !hasDataToPush || !permissions.all;
+
+ return {
+ pushButton: hidePushButton ? (
+ 0 ? errorsMsg[0].title : i18n.PUSH_LOCKED_TITLE(connector.name)}
+ content={{errorsMsg.length > 0 ? errorsMsg[0].description : i18n.PUSH_LOCKED_DESC}
}
+ >
+ {pushToServiceButton}
+
+ ) : (
+ <>{pushToServiceButton}>
+ ),
pushCallouts:
errorsMsg.length > 0 ? (
) : null,
- }),
- [
- connector.name,
- connectors.length,
- errorsMsg,
- hasDataToPush,
- hasLicenseError,
- onEditClick,
- pushToServiceButton,
- ]
- );
+ };
+ }, [
+ connector.name,
+ connectors.length,
+ errorsMsg,
+ hasDataToPush,
+ hasLicenseError,
+ onEditClick,
+ pushToServiceButton,
+ permissions.all,
+ ]);
return objToReturn;
};
diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx
index 589efe48cd188..0dfd5876cea6b 100644
--- a/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx
+++ b/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx
@@ -42,7 +42,6 @@ const getCreateCommentUserAction = ({
caseData,
externalReferenceAttachmentTypeRegistry,
comment,
- userCanCrud,
commentRefs,
manageMarkdownEditIds,
selectedOutlineCommentId,
@@ -68,7 +67,6 @@ const getCreateCommentUserAction = ({
case CommentType.user:
const userBuilder = createUserAttachmentUserActionBuilder({
comment,
- userCanCrud,
outlined: comment.id === selectedOutlineCommentId,
isEdit: manageMarkdownEditIds.includes(comment.id),
commentRefs,
@@ -116,7 +114,6 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({
caseData,
externalReferenceAttachmentTypeRegistry,
userAction,
- userCanCrud,
commentRefs,
manageMarkdownEditIds,
selectedOutlineCommentId,
@@ -152,7 +149,6 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({
userAction: commentUserAction,
externalReferenceAttachmentTypeRegistry,
comment,
- userCanCrud,
commentRefs,
manageMarkdownEditIds,
selectedOutlineCommentId,
diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/user.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/user.tsx
index 6c4c96a95bc46..398f58da97b9c 100644
--- a/x-pack/plugins/cases/public/components/user_actions/comment/user.tsx
+++ b/x-pack/plugins/cases/public/components/user_actions/comment/user.tsx
@@ -20,7 +20,6 @@ import { UserActionBuilderArgs, UserActionBuilder } from '../types';
type BuilderArgs = Pick<
UserActionBuilderArgs,
- | 'userCanCrud'
| 'handleManageMarkdownEditId'
| 'handleSaveComment'
| 'handleManageQuote'
@@ -35,7 +34,6 @@ type BuilderArgs = Pick<
export const createUserAttachmentUserActionBuilder = ({
comment,
- userCanCrud,
outlined,
isEdit,
isLoading,
@@ -95,7 +93,6 @@ export const createUserAttachmentUserActionBuilder = ({
onEdit={handleManageMarkdownEditId.bind(null, comment.id)}
onQuote={handleManageQuote.bind(null, comment.comment)}
onDelete={handleDeleteComment.bind(null, comment.id)}
- userCanCrud={userCanCrud}
/>
),
},
diff --git a/x-pack/plugins/cases/public/components/user_actions/content_toolbar.test.tsx b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.test.tsx
index 74f5205578a1d..bba8303149ae9 100644
--- a/x-pack/plugins/cases/public/components/user_actions/content_toolbar.test.tsx
+++ b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.test.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { UserActionContentToolbar, UserActionContentToolbarProps } from './content_toolbar';
+import { TestProviders } from '../../common/mock';
jest.mock('../../common/navigation/hooks');
jest.mock('../../common/lib/kibana');
@@ -17,7 +18,6 @@ const props: UserActionContentToolbarProps = {
id: '1',
editLabel: 'edit',
quoteLabel: 'quote',
- userCanCrud: true,
isLoading: false,
onEdit: jest.fn(),
onQuote: jest.fn(),
@@ -27,7 +27,11 @@ describe('UserActionContentToolbar ', () => {
let wrapper: ReactWrapper;
beforeAll(() => {
- wrapper = mount();
+ wrapper = mount(
+
+
+
+ );
});
it('it renders', async () => {
diff --git a/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx
index bea47933dccd6..e23a4efa2f0a2 100644
--- a/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx
+++ b/x-pack/plugins/cases/public/components/user_actions/content_toolbar.tsx
@@ -22,7 +22,6 @@ export interface UserActionContentToolbarProps {
onEdit: (id: string) => void;
onQuote: (id: string) => void;
onDelete?: (id: string) => void;
- userCanCrud: boolean;
}
const UserActionContentToolbarComponent = ({
@@ -36,7 +35,6 @@ const UserActionContentToolbarComponent = ({
onEdit,
onQuote,
onDelete,
- userCanCrud,
}: UserActionContentToolbarProps) => (
@@ -53,7 +51,6 @@ const UserActionContentToolbarComponent = ({
onEdit={onEdit}
onQuote={onQuote}
onDelete={onDelete}
- userCanCrud={userCanCrud}
commentMarkdown={commentMarkdown}
/>
diff --git a/x-pack/plugins/cases/public/components/user_actions/description.tsx b/x-pack/plugins/cases/public/components/user_actions/description.tsx
index eae2bd3d1258e..ef8f26bd0e87b 100644
--- a/x-pack/plugins/cases/public/components/user_actions/description.tsx
+++ b/x-pack/plugins/cases/public/components/user_actions/description.tsx
@@ -27,7 +27,6 @@ type GetDescriptionUserActionArgs = Pick<
| 'caseData'
| 'commentRefs'
| 'manageMarkdownEditIds'
- | 'userCanCrud'
| 'handleManageMarkdownEditId'
| 'handleManageQuote'
> &
@@ -38,7 +37,6 @@ export const getDescriptionUserAction = ({
commentRefs,
manageMarkdownEditIds,
isLoadingDescription,
- userCanCrud,
onUpdateField,
handleManageMarkdownEditId,
handleManageQuote,
@@ -85,7 +83,6 @@ export const getDescriptionUserAction = ({
isLoading={isLoadingDescription}
onEdit={handleManageMarkdownEditId.bind(null, DESCRIPTION_ID)}
onQuote={handleManageQuote.bind(null, caseData.description)}
- userCanCrud={userCanCrud}
/>
),
};
diff --git a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx
index b6122ebec4016..9a971552f5ec3 100644
--- a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx
@@ -44,7 +44,6 @@ const defaultProps = {
selectedAlertPatterns: ['some-test-pattern'],
statusActionButton: null,
updateCase,
- userCanCrud: true,
useFetchAlertData: (): [boolean, Record] => [
false,
{ 'some-id': { _id: 'some-id' } },
diff --git a/x-pack/plugins/cases/public/components/user_actions/index.tsx b/x-pack/plugins/cases/public/components/user_actions/index.tsx
index c8d038bb2b0f5..1c456f90a71e7 100644
--- a/x-pack/plugins/cases/public/components/user_actions/index.tsx
+++ b/x-pack/plugins/cases/public/components/user_actions/index.tsx
@@ -91,7 +91,6 @@ export const UserActions = React.memo(
onUpdateField,
statusActionButton,
useFetchAlertData,
- userCanCrud,
}: UserActionTreeProps) => {
const { detailName: caseId, commentId } = useCaseViewParams();
const [initLoading, setInitLoading] = useState(true);
@@ -123,7 +122,6 @@ export const UserActions = React.memo(
+ );
+ };
+
+ return (
+ <>
+
- {(list, search) => (
-
- {search}
- {list}
-
- )}
-
-
+ {
+ setSearchValue(value);
+ },
+ }}
+ options={labels}
+ renderOption={renderOption}
+ noMatchesMessage={
+ {
+ if (!searchValue) {
+ return;
+ }
+ updateTags([searchValue], []);
+ }}
+ >
+ {' '}
+
+
+ }
+ >
+ {(list, search) => (
+
+ {search}
+ {list}
+
+ )}
+
+
+ >
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/server/services/license.ts b/x-pack/plugins/index_lifecycle_management/server/services/license.ts
index e51ecac56327e..4b3b1331ae444 100644
--- a/x-pack/plugins/index_lifecycle_management/server/services/license.ts
+++ b/x-pack/plugins/index_lifecycle_management/server/services/license.ts
@@ -57,12 +57,12 @@ export class License {
});
}
- guardApiRoute(handler: RequestHandler) {
+ guardApiRoute(handler: RequestHandler
) {
const license = this;
return function licenseCheck(
ctx: RequestHandlerContext,
- request: KibanaRequest,
+ request: KibanaRequest
,
response: KibanaResponseFactory
) {
const licenseStatus = license.getStatus();
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
index 7d96db4027bad..28537e7934555 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
@@ -190,6 +190,7 @@ export function TableDimensionEditor(
{!column.isTransposed && (
+
IndexPatternLayer),
+ setter:
+ | IndexPatternLayer
+ | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
+ | GenericIndexPatternColumn,
options: { forceRender?: boolean } = {}
) => {
- const hypotheticalLayer = typeof setter === 'function' ? setter(state.layers[layerId]) : setter;
- const isDimensionComplete = Boolean(hypotheticalLayer.columns[columnId]);
+ const layer = state.layers[layerId];
+ let hypotethicalLayer: IndexPatternLayer;
+ if (isColumn(setter)) {
+ hypotethicalLayer = {
+ ...layer,
+ columns: {
+ ...layer.columns,
+ [columnId]: setter,
+ },
+ };
+ } else {
+ hypotethicalLayer = typeof setter === 'function' ? setter(state.layers[layerId]) : setter;
+ }
+ const isDimensionComplete = Boolean(hypotethicalLayer.columns[columnId]);
+
setState(
(prevState) => {
- const layer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter;
- return mergeLayer({ state: prevState, layerId, newLayer: layer });
+ let outputLayer: IndexPatternLayer;
+ const prevLayer = prevState.layers[layerId];
+ if (isColumn(setter)) {
+ outputLayer = {
+ ...prevLayer,
+ columns: {
+ ...prevLayer.columns,
+ [columnId]: setter,
+ },
+ };
+ } else {
+ outputLayer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter;
+ }
+ return mergeLayer({
+ state: prevState,
+ layerId,
+ newLayer: adjustColumnReferencesForChangedColumn(outputLayer, columnId),
+ });
},
{
isDimensionComplete,
@@ -189,7 +224,10 @@ export function DimensionEditor(props: DimensionEditorProps) {
// Note: it forced a rerender at this point to avoid UI glitches in async updates (another hack upstream)
// TODO: revisit this once we get rid of updateDatasourceAsync upstream
const moveDefinetelyToStaticValueAndUpdate = (
- setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
+ setter:
+ | IndexPatternLayer
+ | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
+ | GenericIndexPatternColumn
) => {
if (temporaryStaticValue) {
setTemporaryState('none');
@@ -206,6 +244,9 @@ export function DimensionEditor(props: DimensionEditorProps) {
}
);
}
+ if (isColumn(setter)) {
+ throw new Error('static value should only be updated by the whole layer');
+ }
};
const ParamEditor = getParamEditor(
@@ -290,23 +331,23 @@ export function DimensionEditor(props: DimensionEditorProps) {
color = 'subdued';
}
- let label: EuiListGroupItemProps['label'] = operationPanels[operationType].displayName;
+ let label: EuiListGroupItemProps['label'] = operationDisplay[operationType].displayName;
if (isActive && disabledStatus) {
label = (
- {operationPanels[operationType].displayName}
+ {operationDisplay[operationType].displayName}
);
} else if (disabledStatus) {
label = (
- {operationPanels[operationType].displayName}
+ {operationDisplay[operationType].displayName}
);
} else if (isActive) {
- label = {operationPanels[operationType].displayName};
+ label = {operationDisplay[operationType].displayName};
}
return {
@@ -438,6 +479,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
if (temporaryQuickFunction) {
setTemporaryState('none');
}
+
const newLayer = replaceColumn({
layer: props.state.layers[props.layerId],
indexPattern: currentIndexPattern,
@@ -475,11 +517,16 @@ export function DimensionEditor(props: DimensionEditorProps) {
const FieldInputComponent = selectedOperationDefinition?.renderFieldInput || FieldInput;
- const paramEditorProps: ParamEditorProps = {
+ const paramEditorProps: ParamEditorProps<
+ GenericIndexPatternColumn,
+ | IndexPatternLayer
+ | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
+ | GenericIndexPatternColumn
+ > = {
layer: state.layers[layerId],
layerId,
activeData: props.activeData,
- updateLayer: (setter) => {
+ paramEditorUpdater: (setter) => {
if (temporaryQuickFunction) {
setTemporaryState('none');
}
@@ -494,6 +541,8 @@ export function DimensionEditor(props: DimensionEditorProps) {
isFullscreen,
setIsCloseable,
paramEditorCustomProps,
+ ReferenceEditor,
+ existingFields: state.existingFields,
...services,
};
@@ -523,21 +572,75 @@ export function DimensionEditor(props: DimensionEditorProps) {
<>
{selectedColumn.references.map((referenceId, index) => {
const validation = selectedOperationDefinition.requiredReferences[index];
-
+ const layer = state.layers[layerId];
return (
{
+ updateLayer(
+ deleteColumn({
+ layer,
+ columnId: referenceId,
+ indexPattern: currentIndexPattern,
+ })
+ );
+ }}
+ onChooseFunction={(operationType: string, field?: IndexPatternField) => {
+ updateLayer(
+ insertOrReplaceColumn({
+ layer,
+ columnId: referenceId,
+ op: operationType,
+ indexPattern: currentIndexPattern,
+ field,
+ visualizationGroups: dimensionGroups,
+ })
+ );
+ }}
+ onChooseField={(choice: FieldChoice) => {
+ trackUiEvent('indexpattern_dimension_field_changed');
+ updateLayer(
+ insertOrReplaceColumn({
+ layer,
+ columnId: referenceId,
+ indexPattern: currentIndexPattern,
+ op: choice.operationType,
+ field: currentIndexPattern.getFieldByName(choice.field),
+ visualizationGroups: dimensionGroups,
+ })
+ );
+ }}
+ paramEditorUpdater={(
setter:
| IndexPatternLayer
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
+ | GenericIndexPatternColumn
) => {
- updateLayer(
- typeof setter === 'function' ? setter(state.layers[layerId]) : setter
+ let newLayer: IndexPatternLayer;
+ if (typeof setter === 'function') {
+ newLayer = setter(layer);
+ } else if (isColumn(setter)) {
+ newLayer = {
+ ...layer,
+ columns: {
+ ...layer.columns,
+ [referenceId]: setter,
+ },
+ };
+ } else {
+ newLayer = setter;
+ }
+ return updateLayer(
+ adjustColumnReferencesForChangedColumn(newLayer, referenceId)
);
}}
validation={validation}
@@ -548,9 +651,8 @@ export function DimensionEditor(props: DimensionEditorProps) {
labelAppend={selectedOperationDefinition?.getHelpMessage?.({
data: props.data,
uiSettings: props.uiSettings,
- currentColumn: state.layers[layerId].columns[columnId],
+ currentColumn: layer.columns[columnId],
})}
- dimensionGroups={dimensionGroups}
isFullscreen={isFullscreen}
toggleFullscreen={toggleFullscreen}
setIsCloseable={setIsCloseable}
@@ -600,19 +702,23 @@ export function DimensionEditor(props: DimensionEditorProps) {
const customParamEditor = ParamEditor ? (
<>
>
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
index 7fc76300a73ec..9606cbbf21592 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
@@ -49,11 +49,16 @@ import { DimensionEditor } from './dimension_editor';
import { AdvancedOptions } from './advanced_options';
import { layerTypes } from '../../../common';
+jest.mock('./reference_editor', () => ({
+ ReferenceEditor: () => null,
+}));
jest.mock('../loader');
jest.mock('../query_input', () => ({
QueryInput: () => null,
}));
+
jest.mock('../operations');
+
jest.mock('lodash', () => {
const original = jest.requireActual('lodash');
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts
index 744033a2428fa..7546ff86b8b6e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts
@@ -36,7 +36,7 @@ interface GetDropPropsArgs {
type DropProps = { dropTypes: DropType[]; nextLabel?: string } | undefined;
-const operationLabels = getOperationDisplay();
+const operationDisplay = getOperationDisplay();
export function getNewOperation(
field: IndexPatternField | undefined | false,
@@ -133,7 +133,7 @@ function getDropPropsForField({
const newOperation = getNewOperation(source.field, target.filterOperations, targetColumn);
if (isTheSameIndexPattern && newOperation) {
- const nextLabel = operationLabels[newOperation].displayName;
+ const nextLabel = operationDisplay[newOperation].displayName;
if (!targetColumn) {
return { dropTypes: ['field_add'], nextLabel };
@@ -227,7 +227,7 @@ function getDropPropsFromIncompatibleGroup(
return {
dropTypes,
- nextLabel: operationLabels[newOperationForSource].displayName,
+ nextLabel: operationDisplay[newOperationForSource].displayName,
};
}
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx
index afaa24d3d34b1..16e70f5657db0 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx
@@ -62,7 +62,6 @@ export function FieldSelect({
fields,
(field) => currentIndexPattern.getFieldByName(field)?.type === 'document'
);
-
const containsData = (field: string) =>
currentIndexPattern.getFieldByName(field)?.type === 'document' ||
fieldExists(existingFields, currentIndexPattern.title, field);
@@ -150,9 +149,11 @@ export function FieldSelect({
(selectedOperationType && selectedField
? [
{
- label: fieldIsInvalid
- ? selectedField
- : currentIndexPattern.getFieldByName(selectedField)?.displayName ?? selectedField,
+ label:
+ (selectedOperationType &&
+ selectedField &&
+ currentIndexPattern.getFieldByName(selectedField)?.displayName) ??
+ selectedField,
value: { type: 'field', field: selectedField },
},
]
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx
index 857d0cfb9c1d2..6304f1ff64f91 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx
@@ -21,25 +21,41 @@ import { ReferenceEditor, ReferenceEditorProps } from './reference_editor';
import {
insertOrReplaceColumn,
LastValueIndexPatternColumn,
+ operationDefinitionMap,
TermsIndexPatternColumn,
} from '../operations';
import { FieldSelect } from './field_select';
+import { IndexPatternLayer } from '../types';
jest.mock('../operations');
describe('reference editor', () => {
let wrapper: ReactWrapper | ShallowWrapper;
- let updateLayer: jest.Mock;
-
+ let paramEditorUpdater: jest.Mock;
+
+ const layer = {
+ indexPatternId: '1',
+ columnOrder: ['ref'],
+ columns: {
+ ref: {
+ label: 'Top values of dest',
+ dataType: 'string',
+ isBucketed: true,
+ operationType: 'terms',
+ sourceField: 'dest',
+ params: { size: 5, orderBy: { type: 'alphabetical' }, orderDirection: 'desc' },
+ } as TermsIndexPatternColumn,
+ },
+ };
function getDefaultArgs() {
return {
- layer: {
- indexPatternId: '1',
- columns: {},
- columnOrder: [],
- },
+ layer,
+ column: layer.columns.ref,
+ onChooseField: jest.fn(),
+ onChooseFunction: jest.fn(),
+ onDeleteColumn: jest.fn(),
columnId: 'ref',
- updateLayer,
+ paramEditorUpdater,
selectionStyle: 'full' as const,
currentIndexPattern: createMockedIndexPattern(),
existingFields: {
@@ -63,11 +79,12 @@ describe('reference editor', () => {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
+ operationDefinitionMap,
};
}
beforeEach(() => {
- updateLayer = jest.fn().mockImplementation((newLayer) => {
+ paramEditorUpdater = jest.fn().mockImplementation((newLayer) => {
if (wrapper instanceof ReactWrapper) {
wrapper.setProps({ layer: newLayer });
}
@@ -90,6 +107,7 @@ describe('reference editor', () => {
input: ['field'],
validateMetadata: (meta: OperationMetadata) => meta.dataType === 'number',
}}
+ column={undefined}
/>
);
@@ -115,27 +133,67 @@ describe('reference editor', () => {
);
});
- it('should indicate functions and fields that are incompatible with the current', () => {
+ it('should indicate fields that are incompatible with the current', () => {
+ const newLayer = {
+ indexPatternId: '1',
+ columnOrder: ['ref'],
+ columns: {
+ ref: {
+ label: 'Average of bytes',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'average',
+ sourceField: 'bytes',
+ },
+ },
+ } as IndexPatternLayer;
wrapper = mount(
!meta.isBucketed,
}}
+ />
+ );
+
+ const fields = wrapper
+ .find(EuiComboBox)
+ .filter('[data-test-subj="indexPattern-dimension-field"]')
+ .prop('options');
+
+ const findFieldDataTestSubj = (l: string) => {
+ return fields![0].options!.find(({ label }) => label === l)!['data-test-subj'];
+ };
+ expect(findFieldDataTestSubj('timestampLabel')).toContain('Incompatible');
+ expect(findFieldDataTestSubj('source')).toContain('Incompatible');
+ expect(findFieldDataTestSubj('memory')).toContain('lns-fieldOption-memory');
+ });
+
+ it('should indicate functions that are incompatible with the current', () => {
+ const newLayer = {
+ indexPatternId: '1',
+ columnOrder: ['ref'],
+ columns: {
+ ref: {
+ label: 'Unique count of dest',
+ dataType: 'string',
+ isBucketed: false,
+ operationType: 'unique_count',
+ sourceField: 'dest',
+ },
+ },
+ } as IndexPatternLayer;
+ wrapper = mount(
+ meta.isBucketed,
+ validateMetadata: (meta: OperationMetadata) => !meta.isBucketed,
}}
/>
);
@@ -144,36 +202,31 @@ describe('reference editor', () => {
.find(EuiComboBox)
.filter('[data-test-subj="indexPattern-reference-function"]')
.prop('options');
- expect(functions.find(({ label }) => label === 'Date histogram')!['data-test-subj']).toContain(
+
+ expect(functions.find(({ label }) => label === 'Average')!['data-test-subj']).toContain(
'incompatible'
);
-
- const fields = wrapper
- .find(EuiComboBox)
- .filter('[data-test-subj="indexPattern-dimension-field"]')
- .prop('options');
- expect(
- fields![0].options!.find(({ label }) => label === 'timestampLabel')!['data-test-subj']
- ).toContain('Incompatible');
});
it('should not update when selecting the same operation', () => {
+ const newLayer = {
+ indexPatternId: '1',
+ columnOrder: ['ref'],
+ columns: {
+ ref: {
+ label: 'Average of bytes',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'average',
+ sourceField: 'bytes',
+ },
+ },
+ } as IndexPatternLayer;
wrapper = mount(
meta.dataType === 'number',
@@ -193,26 +246,30 @@ describe('reference editor', () => {
});
it('should keep the field when replacing an existing reference with a compatible function', () => {
+ const onChooseFunction = jest.fn();
+ const newLayer = {
+ indexPatternId: '1',
+ columnOrder: ['ref'],
+ columns: {
+ ref: {
+ label: 'Average of bytes',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'average',
+ sourceField: 'bytes',
+ },
+ },
+ } as IndexPatternLayer;
wrapper = mount(
meta.dataType === 'number',
}}
+ onChooseFunction={onChooseFunction}
/>
);
@@ -225,31 +282,35 @@ describe('reference editor', () => {
comboBox.prop('onChange')!([option]);
});
- expect(insertOrReplaceColumn).toHaveBeenCalledWith(
+ expect(onChooseFunction).toHaveBeenCalledWith(
+ 'max',
expect.objectContaining({
- op: 'max',
- field: expect.objectContaining({ name: 'bytes' }),
+ name: 'bytes',
})
);
});
it('should transition to another function with incompatible field', () => {
+ const newLayer = {
+ indexPatternId: '1',
+ columnOrder: ['ref'],
+ columns: {
+ ref: {
+ label: 'Unique count of dest',
+ dataType: 'string',
+ isBucketed: false,
+ operationType: 'unique_count',
+ sourceField: 'dest',
+ },
+ },
+ } as IndexPatternLayer;
+ const onChooseFunction = jest.fn();
wrapper = mount(
true,
@@ -260,39 +321,36 @@ describe('reference editor', () => {
const comboBox = wrapper
.find(EuiComboBox)
.filter('[data-test-subj="indexPattern-reference-function"]');
- const option = comboBox.prop('options')!.find(({ label }) => label === 'Date histogram')!;
+ const option = comboBox.prop('options')!.find(({ label }) => label === 'Average')!;
act(() => {
comboBox.prop('onChange')!([option]);
});
- expect(insertOrReplaceColumn).toHaveBeenCalledWith(
- expect.objectContaining({
- op: 'date_histogram',
- field: undefined,
- })
- );
+ expect(onChooseFunction).toHaveBeenCalledWith('average', undefined);
});
it("should show the sub-function as invalid if there's no field compatible with it", () => {
// This may happen for saved objects after changing the type of a field
+ const newLayer = {
+ indexPatternId: '1',
+ columnOrder: ['ref'],
+ columns: {
+ ref: {
+ label: 'Average of bytes',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'average',
+ sourceField: 'bytes',
+ },
+ },
+ } as IndexPatternLayer;
wrapper = mount(
true,
@@ -321,6 +379,8 @@ describe('reference editor', () => {
wrapper = mount(
true,
@@ -331,8 +391,8 @@ describe('reference editor', () => {
const subFunctionSelect = wrapper
.find('[data-test-subj="indexPattern-reference-function"]')
.first();
-
expect(subFunctionSelect.prop('isInvalid')).toEqual(true);
+
expect(subFunctionSelect.prop('selectedOptions')).not.toEqual(
expect.arrayContaining([expect.objectContaining({ value: 'math' })])
);
@@ -345,6 +405,7 @@ describe('reference editor', () => {
wrapper = mount(
{
});
it('should pass the incomplete operation info to FieldSelect', () => {
+ const newLayer = {
+ indexPatternId: '1',
+ columnOrder: ['ref'],
+ columns: {
+ ref: {
+ label: 'Average of bytes',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'average',
+ sourceField: 'bytes',
+ },
+ },
+ incompleteColumns: {
+ ref: { operationType: 'max' },
+ },
+ } as IndexPatternLayer;
wrapper = mount(
true,
@@ -395,25 +459,28 @@ describe('reference editor', () => {
});
it('should pass the incomplete field info to FieldSelect', () => {
+ const newLayer = {
+ indexPatternId: '1',
+ columnOrder: ['ref'],
+ columns: {
+ ref: {
+ label: 'Average of bytes',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'average',
+ sourceField: 'bytes',
+ },
+ },
+ incompleteColumns: {
+ ref: { sourceField: 'timestamp' },
+ },
+ } as IndexPatternLayer;
wrapper = mount(
true,
@@ -432,6 +499,7 @@ describe('reference editor', () => {
wrapper = mount(
{
});
it('should show the FieldSelect as invalid if the selected field is missing', () => {
+ const newLayer = {
+ indexPatternId: '1',
+ columnOrder: ['ref'],
+ columns: {
+ ref: {
+ label: 'Average of missing',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'average',
+ sourceField: 'missing',
+ },
+ },
+ } as IndexPatternLayer;
wrapper = mount(
true,
@@ -481,25 +551,27 @@ describe('reference editor', () => {
});
it('should show the ParamEditor for functions that offer one', () => {
+ const lastValueLayer = {
+ indexPatternId: '1',
+ columnOrder: ['ref'],
+ columns: {
+ ref: {
+ label: 'Last value of bytes',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'last_value',
+ sourceField: 'bytes',
+ params: {
+ sortField: 'timestamp',
+ },
+ } as LastValueIndexPatternColumn,
+ },
+ };
wrapper = mount(
true,
@@ -513,28 +585,31 @@ describe('reference editor', () => {
});
it('should hide the ParamEditor for incomplete functions', () => {
+ const lastValueLayer = {
+ indexPatternId: '1',
+ columnOrder: ['ref'],
+ columns: {
+ ref: {
+ label: 'Last value of bytes',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'last_value',
+ sourceField: 'bytes',
+ params: {
+ sortField: 'timestamp',
+ },
+ } as LastValueIndexPatternColumn,
+ },
+ incompleteColumns: {
+ ref: { operationType: 'max' },
+ },
+ };
wrapper = mount(
true,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx
index 4082580cb456a..53e87ab6620a2 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx
@@ -8,13 +8,7 @@
import './dimension_editor.scss';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
-import {
- EuiFormRow,
- EuiFormRowProps,
- EuiSpacer,
- EuiComboBox,
- EuiComboBoxOptionOption,
-} from '@elastic/eui';
+import { EuiFormRowProps, EuiSpacer, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
@@ -22,24 +16,58 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DateRange } from '../../../common';
import type { OperationSupportMatrix } from './operation_support';
-import type { OperationType } from '../indexpattern';
+import type { GenericIndexPatternColumn, OperationType } from '../indexpattern';
import {
- operationDefinitionMap,
getOperationDisplay,
- insertOrReplaceColumn,
- deleteColumn,
isOperationAllowedAsReference,
FieldBasedIndexPatternColumn,
RequiredReference,
+ IncompleteColumn,
+ GenericOperationDefinition,
} from '../operations';
-import { FieldSelect } from './field_select';
+import { FieldChoice, FieldSelect } from './field_select';
import { hasField } from '../pure_utils';
-import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types';
+import type {
+ IndexPattern,
+ IndexPatternField,
+ IndexPatternLayer,
+ IndexPatternPrivateState,
+} from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
-import type { ParamEditorCustomProps, VisualizationDimensionGroupConfig } from '../../types';
+import type { ParamEditorCustomProps } from '../../types';
import type { IndexPatternDimensionEditorProps } from './dimension_panel';
+import { FormRow } from '../operations/definitions/shared_components';
+
+const operationDisplay = getOperationDisplay();
+
+const getFunctionOptions = (
+ operationSupportMatrix: OperationSupportMatrix & {
+ operationTypes: Set;
+ },
+ operationDefinitionMap: Record,
+ column?: GenericIndexPatternColumn
+): Array> => {
+ return Array.from(operationSupportMatrix.operationTypes).map((operationType) => {
+ const def = operationDefinitionMap[operationType];
+ const label = operationDisplay[operationType].displayName;
+ const isCompatible =
+ !column ||
+ (column &&
+ hasField(column) &&
+ def.input === 'field' &&
+ operationSupportMatrix.fieldByOperation[operationType]?.has(column.sourceField)) ||
+ (column && !hasField(column) && def.input !== 'field');
-const operationPanels = getOperationDisplay();
+ return {
+ label,
+ value: operationType,
+ className: 'lnsIndexPatternDimensionEditor__operation',
+ 'data-test-subj': `lns-indexPatternDimension-${operationType}${
+ isCompatible ? '' : ' incompatible'
+ }`,
+ };
+ });
+};
export interface ReferenceEditorProps {
layer: IndexPatternLayer;
@@ -48,18 +76,29 @@ export interface ReferenceEditorProps {
selectionStyle: 'full' | 'field' | 'hidden';
validation: RequiredReference;
columnId: string;
- updateLayer: (
- setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
- ) => void;
+ column?: GenericIndexPatternColumn;
+ incompleteColumn?: IncompleteColumn;
currentIndexPattern: IndexPattern;
-
+ functionLabel?: string;
+ fieldLabel?: string;
+ operationDefinitionMap: Record;
+ isInline?: boolean;
existingFields: IndexPatternPrivateState['existingFields'];
dateRange: DateRange;
labelAppend?: EuiFormRowProps['labelAppend'];
- dimensionGroups: VisualizationDimensionGroupConfig[];
isFullscreen: boolean;
toggleFullscreen: () => void;
setIsCloseable: (isCloseable: boolean) => void;
+ paramEditorCustomProps?: ParamEditorCustomProps;
+ paramEditorUpdater: (
+ setter:
+ | IndexPatternLayer
+ | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
+ | GenericIndexPatternColumn
+ ) => void;
+ onChooseField: (choice: FieldChoice) => void;
+ onDeleteColumn: () => void;
+ onChooseFunction: (operationType: string, field?: IndexPatternField) => void;
// Services
uiSettings: IUiSettingsClient;
@@ -69,39 +108,28 @@ export interface ReferenceEditorProps {
data: DataPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
- paramEditorCustomProps?: ParamEditorCustomProps;
}
-export function ReferenceEditor(props: ReferenceEditorProps) {
+export const ReferenceEditor = (props: ReferenceEditorProps) => {
const {
- layer,
- layerId,
- activeData,
- columnId,
- updateLayer,
currentIndexPattern,
existingFields,
validation,
selectionStyle,
- dateRange,
labelAppend,
- dimensionGroups,
- isFullscreen,
- toggleFullscreen,
- setIsCloseable,
- paramEditorCustomProps,
- ...services
+ column,
+ incompleteColumn,
+ functionLabel,
+ onChooseField,
+ onDeleteColumn,
+ onChooseFunction,
+ fieldLabel,
+ operationDefinitionMap,
+ isInline,
} = props;
- const column = layer.columns[columnId];
const selectedOperationDefinition = column && operationDefinitionMap[column.operationType];
- const ParamEditor = selectedOperationDefinition?.paramEditor;
-
- const incompleteInfo = layer.incompleteColumns ? layer.incompleteColumns[columnId] : undefined;
- const incompleteOperation = incompleteInfo?.operationType;
- const incompleteField = incompleteInfo?.sourceField ?? null;
-
// Basically the operation support matrix, but different validation
const operationSupportMatrix: OperationSupportMatrix & {
operationTypes: Set;
@@ -111,7 +139,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
const operationByField: Partial>> = {};
const fieldByOperation: Partial>> = {};
Object.values(operationDefinitionMap)
- .filter(({ hidden }) => !hidden)
+ .filter(({ hidden, allowAsReference }) => !hidden && allowAsReference)
.sort((op1, op2) => {
return op1.displayName.localeCompare(op2.displayName);
})
@@ -152,230 +180,163 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
operationByField,
fieldByOperation,
};
- }, [currentIndexPattern, validation]);
-
- const functionOptions: Array> = Array.from(
- operationSupportMatrix.operationTypes
- ).map((operationType) => {
- const def = operationDefinitionMap[operationType];
- const label = operationPanels[operationType].displayName;
- const isCompatible =
- !column ||
- (column &&
- hasField(column) &&
- def.input === 'field' &&
- operationSupportMatrix.fieldByOperation[operationType]?.has(column.sourceField)) ||
- (column && !hasField(column) && def.input !== 'field');
-
- return {
- label,
- value: operationType,
- className: 'lnsIndexPatternDimensionEditor__operation',
- 'data-test-subj': `lns-indexPatternDimension-${operationType}${
- isCompatible ? '' : ' incompatible'
- }`,
- };
- });
-
- function onChooseFunction(operationType: OperationType) {
- if (column?.operationType === operationType) {
- return;
- }
- const possibleFieldNames = operationSupportMatrix.fieldByOperation[operationType];
- if (column && 'sourceField' in column && possibleFieldNames?.has(column.sourceField)) {
- // Reuse the current field if possible
- updateLayer(
- insertOrReplaceColumn({
- layer,
- columnId,
- op: operationType,
- indexPattern: currentIndexPattern,
- field: currentIndexPattern.getFieldByName(column.sourceField),
- visualizationGroups: dimensionGroups,
- })
- );
- } else {
- // If reusing the field is impossible, we generally can't choose for the user.
- // The one exception is if the field is the only possible field, like Count of Records.
- const possibleField =
- possibleFieldNames?.size === 1
- ? currentIndexPattern.getFieldByName(possibleFieldNames.values().next().value)
- : undefined;
-
- updateLayer(
- insertOrReplaceColumn({
- layer,
- columnId,
- op: operationType,
- indexPattern: currentIndexPattern,
- field: possibleField,
- visualizationGroups: dimensionGroups,
- })
- );
- }
- trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
- return;
- }
+ }, [currentIndexPattern, validation, operationDefinitionMap]);
if (selectionStyle === 'hidden') {
return null;
}
+ const incompleteOperation = incompleteColumn?.operationType;
+ const incompleteField = incompleteColumn?.sourceField ?? null;
+
+ const functionOptions = getFunctionOptions(
+ operationSupportMatrix,
+ operationDefinitionMap,
+ column
+ );
+
const selectedOption = incompleteOperation
- ? [functionOptions.find(({ value }) => value === incompleteOperation)!]
+ ? [functionOptions?.find(({ value }) => value === incompleteOperation)!]
: column
- ? [functionOptions.find(({ value }) => value === column.operationType)!]
+ ? [functionOptions?.find(({ value }) => value === column.operationType)!]
: [];
- // If the operationType is incomplete, the user needs to select a field- so
- // the function is marked as valid.
- const showOperationInvalid = !column && !Boolean(incompleteOperation);
- // The field is invalid if the operation has been updated without a field,
- // or if we are in a field-only mode but empty state
- const showFieldInvalid = Boolean(incompleteOperation) || (selectionStyle === 'field' && !column);
- // Check if the field still exists to protect from changes
- const showFieldMissingInvalid = !currentIndexPattern.getFieldByName(
- incompleteField ?? (column as FieldBasedIndexPatternColumn)?.sourceField
- );
-
// what about a field changing type and becoming invalid?
// Let's say this change makes the indexpattern without any number field but the operation was set to a numeric operation.
// At this point the ComboBox will crash.
// Therefore check if the selectedOption is in functionOptions and in case fill it in as disabled option
const showSelectionFunctionInvalid = Boolean(selectedOption.length && selectedOption[0] == null);
if (showSelectionFunctionInvalid) {
- const selectedOperationType = incompleteOperation || column.operationType;
+ const selectedOperationType = incompleteOperation || column?.operationType;
const brokenFunctionOption = {
- label: operationPanels[selectedOperationType].displayName,
+ label: selectedOperationType && operationDisplay[selectedOperationType].displayName,
value: selectedOperationType,
className: 'lnsIndexPatternDimensionEditor__operation',
'data-test-subj': `lns-indexPatternDimension-${selectedOperationType} incompatible`,
- };
- functionOptions.push(brokenFunctionOption);
+ } as EuiComboBoxOptionOption;
+ functionOptions?.push(brokenFunctionOption);
selectedOption[0] = brokenFunctionOption;
}
+ // If the operationType is incomplete, the user needs to select a field- so
+ // the function is marked as valid.
+ const showOperationInvalid = !column && !Boolean(incompleteOperation);
+ // The field is invalid if the operation has been updated without a field,
+ // or if we are in a field-only mode but empty state
+ const showFieldInvalid = Boolean(incompleteOperation) || (selectionStyle === 'field' && !column);
+ // Check if the field still exists to protect from changes
+ const showFieldMissingInvalid = !currentIndexPattern.getFieldByName(
+ incompleteField ?? (column as FieldBasedIndexPatternColumn)?.sourceField
+ );
+
+ const ParamEditor = selectedOperationDefinition?.paramEditor;
+
return (
-
-
- {selectionStyle !== 'field' ? (
- <>
-
+ {selectionStyle !== 'field' ? (
+ <>
+
+
- {
- if (choices.length === 0) {
- updateLayer(
- deleteColumn({
- layer,
- columnId,
- indexPattern: currentIndexPattern,
- })
- );
- return;
- }
+ selectedOptions={selectedOption}
+ singleSelection={{ asPlainText: true }}
+ onChange={(choices: Array>) => {
+ if (choices.length === 0) {
+ return onDeleteColumn();
+ }
- trackUiEvent('indexpattern_dimension_field_changed');
+ const operationType = choices[0].value!;
+ if (column?.operationType === operationType) {
+ return;
+ }
+ const possibleFieldNames = operationSupportMatrix.fieldByOperation[operationType];
- onChooseFunction(choices[0].value!);
- }}
- />
-
-
- >
- ) : null}
+ const field =
+ column && 'sourceField' in column && possibleFieldNames?.has(column.sourceField)
+ ? currentIndexPattern.getFieldByName(column.sourceField)
+ : possibleFieldNames?.size === 1
+ ? currentIndexPattern.getFieldByName(possibleFieldNames.values().next().value)
+ : undefined;
- {!column || selectedOperationDefinition.input === 'field' ? (
-
- {
- updateLayer(
- deleteColumn({
- layer,
- columnId,
- indexPattern: currentIndexPattern,
- })
- );
- }}
- onChoose={(choice) => {
- updateLayer(
- insertOrReplaceColumn({
- layer,
- columnId,
- indexPattern: currentIndexPattern,
- op: choice.operationType,
- field: currentIndexPattern.getFieldByName(choice.field),
- visualizationGroups: dimensionGroups,
- })
- );
+ onChooseFunction(operationType, field);
+ trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
+ return;
}}
/>
-
- ) : null}
+
+
+ >
+ ) : null}
- {column && !incompleteInfo && ParamEditor && (
- <>
-
- >
- )}
-
+ {!column || selectedOperationDefinition?.input === 'field' ? (
+
+
+
+ ) : null}
+
+ {column && !incompleteColumn && ParamEditor && (
+ <>
+
+
+ >
+ )}
);
-}
+};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx
index 20e5690f2f534..332f9664973af 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx
@@ -101,6 +101,7 @@ export function TimeScaling({
({
value: unit,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx
index 6900df51ccbba..b74e26cb24895 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx
@@ -163,7 +163,7 @@ export function TimeShift({
}
isInvalid={Boolean(isLocalValueInvalid || localValueTooSmall || localValueNotMultiple)}
>
-
+
({
+ ReferenceEditor: () => null,
+}));
const fieldsOne = [
{
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts
index d6429fb67e9a1..709bf87e2e6f0 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts
@@ -16,6 +16,7 @@ jest.spyOn(actualHelpers, 'copyColumn');
jest.spyOn(actualHelpers, 'insertOrReplaceColumn');
jest.spyOn(actualHelpers, 'insertNewColumn');
jest.spyOn(actualHelpers, 'replaceColumn');
+jest.spyOn(actualHelpers, 'adjustColumnReferencesForChangedColumn');
jest.spyOn(actualHelpers, 'getErrorMessages');
jest.spyOn(actualHelpers, 'getColumnOrder');
@@ -50,6 +51,7 @@ export const {
isOperationAllowedAsReference,
canTransition,
isColumnValidAsReference,
+ adjustColumnReferencesForChangedColumn,
getManagedColumnsFrom,
} = actualHelpers;
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx
index cf96bcd11b788..c46b6954f7480 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx
@@ -173,7 +173,7 @@ Example: Smooth a line of measurements:
function MovingAverageParamEditor({
layer,
- updateLayer,
+ paramEditorUpdater,
currentColumn,
columnId,
}: ParamEditorProps) {
@@ -183,7 +183,7 @@ function MovingAverageParamEditor({
() => {
if (!isValidNumber(inputValue, true, undefined, 1)) return;
const inputNumber = parseInt(inputValue, 10);
- updateLayer(
+ paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@@ -207,6 +207,7 @@ function MovingAverageParamEditor({
isInvalid={!isValidNumber(inputValue)}
>
) => setInputValue(e.target.value)}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx
index b8f6aa433c5f7..7b3ccf8da067b 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx
@@ -65,11 +65,17 @@ export interface CardinalityIndexPatternColumn extends FieldBasedIndexPatternCol
};
}
-export const cardinalityOperation: OperationDefinition = {
+export const cardinalityOperation: OperationDefinition<
+ CardinalityIndexPatternColumn,
+ 'field',
+ {},
+ true
+> = {
type: OPERATION_TYPE,
displayName: i18n.translate('xpack.lens.indexPattern.cardinality', {
defaultMessage: 'Unique count',
}),
+ allowAsReference: true,
input: 'field',
getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => {
if (
@@ -123,7 +129,7 @@ export const cardinalityOperation: OperationDefinition) => {
return [
{
@@ -141,7 +147,7 @@ export const cardinalityOperation: OperationDefinition {
- updateLayer(
+ paramEditorUpdater(
updateColumnParam({
layer,
columnId,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx
index 104b85651a876..014ff0f726cc7 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx
@@ -40,7 +40,7 @@ export type CountIndexPatternColumn = FieldBasedIndexPatternColumn & {
};
};
-export const countOperation: OperationDefinition = {
+export const countOperation: OperationDefinition = {
type: 'count',
priority: 2,
displayName: i18n.translate('xpack.lens.indexPattern.count', {
@@ -52,6 +52,7 @@ export const countOperation: OperationDefinition {
return {
...oldColumn,
@@ -112,7 +113,7 @@ export const countOperation: OperationDefinition) => {
return [
{
@@ -130,7 +131,7 @@ export const countOperation: OperationDefinition {
- updateLayer(
+ paramEditorUpdater(
updateColumnParam({
layer,
columnId,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx
index d801387c30b29..73cb0a37ad563 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx
@@ -106,6 +106,14 @@ const defaultOptions = {
isFullscreen: false,
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
+ existingFields: {
+ my_index_pattern: {
+ timestamp: true,
+ bytes: true,
+ memory: true,
+ source: true,
+ },
+ },
};
describe('date_histogram', () => {
@@ -310,7 +318,7 @@ describe('date_histogram', () => {
@@ -346,7 +354,7 @@ describe('date_histogram', () => {
{
{
@@ -459,7 +467,7 @@ describe('date_histogram', () => {
{
{
{
@@ -581,7 +589,7 @@ describe('date_histogram', () => {
@@ -598,7 +606,7 @@ describe('date_histogram', () => {
@@ -615,7 +623,7 @@ describe('date_histogram', () => {
@@ -631,7 +639,7 @@ describe('date_histogram', () => {
@@ -655,7 +663,7 @@ describe('date_histogram', () => {
@@ -707,7 +715,7 @@ describe('date_histogram', () => {
{...defaultOptions}
layer={layer}
indexPattern={indexPattern}
- updateLayer={updateLayerSpy}
+ paramEditorUpdater={updateLayerSpy}
columnId="col1"
currentColumn={layer.columns.col1 as DateHistogramIndexPatternColumn}
/>
@@ -741,7 +749,7 @@ describe('date_histogram', () => {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx
index 3bbd329a39396..5ee246f09c2e5 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx
@@ -171,7 +171,7 @@ export const dateHistogramOperation: OperationDefinition<
layer,
columnId,
currentColumn,
- updateLayer,
+ paramEditorUpdater,
dateRange,
data,
indexPattern,
@@ -200,7 +200,7 @@ export const dateHistogramOperation: OperationDefinition<
// updateColumnParam will be called async
// store the checked value before the event pooling clears it
const value = ev.target.checked;
- updateLayer((newLayer) =>
+ paramEditorUpdater((newLayer) =>
updateColumnParam({
layer: newLayer,
columnId,
@@ -209,7 +209,7 @@ export const dateHistogramOperation: OperationDefinition<
})
);
},
- [columnId, updateLayer]
+ [columnId, paramEditorUpdater]
);
const setInterval = useCallback(
@@ -221,11 +221,11 @@ export const dateHistogramOperation: OperationDefinition<
? autoInterval
: `${isCalendarInterval ? '1' : newInterval.value}${newInterval.unit || 'd'}`;
- updateLayer((newLayer) =>
+ paramEditorUpdater((newLayer) =>
updateColumnParam({ layer: newLayer, columnId, paramName: 'interval', value })
);
},
- [columnId, updateLayer]
+ [columnId, paramEditorUpdater]
);
const options = (intervalOptions || [])
@@ -323,7 +323,7 @@ export const dateHistogramOperation: OperationDefinition<
const newValue = opts.length ? opts[0].key! : '';
setIntervalInput(newValue);
if (newValue === autoInterval && currentColumn.params.ignoreTimeRange) {
- updateLayer(
+ paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@@ -397,7 +397,7 @@ export const dateHistogramOperation: OperationDefinition<
});
setIntervalInput(newFixedInterval);
}
- updateLayer(newLayer);
+ paramEditorUpdater(newLayer);
}}
compressed
/>
@@ -410,7 +410,7 @@ export const dateHistogramOperation: OperationDefinition<
checked={Boolean(currentColumn.params.includeEmptyRows)}
data-test-subj="indexPattern-include-empty-rows"
onChange={() => {
- updateLayer(
+ paramEditorUpdater(
updateColumnParam({
layer,
columnId,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx
index 1bfa10be4107b..ef900ee1d7f8b 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx
@@ -36,6 +36,14 @@ const defaultProps = {
toggleFullscreen: jest.fn(),
setIsCloseable: jest.fn(),
layerId: '1',
+ existingFields: {
+ my_index_pattern: {
+ timestamp: true,
+ bytes: true,
+ memory: true,
+ source: true,
+ },
+ },
};
// mocking random id generator function
@@ -304,7 +312,7 @@ describe('filters', () => {
@@ -357,7 +365,7 @@ describe('filters', () => {
@@ -382,7 +390,7 @@ describe('filters', () => {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx
index d3eea6e223401..68798bd11aee5 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx
@@ -145,11 +145,11 @@ export const filtersOperation: OperationDefinition {
+ paramEditor: ({ layer, columnId, currentColumn, indexPattern, paramEditorUpdater }) => {
const filters = currentColumn.params.filters;
const setFilters = (newFilters: Filter[]) =>
- updateLayer(
+ paramEditorUpdater(
updateColumnParam({
layer,
columnId,
@@ -159,7 +159,7 @@ export const filtersOperation: OperationDefinition
+
insertOrReplaceFormulaColumn(
columnId,
@@ -183,7 +183,7 @@ export function FormulaEditor({
monaco.editor.setModelMarkers(editorModel.current, 'LENS', []);
if (currentColumn.params.formula) {
// Only submit if valid
- updateLayer(
+ paramEditorUpdater(
insertOrReplaceFormulaColumn(
columnId,
{
@@ -232,7 +232,7 @@ export function FormulaEditor({
if (previousFormulaWasBroken || previousFormulaWasOkButNoData) {
// If the formula is already broken, show the latest error message in the workspace
if (currentColumn.params.formula !== text) {
- updateLayer(
+ paramEditorUpdater(
insertOrReplaceFormulaColumn(
columnId,
{
@@ -314,7 +314,7 @@ export function FormulaEditor({
}
);
- updateLayer(newLayer);
+ paramEditorUpdater(newLayer);
const managedColumns = getManagedColumnsFrom(columnId, newLayer.columns);
const markers: monaco.editor.IMarkerData[] = managedColumns
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx
index c464ce0da027c..4ca172df112e5 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx
@@ -12,7 +12,7 @@ import {
FormattedIndexPatternColumn,
ReferenceBasedIndexPatternColumn,
} from './column_types';
-import { IndexPattern, IndexPatternField } from '../../types';
+import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types';
import { hasField } from '../../pure_utils';
export function getInvalidFieldMessage(
@@ -128,6 +128,15 @@ export function isColumnOfType(
return column.operationType === type;
}
+export const isColumn = (
+ setter:
+ | GenericIndexPatternColumn
+ | IndexPatternLayer
+ | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
+): setter is GenericIndexPatternColumn => {
+ return 'operationType' in setter;
+};
+
export function isColumnFormatted(
column: GenericIndexPatternColumn
): column is FormattedIndexPatternColumn {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts
index cdf2b0249529e..91a83d22f4a29 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts
@@ -64,6 +64,7 @@ import { DateRange, LayerType } from '../../../../common';
import { rangeOperation } from './ranges';
import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from '../../dimension_panel';
import type { OriginalColumn } from '../../to_expression';
+import { ReferenceEditorProps } from '../../dimension_panel/reference_editor';
export type {
IncompleteColumn,
@@ -160,12 +161,14 @@ export { staticValueOperation } from './static_value';
/**
* Properties passed to the operation-specific part of the popover editor
*/
-export interface ParamEditorProps {
+export interface ParamEditorProps<
+ C,
+ U = IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
+> {
currentColumn: C;
layer: IndexPatternLayer;
- updateLayer: (
- setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
- ) => void;
+ paramEditorUpdater: (setter: U) => void;
+ ReferenceEditor?: (props: ReferenceEditorProps) => JSX.Element | null;
toggleFullscreen: () => void;
setIsCloseable: (isCloseable: boolean) => void;
isFullscreen: boolean;
@@ -183,6 +186,8 @@ export interface ParamEditorProps {
activeData?: IndexPatternDimensionEditorProps['activeData'];
operationDefinitionMap: Record;
paramEditorCustomProps?: ParamEditorCustomProps;
+ existingFields: Record>;
+ isReferenced?: boolean;
}
export interface FieldInputProps {
@@ -227,7 +232,11 @@ export interface AdvancedOption {
helpPopup?: string | null;
}
-interface BaseOperationDefinitionProps {
+interface BaseOperationDefinitionProps<
+ C extends BaseIndexPatternColumn,
+ AR extends boolean,
+ P = {}
+> {
type: C['operationType'];
/**
* The priority of the operation. If multiple operations are possible in
@@ -258,7 +267,10 @@ interface BaseOperationDefinitionProps
/**
* React component for operation specific settings shown in the flyout editor
*/
- paramEditor?: React.ComponentType>;
+ allowAsReference?: AR;
+ paramEditor?: React.ComponentType<
+ AR extends true ? ParamEditorProps : ParamEditorProps