* Initial move of public and setup of server skeleton * Fix public paths and types * Use new usage stats dependency directly in tracker also mark as an optional dependency * WiP on getting server side working * Restore proxy route behaviour for base case, still need to test custom proxy and SSL * Add new type and lib files * Clean up legacy start up code and add comment about issue in kibana.yml config for console * Move console_extensions to new platform and introduce ConsoleSetup API for extending autocomplete Add TODO regarding exposing legacy ES config * Re-introduce injected elasticsearch variable and use it in public * Don't pass stateSetter prop through to checkbox * Refactor of proxy route (split into separate files). Easier testing for now. Refactor file name of request.ts -> proxy_request.ts. This is consistent with the exported function now Started fixing server side tests for the proxy route - Migrated away from sinon - Completed the body.js -> body.test.ts. Still have to do the rest * headers.js test -> headers.test.ts and moved some of the proxy route mocking logic to a common space * Finish migration of rest of proxy route test away from hapi Add test for custom route validation * Bring console application in line with https://github.com/elastic/kibana/blob/master/src/core/CONVENTIONS.md#applications Change log from info level to debug level for console_extensions plugin * Update i18nrc file for console * Add setHeaders when passing back error response Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> # Conflicts: # .github/CODEOWNERS # src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js # src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js # src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_context.json # x-pack/plugins/console_extensions/server/spec/overrides/security.delete_privileges.json # x-pack/plugins/console_extensions/server/spec/overrides/security.put_privileges.json
This commit is contained in:
parent
66066c3bab
commit
077a37bc19
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"paths": {
|
||||
"common.ui": "src/legacy/ui",
|
||||
"console": "src/legacy/core_plugins/console",
|
||||
"console": "src/plugins/console",
|
||||
"core": "src/core",
|
||||
"dashboardEmbeddableContainer": "src/plugins/dashboard_embeddable_container",
|
||||
"data": [
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import { ResponseObject as HapiResponseObject, ResponseToolkit as HapiResponseToolkit } from 'hapi';
|
||||
import typeDetect from 'type-detect';
|
||||
import Boom from 'boom';
|
||||
import * as stream from 'stream';
|
||||
|
||||
import {
|
||||
HttpResponsePayload,
|
||||
|
@ -112,8 +113,18 @@ export class HapiResponseAdapter {
|
|||
return response;
|
||||
}
|
||||
|
||||
private toError(kibanaResponse: KibanaResponse<ResponseError>) {
|
||||
private toError(kibanaResponse: KibanaResponse<ResponseError | Buffer | stream.Readable>) {
|
||||
const { payload } = kibanaResponse;
|
||||
|
||||
// Special case for when we are proxying requests and want to enable streaming back error responses opaquely.
|
||||
if (Buffer.isBuffer(payload) || payload instanceof stream.Readable) {
|
||||
const response = this.responseToolkit
|
||||
.response(kibanaResponse.payload)
|
||||
.code(kibanaResponse.status);
|
||||
setHeaders(response, kibanaResponse.options.headers);
|
||||
return response;
|
||||
}
|
||||
|
||||
// we use for BWC with Boom payload for error responses - {error: string, message: string, statusCode: string}
|
||||
const error = new Boom('', {
|
||||
statusCode: kibanaResponse.status,
|
||||
|
|
|
@ -1,75 +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 { Deprecations } from '../../../deprecation';
|
||||
import expect from '@kbn/expect';
|
||||
import index from '../index';
|
||||
import { noop } from 'lodash';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('plugins/console', function() {
|
||||
describe('#deprecate()', function() {
|
||||
let transformDeprecations;
|
||||
|
||||
before(function() {
|
||||
const Plugin = function(options) {
|
||||
this.deprecations = options.deprecations;
|
||||
};
|
||||
|
||||
const plugin = index({ Plugin });
|
||||
|
||||
const deprecations = plugin.deprecations(Deprecations);
|
||||
transformDeprecations = (settings, log = noop) => {
|
||||
deprecations.forEach(deprecation => deprecation(settings, log));
|
||||
};
|
||||
});
|
||||
|
||||
describe('proxyConfig', function() {
|
||||
it('leaves the proxyConfig settings', function() {
|
||||
const proxyConfigOne = {};
|
||||
const proxyConfigTwo = {};
|
||||
const settings = {
|
||||
proxyConfig: [proxyConfigOne, proxyConfigTwo],
|
||||
};
|
||||
|
||||
transformDeprecations(settings);
|
||||
expect(settings.proxyConfig[0]).to.be(proxyConfigOne);
|
||||
expect(settings.proxyConfig[1]).to.be(proxyConfigTwo);
|
||||
});
|
||||
|
||||
it('logs a warning when proxyConfig is specified', function() {
|
||||
const settings = {
|
||||
proxyConfig: [],
|
||||
};
|
||||
|
||||
const log = sinon.spy();
|
||||
transformDeprecations(settings, log);
|
||||
expect(log.calledOnce).to.be(true);
|
||||
});
|
||||
|
||||
it(`doesn't log a warning when proxyConfig isn't specified`, function() {
|
||||
const settings = {};
|
||||
|
||||
const log = sinon.spy();
|
||||
transformDeprecations(settings, log);
|
||||
expect(log.called).to.be(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,187 +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 Boom from 'boom';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { resolve, join } from 'path';
|
||||
import url from 'url';
|
||||
import { has, isEmpty, head, pick } from 'lodash';
|
||||
|
||||
// @ts-ignore
|
||||
import { addProcessorDefinition } from './server/api_server/es_6_0/ingest';
|
||||
// @ts-ignore
|
||||
import { resolveApi } from './server/api_server/server';
|
||||
// @ts-ignore
|
||||
import { addExtensionSpecFilePath } from './server/api_server/spec';
|
||||
// @ts-ignore
|
||||
import { setHeaders } from './server/set_headers';
|
||||
// @ts-ignore
|
||||
import { ProxyConfigCollection, getElasticsearchProxyConfig, createProxyRoute } from './server';
|
||||
|
||||
function filterHeaders(originalHeaders: any, headersToKeep: any) {
|
||||
const normalizeHeader = function(header: any) {
|
||||
if (!header) {
|
||||
return '';
|
||||
}
|
||||
header = header.toString();
|
||||
return header.trim().toLowerCase();
|
||||
};
|
||||
|
||||
// Normalize list of headers we want to allow in upstream request
|
||||
const headersToKeepNormalized = headersToKeep.map(normalizeHeader);
|
||||
|
||||
return pick(originalHeaders, headersToKeepNormalized);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
export default function(kibana: any) {
|
||||
const npSrc = resolve(__dirname, 'public/np_ready');
|
||||
|
||||
let defaultVars: any;
|
||||
return new kibana.Plugin({
|
||||
id: 'console',
|
||||
require: ['elasticsearch'],
|
||||
|
||||
config(Joi: any) {
|
||||
return Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
proxyFilter: Joi.array()
|
||||
.items(Joi.string())
|
||||
.single()
|
||||
.default(['.*']),
|
||||
ssl: Joi.object({
|
||||
verify: Joi.boolean(),
|
||||
}).default(),
|
||||
proxyConfig: Joi.array()
|
||||
.items(
|
||||
Joi.object().keys({
|
||||
match: Joi.object().keys({
|
||||
protocol: Joi.string().default('*'),
|
||||
host: Joi.string().default('*'),
|
||||
port: Joi.string().default('*'),
|
||||
path: Joi.string().default('*'),
|
||||
}),
|
||||
|
||||
timeout: Joi.number(),
|
||||
ssl: Joi.object()
|
||||
.keys({
|
||||
verify: Joi.boolean(),
|
||||
ca: Joi.array()
|
||||
.single()
|
||||
.items(Joi.string()),
|
||||
cert: Joi.string(),
|
||||
key: Joi.string(),
|
||||
})
|
||||
.default(),
|
||||
})
|
||||
)
|
||||
.default(),
|
||||
}).default();
|
||||
},
|
||||
|
||||
deprecations() {
|
||||
return [
|
||||
(settings: any, log: any) => {
|
||||
if (has(settings, 'proxyConfig')) {
|
||||
log(
|
||||
'Config key "proxyConfig" is deprecated. Configuration can be inferred from the "elasticsearch" settings'
|
||||
);
|
||||
}
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
uiCapabilities() {
|
||||
return {
|
||||
dev_tools: {
|
||||
show: true,
|
||||
save: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
async init(server: any, options: any) {
|
||||
server.expose('addExtensionSpecFilePath', addExtensionSpecFilePath);
|
||||
server.expose('addProcessorDefinition', addProcessorDefinition);
|
||||
|
||||
if (options.ssl && options.ssl.verify) {
|
||||
throw new Error('sense.ssl.verify is no longer supported.');
|
||||
}
|
||||
|
||||
const config = server.config();
|
||||
const legacyEsConfig = await server.newPlatform.__internals.elasticsearch.legacy.config$
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const proxyConfigCollection = new ProxyConfigCollection(options.proxyConfig);
|
||||
const proxyPathFilters = options.proxyFilter.map((str: string) => new RegExp(str));
|
||||
|
||||
defaultVars = {
|
||||
elasticsearchUrl: url.format(
|
||||
Object.assign(url.parse(head(legacyEsConfig.hosts)), { auth: false })
|
||||
),
|
||||
};
|
||||
|
||||
server.route(
|
||||
createProxyRoute({
|
||||
hosts: legacyEsConfig.hosts,
|
||||
pathFilters: proxyPathFilters,
|
||||
getConfigForReq(req: any, uri: any) {
|
||||
const filteredHeaders = filterHeaders(
|
||||
req.headers,
|
||||
legacyEsConfig.requestHeadersWhitelist
|
||||
);
|
||||
const headers = setHeaders(filteredHeaders, legacyEsConfig.customHeaders);
|
||||
|
||||
if (!isEmpty(config.get('console.proxyConfig'))) {
|
||||
return {
|
||||
...proxyConfigCollection.configForUri(uri),
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...getElasticsearchProxyConfig(legacyEsConfig),
|
||||
headers,
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
server.route({
|
||||
path: '/api/console/api_server',
|
||||
method: ['GET', 'POST'],
|
||||
handler(req: any, h: any) {
|
||||
const { sense_version: version, apis } = req.query;
|
||||
if (!apis) {
|
||||
throw Boom.badRequest('"apis" is a required param.');
|
||||
}
|
||||
|
||||
return resolveApi(version, apis.split(','), h);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
uiExports: {
|
||||
devTools: [resolve(__dirname, 'public/legacy')],
|
||||
styleSheetPaths: resolve(npSrc, 'application/styles/index.scss'),
|
||||
injectDefaultVars: () => defaultVars,
|
||||
noParse: [join(npSrc, 'application/models/legacy_core_editor/mode/worker/worker.js')],
|
||||
},
|
||||
} as any);
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"author": "Boaz Leskes <boaz@elastic.co>",
|
||||
"contributors": [
|
||||
"Spencer Alger <spencer.alger@elastic.co>"
|
||||
],
|
||||
"name": "console",
|
||||
"version": "kibana"
|
||||
}
|
|
@ -1,53 +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 { npSetup, npStart } from 'ui/new_platform';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import chrome from 'ui/chrome';
|
||||
import { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
|
||||
|
||||
import { plugin } from './np_ready';
|
||||
import { DevToolsSetup } from '../../../../plugins/dev_tools/public';
|
||||
import { HomePublicPluginSetup } from '../../../../plugins/home/public';
|
||||
import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public';
|
||||
|
||||
export interface XPluginSet {
|
||||
usageCollection: UsageCollectionSetup;
|
||||
dev_tools: DevToolsSetup;
|
||||
home: HomePublicPluginSetup;
|
||||
__LEGACY: {
|
||||
I18nContext: any;
|
||||
elasticsearchUrl: string;
|
||||
category: FeatureCatalogueCategory;
|
||||
};
|
||||
}
|
||||
|
||||
const pluginInstance = plugin({} as any);
|
||||
|
||||
(async () => {
|
||||
await pluginInstance.setup(npSetup.core, {
|
||||
...npSetup.plugins,
|
||||
__LEGACY: {
|
||||
elasticsearchUrl: chrome.getInjected('elasticsearchUrl'),
|
||||
I18nContext,
|
||||
category: FeatureCatalogueCategory.ADMIN,
|
||||
},
|
||||
});
|
||||
await pluginInstance.start(npStart.core);
|
||||
})();
|
|
@ -1,96 +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 { request } from 'http';
|
||||
|
||||
import sinon from 'sinon';
|
||||
import expect from '@kbn/expect';
|
||||
import { Server } from 'hapi';
|
||||
import * as requestModule from '../../request';
|
||||
|
||||
import { createProxyRoute } from '../../';
|
||||
|
||||
import { createResponseStub } from './stubs';
|
||||
|
||||
describe('Console Proxy Route', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
const teardowns = [];
|
||||
let setup;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(requestModule, 'sendRequest').callsFake(createResponseStub());
|
||||
|
||||
setup = () => {
|
||||
const server = new Server();
|
||||
server.route(
|
||||
createProxyRoute({
|
||||
hosts: ['http://localhost:9200'],
|
||||
})
|
||||
);
|
||||
|
||||
teardowns.push(() => server.stop());
|
||||
|
||||
return { server };
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
sandbox.restore();
|
||||
await Promise.all(teardowns.splice(0).map(fn => fn()));
|
||||
});
|
||||
|
||||
describe('headers', function() {
|
||||
this.timeout(Infinity);
|
||||
|
||||
it('forwards the remote header info', async () => {
|
||||
const { server } = setup();
|
||||
await server.start();
|
||||
|
||||
const resp = await new Promise(resolve => {
|
||||
request(
|
||||
{
|
||||
protocol: server.info.protocol + ':',
|
||||
host: server.info.address,
|
||||
port: server.info.port,
|
||||
method: 'POST',
|
||||
path: '/api/console/proxy?method=GET&path=/',
|
||||
},
|
||||
resolve
|
||||
).end();
|
||||
});
|
||||
|
||||
resp.destroy();
|
||||
|
||||
sinon.assert.calledOnce(requestModule.sendRequest);
|
||||
const { headers } = requestModule.sendRequest.getCall(0).args[0];
|
||||
expect(headers)
|
||||
.to.have.property('x-forwarded-for')
|
||||
.and.not.be('');
|
||||
expect(headers)
|
||||
.to.have.property('x-forwarded-port')
|
||||
.and.not.be('');
|
||||
expect(headers)
|
||||
.to.have.property('x-forwarded-proto')
|
||||
.and.not.be('');
|
||||
expect(headers)
|
||||
.to.have.property('x-forwarded-host')
|
||||
.and.not.be('');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,170 +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 { Agent } from 'http';
|
||||
|
||||
import sinon from 'sinon';
|
||||
import * as requestModule from '../../request';
|
||||
import expect from '@kbn/expect';
|
||||
import { Server } from 'hapi';
|
||||
|
||||
import { createProxyRoute } from '../../';
|
||||
|
||||
import { createResponseStub } from './stubs';
|
||||
|
||||
describe('Console Proxy Route', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
const teardowns = [];
|
||||
let setup;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(requestModule, 'sendRequest').callsFake(createResponseStub());
|
||||
|
||||
setup = () => {
|
||||
const server = new Server();
|
||||
teardowns.push(() => server.stop());
|
||||
return { server };
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
sandbox.restore();
|
||||
await Promise.all(teardowns.splice(0).map(fn => fn()));
|
||||
});
|
||||
|
||||
describe('params', () => {
|
||||
describe('pathFilters', () => {
|
||||
describe('no matches', () => {
|
||||
it('rejects with 403', async () => {
|
||||
const { server } = setup();
|
||||
server.route(
|
||||
createProxyRoute({
|
||||
pathFilters: [/^\/foo\//, /^\/bar\//],
|
||||
})
|
||||
);
|
||||
|
||||
const { statusCode } = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/console/proxy?method=GET&path=/baz/type/id',
|
||||
});
|
||||
|
||||
expect(statusCode).to.be(403);
|
||||
});
|
||||
});
|
||||
describe('one match', () => {
|
||||
it('allows the request', async () => {
|
||||
const { server } = setup();
|
||||
server.route(
|
||||
createProxyRoute({
|
||||
hosts: ['http://localhost:9200'],
|
||||
pathFilters: [/^\/foo\//, /^\/bar\//],
|
||||
})
|
||||
);
|
||||
|
||||
const { statusCode } = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/console/proxy?method=GET&path=/foo/type/id',
|
||||
});
|
||||
|
||||
expect(statusCode).to.be(200);
|
||||
sinon.assert.calledOnce(requestModule.sendRequest);
|
||||
});
|
||||
});
|
||||
describe('all match', () => {
|
||||
it('allows the request', async () => {
|
||||
const { server } = setup();
|
||||
server.route(
|
||||
createProxyRoute({
|
||||
hosts: ['http://localhost:9200'],
|
||||
pathFilters: [/^\/foo\//, /^\/bar\//],
|
||||
})
|
||||
);
|
||||
|
||||
const { statusCode } = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/console/proxy?method=GET&path=/foo/type/id',
|
||||
});
|
||||
|
||||
expect(statusCode).to.be(200);
|
||||
sinon.assert.calledOnce(requestModule.sendRequest);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfigForReq()', () => {
|
||||
it('passes the request and targeted uri', async () => {
|
||||
const { server } = setup();
|
||||
|
||||
const getConfigForReq = sinon.stub().returns({});
|
||||
|
||||
server.route(createProxyRoute({ hosts: ['http://localhost:9200'], getConfigForReq }));
|
||||
await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/console/proxy?method=HEAD&path=/index/type/id',
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(getConfigForReq);
|
||||
const args = getConfigForReq.getCall(0).args;
|
||||
expect(args[0]).to.have.property('path', '/api/console/proxy');
|
||||
expect(args[0]).to.have.property('method', 'post');
|
||||
expect(args[0])
|
||||
.to.have.property('query')
|
||||
.eql({ method: 'HEAD', path: '/index/type/id' });
|
||||
expect(args[1]).to.be('http://localhost:9200/index/type/id?pretty=true');
|
||||
});
|
||||
|
||||
it('sends the returned timeout, agent, and base headers to request', async () => {
|
||||
const { server } = setup();
|
||||
|
||||
const timeout = Math.round(Math.random() * 10000);
|
||||
const agent = new Agent();
|
||||
const rejectUnauthorized = !!Math.round(Math.random());
|
||||
const headers = {
|
||||
foo: 'bar',
|
||||
baz: 'bop',
|
||||
};
|
||||
|
||||
server.route(
|
||||
createProxyRoute({
|
||||
hosts: ['http://localhost:9200'],
|
||||
getConfigForReq: () => ({
|
||||
timeout,
|
||||
agent,
|
||||
headers,
|
||||
rejectUnauthorized,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/console/proxy?method=HEAD&path=/index/type/id',
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(requestModule.sendRequest);
|
||||
const opts = requestModule.sendRequest.getCall(0).args[0];
|
||||
expect(opts).to.have.property('timeout', timeout);
|
||||
expect(opts).to.have.property('agent', agent);
|
||||
expect(opts).to.have.property('rejectUnauthorized', rejectUnauthorized);
|
||||
expect(opts.headers).to.have.property('foo', 'bar');
|
||||
expect(opts.headers).to.have.property('baz', 'bop');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,137 +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 sinon from 'sinon';
|
||||
import * as requestModule from '../../request';
|
||||
import expect from '@kbn/expect';
|
||||
import { Server } from 'hapi';
|
||||
|
||||
import { createProxyRoute } from '../../';
|
||||
|
||||
import { createResponseStub } from './stubs';
|
||||
|
||||
describe('Console Proxy Route', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
const teardowns = [];
|
||||
let request;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(requestModule, 'sendRequest').callsFake(createResponseStub());
|
||||
|
||||
request = async (method, path) => {
|
||||
const server = new Server();
|
||||
server.route(
|
||||
createProxyRoute({
|
||||
hosts: ['http://localhost:9200'],
|
||||
})
|
||||
);
|
||||
|
||||
teardowns.push(() => server.stop());
|
||||
|
||||
const params = [];
|
||||
if (path != null) params.push(`path=${path}`);
|
||||
if (method != null) params.push(`method=${method}`);
|
||||
return await server.inject({
|
||||
method: 'POST',
|
||||
url: `/api/console/proxy${params.length ? `?${params.join('&')}` : ''}`,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
sandbox.restore();
|
||||
await Promise.all(teardowns.splice(0).map(fn => fn()));
|
||||
});
|
||||
|
||||
describe('query string', () => {
|
||||
describe('path', () => {
|
||||
describe('contains full url', () => {
|
||||
it('treats the url as a path', async () => {
|
||||
await request('GET', 'http://evil.com/test');
|
||||
sinon.assert.calledOnce(requestModule.sendRequest);
|
||||
const args = requestModule.sendRequest.getCall(0).args;
|
||||
expect(args[0].uri.href).to.be('http://localhost:9200/http://evil.com/test?pretty=true');
|
||||
});
|
||||
});
|
||||
describe('is missing', () => {
|
||||
it('returns a 400 error', async () => {
|
||||
const { statusCode } = await request('GET', undefined);
|
||||
expect(statusCode).to.be(400);
|
||||
sinon.assert.notCalled(requestModule.sendRequest);
|
||||
});
|
||||
});
|
||||
describe('is empty', () => {
|
||||
it('returns a 400 error', async () => {
|
||||
const { statusCode } = await request('GET', '');
|
||||
expect(statusCode).to.be(400);
|
||||
sinon.assert.notCalled(requestModule.sendRequest);
|
||||
});
|
||||
});
|
||||
describe('starts with a slash', () => {
|
||||
it('combines well with the base url', async () => {
|
||||
await request('GET', '/index/type/id');
|
||||
sinon.assert.calledOnce(requestModule.sendRequest);
|
||||
expect(requestModule.sendRequest.getCall(0).args[0].uri.href).to.be(
|
||||
'http://localhost:9200/index/type/id?pretty=true'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe(`doesn't start with a slash`, () => {
|
||||
it('combines well with the base url', async () => {
|
||||
await request('GET', 'index/type/id');
|
||||
sinon.assert.calledOnce(requestModule.sendRequest);
|
||||
expect(requestModule.sendRequest.getCall(0).args[0].uri.href).to.be(
|
||||
'http://localhost:9200/index/type/id?pretty=true'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('method', () => {
|
||||
describe('is missing', () => {
|
||||
it('returns a 400 error', async () => {
|
||||
const { statusCode } = await request(null, '/');
|
||||
expect(statusCode).to.be(400);
|
||||
sinon.assert.notCalled(requestModule.sendRequest);
|
||||
});
|
||||
});
|
||||
describe('is empty', () => {
|
||||
it('returns a 400 error', async () => {
|
||||
const { statusCode } = await request('', '/');
|
||||
expect(statusCode).to.be(400);
|
||||
sinon.assert.notCalled(requestModule.sendRequest);
|
||||
});
|
||||
});
|
||||
describe('is an invalid http method', () => {
|
||||
it('returns a 400 error', async () => {
|
||||
const { statusCode } = await request('foo', '/');
|
||||
expect(statusCode).to.be(400);
|
||||
sinon.assert.notCalled(requestModule.sendRequest);
|
||||
});
|
||||
});
|
||||
describe('is mixed case', () => {
|
||||
it('sends a request with the exact method', async () => {
|
||||
const { statusCode } = await request('HeAd', '/');
|
||||
expect(statusCode).to.be(200);
|
||||
sinon.assert.calledOnce(requestModule.sendRequest);
|
||||
expect(requestModule.sendRequest.getCall(0).args[0].method).to.be('HeAd');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,171 +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 Joi from 'joi';
|
||||
import * as url from 'url';
|
||||
import { IncomingMessage } from 'http';
|
||||
import Boom from 'boom';
|
||||
import { trimLeft, trimRight } from 'lodash';
|
||||
import { sendRequest } from './request';
|
||||
|
||||
function toURL(base: string, path: string) {
|
||||
const urlResult = new url.URL(`${trimRight(base, '/')}/${trimLeft(path, '/')}`);
|
||||
// Appending pretty here to have Elasticsearch do the JSON formatting, as doing
|
||||
// in JS can lead to data loss (7.0 will get munged into 7, thus losing indication of
|
||||
// measurement precision)
|
||||
if (!urlResult.searchParams.get('pretty')) {
|
||||
urlResult.searchParams.append('pretty', 'true');
|
||||
}
|
||||
return urlResult;
|
||||
}
|
||||
|
||||
function getProxyHeaders(req: any) {
|
||||
const headers = Object.create(null);
|
||||
|
||||
// Scope this proto-unsafe functionality to where it is being used.
|
||||
function extendCommaList(obj: Record<string, any>, property: string, value: any) {
|
||||
obj[property] = (obj[property] ? obj[property] + ',' : '') + value;
|
||||
}
|
||||
|
||||
if (req.info.remotePort && req.info.remoteAddress) {
|
||||
// see https://git.io/vytQ7
|
||||
extendCommaList(headers, 'x-forwarded-for', req.info.remoteAddress);
|
||||
extendCommaList(headers, 'x-forwarded-port', req.info.remotePort);
|
||||
extendCommaList(headers, 'x-forwarded-proto', req.server.info.protocol);
|
||||
extendCommaList(headers, 'x-forwarded-host', req.info.host);
|
||||
}
|
||||
|
||||
const contentType = req.headers['content-type'];
|
||||
if (contentType) {
|
||||
headers['content-type'] = contentType;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export const createProxyRoute = ({
|
||||
hosts,
|
||||
pathFilters = [/.*/],
|
||||
getConfigForReq = () => ({}),
|
||||
}: {
|
||||
hosts: string[];
|
||||
pathFilters: RegExp[];
|
||||
getConfigForReq: (...args: any[]) => any;
|
||||
}) => ({
|
||||
path: '/api/console/proxy',
|
||||
method: 'POST',
|
||||
config: {
|
||||
tags: ['access:console'],
|
||||
payload: {
|
||||
output: 'stream',
|
||||
parse: false,
|
||||
},
|
||||
validate: {
|
||||
payload: true,
|
||||
query: Joi.object()
|
||||
.keys({
|
||||
method: Joi.string()
|
||||
.valid('HEAD', 'GET', 'POST', 'PUT', 'DELETE')
|
||||
.insensitive()
|
||||
.required(),
|
||||
path: Joi.string().required(),
|
||||
})
|
||||
.unknown(true),
|
||||
},
|
||||
|
||||
pre: [
|
||||
function filterPath(req: any) {
|
||||
const { path } = req.query;
|
||||
|
||||
if (pathFilters.some(re => re.test(path))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const err = Boom.forbidden();
|
||||
err.output.payload = `Error connecting to '${path}':\n\nUnable to send requests to that path.` as any;
|
||||
err.output.headers['content-type'] = 'text/plain';
|
||||
throw err;
|
||||
},
|
||||
],
|
||||
|
||||
handler: async (req: any, h: any) => {
|
||||
const { payload, query } = req;
|
||||
const { path, method } = query;
|
||||
|
||||
let esIncomingMessage: IncomingMessage;
|
||||
|
||||
for (let idx = 0; idx < hosts.length; ++idx) {
|
||||
const host = hosts[idx];
|
||||
try {
|
||||
const uri = toURL(host, path);
|
||||
|
||||
// Because this can technically be provided by a settings-defined proxy config, we need to
|
||||
// preserve these property names to maintain BWC.
|
||||
const { timeout, agent, headers, rejectUnauthorized } = getConfigForReq(
|
||||
req,
|
||||
uri.toString()
|
||||
);
|
||||
|
||||
const requestHeaders = {
|
||||
...headers,
|
||||
...getProxyHeaders(req),
|
||||
};
|
||||
|
||||
esIncomingMessage = await sendRequest({
|
||||
method,
|
||||
headers: requestHeaders,
|
||||
uri,
|
||||
timeout,
|
||||
payload,
|
||||
rejectUnauthorized,
|
||||
agent,
|
||||
});
|
||||
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e.code !== 'ECONNREFUSED') {
|
||||
throw Boom.boomify(e);
|
||||
}
|
||||
if (idx === hosts.length - 1) {
|
||||
throw Boom.badGateway('Could not reach any configured nodes.');
|
||||
}
|
||||
// Otherwise, try the next host...
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
statusMessage,
|
||||
headers: { warning },
|
||||
} = esIncomingMessage!;
|
||||
|
||||
if (method.toUpperCase() !== 'HEAD') {
|
||||
return h
|
||||
.response(esIncomingMessage!)
|
||||
.code(statusCode)
|
||||
.header('warning', warning!);
|
||||
} else {
|
||||
return h
|
||||
.response(`${statusCode} - ${statusMessage}`)
|
||||
.code(statusCode)
|
||||
.type('text/plain')
|
||||
.header('warning', warning!);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
51
src/legacy/core_plugins/console_legacy/index.ts
Normal file
51
src/legacy/core_plugins/console_legacy/index.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { first } from 'rxjs/operators';
|
||||
import { head } from 'lodash';
|
||||
import { resolve } from 'path';
|
||||
import url from 'url';
|
||||
|
||||
// TODO: Remove this hack once we can get the ES config we need for Console proxy a better way.
|
||||
let _legacyEsConfig: any;
|
||||
export const readLegacyEsConfig = () => {
|
||||
return _legacyEsConfig;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function(kibana: any) {
|
||||
return new kibana.Plugin({
|
||||
id: 'console_legacy',
|
||||
|
||||
async init(server: any) {
|
||||
_legacyEsConfig = await server.newPlatform.__internals.elasticsearch.legacy.config$
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
},
|
||||
|
||||
uiExports: {
|
||||
styleSheetPaths: resolve(__dirname, 'public/styles/index.scss'),
|
||||
injectDefaultVars: () => ({
|
||||
elasticsearchUrl: url.format(
|
||||
Object.assign(url.parse(head(_legacyEsConfig.hosts)), { auth: false })
|
||||
),
|
||||
}),
|
||||
},
|
||||
} as any);
|
||||
}
|
4
src/legacy/core_plugins/console_legacy/package.json
Normal file
4
src/legacy/core_plugins/console_legacy/package.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "console_legacy",
|
||||
"version": "kibana"
|
||||
}
|
21
src/plugins/console/common/types/index.ts
Normal file
21
src/plugins/console/common/types/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 * from './models';
|
||||
export * from './plugin_config';
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { TextObject } from './text_object';
|
||||
import { TextObject } from '../text_object';
|
||||
|
||||
export interface IdObject {
|
||||
id: string;
|
22
src/plugins/console/common/types/plugin_config.ts
Normal file
22
src/plugins/console/common/types/plugin_config.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export interface PluginServerConfig {
|
||||
elasticsearchUrl: string;
|
||||
}
|
|
@ -3,6 +3,6 @@
|
|||
"version": "kibana",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["home"],
|
||||
"requiredPlugins": ["dev_tools", "home"],
|
||||
"optionalPlugins": ["usageCollection"]
|
||||
}
|
|
@ -248,7 +248,10 @@ export function DevToolsSettingsModal(props: Props) {
|
|||
}
|
||||
>
|
||||
<EuiCheckboxGroup
|
||||
options={autoCompleteCheckboxes}
|
||||
options={autoCompleteCheckboxes.map(opts => {
|
||||
const { stateSetter, ...rest } = opts;
|
||||
return rest;
|
||||
})}
|
||||
idToSelectedMap={checkboxIdToSelectedMap}
|
||||
onChange={(e: any) => {
|
||||
onAutocompleteChange(e as AutocompleteOptions);
|
|
@ -21,7 +21,7 @@ import React, { useCallback } from 'react';
|
|||
import { debounce } from 'lodash';
|
||||
|
||||
import { EditorContentSpinner } from '../../components';
|
||||
import { Panel, PanelsContainer } from '../../../../../../../../plugins/kibana_react/public';
|
||||
import { Panel, PanelsContainer } from '../../../../../kibana_react/public';
|
||||
import { Editor as EditorUI, EditorOutput } from './legacy/console_editor';
|
||||
import { StorageKeys } from '../../../services';
|
||||
import { useEditorReadContext, useServicesContext } from '../../contexts';
|
|
@ -25,7 +25,7 @@ import { I18nProvider } from '@kbn/i18n/react';
|
|||
import { act } from 'react-dom/test-utils';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
import { notificationServiceMock } from '../../../../../../../../../../../src/core/public/mocks';
|
||||
import { notificationServiceMock } from '../../../../../../../../core/public/mocks';
|
||||
|
||||
import { nextTick } from 'test_utils/enzyme_helpers';
|
||||
import {
|
|
@ -16,7 +16,8 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ResizeChecker } from '../../../../../../../../../plugins/kibana_utils/public';
|
||||
|
||||
import { ResizeChecker } from '../../../../../../kibana_utils/public';
|
||||
|
||||
export function subscribeResizeChecker(el: HTMLElement, ...editors: any[]) {
|
||||
const checker = new ResizeChecker(el);
|
|
@ -20,7 +20,7 @@
|
|||
import React, { createContext, useContext } from 'react';
|
||||
import { NotificationsSetup } from 'kibana/public';
|
||||
import { History, Storage, Settings } from '../../services';
|
||||
import { ObjectStorageClient } from '../../../../common/types';
|
||||
import { ObjectStorageClient } from '../../../common/types';
|
||||
import { MetricsTracker } from '../../types';
|
||||
|
||||
export interface ContextValue {
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { History } from '../../../services';
|
||||
import { ObjectStorageClient } from '../../../../../common/types';
|
||||
import { ObjectStorageClient } from '../../../../common/types';
|
||||
|
||||
export interface Dependencies {
|
||||
history: History;
|
|
@ -18,27 +18,38 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { NotificationsSetup } from 'src/core/public';
|
||||
import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts';
|
||||
import { Main } from './containers';
|
||||
import { createStorage, createHistory, createSettings, Settings } from '../services';
|
||||
import * as localStorageObjectClient from '../lib/local_storage_object_client';
|
||||
import { createUsageTracker } from '../services/tracker';
|
||||
import { UsageCollectionSetup } from '../../../usage_collection/public';
|
||||
|
||||
let settingsRef: Settings;
|
||||
export function legacyBackDoorToSettings() {
|
||||
return settingsRef;
|
||||
}
|
||||
|
||||
export function boot(deps: {
|
||||
export interface BootDependencies {
|
||||
docLinkVersion: string;
|
||||
I18nContext: any;
|
||||
notifications: NotificationsSetup;
|
||||
elasticsearchUrl: string;
|
||||
}) {
|
||||
const { I18nContext, notifications, docLinkVersion, elasticsearchUrl } = deps;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
element: HTMLElement;
|
||||
}
|
||||
|
||||
const trackUiMetric = createUsageTracker();
|
||||
export function renderApp({
|
||||
I18nContext,
|
||||
notifications,
|
||||
docLinkVersion,
|
||||
elasticsearchUrl,
|
||||
usageCollection,
|
||||
element,
|
||||
}: BootDependencies) {
|
||||
const trackUiMetric = createUsageTracker(usageCollection);
|
||||
trackUiMetric.load('opened_app');
|
||||
|
||||
const storage = createStorage({
|
||||
|
@ -50,7 +61,7 @@ export function boot(deps: {
|
|||
const objectStorageClient = localStorageObjectClient.create(storage);
|
||||
settingsRef = settings;
|
||||
|
||||
return (
|
||||
render(
|
||||
<I18nContext>
|
||||
<ServicesContextProvider
|
||||
value={{
|
||||
|
@ -72,6 +83,9 @@ export function boot(deps: {
|
|||
</EditorContextProvider>
|
||||
</RequestContextProvider>
|
||||
</ServicesContextProvider>
|
||||
</I18nContext>
|
||||
</I18nContext>,
|
||||
element
|
||||
);
|
||||
|
||||
return () => unmountComponentAtNode(element);
|
||||
}
|
Before Width: | Height: | Size: 217 B After Width: | Height: | Size: 217 B |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue