[core/ui] bootstrap the legacy platform within the new platform (#20699) (#20913)

Fixes #20694

Implements super basic new platform `core` system, which includes two services: `core.injectedMetadata` and `core.legacyPlatform`. The `core` currently has two responsibilities:

 1. read the metadata from the DOM and initialize the `ui/metadata` module with legacy metadata, proving out how we plan to expose data from the new platform through the existing APIs/modules to the legacy platform.
 2. bootstrap the legacy platform by loading either `ui/chrome` or `ui/test_harness`

Because `core` mutates the `ui/metadata` module before bootstrapping the legacy platform all existing consumers of `ui/metadata` won't be impacted by the fact that metadata loading was moved into the new platform. We plan to do this for many other services that will need to exist in both the legacy and new platforms, like `ui/chrome` (see #20696).
This commit is contained in:
Spencer 2018-07-17 23:56:03 -07:00 committed by GitHub
parent 31eb567558
commit 360c50b7f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 935 additions and 194 deletions

View file

@ -28,7 +28,6 @@ exports.getWebpackConfig = function(kibanaPath, projectRoot, config) {
const alias = {
// Kibana defaults https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/ui/ui_bundler_env.js#L30-L36
ui: fromKibana('src/ui/public'),
ui_framework: fromKibana('ui_framework'),
test_harness: fromKibana('src/test_harness/public'),
querystring: 'querystring-browser',
moment$: fromKibana('webpackShims/moment'),

View file

@ -0,0 +1,126 @@
/*
* 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 { InjectedMetadataService } from './injected_metadata';
import { LegacyPlatformService } from './legacy_platform';
const MockLegacyPlatformService = jest.fn<LegacyPlatformService>(
function _MockLegacyPlatformService(this: any) {
this.start = jest.fn();
}
);
jest.mock('./legacy_platform', () => ({
LegacyPlatformService: MockLegacyPlatformService,
}));
const mockInjectedMetadataStartContract = {};
const MockInjectedMetadataService = jest.fn<InjectedMetadataService>(
function _MockInjectedMetadataService(this: any) {
this.start = jest.fn().mockReturnValue(mockInjectedMetadataStartContract);
}
);
jest.mock('./injected_metadata', () => ({
InjectedMetadataService: MockInjectedMetadataService,
}));
import { CoreSystem } from './core_system';
const defaultCoreSystemParams = {
rootDomElement: null!,
injectedMetadata: {} as any,
requireLegacyFiles: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('constructor', () => {
it('creates instances of services', () => {
// tslint:disable no-unused-expression
new CoreSystem({
...defaultCoreSystemParams,
});
expect(MockInjectedMetadataService).toHaveBeenCalledTimes(1);
expect(MockLegacyPlatformService).toHaveBeenCalledTimes(1);
});
it('passes injectedMetadata param to InjectedMetadataService', () => {
const injectedMetadata = { injectedMetadata: true } as any;
// tslint:disable no-unused-expression
new CoreSystem({
...defaultCoreSystemParams,
injectedMetadata,
});
expect(MockInjectedMetadataService).toHaveBeenCalledTimes(1);
expect(MockInjectedMetadataService).toHaveBeenCalledWith({
injectedMetadata,
});
});
it('passes rootDomElement, requireLegacyFiles, and useLegacyTestHarness to LegacyPlatformService', () => {
const rootDomElement = { rootDomElement: true } as any;
const requireLegacyFiles = { requireLegacyFiles: true } as any;
const useLegacyTestHarness = { useLegacyTestHarness: true } as any;
// tslint:disable no-unused-expression
new CoreSystem({
...defaultCoreSystemParams,
rootDomElement,
requireLegacyFiles,
useLegacyTestHarness,
});
expect(MockLegacyPlatformService).toHaveBeenCalledTimes(1);
expect(MockLegacyPlatformService).toHaveBeenCalledWith({
rootDomElement,
requireLegacyFiles,
useLegacyTestHarness,
});
});
});
describe('#start()', () => {
function startCore() {
const core = new CoreSystem({
...defaultCoreSystemParams,
});
core.start();
}
it('calls injectedMetadata#start()', () => {
startCore();
const [mockInstance] = MockInjectedMetadataService.mock.instances;
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith();
});
it('calls lifecycleSystem#start()', () => {
startCore();
const [mockInstance] = MockLegacyPlatformService.mock.instances;
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith({
injectedMetadata: mockInjectedMetadataStartContract,
});
});
});

View file

@ -0,0 +1,59 @@
/*
* 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 { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata';
import { LegacyPlatformParams, LegacyPlatformService } from './legacy_platform';
interface Params {
injectedMetadata: InjectedMetadataParams['injectedMetadata'];
rootDomElement: LegacyPlatformParams['rootDomElement'];
requireLegacyFiles: LegacyPlatformParams['requireLegacyFiles'];
useLegacyTestHarness?: LegacyPlatformParams['useLegacyTestHarness'];
}
/**
* The CoreSystem is the root of the new platform, and starts all parts
* of Kibana in the UI, including the LegacyPlatform which is managed
* by the LegacyPlatformService. As we migrate more things to the new
* platform the CoreSystem will get many more Services.
*/
export class CoreSystem {
private injectedMetadata: InjectedMetadataService;
private legacyPlatform: LegacyPlatformService;
constructor(params: Params) {
const { rootDomElement, injectedMetadata, requireLegacyFiles, useLegacyTestHarness } = params;
this.injectedMetadata = new InjectedMetadataService({
injectedMetadata,
});
this.legacyPlatform = new LegacyPlatformService({
rootDomElement,
requireLegacyFiles,
useLegacyTestHarness,
});
}
public start() {
this.legacyPlatform.start({
injectedMetadata: this.injectedMetadata.start(),
});
}
}

20
src/core/public/index.ts Normal file
View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { CoreSystem } from './core_system';

View file

@ -0,0 +1,33 @@
/*
* 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 { deepFreeze } from '../deep_freeze';
const obj = deepFreeze({
foo: {
bar: {
baz: 1,
},
},
});
delete obj.foo;
obj.foo = 1;
obj.foo.bar.baz = 2;
obj.foo.bar.box = false;

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"strict": true,
"skipLibCheck": true,
"lib": [
"esnext"
]
},
"include": [
"frozen_object_mutation.ts",
"../deep_freeze.ts"
]
}

View file

@ -0,0 +1,102 @@
/*
* 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 { resolve } from 'path';
import execa from 'execa';
import { deepFreeze } from './deep_freeze';
it('returns the first argument with all original references', () => {
const a = {};
const b = {};
const c = { a, b };
const frozen = deepFreeze(c);
expect(frozen).toBe(c);
expect(frozen.a).toBe(a);
expect(frozen.b).toBe(b);
});
it('prevents adding properties to argument', () => {
const frozen = deepFreeze({});
expect(() => {
// @ts-ignore ts knows this shouldn't be possible, but just making sure
frozen.foo = true;
}).toThrowError(`object is not extensible`);
});
it('prevents changing properties on argument', () => {
const frozen = deepFreeze({ foo: false });
expect(() => {
// @ts-ignore ts knows this shouldn't be possible, but just making sure
frozen.foo = true;
}).toThrowError(`read only property 'foo'`);
});
it('prevents changing properties on nested children of argument', () => {
const frozen = deepFreeze({ foo: { bar: { baz: { box: 1 } } } });
expect(() => {
// @ts-ignore ts knows this shouldn't be possible, but just making sure
frozen.foo.bar.baz.box = 2;
}).toThrowError(`read only property 'box'`);
});
it('prevents adding items to a frozen array', () => {
const frozen = deepFreeze({ foo: [1] });
expect(() => {
// @ts-ignore ts knows this shouldn't be possible, but just making sure
frozen.foo.push(2);
}).toThrowError(`object is not extensible`);
});
it('prevents reassigning items in a frozen array', () => {
const frozen = deepFreeze({ foo: [1] });
expect(() => {
// @ts-ignore ts knows this shouldn't be possible, but just making sure
frozen.foo[0] = 2;
}).toThrowError(`read only property '0'`);
});
it('types return values to prevent mutations in typescript', async () => {
const result = await execa.stdout(
'tsc',
[
'--noEmit',
'--project',
resolve(__dirname, '__fixtures__/frozen_object_mutation.tsconfig.json'),
],
{
cwd: resolve(__dirname, '__fixtures__'),
reject: false,
}
);
const errorCodeRe = /\serror\s(TS\d{4}):/g;
const errorCodes = [];
while (true) {
const match = errorCodeRe.exec(result);
if (!match) {
break;
}
errorCodes.push(match[1]);
}
expect(errorCodes).toEqual(['TS2704', 'TS2540', 'TS2540', 'TS2339']);
});

View file

@ -0,0 +1,36 @@
/*
* 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.
*/
type Freezable = { [k: string]: any } | any[];
type RecursiveReadOnly<T> = T extends Freezable
? Readonly<{ [K in keyof T]: RecursiveReadOnly<T[K]> }>
: T;
export function deepFreeze<T extends Freezable>(object: T) {
// for any properties that reference an object, makes sure that object is
// recursively frozen as well
for (const value of Object.values(object)) {
if (value !== null && typeof value === 'object') {
deepFreeze(value);
}
}
return Object.freeze(object) as RecursiveReadOnly<T>;
}

View file

@ -0,0 +1,24 @@
/*
* 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.
*/
export {
InjectedMetadataService,
InjectedMetadataParams,
InjectedMetadataStartContract,
} from './injected_metadata_service';

View file

@ -0,0 +1,53 @@
/*
* 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 { InjectedMetadataService } from './injected_metadata_service';
describe('#start()', () => {
it('deeply freezes its injectedMetadata param', () => {
const params = {
injectedMetadata: { foo: true } as any,
};
const injectedMetadata = new InjectedMetadataService(params);
expect(() => {
params.injectedMetadata.foo = false;
}).not.toThrowError();
injectedMetadata.start();
expect(() => {
params.injectedMetadata.foo = true;
}).toThrowError(`read only property 'foo'`);
});
});
describe('start.getLegacyMetadata()', () => {
it('returns injectedMetadata.legacyMetadata', () => {
const injectedMetadata = new InjectedMetadataService({
injectedMetadata: {
legacyMetadata: 'foo',
} as any,
});
const contract = injectedMetadata.start();
expect(contract.getLegacyMetadata()).toBe('foo');
});
});

View file

@ -0,0 +1,50 @@
/*
* 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 { deepFreeze } from './deep_freeze';
export interface InjectedMetadataParams {
injectedMetadata: {
legacyMetadata: {
[key: string]: any;
};
};
}
/**
* Provides access to the metadata that is injected by the
* server into the page. The metadata is actually defined
* in the entry file for the bundle containing the new platform
* and is read from the DOM in most cases.
*/
export class InjectedMetadataService {
constructor(private readonly params: InjectedMetadataParams) {}
public start() {
const state = deepFreeze(this.params.injectedMetadata);
return {
getLegacyMetadata() {
return state.legacyMetadata;
},
};
}
}
export type InjectedMetadataStartContract = ReturnType<InjectedMetadataService['start']>;

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { LegacyPlatformService, LegacyPlatformParams } from './legacy_platform_service';

View file

@ -0,0 +1,151 @@
/*
* 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.
*/
const mockLoadOrder: string[] = [];
const mockUiMetadataInit = jest.fn();
jest.mock('ui/metadata', () => {
mockLoadOrder.push('ui/metadata');
return {
__newPlatformInit__: mockUiMetadataInit,
};
});
const mockUiChromeBootstrap = jest.fn();
jest.mock('ui/chrome', () => {
mockLoadOrder.push('ui/chrome');
return {
bootstrap: mockUiChromeBootstrap,
};
});
const mockUiTestHarnessBootstrap = jest.fn();
jest.mock('ui/test_harness', () => {
mockLoadOrder.push('ui/test_harness');
return {
bootstrap: mockUiTestHarnessBootstrap,
};
});
import { LegacyPlatformService } from './legacy_platform_service';
const injectedMetadataStartContract = {
getLegacyMetadata: jest.fn(),
};
const defaultParams = {
rootDomElement: { someDomElement: true } as any,
requireLegacyFiles: jest.fn(() => {
mockLoadOrder.push('legacy files');
}),
};
afterEach(() => {
jest.clearAllMocks();
injectedMetadataStartContract.getLegacyMetadata.mockReset();
jest.resetModules();
mockLoadOrder.length = 0;
});
describe('#start()', () => {
describe('default', () => {
it('passes legacy metadata from injectedVars to ui/metadata', () => {
const legacyMetadata = { isLegacyMetadata: true };
injectedMetadataStartContract.getLegacyMetadata.mockReturnValue(legacyMetadata);
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.start({
injectedMetadata: injectedMetadataStartContract,
});
expect(mockUiMetadataInit).toHaveBeenCalledTimes(1);
expect(mockUiMetadataInit).toHaveBeenCalledWith(legacyMetadata);
});
describe('useLegacyTestHarness = false', () => {
it('passes the rootDomElement to ui/chrome', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.start({
injectedMetadata: injectedMetadataStartContract,
});
expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled();
expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1);
expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultParams.rootDomElement);
});
});
describe('useLegacyTestHarness = true', () => {
it('passes the rootDomElement to ui/test_harness', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
useLegacyTestHarness: true,
});
legacyPlatform.start({
injectedMetadata: injectedMetadataStartContract,
});
expect(mockUiChromeBootstrap).not.toHaveBeenCalled();
expect(mockUiTestHarnessBootstrap).toHaveBeenCalledTimes(1);
expect(mockUiTestHarnessBootstrap).toHaveBeenCalledWith(defaultParams.rootDomElement);
});
});
});
describe('load order', () => {
describe('useLegacyTestHarness = false', () => {
it('loads ui/modules before ui/chrome, and both before legacy files', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
expect(mockLoadOrder).toEqual([]);
legacyPlatform.start({
injectedMetadata: injectedMetadataStartContract,
});
expect(mockLoadOrder).toEqual(['ui/metadata', 'ui/chrome', 'legacy files']);
});
});
describe('useLegacyTestHarness = true', () => {
it('loads ui/modules before ui/test_harness, and both before legacy files', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
useLegacyTestHarness: true,
});
expect(mockLoadOrder).toEqual([]);
legacyPlatform.start({
injectedMetadata: injectedMetadataStartContract,
});
expect(mockLoadOrder).toEqual(['ui/metadata', 'ui/test_harness', 'legacy files']);
});
});
});
});

View file

@ -0,0 +1,72 @@
/*
* 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 { InjectedMetadataStartContract } from '../injected_metadata';
interface Deps {
injectedMetadata: InjectedMetadataStartContract;
}
export interface LegacyPlatformParams {
rootDomElement: HTMLElement;
requireLegacyFiles: () => void;
useLegacyTestHarness?: boolean;
}
/**
* The LegacyPlatformService is responsible for initializing
* the legacy platform by injecting parts of the new platform
* services into the legacy platform modules, like ui/modules,
* and then bootstraping the ui/chrome or ui/test_harness to
* start either the app or browser tests.
*/
export class LegacyPlatformService {
constructor(private readonly params: LegacyPlatformParams) {}
public start({ injectedMetadata }: Deps) {
// Inject parts of the new platform into parts of the legacy platform
// so that legacy APIs/modules can mimic their new platform counterparts
require('ui/metadata').__newPlatformInit__(injectedMetadata.getLegacyMetadata());
// Load the bootstrap module before loading the legacy platform files so that
// the bootstrap module can modify the environment a bit first
const bootstrapModule = this.loadBootstrapModule();
// require the files that will tie into the legacy platform
this.params.requireLegacyFiles();
bootstrapModule.bootstrap(this.params.rootDomElement);
}
private loadBootstrapModule(): {
bootstrap: (rootDomElement: HTMLElement) => void;
} {
if (this.params.useLegacyTestHarness) {
// wrapped in NODE_ENV check so the `ui/test_harness` module
// is not included in the distributable
if (process.env.NODE_ENV !== 'production') {
return require('ui/test_harness');
}
throw new Error('tests bundle is not available in the distributable');
}
return require('ui/chrome');
}
}

