mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Migrate /bootstrap.js
endpoint to core (#92784)
* move bootstrap endpoint to core * some initial cleanup * hack around the 'try' auth status * some UT * more UT * add try/catch around uISettings access * add 'auth.isEnabled' * remove dead files * use `try` authent mode * adapt UT * revert themeTag movearound * migrate apps route to core * some cleanup * nit * add integration tests * update generated doc * add UT for /app route * add etag IT * nits * remove auth.isEnabled API * add tests on get_apm_config * use string template instead of handlebars for bootstrap template * improve plugin bundle tests * update generated doc * remove response.etag API * update generated doc * update generated doc * update generated doc again * extract getThemeTag * add more unit tests
This commit is contained in:
parent
f9e28831c5
commit
d0949121c8
53 changed files with 1697 additions and 521 deletions
|
@ -572,7 +572,6 @@ module.exports = {
|
|||
{
|
||||
files: [
|
||||
'test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js',
|
||||
'src/legacy/ui/ui_render/bootstrap/kbn_bundles_loader_source.js',
|
||||
'**/browser_exec_scripts/**/*.js',
|
||||
],
|
||||
rules: {
|
||||
|
|
|
@ -10346,7 +10346,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "src/core/server/rendering/types.ts",
|
||||
"lineNumber": 72
|
||||
"lineNumber": 73
|
||||
},
|
||||
"signature": [
|
||||
"boolean | undefined"
|
||||
|
@ -10355,7 +10355,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "src/core/server/rendering/types.ts",
|
||||
"lineNumber": 67
|
||||
"lineNumber": 68
|
||||
},
|
||||
"initialIsOpen": false
|
||||
},
|
||||
|
|
|
@ -2887,7 +2887,7 @@
|
|||
"type": "Function",
|
||||
"label": "notHandled",
|
||||
"description": [
|
||||
"\nUser has no credentials.\nAllows user to access a resource when authRequired: 'optional'\nRejects a request when authRequired: true"
|
||||
"\nUser has no credentials.\nAllows user to access a resource when authRequired is 'optional' or 'try'\nRejects a request when authRequired: true"
|
||||
],
|
||||
"source": {
|
||||
"path": "src/core/server/http/lifecycle/auth.ts",
|
||||
|
@ -4648,7 +4648,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "src/core/server/http/router/route.ts",
|
||||
"lineNumber": 171
|
||||
"lineNumber": 173
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -4661,7 +4661,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "src/core/server/http/router/route.ts",
|
||||
"lineNumber": 229
|
||||
"lineNumber": 231
|
||||
},
|
||||
"signature": [
|
||||
"false | ",
|
||||
|
@ -4685,7 +4685,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "src/core/server/http/router/route.ts",
|
||||
"lineNumber": 234
|
||||
"lineNumber": 236
|
||||
},
|
||||
"signature": [
|
||||
{
|
||||
|
@ -4701,7 +4701,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "src/core/server/http/router/route.ts",
|
||||
"lineNumber": 157
|
||||
"lineNumber": 159
|
||||
},
|
||||
"initialIsOpen": false
|
||||
},
|
||||
|
@ -4732,14 +4732,14 @@
|
|||
"type": "CompoundType",
|
||||
"label": "authRequired",
|
||||
"description": [
|
||||
"\nDefines authentication mode for a route:\n- true. A user has to have valid credentials to access a resource\n- false. A user can access a resource without any credentials.\n- 'optional'. A user can access a resource if has valid credentials or no credentials at all.\nCan be useful when we grant access to a resource but want to identify a user if possible.\n\nDefaults to `true` if an auth mechanism is registered."
|
||||
"\nDefines authentication mode for a route:\n- true. A user has to have valid credentials to access a resource\n- false. A user can access a resource without any credentials.\n- 'optional'. A user can access a resource if has valid credentials or no credentials at all.\n Can be useful when we grant access to a resource but want to identify a user if possible.\n- 'try'. A user can access a resource with valid, invalid or without any credentials.\n Users with valid credentials will be authenticated\n\nDefaults to `true` if an auth mechanism is registered."
|
||||
],
|
||||
"source": {
|
||||
"path": "src/core/server/http/router/route.ts",
|
||||
"lineNumber": 116
|
||||
"lineNumber": 118
|
||||
},
|
||||
"signature": [
|
||||
"boolean | \"optional\" | undefined"
|
||||
"boolean | \"optional\" | \"try\" | undefined"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -4752,7 +4752,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "src/core/server/http/router/route.ts",
|
||||
"lineNumber": 125
|
||||
"lineNumber": 127
|
||||
},
|
||||
"signature": [
|
||||
"(Method extends \"get\" ? never : boolean) | undefined"
|
||||
|
@ -4768,7 +4768,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "src/core/server/http/router/route.ts",
|
||||
"lineNumber": 130
|
||||
"lineNumber": 132
|
||||
},
|
||||
"signature": [
|
||||
"readonly string[] | undefined"
|
||||
|
@ -4784,7 +4784,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "src/core/server/http/router/route.ts",
|
||||
"lineNumber": 135
|
||||
"lineNumber": 137
|
||||
},
|
||||
"signature": [
|
||||
"(Method extends ",
|
||||
|
@ -4816,7 +4816,7 @@
|
|||
],
|
||||
"source": {
|
||||
"path": "src/core/server/http/router/route.ts",
|
||||
"lineNumber": 140
|
||||
"lineNumber": 142
|
||||
},
|
||||
"signature": [
|
||||
"{ payload?: (Method extends ",
|
||||
|
|
|
@ -17,6 +17,6 @@ export interface AuthToolkit
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [authenticated](./kibana-plugin-core-server.authtoolkit.authenticated.md) | <code>(data?: AuthResultParams) => AuthResult</code> | Authentication is successful with given credentials, allow request to pass through |
|
||||
| [notHandled](./kibana-plugin-core-server.authtoolkit.nothandled.md) | <code>() => AuthResult</code> | User has no credentials. Allows user to access a resource when authRequired: 'optional' Rejects a request when authRequired: true |
|
||||
| [notHandled](./kibana-plugin-core-server.authtoolkit.nothandled.md) | <code>() => AuthResult</code> | User has no credentials. Allows user to access a resource when authRequired is 'optional' or 'try' Rejects a request when authRequired: true |
|
||||
| [redirected](./kibana-plugin-core-server.authtoolkit.redirected.md) | <code>(headers: {</code><br/><code> location: string;</code><br/><code> } & ResponseHeaders) => AuthResult</code> | Redirects user to another location to complete authentication when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' |
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## AuthToolkit.notHandled property
|
||||
|
||||
User has no credentials. Allows user to access a resource when authRequired: 'optional' Rejects a request when authRequired: true
|
||||
User has no credentials. Allows user to access a resource when authRequired is 'optional' or 'try' Rejects a request when authRequired: true
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
## RouteConfigOptions.authRequired property
|
||||
|
||||
Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible.
|
||||
Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible. - 'try'. A user can access a resource with valid, invalid or without any credentials. Users with valid credentials will be authenticated
|
||||
|
||||
Defaults to `true` if an auth mechanism is registered.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
authRequired?: boolean | 'optional';
|
||||
authRequired?: boolean | 'optional' | 'try';
|
||||
```
|
||||
|
|
|
@ -16,7 +16,7 @@ export interface RouteConfigOptions<Method extends RouteMethod>
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [authRequired](./kibana-plugin-core-server.routeconfigoptions.authrequired.md) | <code>boolean | 'optional'</code> | Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible.<!-- -->Defaults to <code>true</code> if an auth mechanism is registered. |
|
||||
| [authRequired](./kibana-plugin-core-server.routeconfigoptions.authrequired.md) | <code>boolean | 'optional' | 'try'</code> | Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible. - 'try'. A user can access a resource with valid, invalid or without any credentials. Users with valid credentials will be authenticated<!-- -->Defaults to <code>true</code> if an auth mechanism is registered. |
|
||||
| [body](./kibana-plugin-core-server.routeconfigoptions.body.md) | <code>Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody</code> | Additional body options [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md)<!-- -->. |
|
||||
| [tags](./kibana-plugin-core-server.routeconfigoptions.tags.md) | <code>readonly string[]</code> | Additional metadata tag strings to attach to the route. |
|
||||
| [timeout](./kibana-plugin-core-server.routeconfigoptions.timeout.md) | <code>{</code><br/><code> payload?: Method extends 'get' | 'options' ? undefined : number;</code><br/><code> idleSocket?: number;</code><br/><code> }</code> | Defines per-route timeouts. |
|
||||
|
|
|
@ -57,4 +57,21 @@ describe('CoreApp', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('`/app/{id}/{any*}` route', () => {
|
||||
it('is registered with the correct parameters', () => {
|
||||
coreApp.setup(internalCoreSetup);
|
||||
|
||||
expect(httpResourcesRegistrar.register).toHaveBeenCalledWith(
|
||||
{
|
||||
path: '/app/{id}/{any*}',
|
||||
validate: false,
|
||||
options: {
|
||||
authRequired: true,
|
||||
},
|
||||
},
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,9 +16,11 @@ import { Logger } from '../logging';
|
|||
/** @internal */
|
||||
export class CoreApp {
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(core: CoreContext) {
|
||||
this.logger = core.logger.get('core-app');
|
||||
}
|
||||
|
||||
setup(coreSetup: InternalCoreSetup) {
|
||||
this.logger.debug('Setting up core app.');
|
||||
this.registerDefaultRoutes(coreSetup);
|
||||
|
@ -27,7 +29,9 @@ export class CoreApp {
|
|||
|
||||
private registerDefaultRoutes(coreSetup: InternalCoreSetup) {
|
||||
const httpSetup = coreSetup.http;
|
||||
const router = httpSetup.createRouter('/');
|
||||
const router = httpSetup.createRouter('');
|
||||
const resources = coreSetup.httpResources.createRegistrar(router);
|
||||
|
||||
router.get({ path: '/', validate: false }, async (context, req, res) => {
|
||||
const defaultRoute = await context.core.uiSettings.client.get<string>('defaultRoute');
|
||||
const basePath = httpSetup.basePath.get(req);
|
||||
|
@ -39,12 +43,26 @@ export class CoreApp {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
router.get({ path: '/core', validate: false }, async (context, req, res) =>
|
||||
res.ok({ body: { version: '0.0.1' } })
|
||||
);
|
||||
|
||||
resources.register(
|
||||
{
|
||||
path: '/app/{id}/{any*}',
|
||||
validate: false,
|
||||
options: {
|
||||
authRequired: true,
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
return response.renderCoreApp();
|
||||
}
|
||||
);
|
||||
|
||||
const anonymousStatusPage = coreSetup.status.isStatusPageAnonymous();
|
||||
coreSetup.httpResources.createRegistrar(router).register(
|
||||
resources.register(
|
||||
{
|
||||
path: '/status',
|
||||
validate: false,
|
||||
|
@ -61,6 +79,7 @@ export class CoreApp {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
private registerStaticDirs(coreSetup: InternalCoreSetup) {
|
||||
coreSetup.http.registerStaticDir('/ui/{path*}', Path.resolve(__dirname, './assets'));
|
||||
|
||||
|
|
|
@ -30,11 +30,11 @@ import {
|
|||
SessionStorageCookieOptions,
|
||||
createCookieSessionStorageFactory,
|
||||
} from './cookie_session_storage';
|
||||
import { IsAuthenticated, AuthStateStorage, GetAuthState } from './auth_state_storage';
|
||||
import { AuthStateStorage } from './auth_state_storage';
|
||||
import { AuthHeadersStorage, GetAuthHeaders } from './auth_headers_storage';
|
||||
import { BasePath } from './base_path_service';
|
||||
import { getEcsResponseLog } from './logging';
|
||||
import { HttpServiceSetup, HttpServerInfo } from './types';
|
||||
import { HttpServiceSetup, HttpServerInfo, HttpAuth } from './types';
|
||||
|
||||
/** @internal */
|
||||
export interface HttpServerSetup {
|
||||
|
@ -54,10 +54,7 @@ export interface HttpServerSetup {
|
|||
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
|
||||
registerOnPreResponse: HttpServiceSetup['registerOnPreResponse'];
|
||||
getAuthHeaders: GetAuthHeaders;
|
||||
auth: {
|
||||
get: GetAuthState;
|
||||
isAuthenticated: IsAuthenticated;
|
||||
};
|
||||
auth: HttpAuth;
|
||||
getServerInfo: () => HttpServerInfo;
|
||||
}
|
||||
|
||||
|
@ -228,7 +225,7 @@ export class HttpServer {
|
|||
|
||||
private getAuthOption(
|
||||
authRequired: RouteConfigOptions<any>['authRequired'] = true
|
||||
): undefined | false | { mode: 'required' | 'optional' } {
|
||||
): undefined | false | { mode: 'required' | 'optional' | 'try' } {
|
||||
if (this.authRegistered === false) return undefined;
|
||||
|
||||
if (authRequired === true) {
|
||||
|
@ -237,6 +234,9 @@ export class HttpServer {
|
|||
if (authRequired === 'optional') {
|
||||
return { mode: 'optional' };
|
||||
}
|
||||
if (authRequired === 'try') {
|
||||
return { mode: 'try' };
|
||||
}
|
||||
if (authRequired === false) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ describe('http service', () => {
|
|||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
await registerAuth((req, res, toolkit) => toolkit.authenticated());
|
||||
registerAuth((req, res, toolkit) => toolkit.authenticated());
|
||||
|
||||
const router = createRouter('');
|
||||
router.get({ path: '/is-auth', validate: false }, (context, req, res) =>
|
||||
|
@ -136,6 +136,37 @@ describe('http service', () => {
|
|||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false });
|
||||
});
|
||||
|
||||
it('returns true if authenticated on a route with "try" auth', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { createRouter, auth, registerAuth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => toolkit.authenticated());
|
||||
const router = createRouter('');
|
||||
router.get(
|
||||
{ path: '/is-auth', validate: false, options: { authRequired: 'try' } },
|
||||
(context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } })
|
||||
);
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: true });
|
||||
});
|
||||
|
||||
it('returns false if not authenticated on a route with "try" auth', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { createRouter, auth, registerAuth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => toolkit.notHandled());
|
||||
|
||||
const router = createRouter('');
|
||||
router.get(
|
||||
{ path: '/is-auth', validate: false, options: { authRequired: 'try' } },
|
||||
(context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } })
|
||||
);
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false });
|
||||
});
|
||||
});
|
||||
describe('#get()', () => {
|
||||
it('returns authenticated status and allow associate auth state with request', async () => {
|
||||
|
@ -179,7 +210,7 @@ describe('http service', () => {
|
|||
|
||||
const { http } = await root.setup();
|
||||
const { createRouter, registerAuth, auth } = http;
|
||||
await registerAuth(authenticate);
|
||||
registerAuth(authenticate);
|
||||
const router = createRouter('');
|
||||
router.get(
|
||||
{ path: '/get-auth', validate: false, options: { authRequired: false } },
|
||||
|
|
249
src/core/server/http/integration_tests/http_auth.test.ts
Normal file
249
src/core/server/http/integration_tests/http_auth.test.ts
Normal file
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* 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 * as kbnTestServer from '../../../test_helpers/kbn_server';
|
||||
import { IRouter, RouteConfigOptions } from '../router';
|
||||
import { HttpAuth } from '../types';
|
||||
|
||||
describe('http auth', () => {
|
||||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
|
||||
beforeEach(async () => {
|
||||
root = kbnTestServer.createRoot({ plugins: { initialize: false } });
|
||||
}, 30000);
|
||||
|
||||
afterEach(async () => {
|
||||
await root.shutdown();
|
||||
});
|
||||
|
||||
const registerRoute = (
|
||||
router: IRouter,
|
||||
auth: HttpAuth,
|
||||
authRequired: RouteConfigOptions<'get'>['authRequired']
|
||||
) => {
|
||||
router.get(
|
||||
{
|
||||
path: '/route',
|
||||
validate: false,
|
||||
options: {
|
||||
authRequired,
|
||||
},
|
||||
},
|
||||
(context, req, res) => res.ok({ body: { authenticated: auth.isAuthenticated(req) } })
|
||||
);
|
||||
};
|
||||
|
||||
describe('when auth is registered', () => {
|
||||
describe('when authRequired is `true`', () => {
|
||||
it('allows authenticated access when auth returns `authenticated`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => toolkit.authenticated());
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, true);
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: true });
|
||||
});
|
||||
|
||||
it('blocks access when auth returns `notHandled`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => toolkit.notHandled());
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, true);
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(401);
|
||||
});
|
||||
|
||||
it('blocks access when auth returns `unauthorized`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => res.unauthorized());
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, true);
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(401);
|
||||
});
|
||||
});
|
||||
describe('when authRequired is `false`', () => {
|
||||
it('allows anonymous access when auth returns `authenticated`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => toolkit.authenticated());
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, false);
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false });
|
||||
});
|
||||
|
||||
it('allows anonymous access when auth returns `notHandled`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => toolkit.notHandled());
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, false);
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false });
|
||||
});
|
||||
|
||||
it('allows anonymous access when auth returns `unauthorized`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => res.unauthorized());
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, false);
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false });
|
||||
});
|
||||
});
|
||||
describe('when authRequired is `optional`', () => {
|
||||
it('allows authenticated access when auth returns `authenticated`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => toolkit.authenticated());
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, 'optional');
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: true });
|
||||
});
|
||||
|
||||
it('allows anonymous access when auth returns `notHandled`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => toolkit.notHandled());
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, 'optional');
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false });
|
||||
});
|
||||
|
||||
it('blocks access when auth returns `unauthorized`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => res.unauthorized());
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, 'optional');
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(401);
|
||||
});
|
||||
});
|
||||
describe('when authRequired is `try`', () => {
|
||||
it('allows authenticated access when auth returns `authenticated`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => toolkit.authenticated());
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, 'try');
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: true });
|
||||
});
|
||||
|
||||
it('allows anonymous access when auth returns `notHandled`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => toolkit.notHandled());
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, 'try');
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false });
|
||||
});
|
||||
|
||||
it('allows anonymous access when auth returns `unauthorized`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth, createRouter, auth } = http;
|
||||
|
||||
registerAuth((req, res, toolkit) => res.unauthorized());
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, 'try');
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when auth is not registered', () => {
|
||||
it('allow anonymous access to resources when `authRequired` is `true`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { createRouter, auth } = http;
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, true);
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false });
|
||||
});
|
||||
|
||||
it('allow anonymous access to resources when `authRequired` is `false`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { createRouter, auth } = http;
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, false);
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false });
|
||||
});
|
||||
|
||||
it('allow anonymous access to resources when `authRequired` is `optional`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { createRouter, auth } = http;
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, 'optional');
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false });
|
||||
});
|
||||
|
||||
it('allow access to resources when `authRequired` is `try`', async () => {
|
||||
const { http } = await root.setup();
|
||||
const { createRouter, auth } = http;
|
||||
|
||||
const router = createRouter('');
|
||||
registerRoute(router, auth, 'try');
|
||||
|
||||
await root.start();
|
||||
await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -900,7 +900,7 @@ describe('Response factory', () => {
|
|||
return res.ok({
|
||||
body: 'value',
|
||||
headers: {
|
||||
etag: '1234',
|
||||
age: '42',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -910,7 +910,7 @@ describe('Response factory', () => {
|
|||
const result = await supertest(innerServer.listener).get('/').expect(200);
|
||||
|
||||
expect(result.text).toEqual('value');
|
||||
expect(result.header.etag).toBe('1234');
|
||||
expect(result.header.age).toBe('42');
|
||||
});
|
||||
|
||||
it('supports configuring non-standard headers', async () => {
|
||||
|
@ -921,7 +921,7 @@ describe('Response factory', () => {
|
|||
return res.ok({
|
||||
body: 'value',
|
||||
headers: {
|
||||
etag: '1234',
|
||||
age: '42',
|
||||
'x-kibana': 'key',
|
||||
},
|
||||
});
|
||||
|
@ -932,7 +932,7 @@ describe('Response factory', () => {
|
|||
const result = await supertest(innerServer.listener).get('/').expect(200);
|
||||
|
||||
expect(result.text).toEqual('value');
|
||||
expect(result.header.etag).toBe('1234');
|
||||
expect(result.header.age).toBe('42');
|
||||
expect(result.header['x-kibana']).toBe('key');
|
||||
});
|
||||
|
||||
|
@ -944,7 +944,7 @@ describe('Response factory', () => {
|
|||
return res.ok({
|
||||
body: 'value',
|
||||
headers: {
|
||||
ETag: '1234',
|
||||
AgE: '42',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -953,7 +953,7 @@ describe('Response factory', () => {
|
|||
|
||||
const result = await supertest(innerServer.listener).get('/').expect(200);
|
||||
|
||||
expect(result.header.etag).toBe('1234');
|
||||
expect(result.header.age).toBe('42');
|
||||
});
|
||||
|
||||
it('accept array of headers', async () => {
|
||||
|
@ -1776,3 +1776,55 @@ describe('Response factory', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ETag', () => {
|
||||
it('returns the `etag` header', async () => {
|
||||
const { server: innerServer, createRouter } = await server.setup(setupDeps);
|
||||
|
||||
const router = createRouter('');
|
||||
router.get(
|
||||
{
|
||||
path: '/route',
|
||||
validate: false,
|
||||
},
|
||||
(context, req, res) =>
|
||||
res.ok({
|
||||
body: { foo: 'bar' },
|
||||
headers: {
|
||||
etag: 'etag-1',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await server.start();
|
||||
const response = await supertest(innerServer.listener)
|
||||
.get('/route')
|
||||
.expect(200, { foo: 'bar' });
|
||||
expect(response.get('etag')).toEqual('"etag-1"');
|
||||
});
|
||||
|
||||
it('returns a 304 when the etag value matches', async () => {
|
||||
const { server: innerServer, createRouter } = await server.setup(setupDeps);
|
||||
|
||||
const router = createRouter('');
|
||||
router.get(
|
||||
{
|
||||
path: '/route',
|
||||
validate: false,
|
||||
},
|
||||
(context, req, res) =>
|
||||
res.ok({
|
||||
body: { foo: 'bar' },
|
||||
headers: {
|
||||
etag: 'etag-1',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await server.start();
|
||||
await supertest(innerServer.listener)
|
||||
.get('/route')
|
||||
.set('If-None-Match', '"etag-1"')
|
||||
.expect(304, '');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -123,7 +123,7 @@ export interface AuthToolkit {
|
|||
authenticated: (data?: AuthResultParams) => AuthResult;
|
||||
/**
|
||||
* User has no credentials.
|
||||
* Allows user to access a resource when authRequired: 'optional'
|
||||
* Allows user to access a resource when authRequired is 'optional' or 'try'
|
||||
* Rejects a request when authRequired: true
|
||||
* */
|
||||
notHandled: () => AuthResult;
|
||||
|
|
|
@ -28,6 +28,7 @@ function setHeaders(response: HapiResponseObject, headers: Record<string, string
|
|||
response.header(header, value as any);
|
||||
}
|
||||
});
|
||||
applyEtag(response, headers);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
@ -39,6 +40,7 @@ const statusHelpers = {
|
|||
|
||||
export class HapiResponseAdapter {
|
||||
constructor(private readonly responseToolkit: HapiResponseToolkit) {}
|
||||
|
||||
public toBadRequest(message: string) {
|
||||
const error = Boom.badRequest();
|
||||
error.output.payload.message = message;
|
||||
|
@ -115,6 +117,7 @@ export class HapiResponseAdapter {
|
|||
.response(kibanaResponse.payload)
|
||||
.code(kibanaResponse.status);
|
||||
setHeaders(response, kibanaResponse.options.headers);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
@ -150,3 +153,10 @@ function getErrorMessage(payload?: ResponseError): string {
|
|||
function getErrorAttributes(payload?: ResponseError): ResponseErrorAttributes | undefined {
|
||||
return typeof payload === 'object' && 'attributes' in payload ? payload.attributes : undefined;
|
||||
}
|
||||
|
||||
function applyEtag(response: HapiResponseObject, headers: Record<string, string | string[]>) {
|
||||
const etagHeader = Object.keys(headers).find((header) => header.toLowerCase() === 'etag');
|
||||
if (etagHeader) {
|
||||
response.etag(headers[etagHeader] as string);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,11 +109,13 @@ export interface RouteConfigOptions<Method extends RouteMethod> {
|
|||
* - true. A user has to have valid credentials to access a resource
|
||||
* - false. A user can access a resource without any credentials.
|
||||
* - 'optional'. A user can access a resource if has valid credentials or no credentials at all.
|
||||
* Can be useful when we grant access to a resource but want to identify a user if possible.
|
||||
* Can be useful when we grant access to a resource but want to identify a user if possible.
|
||||
* - 'try'. A user can access a resource with valid, invalid or without any credentials.
|
||||
* Users with valid credentials will be authenticated
|
||||
*
|
||||
* Defaults to `true` if an auth mechanism is registered.
|
||||
*/
|
||||
authRequired?: boolean | 'optional';
|
||||
authRequired?: boolean | 'optional' | 'try';
|
||||
|
||||
/**
|
||||
* Defines xsrf protection requirements for a route:
|
||||
|
|
|
@ -6,4 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { uiMixin } from './ui_mixin';
|
||||
export const getConfigMock = jest.fn();
|
||||
jest.doMock('../../../apm', () => ({
|
||||
getConfig: getConfigMock,
|
||||
}));
|
||||
|
||||
export const agentMock = {} as Record<string, any>;
|
||||
jest.doMock('elastic-apm-node', () => agentMock);
|
76
src/core/server/http_resources/get_apm_config.test.ts
Normal file
76
src/core/server/http_resources/get_apm_config.test.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 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 { getConfigMock, agentMock } from './get_apm_config.test.mocks';
|
||||
import { getApmConfig } from './get_apm_config';
|
||||
|
||||
const defaultApmConfig = {
|
||||
active: true,
|
||||
someConfigProp: 'value',
|
||||
};
|
||||
|
||||
describe('getApmConfig', () => {
|
||||
beforeEach(() => {
|
||||
getConfigMock.mockReturnValue(defaultApmConfig);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getConfigMock.mockReset();
|
||||
agentMock.currentTransaction = null;
|
||||
});
|
||||
|
||||
it('returns null if apm is disabled', () => {
|
||||
getConfigMock.mockReturnValue({
|
||||
active: false,
|
||||
});
|
||||
expect(getApmConfig('/path')).toBeNull();
|
||||
|
||||
getConfigMock.mockReturnValue(undefined);
|
||||
expect(getApmConfig('/path')).toBeNull();
|
||||
});
|
||||
|
||||
it('calls `getConfig` with the correct parameters', () => {
|
||||
getApmConfig('/path');
|
||||
|
||||
expect(getConfigMock).toHaveBeenCalledWith('kibana-frontend');
|
||||
});
|
||||
|
||||
it('returns the configuration from the `getConfig` call', () => {
|
||||
const config = getApmConfig('/path');
|
||||
|
||||
expect(config).toEqual(expect.objectContaining(defaultApmConfig));
|
||||
});
|
||||
|
||||
it('returns the requestPath as `pageLoadTransactionName`', () => {
|
||||
const config = getApmConfig('/some-other-path');
|
||||
|
||||
expect(config).toEqual(
|
||||
expect.objectContaining({
|
||||
pageLoadTransactionName: '/some-other-path',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('enhance the configuration with values from the current server-side transaction', () => {
|
||||
agentMock.currentTransaction = {
|
||||
sampled: 'sampled',
|
||||
traceId: 'traceId',
|
||||
ensureParentId: () => 'parentId',
|
||||
} as any;
|
||||
|
||||
const config = getApmConfig('/some-other-path');
|
||||
|
||||
expect(config).toEqual(
|
||||
expect.objectContaining({
|
||||
pageLoadTraceId: 'traceId',
|
||||
pageLoadSampled: 'sampled',
|
||||
pageLoadSpanId: 'parentId',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -6,36 +6,34 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getConfig } from '../../../apm';
|
||||
import agent from 'elastic-apm-node';
|
||||
// @ts-expect-error apm module is a js file outside of core (need to split APM/rum configuration)
|
||||
import { getConfig } from '../../../apm';
|
||||
|
||||
const apmEnabled = getConfig()?.active;
|
||||
|
||||
export function getApmConfig(requestPath) {
|
||||
if (!apmEnabled) {
|
||||
export const getApmConfig = (requestPath: string) => {
|
||||
const baseConfig = getConfig('kibana-frontend');
|
||||
if (!baseConfig?.active) {
|
||||
return null;
|
||||
}
|
||||
const config = {
|
||||
...getConfig('kibana-frontend'),
|
||||
|
||||
const config: Record<string, any> = {
|
||||
...baseConfig,
|
||||
pageLoadTransactionName: requestPath,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current active backend transaction to make distrubuted tracing
|
||||
* work for rendering the app
|
||||
*/
|
||||
// Get current active backend transaction to make distributed tracing
|
||||
// work for rendering the app
|
||||
const backendTransaction = agent.currentTransaction;
|
||||
|
||||
if (backendTransaction) {
|
||||
const { sampled, traceId } = backendTransaction;
|
||||
const { sampled, traceId } = backendTransaction as any;
|
||||
return {
|
||||
...config,
|
||||
...{
|
||||
pageLoadTraceId: traceId,
|
||||
pageLoadSampled: sampled,
|
||||
pageLoadSpanId: backendTransaction.ensureParentId(),
|
||||
},
|
||||
pageLoadTraceId: traceId,
|
||||
pageLoadSampled: sampled,
|
||||
pageLoadSpanId: backendTransaction.ensureParentId(),
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
};
|
|
@ -6,4 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { AppBootstrap } from './app_bootstrap';
|
||||
export const getApmConfigMock = jest.fn();
|
||||
|
||||
jest.doMock('./get_apm_config', () => ({
|
||||
getApmConfig: getApmConfigMock,
|
||||
}));
|
|
@ -6,6 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getApmConfigMock } from './http_resources_service.test.mocks';
|
||||
|
||||
import { IRouter, RouteConfig } from '../http';
|
||||
|
||||
import { coreMock } from '../mocks';
|
||||
|
@ -24,6 +26,12 @@ describe('HttpResources service', () => {
|
|||
let router: jest.Mocked<IRouter>;
|
||||
const kibanaRequest = httpServerMock.createKibanaRequest();
|
||||
const context = { core: coreMock.createRequestHandlerContext() };
|
||||
const apmConfig = { mockApmConfig: true };
|
||||
|
||||
beforeEach(() => {
|
||||
getApmConfigMock.mockReturnValue(apmConfig);
|
||||
});
|
||||
|
||||
describe('#createRegistrar', () => {
|
||||
beforeEach(() => {
|
||||
setupDeps = {
|
||||
|
@ -52,6 +60,9 @@ describe('HttpResources service', () => {
|
|||
context.core.uiSettings.client,
|
||||
{
|
||||
includeUserSettings: true,
|
||||
vars: {
|
||||
apmConfig,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -101,6 +112,9 @@ describe('HttpResources service', () => {
|
|||
context.core.uiSettings.client,
|
||||
{
|
||||
includeUserSettings: false,
|
||||
vars: {
|
||||
apmConfig,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
HttpResourcesRequestHandler,
|
||||
HttpResourcesServiceToolkit,
|
||||
} from './types';
|
||||
import { getApmConfig } from './get_apm_config';
|
||||
|
||||
export interface SetupDeps {
|
||||
http: InternalHttpServiceSetup;
|
||||
|
@ -37,6 +38,7 @@ export interface SetupDeps {
|
|||
|
||||
export class HttpResourcesService implements CoreService<InternalHttpResourcesSetup> {
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(core: CoreContext) {
|
||||
this.logger = core.logger.get('http-resources');
|
||||
}
|
||||
|
@ -49,6 +51,7 @@ export class HttpResourcesService implements CoreService<InternalHttpResourcesSe
|
|||
}
|
||||
|
||||
start() {}
|
||||
|
||||
stop() {}
|
||||
|
||||
private createRegistrar(deps: SetupDeps, router: IRouter): HttpResources {
|
||||
|
@ -76,8 +79,12 @@ export class HttpResourcesService implements CoreService<InternalHttpResourcesSe
|
|||
const cspHeader = deps.http.csp.header;
|
||||
return {
|
||||
async renderCoreApp(options: HttpResourcesRenderOptions = {}) {
|
||||
const apmConfig = getApmConfig(request.url.pathname);
|
||||
const body = await deps.rendering.render(request, context.core.uiSettings.client, {
|
||||
includeUserSettings: true,
|
||||
vars: {
|
||||
apmConfig,
|
||||
},
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
|
@ -86,8 +93,12 @@ export class HttpResourcesService implements CoreService<InternalHttpResourcesSe
|
|||
});
|
||||
},
|
||||
async renderAnonymousCoreApp(options: HttpResourcesRenderOptions = {}) {
|
||||
const apmConfig = getApmConfig(request.url.pathname);
|
||||
const body = await deps.rendering.render(request, context.core.uiSettings.client, {
|
||||
includeUserSettings: false,
|
||||
vars: {
|
||||
apmConfig,
|
||||
},
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
|
|
|
@ -133,7 +133,10 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
|
|||
registerOnPostAuth: deps.http.registerOnPostAuth,
|
||||
registerOnPreResponse: deps.http.registerOnPreResponse,
|
||||
basePath: deps.http.basePath,
|
||||
auth: { get: deps.http.auth.get, isAuthenticated: deps.http.auth.isAuthenticated },
|
||||
auth: {
|
||||
get: deps.http.auth.get,
|
||||
isAuthenticated: deps.http.auth.isAuthenticated,
|
||||
},
|
||||
csp: deps.http.csp,
|
||||
getServerInfo: deps.http.getServerInfo,
|
||||
},
|
||||
|
|
|
@ -1,15 +1,48 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renderTemplate interpolates templateData into string template 1`] = `
|
||||
"
|
||||
function kbnBundlesLoader() {
|
||||
var modules = {};
|
||||
|
||||
function has(prop) {
|
||||
return Object.prototype.hasOwnProperty.call(modules, prop);
|
||||
}
|
||||
|
||||
function define(key, bundleRequire, bundleModuleKey) {
|
||||
if (has(key)) {
|
||||
throw new Error('__kbnBundles__ already has a module defined for \\"' + key + '\\"');
|
||||
}
|
||||
|
||||
modules[key] = {
|
||||
bundleRequire,
|
||||
bundleModuleKey,
|
||||
};
|
||||
}
|
||||
|
||||
function get(key) {
|
||||
if (!has(key)) {
|
||||
throw new Error('__kbnBundles__ does not have a module defined for \\"' + key + '\\"');
|
||||
}
|
||||
|
||||
return modules[key].bundleRequire(modules[key].bundleModuleKey);
|
||||
}
|
||||
|
||||
return { has: has, define: define, get: get };
|
||||
}
|
||||
|
||||
var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data'));
|
||||
window.__kbnStrictCsp__ = kbnCsp.strictCsp;
|
||||
window.__kbnThemeTag__ = "{{themeTag}}";
|
||||
window.__kbnPublicPath__ = {{publicPathMap}};
|
||||
window.__kbnBundles__ = {{kbnBundlesLoaderSource}}
|
||||
window.__kbnThemeTag__ = \\"v7\\";
|
||||
window.__kbnPublicPath__ = {\\"foo\\": \\"bar\\"};
|
||||
window.__kbnBundles__ = kbnBundlesLoader();
|
||||
|
||||
if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) {
|
||||
var legacyBrowserError = document.getElementById('kbn_legacy_browser_error');
|
||||
legacyBrowserError.style.display = 'flex';
|
||||
} else {
|
||||
if (!window.__kbnCspNotEnforced__ && window.console) {
|
||||
window.console.log("^ A single error about an inline script not firing due to content security policy is expected!");
|
||||
window.console.log(\\"^ A single error about an inline script not firing due to content security policy is expected!\\");
|
||||
}
|
||||
var loadingMessage = document.getElementById('kbn_loading_message');
|
||||
loadingMessage.style.display = 'flex';
|
||||
|
@ -30,7 +63,7 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) {
|
|||
document.body.innerHTML = err.outerHTML;
|
||||
}
|
||||
|
||||
var stylesheetTarget = document.querySelector('head meta[name="add-styles-here"]')
|
||||
var stylesheetTarget = document.querySelector('head meta[name=\\"add-styles-here\\"]')
|
||||
function loadStyleSheet(url, cb) {
|
||||
var dom = document.createElement('link');
|
||||
dom.rel = 'stylesheet';
|
||||
|
@ -41,10 +74,9 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) {
|
|||
document.head.insertBefore(dom, stylesheetTarget);
|
||||
}
|
||||
|
||||
var scriptsTarget = document.querySelector('head meta[name="add-scripts-here"]')
|
||||
var scriptsTarget = document.querySelector('head meta[name=\\"add-scripts-here\\"]')
|
||||
function loadScript(url, cb) {
|
||||
var dom = document.createElement('script');
|
||||
{{!-- NOTE: async = false is used to trigger async-download/ordered-execution as outlined here: https://www.html5rocks.com/en/tutorials/speed/script-loading/ --}}
|
||||
dom.async = false;
|
||||
dom.src = url;
|
||||
dom.addEventListener('error', failure);
|
||||
|
@ -73,17 +105,11 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) {
|
|||
}
|
||||
|
||||
load([
|
||||
{{#each jsDependencyPaths}}
|
||||
'{{this}}',
|
||||
{{/each}}
|
||||
'/js-1','/js-2'
|
||||
], function () {
|
||||
__kbnBundles__.get('entry/core/public').__kbnBootstrap__();
|
||||
|
||||
load([
|
||||
{{#each styleSheetPaths}}
|
||||
'{{this}}',
|
||||
{{/each}}
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const renderTemplateMock = jest.fn();
|
||||
jest.doMock('./render_template', () => ({
|
||||
renderTemplate: renderTemplateMock,
|
||||
}));
|
||||
|
||||
export const getThemeTagMock = jest.fn();
|
||||
jest.doMock('./get_theme_tag', () => ({
|
||||
getThemeTag: getThemeTagMock,
|
||||
}));
|
||||
|
||||
export const getPluginsBundlePathsMock = jest.fn();
|
||||
jest.doMock('./get_plugin_bundle_paths', () => ({
|
||||
getPluginsBundlePaths: getPluginsBundlePathsMock,
|
||||
}));
|
||||
|
||||
export const getJsDependencyPathsMock = jest.fn();
|
||||
jest.doMock('./get_js_dependency_paths', () => ({
|
||||
getJsDependencyPaths: getJsDependencyPathsMock,
|
||||
}));
|
241
src/core/server/rendering/bootstrap/bootstrap_renderer.test.ts
Normal file
241
src/core/server/rendering/bootstrap/bootstrap_renderer.test.ts
Normal file
|
@ -0,0 +1,241 @@
|
|||
/*
|
||||
* 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 {
|
||||
renderTemplateMock,
|
||||
getPluginsBundlePathsMock,
|
||||
getThemeTagMock,
|
||||
getJsDependencyPathsMock,
|
||||
} from './bootstrap_renderer.test.mocks';
|
||||
|
||||
import { PackageInfo } from '@kbn/config';
|
||||
import { UiPlugins } from '../../plugins';
|
||||
import { httpServiceMock } from '../../http/http_service.mock';
|
||||
import { httpServerMock } from '../../http/http_server.mocks';
|
||||
import { AuthStatus } from '../../http';
|
||||
import { uiSettingsServiceMock } from '../../ui_settings/ui_settings_service.mock';
|
||||
import { bootstrapRendererFactory, BootstrapRenderer } from './bootstrap_renderer';
|
||||
|
||||
const createPackageInfo = (parts: Partial<PackageInfo> = {}): PackageInfo => ({
|
||||
branch: 'master',
|
||||
buildNum: 42,
|
||||
buildSha: 'buildSha',
|
||||
dist: false,
|
||||
version: '8.0.0',
|
||||
...parts,
|
||||
});
|
||||
|
||||
const createUiPlugins = (): UiPlugins => ({
|
||||
public: new Map(),
|
||||
internal: new Map(),
|
||||
browserConfigs: new Map(),
|
||||
});
|
||||
|
||||
describe('bootstrapRenderer', () => {
|
||||
let auth: ReturnType<typeof httpServiceMock.createAuth>;
|
||||
let uiSettingsClient: ReturnType<typeof uiSettingsServiceMock.createClient>;
|
||||
let renderer: BootstrapRenderer;
|
||||
let uiPlugins: UiPlugins;
|
||||
let packageInfo: PackageInfo;
|
||||
|
||||
beforeEach(() => {
|
||||
auth = httpServiceMock.createAuth();
|
||||
uiSettingsClient = uiSettingsServiceMock.createClient();
|
||||
uiPlugins = createUiPlugins();
|
||||
packageInfo = createPackageInfo();
|
||||
|
||||
getThemeTagMock.mockReturnValue('v8light');
|
||||
getPluginsBundlePathsMock.mockReturnValue(new Map());
|
||||
renderTemplateMock.mockReturnValue('__rendered__');
|
||||
getJsDependencyPathsMock.mockReturnValue([]);
|
||||
|
||||
renderer = bootstrapRendererFactory({
|
||||
auth,
|
||||
packageInfo,
|
||||
uiPlugins,
|
||||
serverBasePath: '/base-path',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getThemeTagMock.mockReset();
|
||||
getPluginsBundlePathsMock.mockReset();
|
||||
renderTemplateMock.mockReset();
|
||||
getJsDependencyPathsMock.mockReset();
|
||||
});
|
||||
|
||||
describe('when the auth status is `authenticated`', () => {
|
||||
beforeEach(() => {
|
||||
auth.get.mockReturnValue({
|
||||
status: 'authenticated' as AuthStatus,
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('calls uiSettingsClient.get with the correct parameters', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await renderer({
|
||||
request,
|
||||
uiSettingsClient,
|
||||
});
|
||||
|
||||
expect(uiSettingsClient.get).toHaveBeenCalledTimes(2);
|
||||
expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:darkMode');
|
||||
expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:version');
|
||||
});
|
||||
|
||||
it('calls getThemeTag with the correct parameters', async () => {
|
||||
uiSettingsClient.get.mockImplementation((settingName) => {
|
||||
return Promise.resolve(settingName === 'theme:darkMode' ? true : 'v8');
|
||||
});
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await renderer({
|
||||
request,
|
||||
uiSettingsClient,
|
||||
});
|
||||
|
||||
expect(getThemeTagMock).toHaveBeenCalledTimes(1);
|
||||
expect(getThemeTagMock).toHaveBeenCalledWith({
|
||||
themeVersion: 'v8',
|
||||
darkMode: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the auth status is `unknown`', () => {
|
||||
beforeEach(() => {
|
||||
auth.get.mockReturnValue({
|
||||
status: 'unknown' as AuthStatus,
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('calls uiSettingsClient.get with the correct parameters', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await renderer({
|
||||
request,
|
||||
uiSettingsClient,
|
||||
});
|
||||
|
||||
expect(uiSettingsClient.get).toHaveBeenCalledTimes(2);
|
||||
expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:darkMode');
|
||||
expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:version');
|
||||
});
|
||||
|
||||
it('calls getThemeTag with the correct parameters', async () => {
|
||||
uiSettingsClient.get.mockImplementation((settingName) => {
|
||||
return Promise.resolve(settingName === 'theme:darkMode' ? true : 'v8');
|
||||
});
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await renderer({
|
||||
request,
|
||||
uiSettingsClient,
|
||||
});
|
||||
|
||||
expect(getThemeTagMock).toHaveBeenCalledTimes(1);
|
||||
expect(getThemeTagMock).toHaveBeenCalledWith({
|
||||
themeVersion: 'v8',
|
||||
darkMode: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the auth status is `unauthenticated`', () => {
|
||||
beforeEach(() => {
|
||||
auth.get.mockReturnValue({
|
||||
status: 'unauthenticated' as AuthStatus,
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not call uiSettingsClient.get', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await renderer({
|
||||
request,
|
||||
uiSettingsClient,
|
||||
});
|
||||
|
||||
expect(uiSettingsClient.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls getThemeTag with the default parameters', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await renderer({
|
||||
request,
|
||||
uiSettingsClient,
|
||||
});
|
||||
|
||||
expect(getThemeTagMock).toHaveBeenCalledTimes(1);
|
||||
expect(getThemeTagMock).toHaveBeenCalledWith({
|
||||
themeVersion: 'v7',
|
||||
darkMode: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('calls getPluginsBundlePaths with the correct parameters', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await renderer({
|
||||
request,
|
||||
uiSettingsClient,
|
||||
});
|
||||
|
||||
expect(getPluginsBundlePathsMock).toHaveBeenCalledTimes(1);
|
||||
expect(getPluginsBundlePathsMock).toHaveBeenCalledWith({
|
||||
uiPlugins,
|
||||
regularBundlePath: '/base-path/42/bundles',
|
||||
});
|
||||
});
|
||||
|
||||
// here
|
||||
it('calls getJsDependencyPaths with the correct parameters', async () => {
|
||||
const pluginsBundlePaths = new Map<string, unknown>();
|
||||
|
||||
getPluginsBundlePathsMock.mockReturnValue(pluginsBundlePaths);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await renderer({
|
||||
request,
|
||||
uiSettingsClient,
|
||||
});
|
||||
|
||||
expect(getJsDependencyPathsMock).toHaveBeenCalledTimes(1);
|
||||
expect(getJsDependencyPathsMock).toHaveBeenCalledWith(
|
||||
'/base-path/42/bundles',
|
||||
pluginsBundlePaths
|
||||
);
|
||||
});
|
||||
|
||||
it('calls renderTemplate with the correct parameters', async () => {
|
||||
getThemeTagMock.mockReturnValue('customThemeTag');
|
||||
getJsDependencyPathsMock.mockReturnValue(['path-1', 'path-2']);
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await renderer({
|
||||
request,
|
||||
uiSettingsClient,
|
||||
});
|
||||
|
||||
expect(renderTemplateMock).toHaveBeenCalledTimes(1);
|
||||
expect(renderTemplateMock).toHaveBeenCalledWith({
|
||||
themeTag: 'customThemeTag',
|
||||
jsDependencyPaths: ['path-1', 'path-2'],
|
||||
publicPathMap: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
102
src/core/server/rendering/bootstrap/bootstrap_renderer.ts
Normal file
102
src/core/server/rendering/bootstrap/bootstrap_renderer.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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 { createHash } from 'crypto';
|
||||
import { PackageInfo } from '@kbn/config';
|
||||
import { UiPlugins } from '../../plugins';
|
||||
import { IUiSettingsClient } from '../../ui_settings';
|
||||
import { HttpAuth, KibanaRequest } from '../../http';
|
||||
import { getPluginsBundlePaths } from './get_plugin_bundle_paths';
|
||||
import { getJsDependencyPaths } from './get_js_dependency_paths';
|
||||
import { getThemeTag } from './get_theme_tag';
|
||||
import { renderTemplate } from './render_template';
|
||||
|
||||
export type BootstrapRendererFactory = (factoryOptions: FactoryOptions) => BootstrapRenderer;
|
||||
export type BootstrapRenderer = (options: RenderedOptions) => Promise<RendererResult>;
|
||||
|
||||
interface FactoryOptions {
|
||||
serverBasePath: string;
|
||||
packageInfo: PackageInfo;
|
||||
uiPlugins: UiPlugins;
|
||||
auth: HttpAuth;
|
||||
}
|
||||
|
||||
interface RenderedOptions {
|
||||
request: KibanaRequest;
|
||||
uiSettingsClient: IUiSettingsClient;
|
||||
}
|
||||
|
||||
interface RendererResult {
|
||||
body: string;
|
||||
etag: string;
|
||||
}
|
||||
|
||||
export const bootstrapRendererFactory: BootstrapRendererFactory = ({
|
||||
packageInfo,
|
||||
serverBasePath,
|
||||
uiPlugins,
|
||||
auth,
|
||||
}) => {
|
||||
const isAuthenticated = (request: KibanaRequest) => {
|
||||
const { status: authStatus } = auth.get(request);
|
||||
// status is 'unknown' when auth is disabled. we just need to not be `unauthenticated` here.
|
||||
return authStatus !== 'unauthenticated';
|
||||
};
|
||||
|
||||
return async function bootstrapRenderer({ uiSettingsClient, request }) {
|
||||
let darkMode = false;
|
||||
let themeVersion = 'v7';
|
||||
|
||||
try {
|
||||
const authenticated = isAuthenticated(request);
|
||||
darkMode = authenticated ? await uiSettingsClient.get('theme:darkMode') : false;
|
||||
themeVersion = authenticated ? await uiSettingsClient.get('theme:version') : 'v7';
|
||||
} catch (e) {
|
||||
// just use the default values in case of connectivity issues with ES
|
||||
}
|
||||
|
||||
const themeTag = getThemeTag({
|
||||
themeVersion,
|
||||
darkMode,
|
||||
});
|
||||
const buildHash = packageInfo.buildNum;
|
||||
const regularBundlePath = `${serverBasePath}/${buildHash}/bundles`;
|
||||
|
||||
const bundlePaths = getPluginsBundlePaths({
|
||||
uiPlugins,
|
||||
regularBundlePath,
|
||||
});
|
||||
|
||||
const jsDependencyPaths = getJsDependencyPaths(regularBundlePath, bundlePaths);
|
||||
|
||||
// These paths should align with the bundle routes configured in
|
||||
// src/optimize/bundles_route/bundles_route.ts
|
||||
const publicPathMap = JSON.stringify({
|
||||
core: `${regularBundlePath}/core/`,
|
||||
'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`,
|
||||
...Object.fromEntries(
|
||||
[...bundlePaths.entries()].map(([pluginId, plugin]) => [pluginId, plugin.publicPath])
|
||||
),
|
||||
});
|
||||
|
||||
const body = renderTemplate({
|
||||
themeTag,
|
||||
jsDependencyPaths,
|
||||
publicPathMap,
|
||||
});
|
||||
|
||||
const hash = createHash('sha1');
|
||||
hash.update(body);
|
||||
const etag = hash.digest('hex');
|
||||
|
||||
return {
|
||||
body,
|
||||
etag,
|
||||
};
|
||||
};
|
||||
};
|
|
@ -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 { getJsDependencyPaths } from './get_js_dependency_paths';
|
||||
import type { PluginInfo } from './get_plugin_bundle_paths';
|
||||
|
||||
describe('getJsDependencyPaths', () => {
|
||||
it('returns the correct list of paths', () => {
|
||||
const bundlePaths = new Map<string, PluginInfo>();
|
||||
bundlePaths.set('plugin1', {
|
||||
bundlePath: 'plugin1/bundle-path.js',
|
||||
publicPath: 'plugin1/public-path',
|
||||
});
|
||||
bundlePaths.set('plugin2', {
|
||||
bundlePath: 'plugin2/bundle-path.js',
|
||||
publicPath: 'plugin2/public-path',
|
||||
});
|
||||
|
||||
expect(getJsDependencyPaths('/regular-bundle-path', bundlePaths)).toEqual([
|
||||
'/regular-bundle-path/kbn-ui-shared-deps/kbn-ui-shared-deps.@elastic.js',
|
||||
'/regular-bundle-path/kbn-ui-shared-deps/kbn-ui-shared-deps.js',
|
||||
'/regular-bundle-path/core/core.entry.js',
|
||||
'plugin1/bundle-path.js',
|
||||
'plugin2/bundle-path.js',
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 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 * as UiSharedDeps from '@kbn/ui-shared-deps';
|
||||
import type { PluginInfo } from './get_plugin_bundle_paths';
|
||||
|
||||
export const getJsDependencyPaths = (
|
||||
regularBundlePath: string,
|
||||
bundlePaths: Map<string, PluginInfo>
|
||||
) => {
|
||||
return [
|
||||
...UiSharedDeps.jsDepFilenames.map(
|
||||
(filename) => `${regularBundlePath}/kbn-ui-shared-deps/${filename}`
|
||||
),
|
||||
`${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`,
|
||||
`${regularBundlePath}/core/core.entry.js`,
|
||||
...[...bundlePaths.values()].map((plugin) => plugin.bundlePath),
|
||||
];
|
||||
};
|
|
@ -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 { UiPlugins } from '../../plugins';
|
||||
import { getPluginsBundlePaths } from './get_plugin_bundle_paths';
|
||||
|
||||
const createUiPlugins = (pluginDeps: Record<string, string[]>) => {
|
||||
const uiPlugins: UiPlugins = {
|
||||
internal: new Map(),
|
||||
public: new Map(),
|
||||
browserConfigs: new Map(),
|
||||
};
|
||||
|
||||
Object.entries(pluginDeps).forEach(([pluginId, deps]) => {
|
||||
uiPlugins.internal.set(pluginId, {
|
||||
requiredBundles: deps,
|
||||
publicTargetDir: '',
|
||||
publicAssetsDir: '',
|
||||
} as any);
|
||||
uiPlugins.public.set(pluginId, {
|
||||
id: pluginId,
|
||||
configPath: 'config-path',
|
||||
optionalPlugins: [],
|
||||
requiredPlugins: [],
|
||||
requiredBundles: deps,
|
||||
});
|
||||
});
|
||||
|
||||
return uiPlugins;
|
||||
};
|
||||
|
||||
describe('getPluginsBundlePaths', () => {
|
||||
it('returns an entry for each plugin and their bundle dependencies', () => {
|
||||
const pluginBundlePaths = getPluginsBundlePaths({
|
||||
regularBundlePath: '/regular-bundle-path',
|
||||
uiPlugins: createUiPlugins({
|
||||
a: ['b', 'c'],
|
||||
b: ['d'],
|
||||
}),
|
||||
});
|
||||
|
||||
expect([...pluginBundlePaths.keys()].sort()).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
|
||||
it('returns correct paths for each bundle', () => {
|
||||
const pluginBundlePaths = getPluginsBundlePaths({
|
||||
regularBundlePath: '/regular-bundle-path',
|
||||
uiPlugins: createUiPlugins({
|
||||
a: ['b'],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(pluginBundlePaths.get('a')).toEqual({
|
||||
bundlePath: '/regular-bundle-path/plugin/a/a.plugin.js',
|
||||
publicPath: '/regular-bundle-path/plugin/a/',
|
||||
});
|
||||
|
||||
expect(pluginBundlePaths.get('b')).toEqual({
|
||||
bundlePath: '/regular-bundle-path/plugin/b/b.plugin.js',
|
||||
publicPath: '/regular-bundle-path/plugin/b/',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { UiPlugins } from '../../plugins';
|
||||
|
||||
export interface PluginInfo {
|
||||
publicPath: string;
|
||||
bundlePath: string;
|
||||
}
|
||||
|
||||
export const getPluginsBundlePaths = ({
|
||||
uiPlugins,
|
||||
regularBundlePath,
|
||||
}: {
|
||||
uiPlugins: UiPlugins;
|
||||
regularBundlePath: string;
|
||||
}) => {
|
||||
const pluginBundlePaths = new Map<string, PluginInfo>();
|
||||
const pluginsToProcess = [...uiPlugins.public.keys()];
|
||||
|
||||
while (pluginsToProcess.length > 0) {
|
||||
const pluginId = pluginsToProcess.pop() as string;
|
||||
pluginBundlePaths.set(pluginId, {
|
||||
publicPath: `${regularBundlePath}/plugin/${pluginId}/`,
|
||||
bundlePath: `${regularBundlePath}/plugin/${pluginId}/${pluginId}.plugin.js`,
|
||||
});
|
||||
|
||||
const pluginBundleIds = uiPlugins.internal.get(pluginId)?.requiredBundles ?? [];
|
||||
pluginBundleIds.forEach((bundleId) => {
|
||||
if (!pluginBundlePaths.has(bundleId)) {
|
||||
pluginsToProcess.push(bundleId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return pluginBundlePaths;
|
||||
};
|
44
src/core/server/rendering/bootstrap/get_theme_tag.test.ts
Normal file
44
src/core/server/rendering/bootstrap/get_theme_tag.test.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { getThemeTag } from './get_theme_tag';
|
||||
|
||||
describe('getThemeTag', () => {
|
||||
it('returns the correct value for version:v7 and darkMode:false', () => {
|
||||
expect(
|
||||
getThemeTag({
|
||||
themeVersion: 'v7',
|
||||
darkMode: false,
|
||||
})
|
||||
).toEqual('v7light');
|
||||
});
|
||||
it('returns the correct value for version:v7 and darkMode:true', () => {
|
||||
expect(
|
||||
getThemeTag({
|
||||
themeVersion: 'v7',
|
||||
darkMode: true,
|
||||
})
|
||||
).toEqual('v7dark');
|
||||
});
|
||||
it('returns the correct value for version:v8 and darkMode:false', () => {
|
||||
expect(
|
||||
getThemeTag({
|
||||
themeVersion: 'v8',
|
||||
darkMode: false,
|
||||
})
|
||||
).toEqual('v8light');
|
||||
});
|
||||
it('returns the correct value for version:v8 and darkMode:true', () => {
|
||||
expect(
|
||||
getThemeTag({
|
||||
themeVersion: 'v8',
|
||||
darkMode: true,
|
||||
})
|
||||
).toEqual('v8dark');
|
||||
});
|
||||
});
|
|
@ -6,8 +6,16 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/src/legacy/ui'],
|
||||
/**
|
||||
* Computes the themeTag that will be used on the client-side as `__kbnThemeTag__`
|
||||
* @see `packages/kbn-ui-shared-deps/theme.ts`
|
||||
*/
|
||||
export const getThemeTag = ({
|
||||
themeVersion,
|
||||
darkMode,
|
||||
}: {
|
||||
themeVersion: string;
|
||||
darkMode: boolean;
|
||||
}) => {
|
||||
return `${themeVersion === 'v7' ? 'v7' : 'v8'}${darkMode ? 'dark' : 'light'}`;
|
||||
};
|
|
@ -6,8 +6,5 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { uiRenderMixin } from './ui_render';
|
||||
|
||||
export async function uiMixin(kbnServer) {
|
||||
await kbnServer.mixin(uiRenderMixin);
|
||||
}
|
||||
export { registerBootstrapRoute } from './register_bootstrap_route';
|
||||
export { bootstrapRendererFactory } from './bootstrap_renderer';
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { IRouter } from '../../http';
|
||||
import type { BootstrapRenderer } from './bootstrap_renderer';
|
||||
|
||||
export const registerBootstrapRoute = ({
|
||||
router,
|
||||
renderer,
|
||||
}: {
|
||||
router: IRouter;
|
||||
renderer: BootstrapRenderer;
|
||||
}) => {
|
||||
router.get(
|
||||
{
|
||||
path: '/bootstrap.js',
|
||||
options: {
|
||||
authRequired: 'try',
|
||||
tags: ['api'],
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
const uiSettingsClient = ctx.core.uiSettings.client;
|
||||
const { body, etag } = await renderer({ uiSettingsClient, request: req });
|
||||
|
||||
return res.ok({
|
||||
body,
|
||||
headers: {
|
||||
etag,
|
||||
'content-type': 'application/javascript',
|
||||
'cache-control': 'must-revalidate',
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
30
src/core/server/rendering/bootstrap/render_template.test.ts
Normal file
30
src/core/server/rendering/bootstrap/render_template.test.ts
Normal file
|
@ -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 { renderTemplate } from './render_template';
|
||||
|
||||
function mockParams() {
|
||||
return {
|
||||
themeTag: 'v7',
|
||||
jsDependencyPaths: ['/js-1', '/js-2'],
|
||||
styleSheetPaths: ['/style-1', '/style-2'],
|
||||
publicPathMap: '{"foo": "bar"}',
|
||||
};
|
||||
}
|
||||
|
||||
describe('renderTemplate', () => {
|
||||
test('resolves to a string', async () => {
|
||||
const content = await renderTemplate(mockParams());
|
||||
expect(typeof content).toEqual('string');
|
||||
});
|
||||
|
||||
test('interpolates templateData into string template', async () => {
|
||||
const content = await renderTemplate(mockParams());
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
});
|
131
src/core/server/rendering/bootstrap/render_template.ts
Normal file
131
src/core/server/rendering/bootstrap/render_template.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface BootstrapTemplateData {
|
||||
themeTag: string;
|
||||
jsDependencyPaths: string[];
|
||||
publicPathMap: string;
|
||||
}
|
||||
|
||||
export const renderTemplate = ({
|
||||
themeTag,
|
||||
jsDependencyPaths,
|
||||
publicPathMap,
|
||||
}: BootstrapTemplateData) => {
|
||||
return `
|
||||
function kbnBundlesLoader() {
|
||||
var modules = {};
|
||||
|
||||
function has(prop) {
|
||||
return Object.prototype.hasOwnProperty.call(modules, prop);
|
||||
}
|
||||
|
||||
function define(key, bundleRequire, bundleModuleKey) {
|
||||
if (has(key)) {
|
||||
throw new Error('__kbnBundles__ already has a module defined for "' + key + '"');
|
||||
}
|
||||
|
||||
modules[key] = {
|
||||
bundleRequire,
|
||||
bundleModuleKey,
|
||||
};
|
||||
}
|
||||
|
||||
function get(key) {
|
||||
if (!has(key)) {
|
||||
throw new Error('__kbnBundles__ does not have a module defined for "' + key + '"');
|
||||
}
|
||||
|
||||
return modules[key].bundleRequire(modules[key].bundleModuleKey);
|
||||
}
|
||||
|
||||
return { has: has, define: define, get: get };
|
||||
}
|
||||
|
||||
var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data'));
|
||||
window.__kbnStrictCsp__ = kbnCsp.strictCsp;
|
||||
window.__kbnThemeTag__ = "${themeTag}";
|
||||
window.__kbnPublicPath__ = ${publicPathMap};
|
||||
window.__kbnBundles__ = kbnBundlesLoader();
|
||||
|
||||
if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) {
|
||||
var legacyBrowserError = document.getElementById('kbn_legacy_browser_error');
|
||||
legacyBrowserError.style.display = 'flex';
|
||||
} else {
|
||||
if (!window.__kbnCspNotEnforced__ && window.console) {
|
||||
window.console.log("^ A single error about an inline script not firing due to content security policy is expected!");
|
||||
}
|
||||
var loadingMessage = document.getElementById('kbn_loading_message');
|
||||
loadingMessage.style.display = 'flex';
|
||||
|
||||
window.onload = function () {
|
||||
function failure() {
|
||||
// make subsequent calls to failure() noop
|
||||
failure = function () {};
|
||||
|
||||
var err = document.createElement('h1');
|
||||
err.style['color'] = 'white';
|
||||
err.style['font-family'] = 'monospace';
|
||||
err.style['text-align'] = 'center';
|
||||
err.style['background'] = '#F44336';
|
||||
err.style['padding'] = '25px';
|
||||
err.innerText = document.querySelector('[data-error-message]').dataset.errorMessage;
|
||||
|
||||
document.body.innerHTML = err.outerHTML;
|
||||
}
|
||||
|
||||
var stylesheetTarget = document.querySelector('head meta[name="add-styles-here"]')
|
||||
function loadStyleSheet(url, cb) {
|
||||
var dom = document.createElement('link');
|
||||
dom.rel = 'stylesheet';
|
||||
dom.type = 'text/css';
|
||||
dom.href = url;
|
||||
dom.addEventListener('error', failure);
|
||||
dom.addEventListener('load', cb);
|
||||
document.head.insertBefore(dom, stylesheetTarget);
|
||||
}
|
||||
|
||||
var scriptsTarget = document.querySelector('head meta[name="add-scripts-here"]')
|
||||
function loadScript(url, cb) {
|
||||
var dom = document.createElement('script');
|
||||
dom.async = false;
|
||||
dom.src = url;
|
||||
dom.addEventListener('error', failure);
|
||||
dom.addEventListener('load', cb);
|
||||
document.head.insertBefore(dom, scriptsTarget);
|
||||
}
|
||||
|
||||
function load(urls, cb) {
|
||||
var pending = urls.length;
|
||||
urls.forEach(function (url) {
|
||||
var innerCb = function () {
|
||||
pending = pending - 1;
|
||||
if (pending === 0 && typeof cb === 'function') {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof url !== 'string') {
|
||||
load(url, innerCb);
|
||||
} else if (url.slice(-4) === '.css') {
|
||||
loadStyleSheet(url, innerCb);
|
||||
} else {
|
||||
loadScript(url, innerCb);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
load([
|
||||
${jsDependencyPaths.map((path) => `'${path}'`).join(',')}
|
||||
], function () {
|
||||
__kbnBundles__.get('entry/core/public').__kbnBootstrap__();
|
||||
});
|
||||
}
|
||||
}
|
||||
`;
|
||||
};
|
92
src/core/server/rendering/render_utils.test.ts
Normal file
92
src/core/server/rendering/render_utils.test.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { getStylesheetPaths } from './render_utils';
|
||||
|
||||
describe('getStylesheetPaths', () => {
|
||||
describe('when darkMode is `true`', () => {
|
||||
describe('when themeVersion is `v7`', () => {
|
||||
it('returns the correct list', () => {
|
||||
expect(
|
||||
getStylesheetPaths({
|
||||
darkMode: true,
|
||||
themeVersion: 'v7',
|
||||
basePath: '/base-path',
|
||||
buildNum: 9000,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/base-path/9000/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.css",
|
||||
"/base-path/9000/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.v7.dark.css",
|
||||
"/base-path/node_modules/@kbn/ui-framework/dist/kui_dark.css",
|
||||
"/base-path/ui/legacy_dark_theme.css",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
describe('when themeVersion is `v8`', () => {
|
||||
it('returns the correct list', () => {
|
||||
expect(
|
||||
getStylesheetPaths({
|
||||
darkMode: true,
|
||||
themeVersion: 'v8',
|
||||
basePath: '/base-path',
|
||||
buildNum: 17,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/base-path/17/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.css",
|
||||
"/base-path/17/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.v8.dark.css",
|
||||
"/base-path/node_modules/@kbn/ui-framework/dist/kui_dark.css",
|
||||
"/base-path/ui/legacy_dark_theme.css",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when darkMode is `false`', () => {
|
||||
describe('when themeVersion is `v7`', () => {
|
||||
it('returns the correct list', () => {
|
||||
expect(
|
||||
getStylesheetPaths({
|
||||
darkMode: false,
|
||||
themeVersion: 'v7',
|
||||
basePath: '/base-path',
|
||||
buildNum: 42,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/base-path/42/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.css",
|
||||
"/base-path/42/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.v7.light.css",
|
||||
"/base-path/node_modules/@kbn/ui-framework/dist/kui_light.css",
|
||||
"/base-path/ui/legacy_light_theme.css",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
describe('when themeVersion is `v8`', () => {
|
||||
it('returns the correct list', () => {
|
||||
expect(
|
||||
getStylesheetPaths({
|
||||
darkMode: false,
|
||||
themeVersion: 'v8',
|
||||
basePath: '/base-path',
|
||||
buildNum: 69,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/base-path/69/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.css",
|
||||
"/base-path/69/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.v8.light.css",
|
||||
"/base-path/node_modules/@kbn/ui-framework/dist/kui_light.css",
|
||||
"/base-path/ui/legacy_light_theme.css",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
54
src/core/server/rendering/render_utils.ts
Normal file
54
src/core/server/rendering/render_utils.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 * as UiSharedDeps from '@kbn/ui-shared-deps';
|
||||
import { PublicUiSettingsParams, UserProvidedValues } from '../ui_settings';
|
||||
|
||||
export const getSettingValue = <T>(
|
||||
settingName: string,
|
||||
settings: {
|
||||
user?: Record<string, UserProvidedValues<unknown>>;
|
||||
defaults: Readonly<Record<string, PublicUiSettingsParams>>;
|
||||
},
|
||||
convert: (raw: unknown) => T
|
||||
): T => {
|
||||
const value = settings.user?.[settingName]?.userValue ?? settings.defaults[settingName].value;
|
||||
return convert(value);
|
||||
};
|
||||
|
||||
export const getStylesheetPaths = ({
|
||||
themeVersion,
|
||||
darkMode,
|
||||
basePath,
|
||||
buildNum,
|
||||
}: {
|
||||
themeVersion: string;
|
||||
darkMode: boolean;
|
||||
buildNum: number;
|
||||
basePath: string;
|
||||
}) => {
|
||||
const regularBundlePath = `${basePath}/${buildNum}/bundles`;
|
||||
return [
|
||||
`${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`,
|
||||
...(darkMode
|
||||
? [
|
||||
themeVersion === 'v7'
|
||||
? `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`
|
||||
: `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkV8CssDistFilename}`,
|
||||
`${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`,
|
||||
`${basePath}/ui/legacy_dark_theme.css`,
|
||||
]
|
||||
: [
|
||||
themeVersion === 'v7'
|
||||
? `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`
|
||||
: `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightV8CssDistFilename}`,
|
||||
`${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`,
|
||||
`${basePath}/ui/legacy_light_theme.css`,
|
||||
]),
|
||||
];
|
||||
};
|
24
src/core/server/rendering/rendering_service.test.mocks.ts
Normal file
24
src/core/server/rendering/rendering_service.test.mocks.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 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.
|
||||
*/
|
||||
|
||||
export const bootstrapRendererMock = jest.fn();
|
||||
export const registerBootstrapRouteMock = jest.fn();
|
||||
export const bootstrapRendererFactoryMock = jest.fn(() => bootstrapRendererMock);
|
||||
|
||||
jest.doMock('./bootstrap', () => ({
|
||||
registerBootstrapRoute: registerBootstrapRouteMock,
|
||||
bootstrapRendererFactory: bootstrapRendererFactoryMock,
|
||||
}));
|
||||
|
||||
export const getSettingValueMock = jest.fn();
|
||||
export const getStylesheetPathsMock = jest.fn();
|
||||
|
||||
jest.doMock('./render_utils', () => ({
|
||||
getSettingValue: getSettingValueMock,
|
||||
getStylesheetPaths: getStylesheetPathsMock,
|
||||
}));
|
|
@ -6,6 +6,13 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
registerBootstrapRouteMock,
|
||||
bootstrapRendererMock,
|
||||
getSettingValueMock,
|
||||
getStylesheetPathsMock,
|
||||
} from './rendering_service.test.mocks';
|
||||
|
||||
import { load } from 'cheerio';
|
||||
|
||||
import { httpServerMock } from '../http/http_server.mocks';
|
||||
|
@ -42,9 +49,22 @@ describe('RenderingService', () => {
|
|||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
service = new RenderingService(mockRenderingServiceParams);
|
||||
|
||||
getSettingValueMock.mockImplementation((settingName: string) => settingName);
|
||||
getStylesheetPathsMock.mockReturnValue(['/style-1.css', '/style-2.css']);
|
||||
});
|
||||
|
||||
describe('setup()', () => {
|
||||
it('calls `registerBootstrapRoute` with the correct parameters', async () => {
|
||||
await service.setup(mockRenderingSetupDeps);
|
||||
|
||||
expect(registerBootstrapRouteMock).toHaveBeenCalledTimes(1);
|
||||
expect(registerBootstrapRouteMock).toHaveBeenCalledWith({
|
||||
router: expect.any(Object),
|
||||
renderer: bootstrapRendererMock,
|
||||
});
|
||||
});
|
||||
|
||||
describe('render()', () => {
|
||||
let uiSettings: ReturnType<typeof uiSettingsServiceMock.createClient>;
|
||||
let render: InternalRenderingServiceSetup['render'];
|
||||
|
@ -101,6 +121,28 @@ describe('RenderingService', () => {
|
|||
|
||||
expect(data).toMatchSnapshot(INJECTED_METADATA);
|
||||
});
|
||||
|
||||
it('calls `getStylesheetPaths` with the correct parameters', async () => {
|
||||
getSettingValueMock.mockImplementation((settingName: string) => {
|
||||
if (settingName === 'theme:darkMode') {
|
||||
return true;
|
||||
}
|
||||
if (settingName === 'theme:version') {
|
||||
return 'v8';
|
||||
}
|
||||
return settingName;
|
||||
});
|
||||
|
||||
await render(createKibanaRequest(), uiSettings);
|
||||
|
||||
expect(getStylesheetPathsMock).toHaveBeenCalledTimes(1);
|
||||
expect(getStylesheetPathsMock).toHaveBeenCalledWith({
|
||||
darkMode: true,
|
||||
themeVersion: 'v8',
|
||||
basePath: '/mock-server-basepath',
|
||||
buildNum: expect.any(Number),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,8 @@ import {
|
|||
InternalRenderingServiceSetup,
|
||||
RenderingMetadata,
|
||||
} from './types';
|
||||
import { registerBootstrapRoute, bootstrapRendererFactory } from './bootstrap';
|
||||
import { getSettingValue, getStylesheetPaths } from './render_utils';
|
||||
|
||||
/** @internal */
|
||||
export class RenderingService {
|
||||
|
@ -30,6 +32,16 @@ export class RenderingService {
|
|||
status,
|
||||
uiPlugins,
|
||||
}: RenderingSetupDeps): Promise<InternalRenderingServiceSetup> {
|
||||
const router = http.createRouter('');
|
||||
|
||||
const bootstrapRenderer = bootstrapRendererFactory({
|
||||
uiPlugins,
|
||||
serverBasePath: http.basePath.serverBasePath,
|
||||
packageInfo: this.coreContext.env.packageInfo,
|
||||
auth: http.auth,
|
||||
});
|
||||
registerBootstrapRoute({ router, renderer: bootstrapRenderer });
|
||||
|
||||
return {
|
||||
render: async (
|
||||
request,
|
||||
|
@ -40,21 +52,32 @@ export class RenderingService {
|
|||
mode: this.coreContext.env.mode,
|
||||
packageInfo: this.coreContext.env.packageInfo,
|
||||
};
|
||||
const buildNum = env.packageInfo.buildNum;
|
||||
const basePath = http.basePath.get(request);
|
||||
const { serverBasePath, publicBaseUrl } = http.basePath;
|
||||
const settings = {
|
||||
defaults: uiSettings.getRegistered(),
|
||||
user: includeUserSettings ? await uiSettings.getUserProvided() : {},
|
||||
};
|
||||
|
||||
const darkMode = getSettingValue('theme:darkMode', settings, Boolean);
|
||||
const themeVersion = getSettingValue('theme:version', settings, String);
|
||||
|
||||
const stylesheetPaths = getStylesheetPaths({
|
||||
darkMode,
|
||||
themeVersion,
|
||||
basePath: serverBasePath,
|
||||
buildNum,
|
||||
});
|
||||
|
||||
const metadata: RenderingMetadata = {
|
||||
strictCsp: http.csp.strict,
|
||||
uiPublicUrl: `${basePath}/ui`,
|
||||
bootstrapScriptUrl: `${basePath}/bootstrap.js`,
|
||||
i18n: i18n.translate,
|
||||
locale: i18n.getLocale(),
|
||||
darkMode: settings.user?.['theme:darkMode']?.userValue
|
||||
? Boolean(settings.user['theme:darkMode'].userValue)
|
||||
: false,
|
||||
darkMode,
|
||||
stylesheetPaths,
|
||||
injectedMetadata: {
|
||||
version: env.packageInfo.version,
|
||||
buildNumber: env.packageInfo.buildNum,
|
||||
|
@ -74,7 +97,7 @@ export class RenderingService {
|
|||
[...uiPlugins.public].map(async ([id, plugin]) => ({
|
||||
id,
|
||||
plugin,
|
||||
config: await this.getUiConfig(uiPlugins, id),
|
||||
config: await getUiConfig(uiPlugins, id),
|
||||
}))
|
||||
),
|
||||
legacyMetadata: {
|
||||
|
@ -89,10 +112,9 @@ export class RenderingService {
|
|||
}
|
||||
|
||||
public async stop() {}
|
||||
|
||||
private async getUiConfig(uiPlugins: UiPlugins, pluginId: string) {
|
||||
const browserConfig = uiPlugins.browserConfigs.get(pluginId);
|
||||
|
||||
return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record<string, any>;
|
||||
}
|
||||
}
|
||||
|
||||
const getUiConfig = async (uiPlugins: UiPlugins, pluginId: string) => {
|
||||
const browserConfig = uiPlugins.browserConfigs.get(pluginId);
|
||||
return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record<string, any>;
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@ export interface RenderingMetadata {
|
|||
i18n: typeof i18n.translate;
|
||||
locale: string;
|
||||
darkMode: boolean;
|
||||
stylesheetPaths: string[];
|
||||
injectedMetadata: {
|
||||
version: string;
|
||||
buildNumber: number;
|
||||
|
|
40
src/core/server/rendering/views/logo.tsx
Normal file
40
src/core/server/rendering/views/logo.tsx
Normal file
|
@ -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 React, { FC } from 'react';
|
||||
|
||||
export const Logo: FC = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<g fill="none">
|
||||
<path
|
||||
fill="#FDD009"
|
||||
d="M11.9338171,13.1522761 L19.2872353,16.5080972 L26.7065664,10.0005147 C26.8139592,9.46384495 26.866377,8.92859725 26.866377,8.36846422 C26.866377,3.78984954 23.1459864,0.0647302752 18.5719941,0.0647302752 C15.8357526,0.0647302752 13.2836129,1.41337248 11.7323847,3.67480826 L10.4983628,10.0839872 L11.9338171,13.1522761 Z"
|
||||
/>
|
||||
<path
|
||||
fill="#23BAB1"
|
||||
d="M4.32214501,20.9473399 C4.21475229,21.4841518 4.1596354,22.0410142 4.1596354,22.6044179 C4.1596354,27.1948353 7.89096419,30.9300509 12.4774572,30.9300509 C15.2361432,30.9300509 17.8007837,29.5687528 19.3495969,27.2841381 L20.5743853,20.8965739 L18.9399136,17.7698399 L11.5573744,14.401505 L4.32214501,20.9473399 Z"
|
||||
/>
|
||||
<path
|
||||
fill="#EE5097"
|
||||
d="M4.27553714,8.20847294 L9.31503995,9.3995555 L10.4190826,3.6639867 C9.73040545,3.1371289 8.88035513,2.84874358 8.00601361,2.84874358 C5.81596922,2.84874358 4.0348979,4.63252339 4.0348979,6.82484908 C4.0348979,7.30904633 4.11572655,7.77333532 4.27553714,8.20847294"
|
||||
/>
|
||||
<path
|
||||
fill="#17A7E0"
|
||||
d="M3.83806807,9.40996468 C1.58651435,10.1568087 0.0210807931,12.3172812 0.0210807931,14.6937583 C0.0210807931,17.0078087 1.45071086,19.0741436 3.5965765,19.8918041 L10.6668813,13.494428 L9.36879313,10.717795 L3.83806807,9.40996468 Z"
|
||||
/>
|
||||
<path
|
||||
fill="#92C73D"
|
||||
d="M20.6421734,27.2838537 C21.3334075,27.8156885 22.1793383,28.1057803 23.0428837,28.1057803 C25.232786,28.1057803 27.0138574,26.3228537 27.0138574,24.130528 C27.0138574,23.6470417 26.9331708,23.1827528 26.7732181,22.7477573 L21.7379769,21.5681931 L20.6421734,27.2838537 Z"
|
||||
/>
|
||||
<path
|
||||
fill="#0678A0"
|
||||
d="M21.6667227,20.2469532 L27.2099485,21.5446872 C29.4623545,20.7995495 31.0277881,18.6382239 31.0277881,16.2608936 C31.0277881,13.9511092 29.5947487,11.8871917 27.4447635,11.0719486 L20.1946185,17.4303615 L21.6667227,20.2469532 Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
|
@ -8,15 +8,25 @@
|
|||
|
||||
/* eslint-disable react/no-danger */
|
||||
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
import { RenderingMetadata } from '../types';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
interface Props {
|
||||
darkMode: RenderingMetadata['darkMode'];
|
||||
darkMode: boolean;
|
||||
stylesheetPaths: string[];
|
||||
}
|
||||
|
||||
export const Styles: FunctionComponent<Props> = ({ darkMode }) => {
|
||||
export const Styles: FC<Props> = ({ darkMode, stylesheetPaths }) => {
|
||||
return (
|
||||
<>
|
||||
<InlineStyles darkMode={darkMode} />
|
||||
{stylesheetPaths.map((path) => (
|
||||
<link key={path} rel="stylesheet" type="text/css" href={path} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const InlineStyles: FC<{ darkMode: boolean }> = ({ darkMode }) => {
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
|
|
|
@ -11,6 +11,7 @@ import React, { FunctionComponent, createElement } from 'react';
|
|||
import { RenderingMetadata } from '../types';
|
||||
import { Fonts } from './fonts';
|
||||
import { Styles } from './styles';
|
||||
import { Logo } from './logo';
|
||||
|
||||
interface Props {
|
||||
metadata: RenderingMetadata;
|
||||
|
@ -21,42 +22,13 @@ export const Template: FunctionComponent<Props> = ({
|
|||
uiPublicUrl,
|
||||
locale,
|
||||
darkMode,
|
||||
stylesheetPaths,
|
||||
injectedMetadata,
|
||||
i18n,
|
||||
bootstrapScriptUrl,
|
||||
strictCsp,
|
||||
},
|
||||
}) => {
|
||||
const logo = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<g fill="none">
|
||||
<path
|
||||
fill="#FDD009"
|
||||
d="M11.9338171,13.1522761 L19.2872353,16.5080972 L26.7065664,10.0005147 C26.8139592,9.46384495 26.866377,8.92859725 26.866377,8.36846422 C26.866377,3.78984954 23.1459864,0.0647302752 18.5719941,0.0647302752 C15.8357526,0.0647302752 13.2836129,1.41337248 11.7323847,3.67480826 L10.4983628,10.0839872 L11.9338171,13.1522761 Z"
|
||||
/>
|
||||
<path
|
||||
fill="#23BAB1"
|
||||
d="M4.32214501,20.9473399 C4.21475229,21.4841518 4.1596354,22.0410142 4.1596354,22.6044179 C4.1596354,27.1948353 7.89096419,30.9300509 12.4774572,30.9300509 C15.2361432,30.9300509 17.8007837,29.5687528 19.3495969,27.2841381 L20.5743853,20.8965739 L18.9399136,17.7698399 L11.5573744,14.401505 L4.32214501,20.9473399 Z"
|
||||
/>
|
||||
<path
|
||||
fill="#EE5097"
|
||||
d="M4.27553714,8.20847294 L9.31503995,9.3995555 L10.4190826,3.6639867 C9.73040545,3.1371289 8.88035513,2.84874358 8.00601361,2.84874358 C5.81596922,2.84874358 4.0348979,4.63252339 4.0348979,6.82484908 C4.0348979,7.30904633 4.11572655,7.77333532 4.27553714,8.20847294"
|
||||
/>
|
||||
<path
|
||||
fill="#17A7E0"
|
||||
d="M3.83806807,9.40996468 C1.58651435,10.1568087 0.0210807931,12.3172812 0.0210807931,14.6937583 C0.0210807931,17.0078087 1.45071086,19.0741436 3.5965765,19.8918041 L10.6668813,13.494428 L9.36879313,10.717795 L3.83806807,9.40996468 Z"
|
||||
/>
|
||||
<path
|
||||
fill="#92C73D"
|
||||
d="M20.6421734,27.2838537 C21.3334075,27.8156885 22.1793383,28.1057803 23.0428837,28.1057803 C25.232786,28.1057803 27.0138574,26.3228537 27.0138574,24.130528 C27.0138574,23.6470417 26.9331708,23.1827528 26.7732181,22.7477573 L21.7379769,21.5681931 L20.6421734,27.2838537 Z"
|
||||
/>
|
||||
<path
|
||||
fill="#0678A0"
|
||||
d="M21.6667227,20.2469532 L27.2099485,21.5446872 C29.4623545,20.7995495 31.0277881,18.6382239 31.0277881,16.2608936 C31.0277881,13.9511092 29.5947487,11.8871917 27.4447635,11.0719486 L20.1946185,17.4303615 L21.6667227,20.2469532 Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<head>
|
||||
|
@ -70,7 +42,7 @@ export const Template: FunctionComponent<Props> = ({
|
|||
<link rel="icon" type="image/svg+xml" href={`${uiPublicUrl}/favicons/favicon.svg`} />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<Styles darkMode={darkMode} />
|
||||
<Styles darkMode={darkMode} stylesheetPaths={stylesheetPaths} />
|
||||
|
||||
{/* Inject stylesheets into the <head> before scripts so that KP plugins with bundled styles will override them */}
|
||||
<meta name="add-styles-here" />
|
||||
|
@ -88,7 +60,7 @@ export const Template: FunctionComponent<Props> = ({
|
|||
data-test-subj="kbnLoadingMessage"
|
||||
>
|
||||
<div className="kbnLoaderWrap">
|
||||
{logo}
|
||||
<Logo />
|
||||
<div
|
||||
className="kbnWelcomeText"
|
||||
data-error-message={i18n('core.ui.welcomeErrorMessage', {
|
||||
|
@ -103,7 +75,7 @@ export const Template: FunctionComponent<Props> = ({
|
|||
</div>
|
||||
|
||||
<div className="kbnWelcomeView" id="kbn_legacy_browser_error" style={{ display: 'none' }}>
|
||||
{logo}
|
||||
<Logo />
|
||||
|
||||
<h2 className="kbnWelcomeTitle">
|
||||
{i18n('core.ui.legacyBrowserTitle', {
|
||||
|
|
|
@ -1966,7 +1966,7 @@ export interface RouteConfig<P, Q, B, Method extends RouteMethod> {
|
|||
|
||||
// @public
|
||||
export interface RouteConfigOptions<Method extends RouteMethod> {
|
||||
authRequired?: boolean | 'optional';
|
||||
authRequired?: boolean | 'optional' | 'try';
|
||||
body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody;
|
||||
tags?: readonly string[];
|
||||
timeout?: {
|
||||
|
|
|
@ -18,7 +18,6 @@ import { loggingMixin } from './logging';
|
|||
import warningsMixin from './warnings';
|
||||
import configCompleteMixin from './config/complete';
|
||||
import { optimizeMixin } from '../../optimize';
|
||||
import { uiMixin } from '../ui';
|
||||
|
||||
/**
|
||||
* @typedef {import('./kbn_server').KibanaConfig} KibanaConfig
|
||||
|
@ -72,8 +71,6 @@ export default class KbnServer {
|
|||
// tell the config we are done loading plugins
|
||||
configCompleteMixin,
|
||||
|
||||
uiMixin,
|
||||
|
||||
// setup routes that serve the @kbn/optimizer output
|
||||
optimizeMixin
|
||||
)
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* 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 Handlebars from 'handlebars';
|
||||
import { createHash } from 'crypto';
|
||||
import { readFile } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
import { kbnBundlesLoaderSource } from './kbn_bundles_loader_source';
|
||||
|
||||
export class AppBootstrap {
|
||||
constructor({ templateData }) {
|
||||
this.templateData = { ...templateData, kbnBundlesLoaderSource };
|
||||
this._rawTemplate = undefined;
|
||||
}
|
||||
|
||||
async getJsFile() {
|
||||
if (!this._rawTemplate) {
|
||||
this._rawTemplate = await loadRawTemplate();
|
||||
}
|
||||
|
||||
const template = Handlebars.compile(this._rawTemplate, {
|
||||
knownHelpersOnly: true,
|
||||
noEscape: true, // this is a js file, so html escaping isn't appropriate
|
||||
strict: true,
|
||||
});
|
||||
|
||||
return template(this.templateData);
|
||||
}
|
||||
|
||||
async getJsFileHash() {
|
||||
const fileContents = await this.getJsFile();
|
||||
const hash = createHash('sha1');
|
||||
hash.update(fileContents);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
}
|
||||
|
||||
function loadRawTemplate() {
|
||||
const templatePath = resolve(__dirname, 'template.js.hbs');
|
||||
return readFileAsync(templatePath);
|
||||
}
|
||||
|
||||
function readFileAsync(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
readFile(filePath, 'utf8', (err, fileContents) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(fileContents);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const mockTemplate = `
|
||||
{{appId}}
|
||||
{{regularBundlePath}}
|
||||
`;
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
readFile: jest.fn().mockImplementation((path, encoding, cb) => cb(null, mockTemplate)),
|
||||
}));
|
||||
|
||||
import { AppBootstrap } from './app_bootstrap';
|
||||
|
||||
describe('ui_render/AppBootstrap', () => {
|
||||
describe('getJsFile()', () => {
|
||||
test('resolves to a string', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const bootstrap = new AppBootstrap(mockConfig());
|
||||
const contents = await bootstrap.getJsFile();
|
||||
|
||||
expect(typeof contents).toEqual('string');
|
||||
});
|
||||
|
||||
test('interpolates templateData into string template', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const bootstrap = new AppBootstrap(mockConfig());
|
||||
const contents = await bootstrap.getJsFile();
|
||||
|
||||
expect(contents).toContain('123');
|
||||
expect(contents).toContain('/foo/bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getJsFileHash()', () => {
|
||||
test('resolves to a 40 character string', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const bootstrap = new AppBootstrap(mockConfig());
|
||||
const hash = await bootstrap.getJsFileHash();
|
||||
|
||||
expect(typeof hash).toEqual('string');
|
||||
expect(hash).toHaveLength(40);
|
||||
});
|
||||
|
||||
test('resolves to the same string for multiple calls with the same config on the same bootstrap object', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const bootstrap = new AppBootstrap(mockConfig());
|
||||
const hash1 = await bootstrap.getJsFileHash();
|
||||
const hash2 = await bootstrap.getJsFileHash();
|
||||
|
||||
expect(hash2).toEqual(hash1);
|
||||
});
|
||||
|
||||
test('resolves to the same string for multiple calls with the same config on different bootstrap objects', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const bootstrap1 = new AppBootstrap(mockConfig());
|
||||
const hash1 = await bootstrap1.getJsFileHash();
|
||||
|
||||
const bootstrap2 = new AppBootstrap(mockConfig());
|
||||
const hash2 = await bootstrap2.getJsFileHash();
|
||||
|
||||
expect(hash2).toEqual(hash1);
|
||||
});
|
||||
|
||||
test('resolves to different 40 character string with different templateData', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const bootstrap1 = new AppBootstrap(mockConfig());
|
||||
const hash1 = await bootstrap1.getJsFileHash();
|
||||
|
||||
const config2 = {
|
||||
...mockConfig(),
|
||||
templateData: {
|
||||
appId: 'not123',
|
||||
regularBundlePath: 'not/foo/bar',
|
||||
},
|
||||
};
|
||||
const bootstrap2 = new AppBootstrap(config2);
|
||||
const hash2 = await bootstrap2.getJsFileHash();
|
||||
|
||||
expect(typeof hash2).toEqual('string');
|
||||
expect(hash2).toHaveLength(40);
|
||||
expect(hash2).not.toEqual(hash1);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
function mockConfig() {
|
||||
return {
|
||||
templateData: {
|
||||
appId: 123,
|
||||
regularBundlePath: '/foo/bar',
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
kbnBundlesLoaderSource: `(${kbnBundlesLoader.toString()})();`,
|
||||
};
|
||||
|
||||
function kbnBundlesLoader() {
|
||||
var modules = {};
|
||||
|
||||
function has(prop) {
|
||||
return Object.prototype.hasOwnProperty.call(modules, prop);
|
||||
}
|
||||
|
||||
function define(key, bundleRequire, bundleModuleKey) {
|
||||
if (has(key)) {
|
||||
throw new Error('__kbnBundles__ already has a module defined for "' + key + '"');
|
||||
}
|
||||
|
||||
modules[key] = {
|
||||
bundleRequire,
|
||||
bundleModuleKey,
|
||||
};
|
||||
}
|
||||
|
||||
function get(key) {
|
||||
if (!has(key)) {
|
||||
throw new Error('__kbnBundles__ does not have a module defined for "' + key + '"');
|
||||
}
|
||||
|
||||
return modules[key].bundleRequire(modules[key].bundleModuleKey);
|
||||
}
|
||||
|
||||
return { has: has, define: define, get: get };
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { uiRenderMixin } from './ui_render_mixin';
|
|
@ -1,165 +0,0 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import * as UiSharedDeps from '@kbn/ui-shared-deps';
|
||||
import { KibanaRequest } from '../../../core/server';
|
||||
import { AppBootstrap } from './bootstrap';
|
||||
import { getApmConfig } from '../apm';
|
||||
|
||||
/**
|
||||
* @typedef {import('../../server/kbn_server').default} KbnServer
|
||||
* @typedef {import('../../server/kbn_server').ResponseToolkit} ResponseToolkit
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {KbnServer} kbnServer
|
||||
* @param {KbnServer['server']} server
|
||||
* @param {KbnServer['config']} config
|
||||
*/
|
||||
export function uiRenderMixin(kbnServer, server, config) {
|
||||
const authEnabled = !!server.auth.settings.default;
|
||||
server.route({
|
||||
path: '/bootstrap.js',
|
||||
method: 'GET',
|
||||
config: {
|
||||
tags: ['api'],
|
||||
auth: authEnabled ? { mode: 'try' } : false,
|
||||
},
|
||||
async handler(request, h) {
|
||||
const soClient = kbnServer.newPlatform.start.core.savedObjects.getScopedClient(
|
||||
KibanaRequest.from(request)
|
||||
);
|
||||
const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient(soClient);
|
||||
|
||||
const darkMode =
|
||||
!authEnabled || request.auth.isAuthenticated
|
||||
? await uiSettings.get('theme:darkMode')
|
||||
: false;
|
||||
|
||||
const themeVersion =
|
||||
!authEnabled || request.auth.isAuthenticated ? await uiSettings.get('theme:version') : 'v7';
|
||||
|
||||
const themeTag = `${themeVersion === 'v7' ? 'v7' : 'v8'}${darkMode ? 'dark' : 'light'}`;
|
||||
|
||||
const buildHash = server.newPlatform.env.packageInfo.buildNum;
|
||||
const basePath = config.get('server.basePath');
|
||||
|
||||
const regularBundlePath = `${basePath}/${buildHash}/bundles`;
|
||||
|
||||
const styleSheetPaths = [
|
||||
`${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`,
|
||||
...(darkMode
|
||||
? [
|
||||
themeVersion === 'v7'
|
||||
? `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`
|
||||
: `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkV8CssDistFilename}`,
|
||||
`${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`,
|
||||
`${basePath}/ui/legacy_dark_theme.css`,
|
||||
]
|
||||
: [
|
||||
themeVersion === 'v7'
|
||||
? `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`
|
||||
: `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightV8CssDistFilename}`,
|
||||
`${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`,
|
||||
`${basePath}/ui/legacy_light_theme.css`,
|
||||
]),
|
||||
];
|
||||
|
||||
const kpUiPlugins = kbnServer.newPlatform.__internals.uiPlugins;
|
||||
const kpPluginPublicPaths = new Map();
|
||||
const kpPluginBundlePaths = new Set();
|
||||
|
||||
// recursively iterate over the kpUiPlugin ids and their required bundles
|
||||
// to populate kpPluginPublicPaths and kpPluginBundlePaths
|
||||
(function readKpPlugins(ids) {
|
||||
for (const id of ids) {
|
||||
if (kpPluginPublicPaths.has(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
kpPluginPublicPaths.set(id, `${regularBundlePath}/plugin/${id}/`);
|
||||
kpPluginBundlePaths.add(`${regularBundlePath}/plugin/${id}/${id}.plugin.js`);
|
||||
readKpPlugins(kpUiPlugins.internal.get(id).requiredBundles);
|
||||
}
|
||||
})(kpUiPlugins.public.keys());
|
||||
|
||||
const jsDependencyPaths = [
|
||||
...UiSharedDeps.jsDepFilenames.map(
|
||||
(filename) => `${regularBundlePath}/kbn-ui-shared-deps/${filename}`
|
||||
),
|
||||
`${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`,
|
||||
|
||||
`${regularBundlePath}/core/core.entry.js`,
|
||||
...kpPluginBundlePaths,
|
||||
];
|
||||
|
||||
// These paths should align with the bundle routes configured in
|
||||
// src/optimize/bundles_route/bundles_route.ts
|
||||
const publicPathMap = JSON.stringify({
|
||||
core: `${regularBundlePath}/core/`,
|
||||
'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`,
|
||||
...Object.fromEntries(kpPluginPublicPaths),
|
||||
});
|
||||
|
||||
const bootstrap = new AppBootstrap({
|
||||
templateData: {
|
||||
themeTag,
|
||||
jsDependencyPaths,
|
||||
styleSheetPaths,
|
||||
publicPathMap,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await bootstrap.getJsFile();
|
||||
const etag = await bootstrap.getJsFileHash();
|
||||
|
||||
return h
|
||||
.response(body)
|
||||
.header('cache-control', 'must-revalidate')
|
||||
.header('content-type', 'application/javascript')
|
||||
.etag(etag);
|
||||
},
|
||||
});
|
||||
|
||||
server.route({
|
||||
path: '/app/{id}/{any*}',
|
||||
method: 'GET',
|
||||
async handler(req, h) {
|
||||
try {
|
||||
return await h.renderApp();
|
||||
} catch (err) {
|
||||
throw Boom.boomify(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async function renderApp(h) {
|
||||
const { http } = kbnServer.newPlatform.setup.core;
|
||||
const { savedObjects } = kbnServer.newPlatform.start.core;
|
||||
const { rendering } = kbnServer.newPlatform.__internals;
|
||||
const req = KibanaRequest.from(h.request);
|
||||
const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient(
|
||||
savedObjects.getScopedClient(req)
|
||||
);
|
||||
const vars = {
|
||||
apmConfig: getApmConfig(h.request.path),
|
||||
};
|
||||
const content = await rendering.render(h.request, uiSettings, {
|
||||
includeUserSettings: true,
|
||||
vars,
|
||||
});
|
||||
|
||||
return h.response(content).type('text/html').header('content-security-policy', http.csp.header);
|
||||
}
|
||||
|
||||
server.decorate('toolkit', 'renderApp', function () {
|
||||
return renderApp(this);
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue