Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [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

<b>Signature:</b>

```typescript
export declare type ExposedToBrowserDescriptor<T> = {
[Key in keyof T]?: T[Key] extends Maybe<any[]> ? boolean : T[Key] extends Maybe<object> ? // can be nested for objects
ExposedToBrowserDescriptor<T[Key]> | boolean : boolean;
};
```
1 change: 1 addition & 0 deletions docs/development/core/server/kibana-plugin-core-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>auth</code> 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. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,5 @@ List of configuration properties that will be available on the client-side plugi
<b>Signature:</b>

```typescript
exposeToBrowser?: {
[P in keyof T]?: boolean;
};
exposeToBrowser?: ExposedToBrowserDescriptor<T>;
```
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const config: PluginConfigDescriptor<ConfigType> = {
| Property | Type | Description |
| --- | --- | --- |
| [deprecations?](./kibana-plugin-core-server.pluginconfigdescriptor.deprecations.md) | ConfigDeprecationProvider | <i>(Optional)</i> Provider for the to apply to the plugin configuration. |
| [exposeToBrowser?](./kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md) | { \[P in keyof T\]?: boolean; } | <i>(Optional)</i> List of configuration properties that will be available on the client-side plugin. |
| [exposeToBrowser?](./kibana-plugin-core-server.pluginconfigdescriptor.exposetobrowser.md) | ExposedToBrowserDescriptor&lt;T&gt; | <i>(Optional)</i> List of configuration properties that will be available on the client-side plugin. |
| [exposeToUsage?](./kibana-plugin-core-server.pluginconfigdescriptor.exposetousage.md) | MakeUsageFromSchema&lt;T&gt; | <i>(Optional)</i> Expose non-default configs to usage collection to be sent via telemetry. set a config to <code>true</code> to report the actual changed config value. set a config to <code>false</code> 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&lt;T&gt; | Schema to use to validate the plugin configuration.[PluginConfigSchema](./kibana-plugin-core-server.pluginconfigschema.md) |

1 change: 1 addition & 0 deletions src/core/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ export type {
PluginName,
SharedGlobalConfig,
MakeUsageFromSchema,
ExposedToBrowserDescriptor,
} from './plugins';

export {
Expand Down
162 changes: 162 additions & 0 deletions src/core/server/plugins/create_browser_config.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof config> = {};

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<typeof config> = {
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<typeof config> = {
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<typeof config> = {
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<typeof config> = {
deeply: {
nested: {
hello: true,
structure: {
propA: true,
},
},
},
};

const browserConfig = createBrowserConfig(config, descriptor);

expect(browserConfig).toEqual({
deeply: {
nested: {
hello: 'dolly',
structure: {
propA: 'propA',
},
},
},
});
});
});
32 changes: 32 additions & 0 deletions src/core/server/plugins/create_browser_config.ts
Original file line number Diff line number Diff line change
@@ -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 = <T = unknown>(
config: T,
descriptor: ExposedToBrowserDescriptor<T>
): unknown => {
return recursiveCreateConfig(config, descriptor);
};

const recursiveCreateConfig = <T = unknown>(
config: T,
descriptor: ExposedToBrowserDescriptor<T> = {}
): unknown => {
return Object.entries(config || {}).reduce((browserConfig, [key, value]) => {
const exposedConfig = descriptor[key as keyof ExposedToBrowserDescriptor<T>];
if (exposedConfig && typeof exposedConfig === 'object') {
browserConfig[key] = recursiveCreateConfig(value, exposedConfig);
}
if (exposedConfig === true) {
browserConfig[key] = value;
}
return browserConfig;
}, {} as Record<string, unknown>);
};
18 changes: 7 additions & 11 deletions src/core/server/plugins/plugins_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -228,16 +229,11 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
const configDescriptor = this.pluginConfigDescriptors.get(pluginId)!;
return [
pluginId,
this.configService.atPath(plugin.configPath).pipe(
map((config: any) =>
pick(
config || {},
Object.entries(configDescriptor.exposeToBrowser!)
.filter(([_, exposed]) => exposed)
.map(([key, _]) => key)
)
)
),
this.configService
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind the filter in line 225 (I couldn't comment there): since the root values do not necessarily are boolean now, are we OK with validating that a key simply exists?

Copy link
Copy Markdown
Contributor Author

@pgayvallet pgayvallet Mar 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're validating that at least one value exists (not a key), but that doesn't change much: Yea, I think we're fine.

Only edge case I see would be a scenario where the descriptor only defines nested keys with false values:

{
   nested: {
      key: false,
   }
}

this would 'bypass' the filtering. But the output of createBrowserConfig would then be an empty object regardless, so I think it's fine.

TBH we could probably get rid of this whole filter block and let createBrowserConfig generates empty configs for all plugins, but I wanted to avoid touching more code than necessary here.

Do you think it would make sense to get rid of this filter block in the current PR?

.atPath(plugin.configPath)
.pipe(
map((config: any) => createBrowserConfig(config, configDescriptor.exposeToBrowser!))
),
];
})
);
Expand Down
90 changes: 90 additions & 0 deletions src/core/server/plugins/types.test.ts
Original file line number Diff line number Diff line change
@@ -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<ConfigType> = {
obj: {
sub1: true,
},
};
expect(exposeToBrowser).toBeDefined();
});

it('allows to use recursion at multiple levels', () => {
const exposeToBrowser: ExposedToBrowserDescriptor<ConfigType> = {
deep: {
foo: true,
nested: {
str: true,
},
},
};
expect(exposeToBrowser).toBeDefined();
});

it('does not allow to use recursion on arrays', () => {
const exposeToBrowser: ExposedToBrowserDescriptor<ConfigType> = {
// @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<ConfigType> = {
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<ConfigType> = {
str: true,
array: false,
obj: {
sub1: true,
},
deep: {
foo: true,
nested: {
arr: false,
str: true,
},
},
};
expect(exposeToBrowser).toBeDefined();
});
});
Loading