View file

@ -33,7 +33,7 @@ describe('Integration', () => {
beforeEach(() => {
// Set up our document body
document.body.innerHTML =
'<div><div id="editor" /><div id="editor_actions" /><div id="copy_as_curl" /><kbn-initial-state data="{}"/></div>';
'<div><div id="editor" /><div id="editor_actions" /><div id="copy_as_curl" /></div>';
input = initializeInput(
$('#editor'),

View file

@ -1,5 +1,5 @@
{
"name": "state_session_storage_redirect",
"version": "kibana",
"description": "When using the state:storeInSessionStorage setting with the short-urls, we need some way to get the full URL's hashed states into sessionStorage, this app will grab the URL from the kbn-initial-state and and put the URL hashed states into sessionStorage before redirecting the user."
"description": "When using the state:storeInSessionStorage setting with the short-urls, we need some way to get the full URL's hashed states into sessionStorage, this app will grab the URL from the injected state and and put the URL hashed states into sessionStorage before redirecting the user."
}

View file

@ -17,8 +17,6 @@
* under the License.
*/
import { union } from 'lodash';
import { fromRoot } from '../../utils';
import findSourceFiles from './find_source_files';
@ -36,7 +34,7 @@ export default (kibana) => {
uiExports: {
async __bundleProvider__(kbnServer) {
let modules = [];
const modules = new Set();
const {
config,
@ -67,7 +65,7 @@ export default (kibana) => {
// add the modules from all of this plugins apps
for (const app of uiApps) {
if (app.getPluginId() === pluginId) {
modules = union(modules, app.getModules());
modules.add(app.getMainModuleId());
}
}
@ -76,7 +74,7 @@ export default (kibana) => {
} else {
// add the modules from all of the apps
for (const app of uiApps) {
modules = union(modules, app.getModules());
modules.add(app.getMainModuleId());
}
for (const plugin of plugins) {
@ -85,7 +83,7 @@ export default (kibana) => {
}
const testFiles = await findSourceFiles(testGlobs);
for (const f of testFiles) modules.push(f);
for (const f of testFiles) modules.add(f);
if (config.get('tests_bundle.instrument')) {
uiBundles.addPostLoader({
@ -97,7 +95,7 @@ export default (kibana) => {
uiBundles.add({
id: 'tests',
modules,
modules: [...modules],
template: createTestEntryTemplate(uiSettingDefaults),
});
},

View file

@ -27,7 +27,15 @@ export const createTestEntryTemplate = (defaultUiSettings) => (bundle) => `
*
*/
window.__KBN__ = {
// import global polyfills before everything else
import 'babel-polyfill';
import 'custom-event-polyfill';
import 'whatwg-fetch';
import 'abortcontroller-polyfill';
import { CoreSystem } from '__kibanaCore__'
const legacyMetadata = {
version: '1.2.3',
buildNum: 1234,
vars: {
@ -62,7 +70,14 @@ window.__KBN__ = {
}
};
require('ui/test_harness');
${bundle.getRequires().join('\n')}
require('ui/test_harness').bootstrap(/* go! */);
new CoreSystem({
injectedMetadata: {
legacyMetadata
},
rootDomElement: document.body,
useLegacyTestHarness: true,
requireLegacyFiles: () => {
${bundle.getRequires().join('\n ')}
}
}).start()
`;

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { dirname, extname, join, relative, resolve } from 'path';
import { dirname, extname, join, relative, resolve, sep } from 'path';
import { REPO_ROOT } from './constants';
@ -48,6 +48,10 @@ export class File {
return this.ext === '.ts' || this.ext === '.tsx';
}
public isFixture() {
return this.relativePath.split(sep).includes('__fixtures__');
}
public getRelativeParentDirs() {
const parents: string[] = [];

View file

@ -22,5 +22,5 @@ import { ToolingLog } from '@kbn/dev-utils';
import { File } from '../file';
export function pickFilesToLint(log: ToolingLog, files: File[]) {
return files.filter(file => file.isTypescript());
return files.filter(file => file.isTypescript() && !file.isFixture());
}

View file

@ -42,12 +42,6 @@ export default async (kbnServer, server, config) => {
basePublicPath: config.get('server.basePath')
}));
await uiBundles.writeEntryFiles();
// Not all entry files produce a css asset. Ensuring they exist prevents
// an error from occurring when the file is missing.
await uiBundles.ensureStyleFiles();
// in prod, only bundle when something is missing or invalid
const reuseCache = config.get('optimize.useBundleCache')
? await uiBundles.areAllBundleCachesValid()
@ -62,6 +56,8 @@ export default async (kbnServer, server, config) => {
return;
}
await uiBundles.resetBundleDir();
// only require the FsOptimizer when we need to
const optimizer = new FsOptimizer({
uiBundles,

View file

@ -45,10 +45,7 @@ export default class WatchOptimizer extends BaseOptimizer {
// log status changes
this.status$.subscribe(this.onStatusChangeHandler);
await this.uiBundles.writeEntryFiles();
await this.uiBundles.ensureStyleFiles();
await this.uiBundles.resetBundleDir();
await this.initCompiler();
this.compiler.plugin('watch-run', this.compilerRunStartHandler);

View file

@ -29,8 +29,8 @@ import KbnServer from '../../server/kbn_server';
const getInjectedVarsFromResponse = (resp) => {
const $ = cheerio.load(resp.payload);
const data = $('kbn-initial-state').attr('data');
return JSON.parse(data).vars;
const data = $('kbn-injected-metadata').attr('data');
return JSON.parse(data).legacyMetadata.vars;
};
const injectReplacer = (kbnServer, replacer) => {

View file

@ -20,13 +20,6 @@
import expect from 'expect.js';
import { metadata } from '../metadata';
describe('ui/metadata', () => {
it('is same data as window.__KBN__', () => {
expect(metadata.version).to.equal(window.__KBN__.version);
expect(metadata.vars.kbnIndex).to.equal(window.__KBN__.vars.kbnIndex);
});
it('is immutable', () => {
expect(() => metadata.foo = 'something').to.throw;
expect(() => metadata.version = 'something').to.throw;

View file

@ -21,13 +21,6 @@ import _ from 'lodash';
import angular from 'angular';
import { metadata } from '../metadata';
// Polyfills
import 'babel-polyfill';
import 'whatwg-fetch';
import 'custom-event-polyfill';
import 'abortcontroller-polyfill';
import '../state_management/global_state';
import '../config';
import '../notify';
@ -79,7 +72,7 @@ themeApi(chrome, internals);
translationsApi(chrome, internals);
const waitForBootstrap = new Promise(resolve => {
chrome.bootstrap = function () {
chrome.bootstrap = function (targetDomElement) {
// import chrome nav controls and hacks now so that they are executed after
// everything else, can safely import the chrome, and interact with services
// and such setup by all other modules
@ -87,8 +80,10 @@ const waitForBootstrap = new Promise(resolve => {
require('uiExports/hacks');
chrome.setupAngular();
angular.bootstrap(document.body, ['kibana']);
resolve();
targetDomElement.setAttribute('id', 'kibana-body');
targetDomElement.setAttribute('kbn-chrome', 'true');
angular.bootstrap(targetDomElement, ['kibana']);
resolve(targetDomElement);
};
});
@ -107,10 +102,10 @@ const waitForBootstrap = new Promise(resolve => {
* tests. Look into 'src/test_utils/public/stub_get_active_injector' for more information.
*/
chrome.dangerouslyGetActiveInjector = () => {
return waitForBootstrap.then(() => {
const $injector = angular.element(document.body).injector();
return waitForBootstrap.then((targetDomElement) => {
const $injector = angular.element(targetDomElement).injector();
if (!$injector) {
return Promise.reject('document.body had no angular context after bootstrapping');
return Promise.reject('targetDomElement had no angular context after bootstrapping');
}
return $injector;
});

View file

@ -17,29 +17,12 @@
* under the License.
*/
import $ from 'jquery';
import _ from 'lodash';
export let metadata = null;
export const metadata = deepFreeze(getState());
function deepFreeze(object) {
// for any properties that reference an object, makes sure that object is
// recursively frozen as well
Object.keys(object).forEach(key => {
const value = object[key];
if (_.isObject(value)) {
deepFreeze(value);
}
});
return Object.freeze(object);
}
function getState() {
const stateKey = '__KBN__';
if (!(stateKey in window)) {
const state = $('kbn-initial-state').attr('data');
window[stateKey] = JSON.parse(state);
export function __newPlatformInit__(legacyMetadata) {
if (metadata === null) {
metadata = legacyMetadata;
} else {
throw new Error('ui/metadata can only be initialized once');
}
return window[stateKey];
}

View file

@ -22,13 +22,12 @@ import ngMock from 'ng_mock';
import expect from 'expect.js';
import sinon from 'sinon';
import { Notifier } from '..';
import { metadata } from 'ui/metadata';
describe('Notifier', function () {
let $interval;
let notifier;
let params;
const version = window.__KBN__.version;
const buildNum = window.__KBN__.buildNum;
const message = 'Oh, the humanity!';
const customText = 'fooMarkup';
const customParams = {
@ -334,13 +333,13 @@ describe('Notifier', function () {
describe('when version is configured', function () {
it('adds version to notification', function () {
const notification = notify(fnName);
expect(notification.info.version).to.equal(version);
expect(notification.info.version).to.equal(metadata.version);
});
});
describe('when build number is configured', function () {
it('adds buildNum to notification', function () {
const notification = notify(fnName);
expect(notification.info.buildNum).to.equal(buildNum);
expect(notification.info.buildNum).to.equal(metadata.buildNum);
});
});
}

View file

@ -17,6 +17,4 @@
* under the License.
*/
import './test_harness';
export { bootstrap } from './test_harness';

View file

@ -41,9 +41,6 @@ if (query && query.mocha) {
setupTestSharding();
// allows test_harness.less to have higher priority selectors
document.body.setAttribute('id', 'test-harness-body');
before(() => {
// prevent accidental ajax requests
sinon.useFakeXMLHttpRequest();
@ -84,7 +81,10 @@ afterEach(function () {
});
// Kick off mocha, called at the end of test entry files
export function bootstrap() {
export function bootstrap(targetDomElement) {
// allows test_harness.less to have higher priority selectors
targetDomElement.setAttribute('id', 'test-harness-body');
// load the hacks since we aren't actually bootstrapping the
// chrome, which is where the hacks would normally be loaded
require('uiExports/hacks');

View file

@ -86,8 +86,8 @@ describe('ui apps / UiApp', () => {
expect(app.getNavLink()).to.be.a(UiNavLink);
});
it('has an empty modules list', () => {
expect(app.getModules()).to.eql([]);
it('has no main module', () => {
expect(app.getMainModuleId()).to.be(undefined);
});
it('has no styleSheetPath', () => {
@ -135,10 +135,8 @@ describe('ui apps / UiApp', () => {
expect(app.getNavLink()).to.be(undefined);
});
it('includes main and hack modules', () => {
expect(app.getModules()).to.eql([
'main.js',
]);
it('has a main module', () => {
expect(app.getMainModuleId()).to.be('main.js');
});
it('has spec values in JSON representation', () => {
@ -303,15 +301,15 @@ describe('ui apps / UiApp', () => {
});
});
describe('#getModules', () => {
it('returns empty array by default', () => {
describe('#getMainModuleId', () => {
it('returns undefined by default', () => {
const app = createUiApp({ id: 'foo' });
expect(app.getModules()).to.eql([]);
expect(app.getMainModuleId()).to.be(undefined);
});
it('returns main module if not using appExtensions', () => {
it('returns main module id', () => {
const app = createUiApp({ id: 'foo', main: 'bar' });
expect(app.getModules()).to.eql(['bar']);
expect(app.getMainModuleId()).to.be('bar');
});
});

View file

@ -101,8 +101,8 @@ export class UiApp {
}
}
getModules() {
return this._main ? [this._main] : [];
getMainModuleId() {
return this._main;
}
getStyleSheetUrlPath() {

View file

@ -44,6 +44,6 @@ describe('ui bundles / appEntryTemplate', () => {
'baz'
];
bundle.getRequires.returns(requires);
expect(appEntryTemplate(bundle)).to.contain(requires.join('\n'));
expect(appEntryTemplate(bundle)).to.contain(requires.join('\n '));
});
});

View file

@ -19,15 +19,26 @@
export const appEntryTemplate = (bundle) => `
/**
* Test entry file
* Kibana entry file
*
* This is programmatically created and updated, do not modify
*
* context: ${bundle.getContext()}
*/
require('ui/chrome');
${bundle.getRequires().join('\n')}
require('ui/chrome').bootstrap(/* xoxo */);
// import global polyfills before everything else
import 'babel-polyfill';
import 'custom-event-polyfill';
import 'whatwg-fetch';
import 'abortcontroller-polyfill';
import { CoreSystem } from '__kibanaCore__'
new CoreSystem({
injectedMetadata: JSON.parse(document.querySelector('kbn-injected-metadata').getAttribute('data')),
rootDomElement: document.body,
requireLegacyFiles: () => {
${bundle.getRequires().join('\n ')}
}
}).start()
`;

View file

@ -18,7 +18,7 @@
*/
import { fromNode as fcb } from 'bluebird';
import { readFile, writeFile, unlink, stat } from 'fs';
import { readFile, writeFile, stat } from 'fs';
// We normalize all path separators to `/` in generated files
function normalizePath(path) {
@ -86,31 +86,31 @@ export class UiBundle {
));
}
async hasStyleFile() {
return await fcb(cb => {
return stat(this.getStylePath(), error => {
cb(null, !(error && error.code === 'ENOENT'));
});
});
}
async touchStyleFile() {
return await fcb(cb => (
writeFile(this.getStylePath(), '', 'utf8', cb)
));
}
async clearBundleFile() {
try {
await fcb(cb => unlink(this.getOutputPath(), cb));
} catch (e) {
return null;
}
}
/**
* Determine if the cache for this bundle is valid by
* checking that the entry file exists, has the content we
* expect based on the argument for this bundle, and that both
* the style file and output for this bundle exist. In this
* scenario we assume the cache is valid.
*
* When the `optimize.useBundleCache` config is set to `false`
* (the default when running in development) we don't even call
* this method and bundles are always recreated.
*/
async isCacheValid() {
if (await this.readEntryFile() !== this.renderContent()) {
return false;
}
try {
await fcb(cb => stat(this.getOutputPath(), cb));
await fcb(cb => stat(this.getStylePath(), cb));
return true;
}
catch (e) {

View file

@ -17,15 +17,20 @@
* under the License.
*/
import { createHash } from 'crypto';
import { resolve } from 'path';
import { createHash } from 'crypto';
import { promisify } from 'util';
import { existsSync } from 'fs';
import { UiBundle } from './ui_bundle';
import { fromNode as fcb } from 'bluebird';
import del from 'del';
import { makeRe } from 'minimatch';
import mkdirp from 'mkdirp';
import { UiBundle } from './ui_bundle';
import { appEntryTemplate } from './app_entry_template';
const mkdirpAsync = promisify(mkdirp);
function getWebpackAliases(pluginSpecs) {
return pluginSpecs.reduce((aliases, spec) => {
const publicDir = spec.getPublicDir();
@ -74,10 +79,11 @@ export class UiBundlesController {
this._postLoaders = [];
this._bundles = [];
// create a bundle for each uiApp
for (const uiApp of uiApps) {
this.add({
id: uiApp.getId(),
modules: uiApp.getModules(),
modules: [uiApp.getMainModuleId()],
template: appEntryTemplate,
});
}
@ -140,58 +146,48 @@ export class UiBundlesController {
return resolve(this._workingDir, ...args);
}
async resetBundleDir() {
if (!existsSync(this._workingDir)) {
// create a fresh working directory
await mkdirpAsync(this._workingDir);
} else {
// delete all children of the working directory
await del(this.resolvePath('*'));
}
// write the entry/style files for each bundle
for (const bundle of this._bundles) {
await bundle.writeEntryFile();
await bundle.touchStyleFile();
}
}
getCacheDirectory(...subPath) {
return this.resolvePath('../.cache', this.hashBundleEntries(), ...subPath);
}
getDescription() {
switch (this._bundles.length) {
const ids = this.getIds();
switch (ids.length) {
case 0:
return '0 bundles';
case 1:
return `bundle for ${this._bundles[0].getId()}`;
return `bundle for ${ids[0]}`;
default:
const ids = this.getIds();
const last = ids.pop();
const commas = ids.join(', ');
return `bundles for ${commas} and ${last}`;
}
}
async ensureDir() {
await fcb(cb => mkdirp(this._workingDir, cb));
}
async writeEntryFiles() {
await this.ensureDir();
for (const bundle of this._bundles) {
const existing = await bundle.readEntryFile();
const expected = bundle.renderContent();
if (existing !== expected) {
await bundle.writeEntryFile();
await bundle.clearBundleFile();
}
}
}
async ensureStyleFiles() {
await this.ensureDir();
for (const bundle of this._bundles) {
if (!await bundle.hasStyleFile()) {
await bundle.touchStyleFile();
}
}
}
hashBundleEntries() {
const hash = createHash('sha1');
for (const bundle of this._bundles) {
hash.update(`bundleEntryPath:${bundle.getEntryPath()}`);
hash.update(`bundleEntryContent:${bundle.renderContent()}`);
}
return hash.digest('hex');
}
@ -216,8 +212,4 @@ export class UiBundlesController {
return this._bundles
.map(bundle => bundle.getId());
}
toJSON() {
return this._bundles;
}
}

View file

@ -28,7 +28,7 @@ export const UI_EXPORT_DEFAULTS = {
webpackAliases: {
ui: resolve(ROOT, 'src/ui/public'),
ui_framework: resolve(ROOT, 'ui_framework'),
'__kibanaCore__$': resolve(ROOT, 'src/core/public'),
test_harness: resolve(ROOT, 'src/test_harness/public'),
querystring: 'querystring-browser',
moment$: resolve(ROOT, 'webpackShims/moment'),

View file

@ -105,7 +105,7 @@ export function uiRenderMixin(kbnServer, server, config) {
}
});
async function getKibanaPayload({ app, request, includeUserProvidedConfig, injectedVarsOverrides }) {
async function getLegacyKibanaPayload({ app, request, includeUserProvidedConfig, injectedVarsOverrides }) {
const uiSettings = request.getUiSettingsService();
const translations = await request.getUiTranslations();
@ -140,17 +140,21 @@ export function uiRenderMixin(kbnServer, server, config) {
try {
const request = reply.request;
const translations = await request.getUiTranslations();
const basePath = config.get('server.basePath');
return reply.view('ui_app', {
app,
kibanaPayload: await getKibanaPayload({
app,
request,
includeUserProvidedConfig,
injectedVarsOverrides
}),
bundlePath: `${config.get('server.basePath')}/bundles`,
uiPublicUrl: `${basePath}/ui`,
bootstrapScriptUrl: `${basePath}/bundles/app/${app.getId()}/bootstrap.js`,
i18n: key => get(translations, key, ''),
injectedMetadata: {
legacyMetadata: await getLegacyKibanaPayload({
app,
request,
includeUserProvidedConfig,
injectedVarsOverrides
}),
},
});
} catch (err) {
reply(err);

View file

@ -1,6 +1,3 @@
-
var appName = 'kibana';
block vars
doctype html
@ -16,63 +13,63 @@ html(lang='en')
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_300.woff2') format('woff2'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_300.woff') format('woff'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_300.ttf') format('truetype'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_300.svg#OpenSans') format('svg');
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_300.woff2') format('woff2'),
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_300.woff') format('woff'),
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_300.ttf') format('truetype'),
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_300.svg#OpenSans') format('svg');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_regular.woff2') format('woff2'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_regular.woff') format('woff'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_regular.ttf') format('truetype'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_regular.svg#OpenSans') format('svg');
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_regular.woff2') format('woff2'),
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_regular.woff') format('woff'),
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_regular.ttf') format('truetype'),
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_regular.svg#OpenSans') format('svg');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_600.woff2') format('woff2'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_600.woff') format('woff'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_600.ttf') format('truetype'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_600.svg#OpenSans') format('svg');
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_600.woff2') format('woff2'),
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_600.woff') format('woff'),
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_600.ttf') format('truetype'),
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_600.svg#OpenSans') format('svg');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_700.woff2') format('woff2'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_700.woff') format('woff'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_700.ttf') format('truetype'),
url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_700.svg#OpenSans') format('svg');
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_700.woff2') format('woff2'),
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_700.woff') format('woff'),
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_700.ttf') format('truetype'),
url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_700.svg#OpenSans') format('svg');
}
//- Favicons (generated from http://realfavicongenerator.net/)
link(
rel='apple-touch-icon' sizes='180x180' href='#{kibanaPayload.basePath}/ui/favicons/apple-touch-icon.png'
rel='apple-touch-icon' sizes='180x180' href='#{uiPublicUrl}/favicons/apple-touch-icon.png'
)
link(
rel='icon' type='image/png' href='#{kibanaPayload.basePath}/ui/favicons/favicon-32x32.png' sizes='32x32'
rel='icon' type='image/png' href='#{uiPublicUrl}/favicons/favicon-32x32.png' sizes='32x32'
)
link(
rel='icon' type='image/png' href='#{kibanaPayload.basePath}/ui/favicons/favicon-16x16.png' sizes='16x16'
rel='icon' type='image/png' href='#{uiPublicUrl}/favicons/favicon-16x16.png' sizes='16x16'
)
link(
rel='manifest' href='#{kibanaPayload.basePath}/ui/favicons/manifest.json'
rel='manifest' href='#{uiPublicUrl}/favicons/manifest.json'
)
link(
rel='mask-icon' href='#{kibanaPayload.basePath}/ui/favicons/safari-pinned-tab.svg' color='#e8488b'
rel='mask-icon' href='#{uiPublicUrl}/favicons/safari-pinned-tab.svg' color='#e8488b'
)
link(
rel='shortcut icon' href='#{kibanaPayload.basePath}/ui/favicons/favicon.ico'
rel='shortcut icon' href='#{uiPublicUrl}/favicons/favicon.ico'
)
meta(
name='msapplication-config' content='#{kibanaPayload.basePath}/ui/favicons/browserconfig.xml'
name='msapplication-config' content='#{uiPublicUrl}/favicons/browserconfig.xml'
)
meta(
name='theme-color' content='#ffffff'
@ -120,6 +117,6 @@ html(lang='en')
//- good because we may use them to override EUI styles.
style#themeCss
body(kbn-chrome, id='#{appName}-body')
kbn-initial-state(data=JSON.stringify(kibanaPayload))
body
kbn-injected-metadata(data=JSON.stringify(injectedMetadata))
block content

View file

@ -110,4 +110,4 @@ block content
.kibanaWelcomeText
| #{i18n('UI-WELCOME_MESSAGE')}
script(src='#{bundlePath}/app/#{app.getId()}/bootstrap.js')
script(src=bootstrapScriptUrl)

View file

@ -46,5 +46,8 @@
},
"include": [
"src/**/*"
],
"exclude": [
"src/**/__fixtures__/**/*"
]
}