mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Migrate ui/notify/fatal_error to new platform (#20752)
Fixes #20695 Extracts the "fatal error" handling logic from the `ui/notify` module and reimplements it in the new platform, using EUI for the fatal error page and continuing to support the `fatalError()` and `addFatalErrorCallback()` methods exported by the `ui/notify` module. 
This commit is contained in:
parent
3b74158b40
commit
1532c5e9aa
34 changed files with 1293 additions and 223 deletions
|
@ -238,6 +238,7 @@
|
|||
"@types/bluebird": "^3.1.1",
|
||||
"@types/chance": "^1.0.0",
|
||||
"@types/classnames": "^2.2.3",
|
||||
"@types/enzyme": "^3.1.12",
|
||||
"@types/eslint": "^4.16.2",
|
||||
"@types/execa": "^0.9.0",
|
||||
"@types/fetch-mock": "^5.12.2",
|
||||
|
|
20
packages/kbn-test-subj-selector/index.d.ts
vendored
Normal file
20
packages/kbn-test-subj-selector/index.d.ts
vendored
Normal 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 default function(selector: string): string;
|
6
packages/kbn-test-subj-selector/tsconfig.json
Normal file
6
packages/kbn-test-subj-selector/tsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": [
|
||||
"index.d.ts"
|
||||
],
|
||||
}
|
|
@ -17,12 +17,14 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { FatalErrorsService } from './fatal_errors';
|
||||
import { InjectedMetadataService } from './injected_metadata';
|
||||
import { LegacyPlatformService } from './legacy_platform';
|
||||
|
||||
const MockLegacyPlatformService = jest.fn<LegacyPlatformService>(
|
||||
function _MockLegacyPlatformService(this: any) {
|
||||
this.start = jest.fn();
|
||||
this.stop = jest.fn();
|
||||
}
|
||||
);
|
||||
jest.mock('./legacy_platform', () => ({
|
||||
|
@ -39,7 +41,19 @@ jest.mock('./injected_metadata', () => ({
|
|||
InjectedMetadataService: MockInjectedMetadataService,
|
||||
}));
|
||||
|
||||
const mockFatalErrorsStartContract = {};
|
||||
const MockFatalErrorsService = jest.fn<FatalErrorsService>(function _MockFatalErrorsService(
|
||||
this: any
|
||||
) {
|
||||
this.start = jest.fn().mockReturnValue(mockFatalErrorsStartContract);
|
||||
this.add = jest.fn();
|
||||
});
|
||||
jest.mock('./fatal_errors', () => ({
|
||||
FatalErrorsService: MockFatalErrorsService,
|
||||
}));
|
||||
|
||||
import { CoreSystem } from './core_system';
|
||||
jest.spyOn(CoreSystem.prototype, 'stop');
|
||||
|
||||
const defaultCoreSystemParams = {
|
||||
rootDomElement: null!,
|
||||
|
@ -97,6 +111,44 @@ describe('constructor', () => {
|
|||
useLegacyTestHarness,
|
||||
});
|
||||
});
|
||||
|
||||
it('passes injectedMetadata, rootDomElement, and a stopCoreSystem function to FatalErrorsService', () => {
|
||||
const rootDomElement = { rootDomElement: true } as any;
|
||||
const injectedMetadata = { injectedMetadata: true } as any;
|
||||
|
||||
const coreSystem = new CoreSystem({
|
||||
...defaultCoreSystemParams,
|
||||
rootDomElement,
|
||||
injectedMetadata,
|
||||
});
|
||||
|
||||
expect(MockFatalErrorsService).toHaveBeenCalledTimes(1);
|
||||
expect(MockFatalErrorsService).toHaveBeenLastCalledWith({
|
||||
rootDomElement,
|
||||
injectedMetadata: expect.any(MockInjectedMetadataService),
|
||||
stopCoreSystem: expect.any(Function),
|
||||
});
|
||||
|
||||
const [{ stopCoreSystem }] = MockFatalErrorsService.mock.calls[0];
|
||||
|
||||
expect(coreSystem.stop).not.toHaveBeenCalled();
|
||||
stopCoreSystem();
|
||||
expect(coreSystem.stop).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stop', () => {
|
||||
it('call legacyPlatform.stop()', () => {
|
||||
const coreSystem = new CoreSystem({
|
||||
...defaultCoreSystemParams,
|
||||
});
|
||||
|
||||
const legacyPlatformService = MockLegacyPlatformService.mock.instances[0];
|
||||
|
||||
expect(legacyPlatformService.stop).not.toHaveBeenCalled();
|
||||
coreSystem.stop();
|
||||
expect(legacyPlatformService.stop).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#start()', () => {
|
||||
|
@ -115,12 +167,20 @@ describe('#start()', () => {
|
|||
expect(mockInstance.start).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('calls lifecycleSystem#start()', () => {
|
||||
it('calls fatalErrors#start()', () => {
|
||||
startCore();
|
||||
const [mockInstance] = MockFatalErrorsService.mock.instances;
|
||||
expect(mockInstance.start).toHaveBeenCalledTimes(1);
|
||||
expect(mockInstance.start).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('calls legacyPlatform#start()', () => {
|
||||
startCore();
|
||||
const [mockInstance] = MockLegacyPlatformService.mock.instances;
|
||||
expect(mockInstance.start).toHaveBeenCalledTimes(1);
|
||||
expect(mockInstance.start).toHaveBeenCalledWith({
|
||||
injectedMetadata: mockInjectedMetadataStartContract,
|
||||
fatalErrors: mockFatalErrorsStartContract,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { FatalErrorsService } from './fatal_errors';
|
||||
import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata';
|
||||
import { LegacyPlatformParams, LegacyPlatformService } from './legacy_platform';
|
||||
|
||||
|
@ -34,6 +35,7 @@ interface Params {
|
|||
* platform the CoreSystem will get many more Services.
|
||||
*/
|
||||
export class CoreSystem {
|
||||
private fatalErrors: FatalErrorsService;
|
||||
private injectedMetadata: InjectedMetadataService;
|
||||
private legacyPlatform: LegacyPlatformService;
|
||||
|
||||
|
@ -44,6 +46,14 @@ export class CoreSystem {
|
|||
injectedMetadata,
|
||||
});
|
||||
|
||||
this.fatalErrors = new FatalErrorsService({
|
||||
rootDomElement,
|
||||
injectedMetadata: this.injectedMetadata,
|
||||
stopCoreSystem: () => {
|
||||
this.stop();
|
||||
},
|
||||
});
|
||||
|
||||
this.legacyPlatform = new LegacyPlatformService({
|
||||
rootDomElement,
|
||||
requireLegacyFiles,
|
||||
|
@ -52,8 +62,16 @@ export class CoreSystem {
|
|||
}
|
||||
|
||||
public start() {
|
||||
this.legacyPlatform.start({
|
||||
injectedMetadata: this.injectedMetadata.start(),
|
||||
});
|
||||
try {
|
||||
const injectedMetadata = this.injectedMetadata.start();
|
||||
const fatalErrors = this.fatalErrors.start();
|
||||
this.legacyPlatform.start({ injectedMetadata, fatalErrors });
|
||||
} catch (error) {
|
||||
this.fatalErrors.add(error);
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.legacyPlatform.stop();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`rendering render matches snapshot 1`] = `
|
||||
<EuiPage
|
||||
restrictWidth={false}
|
||||
style={
|
||||
Object {
|
||||
"minHeight": "100vh",
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiPageBody
|
||||
restrictWidth={false}
|
||||
>
|
||||
<EuiPageContent
|
||||
horizontalPosition="center"
|
||||
panelPaddingSize="l"
|
||||
verticalPosition="center"
|
||||
>
|
||||
<EuiEmptyPrompt
|
||||
actions={
|
||||
Array [
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="clearSession"
|
||||
fill={true}
|
||||
iconSide="left"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Clear your session
|
||||
</EuiButton>,
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
data-test-subj="goBack"
|
||||
iconSide="left"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Go back
|
||||
</EuiButtonEmpty>,
|
||||
]
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
Try refreshing the page. If that doesn't work, go back to the previous page or clear your session data.
|
||||
</p>
|
||||
}
|
||||
iconColor="danger"
|
||||
iconType="alert"
|
||||
title={
|
||||
<h2>
|
||||
Something went wrong
|
||||
</h2>
|
||||
}
|
||||
/>
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
key="0"
|
||||
size="m"
|
||||
title="foo"
|
||||
>
|
||||
<EuiCodeBlock
|
||||
className="eui-textBreakAll"
|
||||
language="bash"
|
||||
>
|
||||
Version: bar
|
||||
Build: 123
|
||||
Error: foo
|
||||
stack...foo.js:1:1
|
||||
</EuiCodeBlock>
|
||||
</EuiCallOut>
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
key="1"
|
||||
size="m"
|
||||
title="bar"
|
||||
>
|
||||
<EuiCodeBlock
|
||||
className="eui-textBreakAll"
|
||||
language="bash"
|
||||
>
|
||||
Version: bar
|
||||
Build: 123
|
||||
Error: bar
|
||||
stack...bar.js:1:1
|
||||
</EuiCodeBlock>
|
||||
</EuiCallOut>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
`;
|
|
@ -0,0 +1,20 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`#add() deletes all children of rootDomElement and renders <FatalErrorScreen /> into it: fatal error screen component 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<FatalErrorsScreen
|
||||
buildNumber="kibanaBuildNumber"
|
||||
errorInfo$={Rx.Observable}
|
||||
kibanaVersion="kibanaVersion"
|
||||
/>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`#add() deletes all children of rootDomElement and renders <FatalErrorScreen /> into it: fatal error screen container 1`] = `
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
`;
|
137
src/core/public/fatal_errors/fatal_errors_screen.test.tsx
Normal file
137
src/core/public/fatal_errors/fatal_errors_screen.test.tsx
Normal file
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @ts-ignore EuiCallOut not available until we upgrade to EUI 3.1.0
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import testSubjSelector from '@kbn/test-subj-selector';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
import { FatalErrorsScreen } from './fatal_errors_screen';
|
||||
|
||||
const errorInfoFoo = {
|
||||
message: 'foo',
|
||||
stack: 'Error: foo\n stack...foo.js:1:1',
|
||||
};
|
||||
const errorInfoBar = {
|
||||
message: 'bar',
|
||||
stack: 'Error: bar\n stack...bar.js:1:1',
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
buildNumber: 123,
|
||||
kibanaVersion: 'bar',
|
||||
errorInfo$: Rx.of(errorInfoFoo, errorInfoBar),
|
||||
};
|
||||
|
||||
const noop = () => {
|
||||
// noop
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('reloading', () => {
|
||||
it('refreshes the page if a `hashchange` event is emitted', () => {
|
||||
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
|
||||
|
||||
const locationReloadSpy = jest.spyOn(window.location, 'reload').mockImplementation(noop);
|
||||
|
||||
shallow(<FatalErrorsScreen {...defaultProps} />);
|
||||
expect(addEventListenerSpy).toHaveBeenCalledTimes(1);
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith('hashchange', expect.any(Function), undefined);
|
||||
|
||||
expect(locationReloadSpy).not.toHaveBeenCalled();
|
||||
const [, handler] = addEventListenerSpy.mock.calls[0];
|
||||
handler();
|
||||
expect(locationReloadSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
it('render matches snapshot', () => {
|
||||
expect(shallow(<FatalErrorsScreen {...defaultProps} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('rerenders when errorInfo$ emits more errors', () => {
|
||||
const errorInfo$ = new Rx.ReplaySubject<typeof errorInfoFoo>();
|
||||
|
||||
const el = shallow(<FatalErrorsScreen {...defaultProps} errorInfo$={errorInfo$} />);
|
||||
|
||||
expect(el.find(EuiCallOut)).toHaveLength(0);
|
||||
|
||||
errorInfo$.next(errorInfoFoo);
|
||||
el.update(); // allow setState() to cause a render
|
||||
|
||||
expect(el.find(EuiCallOut)).toHaveLength(1);
|
||||
|
||||
errorInfo$.next(errorInfoBar);
|
||||
el.update(); // allow setState() to cause a render
|
||||
|
||||
expect(el.find(EuiCallOut)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buttons', () => {
|
||||
beforeAll(() => {
|
||||
Object.assign(window, {
|
||||
localStorage: {
|
||||
clear: jest.fn(),
|
||||
},
|
||||
sessionStorage: {
|
||||
clear: jest.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete (window as any).localStorage;
|
||||
delete (window as any).sessionStorage;
|
||||
});
|
||||
|
||||
describe('"Clear your session"', () => {
|
||||
it('clears localStorage, sessionStorage, the location.hash, and reloads the page', () => {
|
||||
window.location.hash = '/foo/bar';
|
||||
jest.spyOn(window.location, 'reload').mockImplementation(noop);
|
||||
|
||||
const el = mount(<FatalErrorsScreen {...defaultProps} />);
|
||||
const button = el.find('button').find(testSubjSelector('clearSession'));
|
||||
button.simulate('click');
|
||||
|
||||
expect(window.localStorage.clear).toHaveBeenCalled();
|
||||
expect(window.sessionStorage.clear).toHaveBeenCalled();
|
||||
expect(window.location.reload).toHaveBeenCalled();
|
||||
expect(window.location.hash).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('"Go back"', () => {
|
||||
it('calls window.history.back()', () => {
|
||||
jest.spyOn(window.history, 'back').mockImplementation(noop);
|
||||
|
||||
const el = mount(<FatalErrorsScreen {...defaultProps} />);
|
||||
const button = el.find('button').find(testSubjSelector('goBack'));
|
||||
button.simulate('click');
|
||||
|
||||
expect(window.history.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
147
src/core/public/fatal_errors/fatal_errors_screen.tsx
Normal file
147
src/core/public/fatal_errors/fatal_errors_screen.tsx
Normal file
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
// @ts-ignore EuiCallOut not available until we upgrade to EUI 3.1.0
|
||||
EuiCallOut,
|
||||
// @ts-ignore EuiCodeBlock not available until we upgrade to EUI 3.1.0
|
||||
EuiCodeBlock,
|
||||
// @ts-ignore EuiEmptyPrompt not available until we upgrade to EUI 3.1.0
|
||||
EuiEmptyPrompt,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
import { ErrorInfo } from './get_error_info';
|
||||
|
||||
interface Props {
|
||||
kibanaVersion: string;
|
||||
buildNumber: number;
|
||||
errorInfo$: Rx.Observable<ErrorInfo>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
errors: ErrorInfo[];
|
||||
}
|
||||
|
||||
export class FatalErrorsScreen extends React.Component<Props, State> {
|
||||
public state: State = {
|
||||
errors: [],
|
||||
};
|
||||
|
||||
private subscription?: Rx.Subscription;
|
||||
|
||||
public componentDidMount() {
|
||||
this.subscription = Rx.merge(
|
||||
// reload the page if hash-based navigation is attempted
|
||||
Rx.fromEvent(window, 'hashchange').pipe(
|
||||
tap(() => {
|
||||
window.location.reload();
|
||||
})
|
||||
),
|
||||
|
||||
// consume error notifications and set them to the component state
|
||||
this.props.errorInfo$.pipe(
|
||||
tap(error => {
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
errors: [...state.errors, error],
|
||||
}));
|
||||
})
|
||||
)
|
||||
).subscribe({
|
||||
error(error) {
|
||||
// tslint:disable-next-line no-console
|
||||
console.error('Uncaught error in fatal error screen internals', error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
this.subscription = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<EuiPage style={{ minHeight: '100vh' }}>
|
||||
<EuiPageBody>
|
||||
<EuiPageContent verticalPosition="center" horizontalPosition="center">
|
||||
<EuiEmptyPrompt
|
||||
iconType="alert"
|
||||
iconColor="danger"
|
||||
title={<h2>Something went wrong</h2>}
|
||||
body={
|
||||
<p>
|
||||
Try refreshing the page. If that doesn't work, go back to the previous page or
|
||||
clear your session data.
|
||||
</p>
|
||||
}
|
||||
actions={[
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={this.onClickClearSession}
|
||||
data-test-subj="clearSession"
|
||||
>
|
||||
Clear your session
|
||||
</EuiButton>,
|
||||
<EuiButtonEmpty onClick={this.onClickGoBack} data-test-subj="goBack">
|
||||
Go back
|
||||
</EuiButtonEmpty>,
|
||||
]}
|
||||
/>
|
||||
{this.state.errors.map((error, i) => (
|
||||
<EuiCallOut key={i} title={error.message} color="danger" iconType="alert">
|
||||
<EuiCodeBlock language="bash" className="eui-textBreakAll">
|
||||
{`Version: ${this.props.kibanaVersion}` +
|
||||
'\n' +
|
||||
`Build: ${this.props.buildNumber}` +
|
||||
'\n' +
|
||||
(error.stack ? error.stack : '')}
|
||||
</EuiCodeBlock>
|
||||
</EuiCallOut>
|
||||
))}
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
}
|
||||
|
||||
private onClickGoBack = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
private onClickClearSession = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
window.location.hash = '';
|
||||
window.location.reload();
|
||||
};
|
||||
}
|
128
src/core/public/fatal_errors/fatal_errors_service.test.ts
Normal file
128
src/core/public/fatal_errors/fatal_errors_service.test.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 * as Rx from 'rxjs';
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test: val => val instanceof Rx.Observable,
|
||||
print: () => `Rx.Observable`,
|
||||
});
|
||||
|
||||
const mockRender = jest.fn();
|
||||
jest.mock('react-dom', () => {
|
||||
return {
|
||||
render: mockRender,
|
||||
};
|
||||
});
|
||||
|
||||
import { FatalErrorsService } from './fatal_errors_service';
|
||||
|
||||
function setup() {
|
||||
const rootDomElement = document.createElement('div');
|
||||
|
||||
const injectedMetadata = {
|
||||
getKibanaBuildNumber: jest.fn().mockReturnValue('kibanaBuildNumber'),
|
||||
getKibanaVersion: jest.fn().mockReturnValue('kibanaVersion'),
|
||||
};
|
||||
|
||||
const stopCoreSystem = jest.fn();
|
||||
|
||||
return {
|
||||
rootDomElement,
|
||||
injectedMetadata,
|
||||
stopCoreSystem,
|
||||
fatalErrors: new FatalErrorsService({
|
||||
injectedMetadata: injectedMetadata as any,
|
||||
rootDomElement,
|
||||
stopCoreSystem,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('#add()', () => {
|
||||
it('calls stopCoreSystem() param', () => {
|
||||
const { stopCoreSystem, fatalErrors } = setup();
|
||||
|
||||
expect(stopCoreSystem).not.toHaveBeenCalled();
|
||||
expect(() => {
|
||||
fatalErrors.add(new Error('foo'));
|
||||
}).toThrowError();
|
||||
expect(stopCoreSystem).toHaveBeenCalled();
|
||||
expect(stopCoreSystem).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('deletes all children of rootDomElement and renders <FatalErrorScreen /> into it', () => {
|
||||
const { fatalErrors, rootDomElement } = setup();
|
||||
|
||||
rootDomElement.innerHTML = `
|
||||
<h1>Loading...</h1>
|
||||
<div class="someSpinner"></div>
|
||||
`;
|
||||
|
||||
expect(mockRender).not.toHaveBeenCalled();
|
||||
expect(rootDomElement.children).toHaveLength(2);
|
||||
expect(() => {
|
||||
fatalErrors.add(new Error('foo'));
|
||||
}).toThrowError();
|
||||
expect(rootDomElement).toMatchSnapshot('fatal error screen container');
|
||||
expect(mockRender.mock.calls).toMatchSnapshot('fatal error screen component');
|
||||
});
|
||||
});
|
||||
|
||||
describe('start.add()', () => {
|
||||
it('exposes a function that passes its two arguments to fatalErrors.add()', () => {
|
||||
const { fatalErrors } = setup();
|
||||
|
||||
jest.spyOn(fatalErrors, 'add').mockImplementation(() => {
|
||||
/* noop */
|
||||
});
|
||||
|
||||
expect(fatalErrors.add).not.toHaveBeenCalled();
|
||||
const { add } = fatalErrors.start();
|
||||
add('foo', 'bar');
|
||||
expect(fatalErrors.add).toHaveBeenCalledTimes(1);
|
||||
expect(fatalErrors.add).toHaveBeenCalledWith('foo', 'bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('start.get$()', () => {
|
||||
it('provides info about the errors passed to fatalErrors.add()', () => {
|
||||
const { fatalErrors } = setup();
|
||||
|
||||
const startContract = fatalErrors.start();
|
||||
|
||||
const onError = jest.fn();
|
||||
startContract.get$().subscribe(onError);
|
||||
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
expect(() => {
|
||||
fatalErrors.add(new Error('bar'));
|
||||
}).toThrowError();
|
||||
|
||||
expect(onError).toHaveBeenCalled();
|
||||
expect(onError).toHaveBeenCalledWith({
|
||||
message: 'bar',
|
||||
stack: expect.stringMatching(/Error: bar[\w\W]+fatal_errors_service\.test\.ts/),
|
||||
});
|
||||
});
|
||||
});
|
93
src/core/public/fatal_errors/fatal_errors_service.tsx
Normal file
93
src/core/public/fatal_errors/fatal_errors_service.tsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 } from 'react-dom';
|
||||
import * as Rx from 'rxjs';
|
||||
import { first, tap } from 'rxjs/operators';
|
||||
|
||||
import { InjectedMetadataService } from '../injected_metadata';
|
||||
import { FatalErrorsScreen } from './fatal_errors_screen';
|
||||
import { ErrorInfo, getErrorInfo } from './get_error_info';
|
||||
|
||||
export interface FatalErrorsParams {
|
||||
rootDomElement: HTMLElement;
|
||||
injectedMetadata: InjectedMetadataService;
|
||||
stopCoreSystem: () => void;
|
||||
}
|
||||
|
||||
export class FatalErrorsService {
|
||||
private readonly errorInfo$ = new Rx.ReplaySubject<ErrorInfo>();
|
||||
|
||||
constructor(private params: FatalErrorsParams) {
|
||||
this.errorInfo$.pipe(first(), tap(() => this.onFirstError())).subscribe({
|
||||
error: error => {
|
||||
// tslint:disable-next-line no-console
|
||||
console.error('Uncaught error in fatal error screen internals', error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public add = (error: Error | string, source?: string) => {
|
||||
const errorInfo = getErrorInfo(error, source);
|
||||
|
||||
this.errorInfo$.next(errorInfo);
|
||||
|
||||
if (error instanceof Error) {
|
||||
// make stack traces clickable by putting whole error in the console
|
||||
// tslint:disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
|
||||
public start() {
|
||||
return {
|
||||
add: this.add,
|
||||
get$: () => {
|
||||
return this.errorInfo$.asObservable();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private onFirstError() {
|
||||
// stop the core systems so that things like the legacy platform are stopped
|
||||
// and angular/react components are unmounted;
|
||||
this.params.stopCoreSystem();
|
||||
|
||||
// delete all content in the rootDomElement
|
||||
this.params.rootDomElement.textContent = '';
|
||||
|
||||
// create and mount a container for the <FatalErrorScreen>
|
||||
const container = document.createElement('div');
|
||||
this.params.rootDomElement.appendChild(container);
|
||||
|
||||
render(
|
||||
<FatalErrorsScreen
|
||||
buildNumber={this.params.injectedMetadata.getKibanaBuildNumber()}
|
||||
kibanaVersion={this.params.injectedMetadata.getKibanaVersion()}
|
||||
errorInfo$={this.errorInfo$}
|
||||
/>,
|
||||
container
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type FatalErrorsStartContract = ReturnType<FatalErrorsService['start']>;
|
104
src/core/public/fatal_errors/get_error_info.test.ts
Normal file
104
src/core/public/fatal_errors/get_error_info.test.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 { getErrorInfo } from './get_error_info';
|
||||
|
||||
class StubEsError<T> extends Error {
|
||||
constructor(public resp: T) {
|
||||
super('This is an elasticsearch error');
|
||||
Error.captureStackTrace(this, StubEsError);
|
||||
}
|
||||
}
|
||||
|
||||
it('should prepend the `source` to the message', () => {
|
||||
expect(getErrorInfo('error message', 'unit_test')).toEqual({
|
||||
message: 'unit_test: error message',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle a simple string', () => {
|
||||
expect(getErrorInfo('error message')).toEqual({
|
||||
message: 'error message',
|
||||
});
|
||||
});
|
||||
|
||||
it('reads the message and stack from an Error object', () => {
|
||||
const err = new Error('error message');
|
||||
expect(getErrorInfo(err)).toEqual({
|
||||
message: 'error message',
|
||||
stack: expect.stringContaining(__filename),
|
||||
});
|
||||
});
|
||||
|
||||
it('reads the root cause reason from elasticsearch errors', () => {
|
||||
const err = new StubEsError({
|
||||
error: {
|
||||
root_cause: [
|
||||
{
|
||||
reason: 'I am the detailed message',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getErrorInfo(err, 'foo')).toEqual({
|
||||
message: 'foo: I am the detailed message',
|
||||
stack: expect.stringContaining(__filename),
|
||||
});
|
||||
});
|
||||
|
||||
it('should combine the root cause reasons if elasticsearch error has more than one', () => {
|
||||
const err = new StubEsError({
|
||||
error: {
|
||||
root_cause: [
|
||||
{
|
||||
reason: 'I am the detailed message 1',
|
||||
},
|
||||
{
|
||||
reason: 'I am the detailed message 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getErrorInfo(err)).toEqual({
|
||||
message: 'I am the detailed message 1\nI am the detailed message 2',
|
||||
stack: expect.stringContaining(__filename),
|
||||
});
|
||||
});
|
||||
|
||||
it('should prepend the stack with the error message if it is not already there', () => {
|
||||
const error = new Error('Foo');
|
||||
error.stack = 'bar.js:1:1\nbaz.js:2:1\n';
|
||||
|
||||
expect(getErrorInfo(error)).toEqual({
|
||||
message: 'Foo',
|
||||
stack: 'Error: Foo\nbar.js:1:1\nbaz.js:2:1\n',
|
||||
});
|
||||
});
|
||||
|
||||
it('should just return the stack if it already includes the message', () => {
|
||||
const error = new Error('Foo');
|
||||
error.stack = 'Foo\n bar.js:1:1\n baz.js:2:1\n';
|
||||
|
||||
expect(getErrorInfo(error)).toEqual({
|
||||
message: 'Foo',
|
||||
stack: 'Foo\n bar.js:1:1\n baz.js:2:1\n',
|
||||
});
|
||||
});
|
78
src/core/public/fatal_errors/get_error_info.ts
Normal file
78
src/core/public/fatal_errors/get_error_info.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { inspect } from 'util';
|
||||
|
||||
/**
|
||||
* Produce a string version of an error,
|
||||
*/
|
||||
function formatMessage(error: any) {
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
// stringify undefined/null/0/whatever this falsy value is
|
||||
return inspect(error);
|
||||
}
|
||||
|
||||
// handle es error response with `root_cause`s
|
||||
if (error.resp && error.resp.error && error.resp.error.root_cause) {
|
||||
return error.resp.error.root_cause.map((cause: { reason: string }) => cause.reason).join('\n');
|
||||
}
|
||||
|
||||
// handle http response errors with error messages
|
||||
if (error.body && typeof error.body.message === 'string') {
|
||||
return error.body.message;
|
||||
}
|
||||
|
||||
// handle standard error objects with messages
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
// everything else can just be serialized using util.inspect()
|
||||
return inspect(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the stack trace from a message so that it starts with the message, which
|
||||
* some browsers do automatically and some don't
|
||||
*/
|
||||
function formatStack(err: Error) {
|
||||
if (err.stack && !err.stack.includes(err.message)) {
|
||||
return 'Error: ' + err.message + '\n' + err.stack;
|
||||
}
|
||||
|
||||
return err.stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a simple ErrorInfo object from some error and optional source, used for
|
||||
* displaying error information on the fatal error screen
|
||||
*/
|
||||
export function getErrorInfo(error: any, source?: string) {
|
||||
const prefix = source ? source + ': ' : '';
|
||||
return {
|
||||
message: prefix + formatMessage(error),
|
||||
stack: formatStack(error),
|
||||
};
|
||||
}
|
||||
|
||||
export type ErrorInfo = ReturnType<typeof getErrorInfo>;
|
20
src/core/public/fatal_errors/index.ts
Normal file
20
src/core/public/fatal_errors/index.ts
Normal 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 { FatalErrorsStartContract, FatalErrorsService } from './fatal_errors_service';
|
|
@ -19,23 +19,27 @@
|
|||
|
||||
import { InjectedMetadataService } from './injected_metadata_service';
|
||||
|
||||
describe('#start()', () => {
|
||||
it('deeply freezes its injectedMetadata param', () => {
|
||||
const params = {
|
||||
injectedMetadata: { foo: true } as any,
|
||||
};
|
||||
describe('#getKibanaVersion', () => {
|
||||
it('returns version from injectedMetadata', () => {
|
||||
const injectedMetadata = new InjectedMetadataService({
|
||||
injectedMetadata: {
|
||||
version: 'foo',
|
||||
},
|
||||
} as any);
|
||||
|
||||
const injectedMetadata = new InjectedMetadataService(params);
|
||||
expect(injectedMetadata.getKibanaVersion()).toBe('foo');
|
||||
});
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
params.injectedMetadata.foo = false;
|
||||
}).not.toThrowError();
|
||||
describe('#getKibanaBuildNumber', () => {
|
||||
it('returns buildNumber from injectedMetadata', () => {
|
||||
const injectedMetadata = new InjectedMetadataService({
|
||||
injectedMetadata: {
|
||||
buildNumber: 'foo',
|
||||
},
|
||||
} as any);
|
||||
|
||||
injectedMetadata.start();
|
||||
|
||||
expect(() => {
|
||||
params.injectedMetadata.foo = true;
|
||||
}).toThrowError(`read only property 'foo'`);
|
||||
expect(injectedMetadata.getKibanaBuildNumber()).toBe('foo');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -44,10 +48,29 @@ describe('start.getLegacyMetadata()', () => {
|
|||
const injectedMetadata = new InjectedMetadataService({
|
||||
injectedMetadata: {
|
||||
legacyMetadata: 'foo',
|
||||
} as any,
|
||||
});
|
||||
},
|
||||
} as any);
|
||||
|
||||
const contract = injectedMetadata.start();
|
||||
expect(contract.getLegacyMetadata()).toBe('foo');
|
||||
});
|
||||
|
||||
it('exposes frozen version of legacyMetadata', () => {
|
||||
const injectedMetadata = new InjectedMetadataService({
|
||||
injectedMetadata: {
|
||||
legacyMetadata: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
const legacyMetadata = injectedMetadata.start().getLegacyMetadata();
|
||||
expect(legacyMetadata).toEqual({
|
||||
foo: true,
|
||||
});
|
||||
expect(() => {
|
||||
// @ts-ignore TS knows this shouldn't be possible
|
||||
legacyMetadata.foo = false;
|
||||
}).toThrowError();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,8 @@ import { deepFreeze } from './deep_freeze';
|
|||
|
||||
export interface InjectedMetadataParams {
|
||||
injectedMetadata: {
|
||||
version: string;
|
||||
buildNumber: number;
|
||||
legacyMetadata: {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
@ -34,17 +36,25 @@ export interface InjectedMetadataParams {
|
|||
* and is read from the DOM in most cases.
|
||||
*/
|
||||
export class InjectedMetadataService {
|
||||
private state = deepFreeze(this.params.injectedMetadata);
|
||||
|
||||
constructor(private readonly params: InjectedMetadataParams) {}
|
||||
|
||||
public start() {
|
||||
const state = deepFreeze(this.params.injectedMetadata);
|
||||
|
||||
return {
|
||||
getLegacyMetadata() {
|
||||
return state.legacyMetadata;
|
||||
getLegacyMetadata: () => {
|
||||
return this.state.legacyMetadata;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public getKibanaVersion() {
|
||||
return this.state.version;
|
||||
}
|
||||
|
||||
public getKibanaBuildNumber() {
|
||||
return this.state.buildNumber;
|
||||
}
|
||||
}
|
||||
|
||||
export type InjectedMetadataStartContract = ReturnType<InjectedMetadataService['start']>;
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`#stop() destroys the angular scope and empties the rootDomElement if angular is bootstraped to rootDomElement 1`] = `
|
||||
<div
|
||||
class="ng-scope"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`#stop() does nothing if angular was not bootstrapped to rootDomElement 1`] = `
|
||||
<div>
|
||||
|
||||
|
||||
<h1>
|
||||
foo
|
||||
</h1>
|
||||
|
||||
|
||||
<h2>
|
||||
bar
|
||||
</h2>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -17,6 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import angular from 'angular';
|
||||
|
||||
const mockLoadOrder: string[] = [];
|
||||
|
||||
const mockUiMetadataInit = jest.fn();
|
||||
|
@ -43,8 +45,18 @@ jest.mock('ui/test_harness', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const mockFatalErrorInit = jest.fn();
|
||||
jest.mock('ui/notify/fatal_error', () => {
|
||||
mockLoadOrder.push('ui/notify/fatal_error');
|
||||
return {
|
||||
__newPlatformInit__: mockFatalErrorInit,
|
||||
};
|
||||
});
|
||||
|
||||
import { LegacyPlatformService } from './legacy_platform_service';
|
||||
|
||||
const fatalErrorsStartContract = {} as any;
|
||||
|
||||
const injectedMetadataStartContract = {
|
||||
getLegacyMetadata: jest.fn(),
|
||||
};
|
||||
|
@ -74,6 +86,7 @@ describe('#start()', () => {
|
|||
});
|
||||
|
||||
legacyPlatform.start({
|
||||
fatalErrors: fatalErrorsStartContract,
|
||||
injectedMetadata: injectedMetadataStartContract,
|
||||
});
|
||||
|
||||
|
@ -81,6 +94,20 @@ describe('#start()', () => {
|
|||
expect(mockUiMetadataInit).toHaveBeenCalledWith(legacyMetadata);
|
||||
});
|
||||
|
||||
it('passes fatalErrors service to ui/notify/fatal_errors', () => {
|
||||
const legacyPlatform = new LegacyPlatformService({
|
||||
...defaultParams,
|
||||
});
|
||||
|
||||
legacyPlatform.start({
|
||||
fatalErrors: fatalErrorsStartContract,
|
||||
injectedMetadata: injectedMetadataStartContract,
|
||||
});
|
||||
|
||||
expect(mockFatalErrorInit).toHaveBeenCalledTimes(1);
|
||||
expect(mockFatalErrorInit).toHaveBeenCalledWith(fatalErrorsStartContract);
|
||||
});
|
||||
|
||||
describe('useLegacyTestHarness = false', () => {
|
||||
it('passes the rootDomElement to ui/chrome', () => {
|
||||
const legacyPlatform = new LegacyPlatformService({
|
||||
|
@ -88,6 +115,7 @@ describe('#start()', () => {
|
|||
});
|
||||
|
||||
legacyPlatform.start({
|
||||
fatalErrors: fatalErrorsStartContract,
|
||||
injectedMetadata: injectedMetadataStartContract,
|
||||
});
|
||||
|
||||
|
@ -104,6 +132,7 @@ describe('#start()', () => {
|
|||
});
|
||||
|
||||
legacyPlatform.start({
|
||||
fatalErrors: fatalErrorsStartContract,
|
||||
injectedMetadata: injectedMetadataStartContract,
|
||||
});
|
||||
|
||||
|
@ -124,10 +153,16 @@ describe('#start()', () => {
|
|||
expect(mockLoadOrder).toEqual([]);
|
||||
|
||||
legacyPlatform.start({
|
||||
fatalErrors: fatalErrorsStartContract,
|
||||
injectedMetadata: injectedMetadataStartContract,
|
||||
});
|
||||
|
||||
expect(mockLoadOrder).toEqual(['ui/metadata', 'ui/chrome', 'legacy files']);
|
||||
expect(mockLoadOrder).toEqual([
|
||||
'ui/metadata',
|
||||
'ui/notify/fatal_error',
|
||||
'ui/chrome',
|
||||
'legacy files',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -141,11 +176,64 @@ describe('#start()', () => {
|
|||
expect(mockLoadOrder).toEqual([]);
|
||||
|
||||
legacyPlatform.start({
|
||||
fatalErrors: fatalErrorsStartContract,
|
||||
injectedMetadata: injectedMetadataStartContract,
|
||||
});
|
||||
|
||||
expect(mockLoadOrder).toEqual(['ui/metadata', 'ui/test_harness', 'legacy files']);
|
||||
expect(mockLoadOrder).toEqual([
|
||||
'ui/metadata',
|
||||
'ui/notify/fatal_error',
|
||||
'ui/test_harness',
|
||||
'legacy files',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stop()', () => {
|
||||
it('does nothing if angular was not bootstrapped to rootDomElement', () => {
|
||||
const rootDomElement = document.createElement('div');
|
||||
rootDomElement.innerHTML = `
|
||||
<h1>foo</h1>
|
||||
<h2>bar</h2>
|
||||
`;
|
||||
|
||||
const legacyPlatform = new LegacyPlatformService({
|
||||
...defaultParams,
|
||||
rootDomElement,
|
||||
});
|
||||
|
||||
legacyPlatform.stop();
|
||||
expect(rootDomElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('destroys the angular scope and empties the rootDomElement if angular is bootstraped to rootDomElement', () => {
|
||||
const rootDomElement = document.createElement('div');
|
||||
const scopeDestroySpy = jest.fn();
|
||||
|
||||
const legacyPlatform = new LegacyPlatformService({
|
||||
...defaultParams,
|
||||
rootDomElement,
|
||||
});
|
||||
|
||||
// simulate bootstraping with a module "foo"
|
||||
angular.module('foo', []).directive('bar', () => ({
|
||||
restrict: 'E',
|
||||
link($scope) {
|
||||
$scope.$on('$destroy', scopeDestroySpy);
|
||||
},
|
||||
}));
|
||||
|
||||
rootDomElement.innerHTML = `
|
||||
<bar></bar>
|
||||
`;
|
||||
|
||||
angular.bootstrap(rootDomElement, ['foo']);
|
||||
|
||||
legacyPlatform.stop();
|
||||
|
||||
expect(rootDomElement).toMatchSnapshot();
|
||||
expect(scopeDestroySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,10 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import angular from 'angular';
|
||||
import { FatalErrorsStartContract } from '../fatal_errors';
|
||||
import { InjectedMetadataStartContract } from '../injected_metadata';
|
||||
|
||||
interface Deps {
|
||||
injectedMetadata: InjectedMetadataStartContract;
|
||||
fatalErrors: FatalErrorsStartContract;
|
||||
}
|
||||
|
||||
export interface LegacyPlatformParams {
|
||||
|
@ -39,10 +42,11 @@ export interface LegacyPlatformParams {
|
|||
export class LegacyPlatformService {
|
||||
constructor(private readonly params: LegacyPlatformParams) {}
|
||||
|
||||
public start({ injectedMetadata }: Deps) {
|
||||
public start({ injectedMetadata, fatalErrors }: 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());
|
||||
require('ui/notify/fatal_error').__newPlatformInit__(fatalErrors);
|
||||
|
||||
// Load the bootstrap module before loading the legacy platform files so that
|
||||
// the bootstrap module can modify the environment a bit first
|
||||
|
@ -54,6 +58,23 @@ export class LegacyPlatformService {
|
|||
bootstrapModule.bootstrap(this.params.rootDomElement);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
const angularRoot = angular.element(this.params.rootDomElement);
|
||||
const injector$ = angularRoot.injector();
|
||||
|
||||
// if we haven't gotten to the point of bootstraping
|
||||
// angular, injector$ won't be defined
|
||||
if (!injector$) {
|
||||
return;
|
||||
}
|
||||
|
||||
// destroy the root angular scope
|
||||
injector$.get('$rootScope').$destroy();
|
||||
|
||||
// clear the inner html of the root angular element
|
||||
this.params.rootDomElement.textContent = '';
|
||||
}
|
||||
|
||||
private loadBootstrapModule(): {
|
||||
bootstrap: (rootDomElement: HTMLElement) => void;
|
||||
} {
|
||||
|
|
|
@ -34,7 +34,7 @@ import { timefilter } from 'ui/timefilter';
|
|||
import 'ui/share';
|
||||
import 'ui/query_bar';
|
||||
import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier';
|
||||
import { toastNotifications, getPainlessError } from 'ui/notify';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { VisProvider } from 'ui/vis';
|
||||
import { BasicResponseHandlerProvider } from 'ui/vis/response_handlers/basic';
|
||||
import { DocTitleProvider } from 'ui/doc_title';
|
||||
|
@ -53,6 +53,7 @@ import { visualizationLoader } from 'ui/visualize/loader/visualization_loader';
|
|||
import { recentlyAccessed } from 'ui/persisted_log';
|
||||
import { getDocLink } from 'ui/documentation_links';
|
||||
import '../components/fetch_error';
|
||||
import { getPainlessError } from './get_painless_error';
|
||||
|
||||
const app = uiModules.get('apps/discover', [
|
||||
'kibana/notify',
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { get } from 'lodash';
|
||||
|
||||
export function getPainlessError(error: Error) {
|
||||
const rootCause: Array<{ lang: string; script: string }> | undefined = get(
|
||||
error,
|
||||
'resp.error.root_cause'
|
||||
);
|
||||
|
||||
if (!rootCause) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [{ lang, script }] = rootCause;
|
||||
|
||||
if (lang !== 'painless') {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
lang,
|
||||
script,
|
||||
message: `Error with Painless scripted field '${script}'`,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
|
@ -74,6 +74,8 @@ const legacyMetadata = {
|
|||
|
||||
new CoreSystem({
|
||||
injectedMetadata: {
|
||||
version: legacyMetadata.version,
|
||||
buildNumber: legacyMetadata.buildNum,
|
||||
legacyMetadata
|
||||
},
|
||||
rootDomElement: document.body,
|
||||
|
|
|
@ -21,6 +21,7 @@ export default {
|
|||
rootDir: '../../..',
|
||||
roots: [
|
||||
'<rootDir>/src/ui',
|
||||
'<rootDir>/src/core',
|
||||
'<rootDir>/src/core_plugins',
|
||||
'<rootDir>/src/server',
|
||||
'<rootDir>/src/cli',
|
||||
|
@ -30,7 +31,6 @@ export default {
|
|||
'<rootDir>/src/utils',
|
||||
'<rootDir>/src/setup_node_env',
|
||||
'<rootDir>/packages',
|
||||
'<rootDir>/src/core',
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
'packages/kbn-ui-framework/src/components/**/*.js',
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
* 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 _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import { metadata } from '../metadata';
|
||||
import { formatMsg, formatStack } from './lib';
|
||||
import fatalSplashScreen from './partials/fatal_splash_screen.html';
|
||||
import { callEach } from '../utils/function';
|
||||
|
||||
const {
|
||||
version,
|
||||
buildNum,
|
||||
} = metadata;
|
||||
|
||||
// used to identify the first call to fatal, set to false there
|
||||
let firstFatal = true;
|
||||
|
||||
const fatalToastTemplate = (function lazyTemplate(tmpl) {
|
||||
let compiled;
|
||||
return function (vars) {
|
||||
return (compiled || (compiled = _.template(tmpl)))(vars);
|
||||
};
|
||||
}(require('./partials/fatal.html')));
|
||||
|
||||
// to be notified when the first fatal error occurs, push a function into this array.
|
||||
const fatalCallbacks = [];
|
||||
|
||||
export const addFatalErrorCallback = callback => {
|
||||
fatalCallbacks.push(callback);
|
||||
};
|
||||
|
||||
function formatInfo() {
|
||||
const info = [];
|
||||
|
||||
if (!_.isUndefined(version)) {
|
||||
info.push(`Version: ${version}`);
|
||||
}
|
||||
|
||||
if (!_.isUndefined(buildNum)) {
|
||||
info.push(`Build: ${buildNum}`);
|
||||
}
|
||||
|
||||
return info.join('\n');
|
||||
}
|
||||
|
||||
// We're exporting this because state_management/state.js calls fatalError, which makes it
|
||||
// impossible to test unless we stub this stuff out.
|
||||
export const fatalErrorInternals = {
|
||||
show: (err, location) => {
|
||||
if (firstFatal) {
|
||||
callEach(fatalCallbacks);
|
||||
firstFatal = false;
|
||||
window.addEventListener('hashchange', function () {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
const html = fatalToastTemplate({
|
||||
info: formatInfo(),
|
||||
msg: formatMsg(err, location),
|
||||
stack: formatStack(err)
|
||||
});
|
||||
|
||||
let $container = $('#fatal-splash-screen');
|
||||
|
||||
if (!$container.length) {
|
||||
$(document.body)
|
||||
// in case the app has not completed boot
|
||||
.removeAttr('ng-cloak')
|
||||
.html(fatalSplashScreen);
|
||||
|
||||
$container = $('#fatal-splash-screen');
|
||||
}
|
||||
|
||||
$container.append(html);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Kill the page, display an error, then throw the error.
|
||||
* Used as a last-resort error back in many promise chains
|
||||
* so it rethrows the error that's displayed on the page.
|
||||
*
|
||||
* @param {Error} err - The error that occured
|
||||
*/
|
||||
export function fatalError(err, location) {
|
||||
fatalErrorInternals.show(err, location);
|
||||
console.error(err.stack); // eslint-disable-line no-console
|
||||
|
||||
throw err;
|
||||
}
|
50
src/ui/public/notify/fatal_error.ts
Normal file
50
src/ui/public/notify/fatal_error.ts
Normal 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 { FatalErrorsStartContract } from '../../../core/public/fatal_errors';
|
||||
import {
|
||||
AngularHttpError,
|
||||
formatAngularHttpError,
|
||||
isAngularHttpError,
|
||||
} from './lib/format_angular_http_error';
|
||||
|
||||
let newPlatformFatalErrors: FatalErrorsStartContract;
|
||||
|
||||
export function __newPlatformInit__(instance: FatalErrorsStartContract) {
|
||||
if (newPlatformFatalErrors) {
|
||||
throw new Error('ui/notify/fatal_error already initialized with new platform apis');
|
||||
}
|
||||
|
||||
newPlatformFatalErrors = instance;
|
||||
}
|
||||
|
||||
export function addFatalErrorCallback(callback: () => void) {
|
||||
newPlatformFatalErrors.get$().subscribe(() => {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
export function fatalError(error: AngularHttpError | Error | string, location?: string) {
|
||||
// add support for angular http errors to newPlatformFatalErrors
|
||||
if (isAngularHttpError(error)) {
|
||||
error = formatAngularHttpError(error);
|
||||
}
|
||||
|
||||
newPlatformFatalErrors.add(error, location);
|
||||
}
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
export { notify } from './notify';
|
||||
export { Notifier } from './notifier';
|
||||
export { getPainlessError } from './lib';
|
||||
export { fatalError, fatalErrorInternals, addFatalErrorCallback } from './fatal_error';
|
||||
export { fatalError, addFatalErrorCallback } from './fatal_error';
|
||||
export { GlobalToastList, toastNotifications } from './toasts';
|
||||
export { GlobalBannerList, banners } from './banners';
|
||||
|
|
46
src/ui/public/notify/lib/format_angular_http_error.ts
Normal file
46
src/ui/public/notify/lib/format_angular_http_error.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { IHttpResponse } from 'angular';
|
||||
|
||||
export type AngularHttpError = IHttpResponse<{ message: string }>;
|
||||
|
||||
export function isAngularHttpError(error: any): error is AngularHttpError {
|
||||
return (
|
||||
error &&
|
||||
typeof error.status === 'number' &&
|
||||
typeof error.statusText === 'string' &&
|
||||
error.data &&
|
||||
typeof error.data.message === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
export function formatAngularHttpError(error: AngularHttpError) {
|
||||
// is an Angular $http "error object"
|
||||
if (error.status === -1) {
|
||||
// status = -1 indicates that the request was failed to reach the server
|
||||
return (
|
||||
'An HTTP request has failed to connect. ' +
|
||||
'Please check if the Kibana server is running and that your browser has a working connection, ' +
|
||||
'or contact your system administrator.'
|
||||
);
|
||||
}
|
||||
|
||||
return `Error ${error.status} ${error.statusText}: ${error.data.message}`;
|
||||
}
|
|
@ -36,24 +36,3 @@ export const formatESMsg = (err) => {
|
|||
const result = _.pluck(rootCause, 'reason').join('\n');
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getPainlessError = (err) => {
|
||||
const rootCause = getRootCause(err);
|
||||
|
||||
if (!rootCause) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { lang, script } = rootCause[0];
|
||||
|
||||
if (lang !== 'painless') {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
lang,
|
||||
script,
|
||||
message: `Error with Painless scripted field '${script}'`,
|
||||
error: err.message,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { formatESMsg, getPainlessError } from './format_es_msg';
|
||||
export { formatESMsg } from './format_es_msg';
|
||||
export { formatMsg } from './format_msg';
|
||||
export { formatStack } from './format_stack';
|
||||
export { isAngularHttpError, formatAngularHttpError } from './format_angular_http_error';
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
<!--
|
||||
!!!!
|
||||
Since fatal error could prevent angular from starting
|
||||
this template is just a simple lodash template
|
||||
!!!!
|
||||
-->
|
||||
<h1><i class="fa fa-warning-triangle"></i></h1>
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading">
|
||||
<h1 class="panel-title">
|
||||
<i class="fa fa-warning"></i> Fatal Error
|
||||
</h1>
|
||||
</div>
|
||||
<div class="panel-body fatal-body"><%- msg %></div>
|
||||
<% if (info) { %>
|
||||
<div class="panel-footer"><pre><%- info %></pre></div>
|
||||
<% } %>
|
||||
<% if (stack) { %>
|
||||
<div class="panel-footer"><pre><%- stack %></pre></div>
|
||||
<% } %>
|
||||
</div>
|
|
@ -1,35 +0,0 @@
|
|||
<div
|
||||
id="fatal-splash-screen-header"
|
||||
class="kuiViewContent kuiViewContent--constrainedWidth kuiViewContentItem"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<h1 class="kuiTitle kuiVerticalRhythm">
|
||||
Oops!
|
||||
</h1>
|
||||
|
||||
<p class="kuiText kuiVerticalRhythm">
|
||||
Looks like something went wrong. Refreshing may do the trick.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="kuiButtonGroup kuiVerticalRhythm"
|
||||
style="text-align: center; display: inline-block;"
|
||||
>
|
||||
<button
|
||||
class="kuiButton kuiButton--primary"
|
||||
onclick="window.history.back();"
|
||||
>
|
||||
Go back
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="kuiButton kuiButton--hollow"
|
||||
onclick="localStorage.clear(); sessionStorage.clear(); window.location.hash = ''; window.location.reload();"
|
||||
>
|
||||
Clear your session
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fatal-splash-screen">
|
||||
</div>
|
||||
</div>
|
|
@ -22,7 +22,8 @@ import expect from 'expect.js';
|
|||
import ngMock from 'ng_mock';
|
||||
import { encode as encodeRison } from 'rison-node';
|
||||
import '../../private';
|
||||
import { fatalErrorInternals, toastNotifications } from '../../notify';
|
||||
import { toastNotifications } from '../../notify';
|
||||
import * as FatalErrorNS from '../../notify/fatal_error';
|
||||
import { StateProvider } from '../state';
|
||||
import {
|
||||
unhashQueryString,
|
||||
|
@ -36,6 +37,9 @@ import StubBrowserStorage from 'test_utils/stub_browser_storage';
|
|||
import { EventsProvider } from '../../events';
|
||||
|
||||
describe('State Management', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
afterEach(() => sandbox.restore());
|
||||
|
||||
describe('Enabled', () => {
|
||||
let $rootScope;
|
||||
let $location;
|
||||
|
@ -289,13 +293,16 @@ describe('State Management', () => {
|
|||
expect(toastNotifications.list[0].title).to.match(/use the share functionality/i);
|
||||
});
|
||||
|
||||
it('throws error linking to github when setting item fails', () => {
|
||||
it('triggers fatal error linking to github when setting item fails', () => {
|
||||
const { state, hashedItemStore } = setup({ storeInHash: true });
|
||||
sinon.stub(fatalErrorInternals, 'show');
|
||||
const fatalErrorStub = sandbox.stub(FatalErrorNS, 'fatalError');
|
||||
sinon.stub(hashedItemStore, 'setItem').returns(false);
|
||||
expect(() => {
|
||||
state.toQueryParam();
|
||||
}).to.throwError(/github\.com/);
|
||||
state.toQueryParam();
|
||||
sinon.assert.calledOnce(fatalErrorStub);
|
||||
sinon.assert.calledWith(fatalErrorStub, sinon.match(error => (
|
||||
error instanceof Error &&
|
||||
error.message.includes('github.com'))
|
||||
));
|
||||
});
|
||||
|
||||
it('translateHashToRison should gracefully fallback if parameter can not be parsed', () => {
|
||||
|
|
|
@ -151,6 +151,8 @@ export function uiRenderMixin(kbnServer, server, config) {
|
|||
i18n: (id, options) => i18n.translate(id, options),
|
||||
|
||||
injectedMetadata: {
|
||||
version: kbnServer.version,
|
||||
buildNumber: config.get('pkg.buildNum'),
|
||||
legacyMetadata: await getLegacyKibanaPayload({
|
||||
app,
|
||||
request,
|
||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -289,6 +289,10 @@
|
|||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.0.1.tgz#c10703020369602c40dd9428cc6e1437027116df"
|
||||
|
||||
"@types/cheerio@*":
|
||||
version "0.22.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.8.tgz#5702f74f78b73e13f1eb1bd435c2c9de61a250d4"
|
||||
|
||||
"@types/classnames@^2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.3.tgz#3f0ff6873da793870e20a260cada55982f38a9e5"
|
||||
|
@ -301,6 +305,13 @@
|
|||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901"
|
||||
|
||||
"@types/enzyme@^3.1.12":
|
||||
version "3.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.12.tgz#293bb07c1ef5932d37add3879e72e0f5bc614f3c"
|
||||
dependencies:
|
||||
"@types/cheerio" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/eslint@^4.16.2":
|
||||
version "4.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-4.16.2.tgz#30f4f026019eb78a6ef12f276b75cd16ea2afb27"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue