From dd0a19033fc74b85855b6bf7f6b3efa4b9ef6529 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 30 Mar 2022 17:22:40 +0200 Subject: [PATCH] Allow nested declaration for `exposeToBrowser` (#128864) * Allow nested declaration for `exposeToBrowser` * update generated doc * add utest --- ...-core-server.exposedtobrowserdescriptor.md | 16 ++ .../core/server/kibana-plugin-core-server.md | 1 + ....pluginconfigdescriptor.exposetobrowser.md | 4 +- ...ugin-core-server.pluginconfigdescriptor.md | 2 +- src/core/server/index.ts | 1 + .../plugins/create_browser_config.test.ts | 162 ++++++++++++++++++ .../server/plugins/create_browser_config.ts | 32 ++++ src/core/server/plugins/plugins_service.ts | 18 +- src/core/server/plugins/types.test.ts | 90 ++++++++++ src/core/server/plugins/types.ts | 19 +- src/core/server/server.api.md | 20 ++- 11 files changed, 341 insertions(+), 24 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.exposedtobrowserdescriptor.md create mode 100644 src/core/server/plugins/create_browser_config.test.ts create mode 100644 src/core/server/plugins/create_browser_config.ts create mode 100644 src/core/server/plugins/types.test.ts 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 00000000000000..b2bb3f5928dcc6 --- /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 450af99a5b2340..60bbd9af2c9d3c 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 bf124b97502d43..212a0d1c9a26b0 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 b9cf0eea3362d3..f5d18c9f40f4d4 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 6907f7ef1238b5..3912585b7b6971 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 00000000000000..8b27ba286c53fe --- /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 00000000000000..95c8de7f4c8cdd --- /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 cde34cea11192c..f202f09735d458 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 00000000000000..4a0e6052a99019 --- /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 991c5628993b0b..9da4eb2742acf1 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 82b4012703be8c..c89a5fc89d2fae 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" ```