diff --git a/docs/development/core/server/kibana-plugin-core-server.exposedtobrowserdescriptor.md b/docs/development/core/server/kibana-plugin-core-server.exposedtobrowserdescriptor.md
new file mode 100644
index 0000000000000..b2bb3f5928dcc
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.exposedtobrowserdescriptor.md
@@ -0,0 +1,16 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExposedToBrowserDescriptor](./kibana-plugin-core-server.exposedtobrowserdescriptor.md)
+
+## ExposedToBrowserDescriptor type
+
+Type defining the list of configuration properties that will be exposed on the client-side Object properties can either be fully exposed
+
+Signature:
+
+```typescript
+export declare type ExposedToBrowserDescriptor = {
+ [Key in keyof T]?: T[Key] extends Maybe ? boolean : T[Key] extends Maybe ? // can be nested for objects
+ ExposedToBrowserDescriptor | boolean : boolean;
+};
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md
index 450af99a5b234..60bbd9af2c9d3 100644
--- a/docs/development/core/server/kibana-plugin-core-server.md
+++ b/docs/development/core/server/kibana-plugin-core-server.md
@@ -265,6 +265,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [ElasticsearchClient](./kibana-plugin-core-server.elasticsearchclient.md) | Client used to query the elasticsearch cluster. |
| [ElasticsearchClientConfig](./kibana-plugin-core-server.elasticsearchclientconfig.md) | Configuration options to be used to create a [cluster client](./kibana-plugin-core-server.iclusterclient.md) using the [createClient API](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) |
| [ExecutionContextStart](./kibana-plugin-core-server.executioncontextstart.md) | |
+| [ExposedToBrowserDescriptor](./kibana-plugin-core-server.exposedtobrowserdescriptor.md) | Type defining the list of configuration properties that will be exposed on the client-side Object properties can either be fully exposed |
| [GetAuthHeaders](./kibana-plugin-core-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. |
| [GetAuthState](./kibana-plugin-core-server.getauthstate.md) | Gets authentication state for a request. Returned by auth interceptor. |
| [HandlerContextType](./kibana-plugin-core-server.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md) to represent the type of the context. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md b/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md
index bf124b97502d4..212a0d1c9a26b 100644
--- a/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md
+++ b/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md
@@ -9,7 +9,5 @@ List of configuration properties that will be available on the client-side plugi
Signature:
```typescript
-exposeToBrowser?: {
- [P in keyof T]?: boolean;
- };
+exposeToBrowser?: ExposedToBrowserDescriptor;
```
diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md b/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md
index b9cf0eea3362d..f5d18c9f40f4d 100644
--- a/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md
+++ b/docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md
@@ -44,7 +44,7 @@ export const config: PluginConfigDescriptor = {
| Property | Type | Description |
| --- | --- | --- |
| [deprecations?](./kibana-plugin-core-server.pluginconfigdescriptor.deprecations.md) | ConfigDeprecationProvider | (Optional) Provider for the to apply to the plugin configuration. |
-| [exposeToBrowser?](./kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md) | { \[P in keyof T\]?: boolean; } | (Optional) List of configuration properties that will be available on the client-side plugin. |
+| [exposeToBrowser?](./kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md) | ExposedToBrowserDescriptor<T> | (Optional) List of configuration properties that will be available on the client-side plugin. |
| [exposeToUsage?](./kibana-plugin-core-server.pluginconfigdescriptor.exposetousage.md) | MakeUsageFromSchema<T> | (Optional) Expose non-default configs to usage collection to be sent via telemetry. set a config to true to report the actual changed config value. set a config to false to report the changed config value as \[redacted\].All changed configs except booleans and numbers will be reported as \[redacted\] unless otherwise specified.[MakeUsageFromSchema](./kibana-plugin-core-server.makeusagefromschema.md) |
| [schema](./kibana-plugin-core-server.pluginconfigdescriptor.schema.md) | PluginConfigSchema<T> | Schema to use to validate the plugin configuration.[PluginConfigSchema](./kibana-plugin-core-server.pluginconfigschema.md) |
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 6907f7ef1238b..3912585b7b697 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -268,6 +268,7 @@ export type {
PluginName,
SharedGlobalConfig,
MakeUsageFromSchema,
+ ExposedToBrowserDescriptor,
} from './plugins';
export {
diff --git a/src/core/server/plugins/create_browser_config.test.ts b/src/core/server/plugins/create_browser_config.test.ts
new file mode 100644
index 0000000000000..8b27ba286c53f
--- /dev/null
+++ b/src/core/server/plugins/create_browser_config.test.ts
@@ -0,0 +1,162 @@
+/*
+ * 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 { ExposedToBrowserDescriptor } from './types';
+import { createBrowserConfig } from './create_browser_config';
+
+describe('createBrowserConfig', () => {
+ it('picks nothing by default', () => {
+ const config = {
+ foo: 'bar',
+ nested: {
+ str: 'string',
+ num: 42,
+ },
+ };
+ const descriptor: ExposedToBrowserDescriptor = {};
+
+ const browserConfig = createBrowserConfig(config, descriptor);
+
+ expect(browserConfig).toEqual({});
+ });
+
+ it('picks all the nested properties when using `true`', () => {
+ const config = {
+ foo: 'bar',
+ nested: {
+ str: 'string',
+ num: 42,
+ },
+ };
+
+ const descriptor: ExposedToBrowserDescriptor = {
+ foo: true,
+ nested: true,
+ };
+
+ const browserConfig = createBrowserConfig(config, descriptor);
+
+ expect(browserConfig).toEqual({
+ foo: 'bar',
+ nested: {
+ str: 'string',
+ num: 42,
+ },
+ });
+ });
+
+ it('picks specific nested properties when using a nested declaration', () => {
+ const config = {
+ foo: 'bar',
+ nested: {
+ str: 'string',
+ num: 42,
+ },
+ };
+
+ const descriptor: ExposedToBrowserDescriptor = {
+ foo: true,
+ nested: {
+ str: true,
+ num: false,
+ },
+ };
+
+ const browserConfig = createBrowserConfig(config, descriptor);
+
+ expect(browserConfig).toEqual({
+ foo: 'bar',
+ nested: {
+ str: 'string',
+ },
+ });
+ });
+
+ it('accepts deeply nested structures', () => {
+ const config = {
+ foo: 'bar',
+ deeply: {
+ str: 'string',
+ nested: {
+ hello: 'dolly',
+ structure: {
+ propA: 'propA',
+ propB: 'propB',
+ },
+ },
+ },
+ };
+
+ const descriptor: ExposedToBrowserDescriptor = {
+ foo: false,
+ deeply: {
+ str: false,
+ nested: {
+ hello: true,
+ structure: {
+ propA: true,
+ propB: false,
+ },
+ },
+ },
+ };
+
+ const browserConfig = createBrowserConfig(config, descriptor);
+
+ expect(browserConfig).toEqual({
+ deeply: {
+ nested: {
+ hello: 'dolly',
+ structure: {
+ propA: 'propA',
+ },
+ },
+ },
+ });
+ });
+
+ it('only includes leaf properties that are `true` when in nested structures', () => {
+ const config = {
+ foo: 'bar',
+ deeply: {
+ str: 'string',
+ nested: {
+ hello: 'dolly',
+ structure: {
+ propA: 'propA',
+ propB: 'propB',
+ },
+ },
+ },
+ };
+
+ const descriptor: ExposedToBrowserDescriptor = {
+ deeply: {
+ nested: {
+ hello: true,
+ structure: {
+ propA: true,
+ },
+ },
+ },
+ };
+
+ const browserConfig = createBrowserConfig(config, descriptor);
+
+ expect(browserConfig).toEqual({
+ deeply: {
+ nested: {
+ hello: 'dolly',
+ structure: {
+ propA: 'propA',
+ },
+ },
+ },
+ });
+ });
+});
diff --git a/src/core/server/plugins/create_browser_config.ts b/src/core/server/plugins/create_browser_config.ts
new file mode 100644
index 0000000000000..95c8de7f4c8cd
--- /dev/null
+++ b/src/core/server/plugins/create_browser_config.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 { ExposedToBrowserDescriptor } from './types';
+
+export const createBrowserConfig = (
+ config: T,
+ descriptor: ExposedToBrowserDescriptor
+): unknown => {
+ return recursiveCreateConfig(config, descriptor);
+};
+
+const recursiveCreateConfig = (
+ config: T,
+ descriptor: ExposedToBrowserDescriptor = {}
+): unknown => {
+ return Object.entries(config || {}).reduce((browserConfig, [key, value]) => {
+ const exposedConfig = descriptor[key as keyof ExposedToBrowserDescriptor];
+ if (exposedConfig && typeof exposedConfig === 'object') {
+ browserConfig[key] = recursiveCreateConfig(value, exposedConfig);
+ }
+ if (exposedConfig === true) {
+ browserConfig[key] = value;
+ }
+ return browserConfig;
+ }, {} as Record);
+};
diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts
index cde34cea11192..f202f09735d45 100644
--- a/src/core/server/plugins/plugins_service.ts
+++ b/src/core/server/plugins/plugins_service.ts
@@ -9,7 +9,7 @@
import Path from 'path';
import { Observable } from 'rxjs';
import { filter, first, map, tap, toArray } from 'rxjs/operators';
-import { getFlattenedObject, pick } from '@kbn/std';
+import { getFlattenedObject } from '@kbn/std';
import { CoreService } from '../../types';
import { CoreContext } from '../core_context';
@@ -26,6 +26,7 @@ import {
} from './types';
import { PluginsConfig, PluginsConfigType } from './plugins_config';
import { PluginsSystem } from './plugins_system';
+import { createBrowserConfig } from './create_browser_config';
import { InternalCorePreboot, InternalCoreSetup, InternalCoreStart } from '../internal_types';
import { IConfigService } from '../config';
import { InternalEnvironmentServicePreboot } from '../environment';
@@ -228,16 +229,11 @@ export class PluginsService implements CoreService
- pick(
- config || {},
- Object.entries(configDescriptor.exposeToBrowser!)
- .filter(([_, exposed]) => exposed)
- .map(([key, _]) => key)
- )
- )
- ),
+ this.configService
+ .atPath(plugin.configPath)
+ .pipe(
+ map((config: any) => createBrowserConfig(config, configDescriptor.exposeToBrowser!))
+ ),
];
})
);
diff --git a/src/core/server/plugins/types.test.ts b/src/core/server/plugins/types.test.ts
new file mode 100644
index 0000000000000..4a0e6052a9901
--- /dev/null
+++ b/src/core/server/plugins/types.test.ts
@@ -0,0 +1,90 @@
+/*
+ * 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 { ExposedToBrowserDescriptor } from './types';
+
+describe('ExposedToBrowserDescriptor', () => {
+ interface ConfigType {
+ str: string;
+ array: number[];
+ obj: {
+ sub1: string;
+ sub2: number;
+ };
+ deep: {
+ foo: number;
+ nested: {
+ str: string;
+ arr: number[];
+ };
+ };
+ }
+
+ it('allows to use recursion on objects', () => {
+ const exposeToBrowser: ExposedToBrowserDescriptor = {
+ obj: {
+ sub1: true,
+ },
+ };
+ expect(exposeToBrowser).toBeDefined();
+ });
+
+ it('allows to use recursion at multiple levels', () => {
+ const exposeToBrowser: ExposedToBrowserDescriptor = {
+ deep: {
+ foo: true,
+ nested: {
+ str: true,
+ },
+ },
+ };
+ expect(exposeToBrowser).toBeDefined();
+ });
+
+ it('does not allow to use recursion on arrays', () => {
+ const exposeToBrowser: ExposedToBrowserDescriptor = {
+ // @ts-expect-error Type '{ 0: true; }' is not assignable to type 'boolean | undefined'.
+ array: {
+ 0: true,
+ },
+ };
+ expect(exposeToBrowser).toBeDefined();
+ });
+
+ it('does not allow to use recursion on arrays at lower levels', () => {
+ const exposeToBrowser: ExposedToBrowserDescriptor = {
+ deep: {
+ nested: {
+ // @ts-expect-error Type '{ 0: true; }' is not assignable to type 'boolean | undefined'.
+ arr: {
+ 0: true,
+ },
+ },
+ },
+ };
+ expect(exposeToBrowser).toBeDefined();
+ });
+
+ it('allows to specify all the properties', () => {
+ const exposeToBrowser: ExposedToBrowserDescriptor = {
+ str: true,
+ array: false,
+ obj: {
+ sub1: true,
+ },
+ deep: {
+ foo: true,
+ nested: {
+ arr: false,
+ str: true,
+ },
+ },
+ };
+ expect(exposeToBrowser).toBeDefined();
+ });
+});
diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts
index 991c5628993b0..9da4eb2742acf 100644
--- a/src/core/server/plugins/types.ts
+++ b/src/core/server/plugins/types.ts
@@ -26,6 +26,23 @@ type Maybe = T | undefined;
*/
export type PluginConfigSchema = Type;
+/**
+ * Type defining the list of configuration properties that will be exposed on the client-side
+ * Object properties can either be fully exposed
+ *
+ * @public
+ */
+export type ExposedToBrowserDescriptor = {
+ [Key in keyof T]?: T[Key] extends Maybe
+ ? // handles arrays as primitive values
+ boolean
+ : T[Key] extends Maybe
+ ? // can be nested for objects
+ ExposedToBrowserDescriptor | boolean
+ : // primitives
+ boolean;
+};
+
/**
* Describes a plugin configuration properties.
*
@@ -64,7 +81,7 @@ export interface PluginConfigDescriptor {
/**
* List of configuration properties that will be available on the client-side plugin.
*/
- exposeToBrowser?: { [P in keyof T]?: boolean };
+ exposeToBrowser?: ExposedToBrowserDescriptor;
/**
* Schema to use to validate the plugin configuration.
*
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 82b4012703be8..c89a5fc89d2fa 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -1001,6 +1001,14 @@ export interface ExecutionContextSetup {
// @public (undocumented)
export type ExecutionContextStart = ExecutionContextSetup;
+// Warning: (ae-forgotten-export) The symbol "Maybe" needs to be exported by the entry point index.d.ts
+//
+// @public
+export type ExposedToBrowserDescriptor = {
+ [Key in keyof T]?: T[Key] extends Maybe ? boolean : T[Key] extends Maybe ? // can be nested for objects
+ ExposedToBrowserDescriptor | boolean : boolean;
+};
+
// @public
export interface FakeRequest {
headers: Headers_2;
@@ -1454,8 +1462,6 @@ export { LogMeta }
export { LogRecord }
-// Warning: (ae-forgotten-export) The symbol "Maybe" needs to be exported by the entry point index.d.ts
-//
// @public
export type MakeUsageFromSchema = {
[Key in keyof T]?: T[Key] extends Maybe ? false : T[Key] extends Maybe ? boolean : T[Key] extends Maybe ? MakeUsageFromSchema | boolean : boolean;
@@ -1647,9 +1653,7 @@ export { Plugin_2 as Plugin }
export interface PluginConfigDescriptor {
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
deprecations?: ConfigDeprecationProvider;
- exposeToBrowser?: {
- [P in keyof T]?: boolean;
- };
+ exposeToBrowser?: ExposedToBrowserDescriptor;
exposeToUsage?: MakeUsageFromSchema;
schema: PluginConfigSchema;
}
@@ -3161,8 +3165,8 @@ export const validBodyOutput: readonly ["data", "stream"];
//
// src/core/server/elasticsearch/client/types.ts:81:7 - (ae-forgotten-export) The symbol "Explanation" needs to be exported by the entry point index.d.ts
// src/core/server/http/router/response.ts:302:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts
-// src/core/server/plugins/types.ts:376:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
-// src/core/server/plugins/types.ts:378:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts
-// src/core/server/plugins/types.ts:485:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create"
+// src/core/server/plugins/types.ts:393:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
+// src/core/server/plugins/types.ts:395:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts
+// src/core/server/plugins/types.ts:502:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create"
```