Generate legacy vars when rendering all applications (#54768) (#55079)

* Generate legacy vars when rendering all applications

* Move rendering functional tests and add user settings tests

* Make rendering integration tests more robust, get data from page

* Address review nits, fix CI failures

* Remove extraneous file

* Fix type error
This commit is contained in:
Eli Perelman 2020-01-16 13:00:20 -06:00 committed by GitHub
parent 6559224c51
commit dc1b1f504e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 368 additions and 73 deletions

View file

@ -9,14 +9,14 @@ Generate a `KibanaResponse` which renders an HTML page bootstrapped with the `co
<b>Signature:</b>
```typescript
render(options?: IRenderOptions): Promise<string>;
render(options?: Pick<IRenderOptions, 'includeUserSettings'>): Promise<string>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| options | <code>IRenderOptions</code> | |
| options | <code>Pick&lt;IRenderOptions, 'includeUserSettings'&gt;</code> | |
<b>Returns:</b>

View file

@ -152,7 +152,7 @@ export {
SessionCookieValidationResult,
SessionStorageFactory,
} from './http';
export { RenderingServiceSetup, IRenderOptions, LegacyRenderOptions } from './rendering';
export { RenderingServiceSetup, IRenderOptions } from './rendering';
export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging';
export {

View file

@ -19,7 +19,8 @@
import { Server } from 'hapi';
import { LegacyRequest } from '../http';
import { KibanaRequest, LegacyRequest } from '../http';
import { ensureRawRequest } from '../http/router';
import { mergeVars } from './merge_vars';
import { ILegacyInternals, LegacyVars, VarsInjector, LegacyConfig, LegacyUiExports } from './types';
@ -51,11 +52,12 @@ export class LegacyInternals implements ILegacyInternals {
));
}
private replaceVars(vars: LegacyVars, request: LegacyRequest) {
private replaceVars(vars: LegacyVars, request: KibanaRequest | LegacyRequest) {
const { injectedVarsReplacers = [] } = this.uiExports;
return injectedVarsReplacers.reduce(
async (injected, replacer) => replacer(await injected, request, this.server),
async (injected, replacer) =>
replacer(await injected, ensureRawRequest(request), this.server),
Promise.resolve(vars)
);
}
@ -78,7 +80,11 @@ export class LegacyInternals implements ILegacyInternals {
);
}
public async getVars(id: string, request: LegacyRequest, injected: LegacyVars = {}) {
public async getVars(
id: string,
request: KibanaRequest | LegacyRequest,
injected: LegacyVars = {}
) {
return this.replaceVars(
mergeVars(this.defaultVars, await this.getInjectedUiAppVars(id), injected),
request

View file

@ -31,6 +31,7 @@ import { PathConfigType } from '../path';
import { findLegacyPluginSpecs } from './plugins';
import { convertLegacyDeprecationProvider } from './config';
import {
ILegacyInternals,
LegacyServiceSetupDeps,
LegacyServiceStartDeps,
LegacyPlugins,
@ -82,6 +83,7 @@ export class LegacyService implements CoreService {
private legacyRawConfig?: LegacyConfig;
private legacyPlugins?: LegacyPlugins;
private settings?: LegacyVars;
public legacyInternals?: ILegacyInternals;
constructor(private readonly coreContext: CoreContext) {
const { logger, configService } = coreContext;
@ -183,6 +185,11 @@ export class LegacyService implements CoreService {
// propagate the instance uuid to the legacy config, as it was the legacy way to access it.
this.legacyRawConfig!.set('server.uuid', setupDeps.core.uuid.getInstanceUuid());
this.setupDeps = setupDeps;
this.legacyInternals = new LegacyInternals(
this.legacyPlugins.uiExports,
this.legacyRawConfig!,
setupDeps.core.http.server
);
}
public async start(startDeps: LegacyServiceStartDeps) {
@ -317,7 +324,7 @@ export class LegacyService implements CoreService {
rendering: setupDeps.core.rendering,
uiSettings: setupDeps.core.uiSettings,
savedObjectsClientProvider: startDeps.core.savedObjects.clientProvider,
legacy: new LegacyInternals(legacyPlugins.uiExports, config, setupDeps.core.http.server),
legacy: this.legacyInternals,
},
logger: this.coreContext.logger,
},

View file

@ -20,7 +20,7 @@
import { Server } from 'hapi';
import { ChromeNavLink } from '../../public';
import { LegacyRequest } from '../http';
import { KibanaRequest, LegacyRequest } from '../http';
import { InternalCoreSetup, InternalCoreStart } from '../internal_types';
import { PluginsServiceSetup, PluginsServiceStart } from '../plugins';
import { RenderingServiceSetup } from '../rendering';
@ -198,7 +198,11 @@ export interface ILegacyInternals {
/**
* Get the metadata vars for a particular plugin
*/
getVars(id: string, request: LegacyRequest, injected?: LegacyVars): Promise<LegacyVars>;
getVars(
id: string,
request: KibanaRequest | LegacyRequest,
injected?: LegacyVars
): Promise<LegacyVars>;
}
/**

View file

@ -27,10 +27,10 @@ import { CoreService } from '../../types';
import { CoreContext } from '../core_context';
import { Template } from './views';
import {
IRenderOptions,
RenderingSetupDeps,
RenderingServiceSetup,
RenderingMetadata,
LegacyRenderOptions,
} from './types';
/** @internal */
@ -56,7 +56,7 @@ export class RenderingService implements CoreService<RenderingServiceSetup> {
app = { getId: () => 'core' },
includeUserSettings = true,
vars = {},
}: LegacyRenderOptions = {}
}: IRenderOptions = {}
) => {
const { env } = this.coreContext;
const basePath = http.basePath.get(request);

View file

@ -84,21 +84,19 @@ export interface IRenderOptions {
* `true` by default.
*/
includeUserSettings?: boolean;
}
/**
* @internal
* @deprecated for legacy use only, remove with ui_render_mixin
*/
export interface LegacyRenderOptions extends IRenderOptions {
/**
* Render the bootstrapped HTML content for an optional legacy application.
* Defaults to `core`.
* @deprecated for legacy use only, remove with ui_render_mixin
* @internal
*/
app?: { getId(): string };
/**
* Inject custom vars into the page metadata.
* @deprecated for legacy use only, remove with ui_render_mixin
* @internal
*/
vars?: Record<string, any>;
}
@ -123,7 +121,7 @@ export interface IScopedRenderingClient {
* );
* ```
*/
render(options?: IRenderOptions): Promise<string>;
render(options?: Pick<IRenderOptions, 'includeUserSettings'>): Promise<string>;
}
/** @internal */
@ -140,6 +138,6 @@ export interface RenderingServiceSetup {
render<R extends KibanaRequest | LegacyRequest>(
request: R,
uiSettings: IUiSettingsClient,
options?: R extends LegacyRequest ? LegacyRenderOptions : IRenderOptions
options?: IRenderOptions
): Promise<string>;
}

View file

@ -803,7 +803,13 @@ export interface IndexSettingsDeprecationInfo {
// @public (undocumented)
export interface IRenderOptions {
// @internal @deprecated
app?: {
getId(): string;
};
includeUserSettings?: boolean;
// @internal @deprecated
vars?: Record<string, any>;
}
// @public
@ -832,7 +838,7 @@ export type IScopedClusterClient = Pick<ScopedClusterClient, 'callAsCurrentUser'
// @public (undocumented)
export interface IScopedRenderingClient {
render(options?: IRenderOptions): Promise<string>;
render(options?: Pick<IRenderOptions, 'includeUserSettings'>): Promise<string>;
}
// @public
@ -932,21 +938,13 @@ export class LegacyInternals implements ILegacyInternals {
// (undocumented)
getInjectedUiAppVars(id: string): Promise<Record<string, any>>;
// (undocumented)
getVars(id: string, request: LegacyRequest, injected?: LegacyVars): Promise<Record<string, any>>;
getVars(id: string, request: KibanaRequest | LegacyRequest, injected?: LegacyVars): Promise<Record<string, any>>;
// Warning: (ae-forgotten-export) The symbol "VarsInjector" needs to be exported by the entry point index.d.ts
//
// (undocumented)
injectUiAppVars(id: string, injector: VarsInjector): void;
}
// @internal @deprecated (undocumented)
export interface LegacyRenderOptions extends IRenderOptions {
app?: {
getId(): string;
};
vars?: Record<string, any>;
}
// @public @deprecated (undocumented)
export interface LegacyRequest extends Request {
}
@ -1233,7 +1231,7 @@ export type RedirectResponseOptions = HttpResponseOptions & {
// @internal (undocumented)
export interface RenderingServiceSetup {
render<R extends KibanaRequest | LegacyRequest>(request: R, uiSettings: IUiSettingsClient, options?: R extends LegacyRequest ? LegacyRenderOptions : IRenderOptions): Promise<string>;
render<R extends KibanaRequest | LegacyRequest>(request: R, uiSettings: IUiSettingsClient, options?: IRenderOptions): Promise<string>;
}
// @public

View file

@ -220,7 +220,11 @@ export class Server {
return {
rendering: {
render: rendering.render.bind(rendering, req, uiSettingsClient),
render: async (options = {}) =>
rendering.render(req, uiSettingsClient, {
...options,
vars: await this.legacy.legacyInternals!.getVars('core', req),
}),
},
savedObjects: {
client: savedObjectsClient,

View file

@ -23,7 +23,6 @@ import { schema, TypeOf } from '@kbn/config-schema';
import {
CoreSetup,
CoreStart,
LegacyRenderOptions,
Logger,
PluginInitializerContext,
PluginConfigDescriptor,
@ -78,29 +77,6 @@ class Plugin {
}
);
router.get(
{
path: '/requestcontext/render/{id}',
validate: {
params: schema.object({
id: schema.maybe(schema.string()),
}),
},
},
async (context, req, res) => {
const { id } = req.params;
const options: Partial<LegacyRenderOptions> = { app: { getId: () => id! } };
const body = await context.core.rendering.render(options);
return res.ok({
body,
headers: {
'content-securty-policy': core.http.csp.header,
},
});
}
);
return {
data$: this.initializerContext.config.create<ConfigType>().pipe(
map(configValue => {

View file

@ -33,11 +33,6 @@ export default function({ getService }) {
200,
'SavedObjects client: {"page":1,"per_page":20,"total":0,"saved_objects":[]}'
));
it('provides access to application rendering client', async () => {
await supertest.get('/requestcontext/render/core').expect(200, /app:core/);
await supertest.get('/requestcontext/render/testbed').expect(200, /app:testbed/);
});
});
describe('compression', () => {

View file

@ -0,0 +1,8 @@
{
"id": "rendering_plugin",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["rendering_plugin"],
"server": true,
"ui": true
}

View file

@ -0,0 +1,17 @@
{
"name": "rendering_plugin",
"version": "1.0.0",
"main": "target/test/plugin_functional/plugins/rendering_plugin",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "Apache-2.0",
"scripts": {
"kbn": "node ../../../../scripts/kbn.js",
"build": "rm -rf './target' && tsc"
},
"devDependencies": {
"typescript": "3.5.3"
}
}

View file

@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PluginInitializer } from 'kibana/public';
import { RenderingPlugin } from './plugin';
export const plugin: PluginInitializer<void, void> = () => new RenderingPlugin();

View file

@ -0,0 +1,41 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Plugin, CoreSetup } from 'kibana/public';
export class RenderingPlugin implements Plugin {
public setup(core: CoreSetup) {
core.application.register({
id: 'rendering',
title: 'Rendering',
appRoute: '/render',
async mount(context, { element }) {
render(<h1 data-test-subj="renderingHeader">rendering service</h1>, element);
return () => unmountComponentAtNode(element);
},
});
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { RenderingPlugin } from './plugin';
export const plugin = () => new RenderingPlugin();

View file

@ -0,0 +1,63 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Plugin, CoreSetup, IRenderOptions } from 'kibana/server';
import { schema } from '@kbn/config-schema';
export class RenderingPlugin implements Plugin {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
router.get(
{
path: '/render/{id}',
validate: {
query: schema.object(
{
includeUserSettings: schema.boolean({ defaultValue: true }),
},
{ allowUnknowns: true }
),
params: schema.object({
id: schema.maybe(schema.string()),
}),
},
},
async (context, req, res) => {
const { id } = req.params;
const { includeUserSettings } = req.query;
const app = { getId: () => id! };
const options: Partial<IRenderOptions> = { app, includeUserSettings };
const body = await context.core.rendering.render(options);
return res.ok({
body,
headers: {
'content-security-policy': core.http.csp.header,
},
});
}
);
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,15 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../../../typings/**/*",
],
"exclude": []
}

View file

@ -121,13 +121,13 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
expect(wrapperWidth).to.be.below(windowWidth);
});
it.skip('can navigate from NP apps to legacy apps', async () => {
it('can navigate from NP apps to legacy apps', async () => {
await appsMenu.clickLink('Management');
await loadingScreenShown();
await testSubjects.existOrFail('managementNav');
});
it.skip('can navigate from legacy apps to NP apps', async () => {
it('can navigate from legacy apps to NP apps', async () => {
await appsMenu.clickLink('Foo');
await loadingScreenShown();
await testSubjects.existOrFail('fooAppHome');

View file

@ -29,5 +29,6 @@ export default function({ loadTestFile }: PluginFunctionalProviderContext) {
loadTestFile(require.resolve('./top_nav'));
loadTestFile(require.resolve('./application_leave_confirm'));
loadTestFile(require.resolve('./application_status'));
loadTestFile(require.resolve('./rendering'));
});
}

View file

@ -0,0 +1,127 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import expect from '@kbn/expect';
import '../../plugins/core_provider_plugin/types';
import { PluginFunctionalProviderContext } from '../../services';
// eslint-disable-next-line import/no-default-export
export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) {
const PageObjects = getPageObjects(['common']);
const browser = getService('browser');
const find = getService('find');
const testSubjects = getService('testSubjects');
function navigate(path: string) {
return browser.get(`${PageObjects.common.getHostPort()}${path}`);
}
function getLegacyMode() {
return browser.execute(() => {
return JSON.parse(document.querySelector('kbn-injected-metadata')!.getAttribute('data')!)
.legacyMode;
});
}
function getUserSettings() {
return browser.execute(() => {
return JSON.parse(document.querySelector('kbn-injected-metadata')!.getAttribute('data')!)
.legacyMetadata.uiSettings.user;
});
}
async function init() {
const loading = await testSubjects.find('kbnLoadingMessage', 5000);
return () => find.waitForElementStale(loading);
}
describe('rendering service', () => {
it('renders "core" application', async () => {
await navigate('/render/core');
const [loaded, legacyMode, userSettings] = await Promise.all([
init(),
getLegacyMode(),
getUserSettings(),
]);
expect(legacyMode).to.be(false);
expect(userSettings).to.not.be.empty();
await loaded();
expect(await testSubjects.exists('renderingHeader')).to.be(true);
});
it('renders "core" application without user settings', async () => {
await navigate('/render/core?includeUserSettings=false');
const [loaded, legacyMode, userSettings] = await Promise.all([
init(),
getLegacyMode(),
getUserSettings(),
]);
expect(legacyMode).to.be(false);
expect(userSettings).to.be.empty();
await loaded();
expect(await testSubjects.exists('renderingHeader')).to.be(true);
});
it('renders "legacy" application', async () => {
await navigate('/render/core_plugin_legacy');
const [loaded, legacyMode, userSettings] = await Promise.all([
init(),
getLegacyMode(),
getUserSettings(),
]);
expect(legacyMode).to.be(true);
expect(userSettings).to.not.be.empty();
await loaded();
expect(await testSubjects.exists('coreLegacyCompatH1')).to.be(true);
expect(await testSubjects.exists('renderingHeader')).to.be(false);
});
it('renders "legacy" application without user settings', async () => {
await navigate('/render/core_plugin_legacy?includeUserSettings=false');
const [loaded, legacyMode, userSettings] = await Promise.all([
init(),
getLegacyMode(),
getUserSettings(),
]);
expect(legacyMode).to.be(true);
expect(userSettings).to.be.empty();
await loaded();
expect(await testSubjects.exists('coreLegacyCompatH1')).to.be(true);
expect(await testSubjects.exists('renderingHeader')).to.be(false);
});
});
}

View file

@ -54,15 +54,5 @@ export default function({ getService }: PluginFunctionalProviderContext) {
statusCode: 400,
});
});
it('renders core application explicitly', async () => {
await supertest.get('/requestcontext/render/core').expect(200, /app:core/);
});
it('renders legacy application', async () => {
await supertest
.get('/requestcontext/render/core_plugin_legacy')
.expect(200, /app:core_plugin_legacy/);
});
});
}