+
+
+
+
+
+
+
-
-
-
-
- Recently viewed
-
-
-
-
- }
- className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
- data-test-subj="collapsibleNavGroup-recentlyViewed"
- id="mockId"
- initialIsOpen={true}
- onToggle={[Function]}
- paddingSize="none"
+
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- Recently viewed
-
-
-
-
-
-
+ Undock navigation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- kibana
-
-
-
-
-
-
-
-
-
-
,
}
- >
-
-
-
-
-
-
-
-
- Undock navigation
-
- ,
- }
- }
- color="subdued"
- data-test-subj="collapsible-nav-lock"
- iconType="lock"
- label="Undock navigation"
- onClick={[Function]}
- size="xs"
- >
-
-
-
-
-
-
- Undock navigation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
- close
-
-
+ Undock navigation
-
-
-
-
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+ }
+ >
+
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index bd279baa78d98..fc753517fd940 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -41,8 +41,8 @@ export class DocLinksService {
},
filebeat: {
base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`,
- installation: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-installation.html`,
- configuration: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-configuration.html`,
+ installation: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-installation-configuration.html`,
+ configuration: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/configuring-howto-filebeat.html`,
elasticsearchOutput: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/elasticsearch-output.html`,
startup: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-starting.html`,
exportedFields: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/exported-fields.html`,
diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
index fa83b34e06b81..a5c1d46f74709 100644
--- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
+++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
@@ -26,7 +26,7 @@ Array [
]
`;
-exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `""`;
+exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `""`;
exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = `
Array [
@@ -59,4 +59,4 @@ Array [
]
`;
-exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `""`;
+exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `""`;
diff --git a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap
index fb00ddc38c6dc..aea52eb8e7ab7 100644
--- a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap
+++ b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap
@@ -31,7 +31,7 @@ Array [
]
`;
-exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
"`;
+exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `""`;
exports[`ModalService openConfirm() renders a string confirm message 1`] = `
Array [
@@ -53,7 +53,7 @@ Array [
]
`;
-exports[`ModalService openConfirm() renders a string confirm message 2`] = `"
"`;
+exports[`ModalService openConfirm() renders a string confirm message 2`] = `""`;
exports[`ModalService openConfirm() with a currently active confirm replaces the current confirm with the new one 1`] = `
Array [
@@ -145,7 +145,7 @@ Array [
]
`;
-exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
"`;
+exports[`ModalService openModal() renders a modal to the DOM 2`] = `""`;
exports[`ModalService openModal() with a currently active confirm replaces the current confirm with the new one 1`] = `
Array [
diff --git a/src/core/server/elasticsearch/client/cluster_client.test.ts b/src/core/server/elasticsearch/client/cluster_client.test.ts
index 85517b80745f1..121ef3aa42d51 100644
--- a/src/core/server/elasticsearch/client/cluster_client.test.ts
+++ b/src/core/server/elasticsearch/client/cluster_client.test.ts
@@ -96,7 +96,7 @@ describe('ClusterClient', () => {
expect(scopedClusterClient.asCurrentUser).toBe(scopedClient.child.mock.results[0].value);
});
- it('returns a distinct scoped cluster client on each call', () => {
+ it('returns a distinct scoped cluster client on each call', () => {
const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders);
const request = httpServerMock.createKibanaRequest();
@@ -127,7 +127,7 @@ describe('ClusterClient', () => {
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
- headers: { foo: 'bar' },
+ headers: { foo: 'bar', 'x-opaque-id': expect.any(String) },
});
});
@@ -147,7 +147,7 @@ describe('ClusterClient', () => {
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
- headers: { authorization: 'auth' },
+ headers: { authorization: 'auth', 'x-opaque-id': expect.any(String) },
});
});
@@ -171,7 +171,7 @@ describe('ClusterClient', () => {
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
- headers: { authorization: 'auth' },
+ headers: { authorization: 'auth', 'x-opaque-id': expect.any(String) },
});
});
@@ -195,6 +195,26 @@ describe('ClusterClient', () => {
headers: {
foo: 'bar',
hello: 'dolly',
+ 'x-opaque-id': expect.any(String),
+ },
+ });
+ });
+
+ it('adds the x-opaque-id header based on the request id', () => {
+ const config = createConfig();
+ getAuthHeaders.mockReturnValue({});
+
+ const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
+ const request = httpServerMock.createKibanaRequest({
+ kibanaRequestState: { requestId: 'my-fake-id' },
+ });
+
+ clusterClient.asScoped(request);
+
+ expect(scopedClient.child).toHaveBeenCalledTimes(1);
+ expect(scopedClient.child).toHaveBeenCalledWith({
+ headers: {
+ 'x-opaque-id': 'my-fake-id',
},
});
});
@@ -221,6 +241,7 @@ describe('ClusterClient', () => {
headers: {
foo: 'auth',
hello: 'dolly',
+ 'x-opaque-id': expect.any(String),
},
});
});
@@ -247,6 +268,31 @@ describe('ClusterClient', () => {
headers: {
foo: 'request',
hello: 'dolly',
+ 'x-opaque-id': expect.any(String),
+ },
+ });
+ });
+
+ it('respect the precedence of x-opaque-id header over config headers', () => {
+ const config = createConfig({
+ customHeaders: {
+ 'x-opaque-id': 'from config',
+ },
+ });
+ getAuthHeaders.mockReturnValue({});
+
+ const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
+ const request = httpServerMock.createKibanaRequest({
+ headers: { foo: 'request' },
+ kibanaRequestState: { requestId: 'from request' },
+ });
+
+ clusterClient.asScoped(request);
+
+ expect(scopedClient.child).toHaveBeenCalledTimes(1);
+ expect(scopedClient.child).toHaveBeenCalledWith({
+ headers: {
+ 'x-opaque-id': 'from request',
},
});
});
diff --git a/src/core/server/elasticsearch/client/cluster_client.ts b/src/core/server/elasticsearch/client/cluster_client.ts
index d9a0e6fe3f238..ffe0c10321fff 100644
--- a/src/core/server/elasticsearch/client/cluster_client.ts
+++ b/src/core/server/elasticsearch/client/cluster_client.ts
@@ -19,7 +19,7 @@
import { Client } from '@elastic/elasticsearch';
import { Logger } from '../../logging';
-import { GetAuthHeaders, isRealRequest, Headers } from '../../http';
+import { GetAuthHeaders, Headers, isKibanaRequest, isRealRequest } from '../../http';
import { ensureRawRequest, filterHeaders } from '../../http/router';
import { ScopeableRequest } from '../types';
import { ElasticsearchClient } from './types';
@@ -95,12 +95,14 @@ export class ClusterClient implements ICustomClusterClient {
private getScopedHeaders(request: ScopeableRequest): Headers {
let scopedHeaders: Headers;
if (isRealRequest(request)) {
- const authHeaders = this.getAuthHeaders(request);
const requestHeaders = ensureRawRequest(request).headers;
- scopedHeaders = filterHeaders(
- { ...requestHeaders, ...authHeaders },
- this.config.requestHeadersWhitelist
- );
+ const requestIdHeaders = isKibanaRequest(request) ? { 'x-opaque-id': request.id } : {};
+ const authHeaders = this.getAuthHeaders(request);
+
+ scopedHeaders = filterHeaders({ ...requestHeaders, ...requestIdHeaders, ...authHeaders }, [
+ 'x-opaque-id',
+ ...this.config.requestHeadersWhitelist,
+ ]);
} else {
scopedHeaders = filterHeaders(request?.headers ?? {}, this.config.requestHeadersWhitelist);
}
diff --git a/src/core/server/elasticsearch/legacy/cluster_client.test.ts b/src/core/server/elasticsearch/legacy/cluster_client.test.ts
index fd57d06e61eee..73d941053e84b 100644
--- a/src/core/server/elasticsearch/legacy/cluster_client.test.ts
+++ b/src/core/server/elasticsearch/legacy/cluster_client.test.ts
@@ -349,6 +349,20 @@ describe('#asScoped', () => {
);
});
+ test('passes x-opaque-id header with request id', () => {
+ clusterClient.asScoped(
+ httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'alpha' } })
+ );
+
+ expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
+ expect(MockScopedClusterClient).toHaveBeenCalledWith(
+ expect.any(Function),
+ expect.any(Function),
+ { 'x-opaque-id': 'alpha' },
+ expect.any(Object)
+ );
+ });
+
test('both scoped and internal API caller fail if cluster client is closed', async () => {
clusterClient.asScoped(
httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } })
@@ -482,7 +496,7 @@ describe('#asScoped', () => {
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
- {},
+ expect.objectContaining({ 'x-opaque-id': expect.any(String) }),
auditor
);
});
diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts
index f8b2d39a4251c..81cbb5a10d7c6 100644
--- a/src/core/server/elasticsearch/legacy/cluster_client.ts
+++ b/src/core/server/elasticsearch/legacy/cluster_client.ts
@@ -20,7 +20,7 @@ import { Client } from 'elasticsearch';
import { get } from 'lodash';
import { LegacyElasticsearchErrorHelpers } from './errors';
-import { GetAuthHeaders, isRealRequest, KibanaRequest } from '../../http';
+import { GetAuthHeaders, KibanaRequest, isKibanaRequest, isRealRequest } from '../../http';
import { AuditorFactory } from '../../audit_trail';
import { filterHeaders, ensureRawRequest } from '../../http/router';
import { Logger } from '../../logging';
@@ -207,7 +207,10 @@ export class LegacyClusterClient implements ILegacyClusterClient {
return new LegacyScopedClusterClient(
this.callAsInternalUser,
this.callAsCurrentUser,
- filterHeaders(this.getHeaders(request), this.config.requestHeadersWhitelist),
+ filterHeaders(this.getHeaders(request), [
+ 'x-opaque-id',
+ ...this.config.requestHeadersWhitelist,
+ ]),
this.getScopedAuditor(request)
);
}
@@ -215,8 +218,7 @@ export class LegacyClusterClient implements ILegacyClusterClient {
private getScopedAuditor(request?: ScopeableRequest) {
// TODO: support alternative credential owners from outside of Request context in #39430
if (request && isRealRequest(request)) {
- const kibanaRequest =
- request instanceof KibanaRequest ? request : KibanaRequest.from(request);
+ const kibanaRequest = isKibanaRequest(request) ? request : KibanaRequest.from(request);
const auditorFactory = this.getAuditorFactory();
return auditorFactory.asScoped(kibanaRequest);
}
@@ -256,8 +258,9 @@ export class LegacyClusterClient implements ILegacyClusterClient {
return request && request.headers ? request.headers : {};
}
const authHeaders = this.getAuthHeaders(request);
- const headers = ensureRawRequest(request).headers;
+ const requestHeaders = ensureRawRequest(request).headers;
+ const requestIdHeaders = isKibanaRequest(request) ? { 'x-opaque-id': request.id } : {};
- return { ...headers, ...authHeaders };
+ return { ...requestHeaders, ...requestIdHeaders, ...authHeaders };
}
}
diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap
index d48ead3cec8e1..e9b818fe859ec 100644
--- a/src/core/server/http/__snapshots__/http_config.test.ts.snap
+++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap
@@ -39,6 +39,10 @@ Object {
},
"name": "kibana-hostname",
"port": 5601,
+ "requestId": Object {
+ "allowFromAnyIp": false,
+ "ipAllowlist": Array [],
+ },
"rewriteBasePath": false,
"socketTimeout": 120000,
"ssl": Object {
diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts
index 1fb2b5693bb61..8e5dec7d4eadd 100644
--- a/src/core/server/http/cookie_session_storage.test.ts
+++ b/src/core/server/http/cookie_session_storage.test.ts
@@ -63,6 +63,10 @@ configService.atPath.mockReturnValue(
whitelist: [],
},
customResponseHeaders: {},
+ requestId: {
+ allowFromAnyIp: true,
+ ipAllowlist: [],
+ },
} as any)
);
diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts
index 0698f118be03f..58e6699582e13 100644
--- a/src/core/server/http/http_config.test.ts
+++ b/src/core/server/http/http_config.test.ts
@@ -54,6 +54,63 @@ test('throws if invalid hostname', () => {
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot();
});
+describe('requestId', () => {
+ test('accepts valid ip addresses', () => {
+ const {
+ requestId: { ipAllowlist },
+ } = config.schema.validate({
+ requestId: {
+ allowFromAnyIp: false,
+ ipAllowlist: ['0.0.0.0', '123.123.123.123', '1200:0000:AB00:1234:0000:2552:7777:1313'],
+ },
+ });
+ expect(ipAllowlist).toMatchInlineSnapshot(`
+ Array [
+ "0.0.0.0",
+ "123.123.123.123",
+ "1200:0000:AB00:1234:0000:2552:7777:1313",
+ ]
+ `);
+ });
+
+ test('rejects invalid ip addresses', () => {
+ expect(() => {
+ config.schema.validate({
+ requestId: {
+ allowFromAnyIp: false,
+ ipAllowlist: ['1200:0000:AB00:1234:O000:2552:7777:1313', '[2001:db8:0:1]:80'],
+ },
+ });
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"[requestId.ipAllowlist.0]: value must be a valid ipv4 or ipv6 address"`
+ );
+ });
+
+ test('rejects if allowFromAnyIp is `true` and `ipAllowlist` is non-empty', () => {
+ expect(() => {
+ config.schema.validate({
+ requestId: {
+ allowFromAnyIp: true,
+ ipAllowlist: ['0.0.0.0', '123.123.123.123', '1200:0000:AB00:1234:0000:2552:7777:1313'],
+ },
+ });
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"[requestId]: allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist"`
+ );
+
+ expect(() => {
+ config.schema.validate({
+ requestId: {
+ allowFromAnyIp: true,
+ ipAllowlist: ['0.0.0.0', '123.123.123.123', '1200:0000:AB00:1234:0000:2552:7777:1313'],
+ },
+ });
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"[requestId]: allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist"`
+ );
+ });
+});
+
test('can specify max payload as string', () => {
const obj = {
maxPayload: '2mb',
diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts
index e74f6d32e92b0..7d41b4ea9e915 100644
--- a/src/core/server/http/http_config.ts
+++ b/src/core/server/http/http_config.ts
@@ -87,6 +87,19 @@ export const config = {
{ defaultValue: [] }
),
}),
+ requestId: schema.object(
+ {
+ allowFromAnyIp: schema.boolean({ defaultValue: false }),
+ ipAllowlist: schema.arrayOf(schema.ip(), { defaultValue: [] }),
+ },
+ {
+ validate(value) {
+ if (value.allowFromAnyIp === true && value.ipAllowlist?.length > 0) {
+ return `allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist`;
+ }
+ },
+ }
+ ),
},
{
validate: (rawConfig) => {
@@ -130,6 +143,7 @@ export class HttpConfig {
public compression: { enabled: boolean; referrerWhitelist?: string[] };
public csp: ICspConfig;
public xsrf: { disableProtection: boolean; whitelist: string[] };
+ public requestId: { allowFromAnyIp: boolean; ipAllowlist: string[] };
/**
* @internal
@@ -158,6 +172,7 @@ export class HttpConfig {
this.compression = rawHttpConfig.compression;
this.csp = new CspConfig(rawCspConfig);
this.xsrf = rawHttpConfig.xsrf;
+ this.requestId = rawHttpConfig.requestId;
}
}
diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts
index ba6662db3655e..6d096b76263b5 100644
--- a/src/core/server/http/http_server.mocks.ts
+++ b/src/core/server/http/http_server.mocks.ts
@@ -29,7 +29,8 @@ import {
RouteMethod,
KibanaResponseFactory,
RouteValidationSpec,
- KibanaRouteState,
+ KibanaRouteOptions,
+ KibanaRequestState,
} from './router';
import { OnPreResponseToolkit } from './lifecycle/on_pre_response';
import { OnPostAuthToolkit } from './lifecycle/on_post_auth';
@@ -45,7 +46,8 @@ interface RequestFixtureOptions {
method?: RouteMethod;
socket?: Socket;
routeTags?: string[];
- kibanaRouteState?: KibanaRouteState;
+ kibanaRouteOptions?: KibanaRouteOptions;
+ kibanaRequestState?: KibanaRequestState;
routeAuthRequired?: false;
validation?: {
params?: RouteValidationSpec
;
@@ -65,13 +67,15 @@ function createKibanaRequestMock
({
routeTags,
routeAuthRequired,
validation = {},
- kibanaRouteState = { xsrfRequired: true },
+ kibanaRouteOptions = { xsrfRequired: true },
+ kibanaRequestState = { requestId: '123' },
auth = { isAuthenticated: true },
}: RequestFixtureOptions
= {}) {
const queryString = stringify(query, { sort: false });
return KibanaRequest.from
(
createRawRequestMock({
+ app: kibanaRequestState,
auth,
headers,
params,
@@ -86,7 +90,7 @@ function createKibanaRequestMock
({
search: queryString ? `?${queryString}` : queryString,
},
route: {
- settings: { tags: routeTags, auth: routeAuthRequired, app: kibanaRouteState },
+ settings: { tags: routeTags, auth: routeAuthRequired, app: kibanaRouteOptions },
},
raw: {
req: {
@@ -133,6 +137,7 @@ function createRawRequestMock(customization: DeepPartial = {}) {
raw: {
req: {
url: '/',
+ socket: {},
},
},
},
diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts
index abe70e003732b..7507a08dd150a 100644
--- a/src/core/server/http/http_server.test.ts
+++ b/src/core/server/http/http_server.test.ts
@@ -68,7 +68,11 @@ beforeEach(() => {
port: 10002,
ssl: { enabled: false },
compression: { enabled: true },
- } as HttpConfig;
+ requestId: {
+ allowFromAnyIp: true,
+ ipAllowlist: [],
+ },
+ } as any;
configWithSSL = {
...config,
diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts
index 99ab0ef16c2f9..7609f23fe0c51 100644
--- a/src/core/server/http/http_server.ts
+++ b/src/core/server/http/http_server.ts
@@ -22,13 +22,19 @@ import url from 'url';
import { Logger, LoggerFactory } from '../logging';
import { HttpConfig } from './http_config';
-import { createServer, getListenerOptions, getServerOptions } from './http_tools';
+import { createServer, getListenerOptions, getServerOptions, getRequestId } from './http_tools';
import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth';
import { adoptToHapiOnPreAuth, OnPreAuthHandler } from './lifecycle/on_pre_auth';
import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth';
import { adoptToHapiOnRequest, OnPreRoutingHandler } from './lifecycle/on_pre_routing';
import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response';
-import { IRouter, RouteConfigOptions, KibanaRouteState, isSafeMethod } from './router';
+import {
+ IRouter,
+ RouteConfigOptions,
+ KibanaRouteOptions,
+ KibanaRequestState,
+ isSafeMethod,
+} from './router';
import {
SessionStorageCookieOptions,
createCookieSessionStorageFactory,
@@ -115,6 +121,7 @@ export class HttpServer {
const basePathService = new BasePath(config.basePath);
this.setupBasePathRewrite(config, basePathService);
this.setupConditionalCompression(config);
+ this.setupRequestStateAssignment(config);
return {
registerRouter: this.registerRouter.bind(this),
@@ -164,7 +171,7 @@ export class HttpServer {
const { authRequired, tags, body = {}, timeout } = route.options;
const { accepts: allow, maxBytes, output, parse } = body;
- const kibanaRouteState: KibanaRouteState = {
+ const kibanaRouteOptions: KibanaRouteOptions = {
xsrfRequired: route.options.xsrfRequired ?? !isSafeMethod(route.method),
};
@@ -180,7 +187,7 @@ export class HttpServer {
path: route.path,
options: {
auth: this.getAuthOption(authRequired),
- app: kibanaRouteState,
+ app: kibanaRouteOptions,
ext: {
onPreAuth: {
method: (request, h) => {
@@ -303,6 +310,16 @@ export class HttpServer {
}
}
+ private setupRequestStateAssignment(config: HttpConfig) {
+ this.server!.ext('onRequest', (request, responseToolkit) => {
+ request.app = {
+ ...(request.app ?? {}),
+ requestId: getRequestId(request, config.requestId),
+ } as KibanaRequestState;
+ return responseToolkit.continue;
+ });
+ }
+
private registerOnPreAuth(fn: OnPreAuthHandler) {
if (this.server === undefined) {
throw new Error('Server is not created yet');
diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts
index f09d862f9edac..bdeca3a87799a 100644
--- a/src/core/server/http/http_tools.test.ts
+++ b/src/core/server/http/http_tools.test.ts
@@ -26,11 +26,20 @@ jest.mock('fs', () => {
};
});
+jest.mock('uuid', () => ({
+ v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'),
+}));
+
import supertest from 'supertest';
import { Request, ResponseToolkit } from 'hapi';
import Joi from 'joi';
-import { defaultValidationErrorHandler, HapiValidationError, getServerOptions } from './http_tools';
+import {
+ defaultValidationErrorHandler,
+ HapiValidationError,
+ getServerOptions,
+ getRequestId,
+} from './http_tools';
import { HttpServer } from './http_server';
import { HttpConfig, config } from './http_config';
import { Router } from './router';
@@ -94,7 +103,11 @@ describe('timeouts', () => {
maxPayload: new ByteSizeValue(1024),
ssl: {},
compression: { enabled: true },
- } as HttpConfig);
+ requestId: {
+ allowFromAnyIp: true,
+ ipAllowlist: [],
+ },
+ } as any);
registerRouter(router);
await server.start();
@@ -173,3 +186,75 @@ describe('getServerOptions', () => {
`);
});
});
+
+describe('getRequestId', () => {
+ describe('when allowFromAnyIp is true', () => {
+ it('generates a UUID if no x-opaque-id header is present', () => {
+ const request = {
+ headers: {},
+ raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
+ } as any;
+ expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual(
+ 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
+ );
+ });
+
+ it('uses x-opaque-id header value if present', () => {
+ const request = {
+ headers: {
+ 'x-opaque-id': 'id from header',
+ raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
+ },
+ } as any;
+ expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual(
+ 'id from header'
+ );
+ });
+ });
+
+ describe('when allowFromAnyIp is false', () => {
+ describe('and ipAllowlist is empty', () => {
+ it('generates a UUID even if x-opaque-id header is present', () => {
+ const request = {
+ headers: { 'x-opaque-id': 'id from header' },
+ raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
+ } as any;
+ expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: [] })).toEqual(
+ 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
+ );
+ });
+ });
+
+ describe('and ipAllowlist is not empty', () => {
+ it('uses x-opaque-id header if request comes from trusted IP address', () => {
+ const request = {
+ headers: { 'x-opaque-id': 'id from header' },
+ raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
+ } as any;
+ expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual(
+ 'id from header'
+ );
+ });
+
+ it('generates a UUID if request comes from untrusted IP address', () => {
+ const request = {
+ headers: { 'x-opaque-id': 'id from header' },
+ raw: { req: { socket: { remoteAddress: '5.5.5.5' } } },
+ } as any;
+ expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual(
+ 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
+ );
+ });
+
+ it('generates UUID if request comes from trusted IP address but no x-opaque-id header is present', () => {
+ const request = {
+ headers: {},
+ raw: { req: { socket: { remoteAddress: '1.1.1.1' } } },
+ } as any;
+ expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual(
+ 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
+ );
+ });
+ });
+ });
+});
diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts
index 4e47cf492e287..71900ab982f3d 100644
--- a/src/core/server/http/http_tools.ts
+++ b/src/core/server/http/http_tools.ts
@@ -21,6 +21,7 @@ import { Lifecycle, Request, ResponseToolkit, Server, ServerOptions, Util } from
import Hoek from 'hoek';
import { ServerOptions as TLSOptions } from 'https';
import { ValidationError } from 'joi';
+import uuid from 'uuid';
import { HttpConfig } from './http_config';
import { validateObject } from './prototype_pollution';
@@ -169,3 +170,12 @@ export function defaultValidationErrorHandler(
throw err;
}
+
+export function getRequestId(request: Request, options: HttpConfig['requestId']): string {
+ return options.allowFromAnyIp ||
+ // socket may be undefined in integration tests that connect via the http listener directly
+ (request.raw.req.socket?.remoteAddress &&
+ options.ipAllowlist.includes(request.raw.req.socket.remoteAddress))
+ ? request.headers['x-opaque-id'] ?? uuid.v4()
+ : uuid.v4();
+}
diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts
index e91f7d9375842..7513e60966085 100644
--- a/src/core/server/http/index.ts
+++ b/src/core/server/http/index.ts
@@ -24,6 +24,7 @@ export { AuthStatus, GetAuthState, IsAuthenticated } from './auth_state_storage'
export {
CustomHttpResponseOptions,
IKibanaSocket,
+ isKibanaRequest,
isRealRequest,
Headers,
HttpResponseOptions,
diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts
index 6a00db5a6cc4a..2b9193a280aec 100644
--- a/src/core/server/http/integration_tests/core_services.test.ts
+++ b/src/core/server/http/integration_tests/core_services.test.ts
@@ -406,7 +406,10 @@ describe('http service', () => {
// client contains authHeaders for BWC with legacy platform.
const [client] = MockLegacyScopedClusterClient.mock.calls;
const [, , clientHeaders] = client;
- expect(clientHeaders).toEqual(authHeaders);
+ expect(clientHeaders).toEqual({
+ ...authHeaders,
+ 'x-opaque-id': expect.any(String),
+ });
});
it('passes request authorization header to Elasticsearch if registerAuth was not set', async () => {
@@ -430,7 +433,10 @@ describe('http service', () => {
const [client] = MockLegacyScopedClusterClient.mock.calls;
const [, , clientHeaders] = client;
- expect(clientHeaders).toEqual({ authorization: authorizationHeader });
+ expect(clientHeaders).toEqual({
+ authorization: authorizationHeader,
+ 'x-opaque-id': expect.any(String),
+ });
});
it('forwards 401 errors returned from elasticsearch', async () => {
diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
index e23426e630455..a1401ba73813b 100644
--- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
+++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
@@ -62,6 +62,10 @@ describe('core lifecycle handlers', () => {
'some-header': 'some-value',
},
xsrf: { disableProtection: false, whitelist: [whitelistedTestPath] },
+ requestId: {
+ allowFromAnyIp: true,
+ ipAllowlist: [],
+ },
} as any)
);
server = createHttpServer({ configService });
diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts
index 3a7335583296e..0727ff848c189 100644
--- a/src/core/server/http/integration_tests/request.test.ts
+++ b/src/core/server/http/integration_tests/request.test.ts
@@ -288,4 +288,24 @@ describe('KibanaRequest', () => {
});
});
});
+
+ describe('request id', () => {
+ it('accepts x-opaque-id header case-insensitively', async () => {
+ const { server: innerServer, createRouter } = await server.setup(setupDeps);
+ const router = createRouter('/');
+ router.get({ path: '/', validate: false }, async (context, req, res) => {
+ return res.ok({ body: { requestId: req.id } });
+ });
+ await server.start();
+
+ const st = supertest(innerServer.listener);
+
+ const resp1 = await st.get('/').set({ 'x-opaque-id': 'alpha' }).expect(200);
+ expect(resp1.body).toEqual({ requestId: 'alpha' });
+ const resp2 = await st.get('/').set({ 'X-Opaque-Id': 'beta' }).expect(200);
+ expect(resp2.body).toEqual({ requestId: 'beta' });
+ const resp3 = await st.get('/').set({ 'X-OPAQUE-ID': 'gamma' }).expect(200);
+ expect(resp3.body).toEqual({ requestId: 'gamma' });
+ });
+ });
});
diff --git a/src/core/server/http/lifecycle_handlers.test.ts b/src/core/server/http/lifecycle_handlers.test.ts
index a80e432e0d4cb..fdcf2a173b906 100644
--- a/src/core/server/http/lifecycle_handlers.test.ts
+++ b/src/core/server/http/lifecycle_handlers.test.ts
@@ -24,7 +24,7 @@ import {
} from './lifecycle_handlers';
import { httpServerMock } from './http_server.mocks';
import { HttpConfig } from './http_config';
-import { KibanaRequest, RouteMethod, KibanaRouteState } from './router';
+import { KibanaRequest, RouteMethod, KibanaRouteOptions } from './router';
const createConfig = (partial: Partial): HttpConfig => partial as HttpConfig;
@@ -32,14 +32,19 @@ const forgeRequest = ({
headers = {},
path = '/',
method = 'get',
- kibanaRouteState,
+ kibanaRouteOptions,
}: Partial<{
headers: Record;
path: string;
method: RouteMethod;
- kibanaRouteState: KibanaRouteState;
+ kibanaRouteOptions: KibanaRouteOptions;
}>): KibanaRequest => {
- return httpServerMock.createKibanaRequest({ headers, path, method, kibanaRouteState });
+ return httpServerMock.createKibanaRequest({
+ headers,
+ path,
+ method,
+ kibanaRouteOptions,
+ });
};
describe('xsrf post-auth handler', () => {
@@ -154,7 +159,7 @@ describe('xsrf post-auth handler', () => {
method: 'post',
headers: {},
path: '/some-path',
- kibanaRouteState: {
+ kibanaRouteOptions: {
xsrfRequired: false,
},
});
diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts
index 83ceff4a25d86..e09833ef6b2da 100644
--- a/src/core/server/http/router/index.ts
+++ b/src/core/server/http/router/index.ts
@@ -24,7 +24,9 @@ export {
KibanaRequestEvents,
KibanaRequestRoute,
KibanaRequestRouteOptions,
- KibanaRouteState,
+ KibanaRouteOptions,
+ KibanaRequestState,
+ isKibanaRequest,
isRealRequest,
LegacyRequest,
ensureRawRequest,
diff --git a/src/core/server/http/router/request.test.ts b/src/core/server/http/router/request.test.ts
index fb999dc60e39c..e741121f3d70c 100644
--- a/src/core/server/http/router/request.test.ts
+++ b/src/core/server/http/router/request.test.ts
@@ -16,12 +16,45 @@
* specific language governing permissions and limitations
* under the License.
*/
+
+jest.mock('uuid', () => ({
+ v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'),
+}));
+
import { RouteOptions } from 'hapi';
import { KibanaRequest } from './request';
import { httpServerMock } from '../http_server.mocks';
import { schema } from '@kbn/config-schema';
describe('KibanaRequest', () => {
+ describe('id property', () => {
+ it('uses the request.app.requestId property if present', () => {
+ const request = httpServerMock.createRawRequest({
+ app: { requestId: 'fakeId' },
+ });
+ const kibanaRequest = KibanaRequest.from(request);
+ expect(kibanaRequest.id).toEqual('fakeId');
+ });
+
+ it('generates a new UUID if request.app property is not present', () => {
+ // Undefined app property
+ const request = httpServerMock.createRawRequest({
+ app: undefined,
+ });
+ const kibanaRequest = KibanaRequest.from(request);
+ expect(kibanaRequest.id).toEqual('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx');
+ });
+
+ it('generates a new UUID if request.app.requestId property is not present', () => {
+ // Undefined app.requestId property
+ const request = httpServerMock.createRawRequest({
+ app: {},
+ });
+ const kibanaRequest = KibanaRequest.from(request);
+ expect(kibanaRequest.id).toEqual('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx');
+ });
+ });
+
describe('get all headers', () => {
it('returns all headers', () => {
const request = httpServerMock.createRawRequest({
diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts
index 278bc222b754b..76f8761a7e998 100644
--- a/src/core/server/http/router/request.ts
+++ b/src/core/server/http/router/request.ts
@@ -18,7 +18,8 @@
*/
import { Url } from 'url';
-import { Request, ApplicationState } from 'hapi';
+import uuid from 'uuid';
+import { Request, RouteOptionsApp, ApplicationState } from 'hapi';
import { Observable, fromEvent, merge } from 'rxjs';
import { shareReplay, first, takeUntil } from 'rxjs/operators';
import { RecursiveReadonly } from '@kbn/utility-types';
@@ -34,9 +35,17 @@ const requestSymbol = Symbol('request');
/**
* @internal
*/
-export interface KibanaRouteState extends ApplicationState {
+export interface KibanaRouteOptions extends RouteOptionsApp {
xsrfRequired: boolean;
}
+
+/**
+ * @internal
+ */
+export interface KibanaRequestState extends ApplicationState {
+ requestId: string;
+}
+
/**
* Route options: If 'GET' or 'OPTIONS' method, body options won't be returned.
* @public
@@ -134,6 +143,15 @@ export class KibanaRequest<
return { query, params, body };
}
+ /**
+ * A identifier to identify this request.
+ *
+ * @remarks
+ * Depending on the user's configuration, this value may be sourced from the
+ * incoming request's `X-Opaque-Id` header which is not guaranteed to be unique
+ * per request.
+ */
+ public readonly id: string;
/** a WHATWG URL standard object. */
public readonly url: Url;
/** matched route details */
@@ -171,6 +189,11 @@ export class KibanaRequest<
// until that time we have to expose all the headers
private readonly withoutSecretHeaders: boolean
) {
+ // The `requestId` property will not be populated for requests that are 'faked' by internal systems that leverage
+ // KibanaRequest in conjunction with scoped Elaticcsearch and SavedObjectsClient in order to pass credentials.
+ // In these cases, the id defaults to a newly generated UUID.
+ this.id = (request.app as KibanaRequestState | undefined)?.requestId ?? uuid.v4();
+
this.url = request.url;
this.headers = deepFreeze({ ...request.headers });
this.isSystemRequest =
@@ -220,7 +243,7 @@ export class KibanaRequest<
const options = ({
authRequired: this.getAuthRequired(request),
// some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8
- xsrfRequired: (request.route.settings.app as KibanaRouteState)?.xsrfRequired ?? true,
+ xsrfRequired: (request.route.settings.app as KibanaRouteOptions)?.xsrfRequired ?? true,
tags: request.route.settings.tags || [],
timeout: {
payload: payloadTimeout,
@@ -276,7 +299,11 @@ export class KibanaRequest<
export const ensureRawRequest = (request: KibanaRequest | LegacyRequest) =>
isKibanaRequest(request) ? request[requestSymbol] : request;
-function isKibanaRequest(request: unknown): request is KibanaRequest {
+/**
+ * Checks if an incoming request is a {@link KibanaRequest}
+ * @internal
+ */
+export function isKibanaRequest(request: unknown): request is KibanaRequest {
return request instanceof KibanaRequest;
}
diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts
index bda66e1de8168..c3afae108027e 100644
--- a/src/core/server/http/test_utils.ts
+++ b/src/core/server/http/test_utils.ts
@@ -46,6 +46,10 @@ configService.atPath.mockReturnValue(
whitelist: [],
},
customResponseHeaders: {},
+ requestId: {
+ allowFromAnyIp: true,
+ ipAllowlist: [],
+ },
} as any)
);
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 382318ea86a34..76bcf5f7df665 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -274,7 +274,9 @@ export {
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
SavedObjectsAddToNamespacesOptions,
+ SavedObjectsAddToNamespacesResponse,
SavedObjectsDeleteFromNamespacesOptions,
+ SavedObjectsDeleteFromNamespacesResponse,
SavedObjectsServiceStart,
SavedObjectsServiceSetup,
SavedObjectStatusMeta,
diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts
index 6065e03fb1628..4956491a79aa9 100644
--- a/src/core/server/saved_objects/import/import_saved_objects.ts
+++ b/src/core/server/saved_objects/import/import_saved_objects.ts
@@ -25,6 +25,7 @@ import {
SavedObjectsImportOptions,
} from './types';
import { validateReferences } from './validate_references';
+import { SavedObject } from '../types';
/**
* Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more
@@ -67,7 +68,7 @@ export async function importSavedObjectsFromStream({
}
// Create objects in bulk
- const bulkCreateResult = await savedObjectsClient.bulkCreate(filteredObjects, {
+ const bulkCreateResult = await savedObjectsClient.bulkCreate(omitVersion(filteredObjects), {
overwrite,
namespace,
});
@@ -82,3 +83,7 @@ export async function importSavedObjectsFromStream({
...(errorAccumulator.length ? { errors: errorAccumulator } : {}),
};
}
+
+export function omitVersion(objects: SavedObject[]): SavedObject[] {
+ return objects.map(({ version, ...object }) => object);
+}
diff --git a/src/core/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts
index a5175aa080598..dce044a31a577 100644
--- a/src/core/server/saved_objects/import/resolve_import_errors.ts
+++ b/src/core/server/saved_objects/import/resolve_import_errors.ts
@@ -26,6 +26,7 @@ import {
SavedObjectsResolveImportErrorsOptions,
} from './types';
import { validateReferences } from './validate_references';
+import { omitVersion } from './import_saved_objects';
/**
* Resolve and return saved object import errors.
@@ -91,7 +92,7 @@ export async function resolveSavedObjectsImportErrors({
// Bulk create in two batches, overwrites and non-overwrites
const { objectsToOverwrite, objectsToNotOverwrite } = splitOverwrites(filteredObjects, retries);
if (objectsToOverwrite.length) {
- const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToOverwrite, {
+ const bulkCreateResult = await savedObjectsClient.bulkCreate(omitVersion(objectsToOverwrite), {
overwrite: true,
namespace,
});
@@ -102,9 +103,12 @@ export async function resolveSavedObjectsImportErrors({
successCount += bulkCreateResult.saved_objects.filter((obj) => !obj.error).length;
}
if (objectsToNotOverwrite.length) {
- const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToNotOverwrite, {
- namespace,
- });
+ const bulkCreateResult = await savedObjectsClient.bulkCreate(
+ omitVersion(objectsToNotOverwrite),
+ {
+ namespace,
+ }
+ );
errorAccumulator = [
...errorAccumulator,
...extractErrors(bulkCreateResult.saved_objects, objectsToNotOverwrite),
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index 4a9fceb9bf357..6d85223d1fc88 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -393,14 +393,14 @@ describe('SavedObjectsRepository', () => {
});
describe('returns', () => {
- it(`returns an empty object on success`, async () => {
+ it(`returns all existing and new namespaces on success`, async () => {
const result = await addToNamespacesSuccess(type, id, [newNs1, newNs2]);
- expect(result).toEqual({});
+ expect(result).toEqual({ namespaces: [currentNs1, currentNs2, newNs1, newNs2] });
});
it(`succeeds when adding existing namespaces`, async () => {
const result = await addToNamespacesSuccess(type, id, [currentNs1]);
- expect(result).toEqual({});
+ expect(result).toEqual({ namespaces: [currentNs1, currentNs2] });
});
});
});
@@ -464,8 +464,16 @@ describe('SavedObjectsRepository', () => {
{ method, _index = expect.any(String), getId = () => expect.any(String) }
) => {
const body = [];
- for (const { type, id } of objects) {
- body.push({ [method]: { _index, _id: getId(type, id) } });
+ for (const { type, id, if_primary_term: ifPrimaryTerm, if_seq_no: ifSeqNo } of objects) {
+ body.push({
+ [method]: {
+ _index,
+ _id: getId(type, id),
+ ...(ifPrimaryTerm && ifSeqNo
+ ? { if_primary_term: expect.any(Number), if_seq_no: expect.any(Number) }
+ : {}),
+ },
+ });
body.push(expect.any(Object));
}
expect(client.bulk).toHaveBeenCalledWith(
@@ -525,6 +533,27 @@ describe('SavedObjectsRepository', () => {
expectClientCallArgsAction([obj1, obj2], { method: 'index' });
});
+ it(`should use the ES index method with version if ID and version are defined and overwrite=true`, async () => {
+ await bulkCreateSuccess(
+ [
+ {
+ ...obj1,
+ version: mockVersion,
+ },
+ obj2,
+ ],
+ { overwrite: true }
+ );
+
+ const obj1WithSeq = {
+ ...obj1,
+ if_seq_no: mockVersionProps._seq_no,
+ if_primary_term: mockVersionProps._primary_term,
+ };
+
+ expectClientCallArgsAction([obj1WithSeq, obj2], { method: 'index' });
+ });
+
it(`should use the ES create method if ID is defined and overwrite=false`, async () => {
await bulkCreateSuccess([obj1, obj2]);
expectClientCallArgsAction([obj1, obj2], { method: 'create' });
@@ -1516,6 +1545,16 @@ describe('SavedObjectsRepository', () => {
expect(client.index).toHaveBeenCalled();
});
+ it(`should use the ES index with version if ID and version are defined and overwrite=true`, async () => {
+ await createSuccess(type, attributes, { id, overwrite: true, version: mockVersion });
+ expect(client.index).toHaveBeenCalled();
+
+ expect(client.index.mock.calls[0][0]).toMatchObject({
+ if_seq_no: mockVersionProps._seq_no,
+ if_primary_term: mockVersionProps._primary_term,
+ });
+ });
+
it(`should use the ES create action if ID is defined and overwrite=false`, async () => {
await createSuccess(type, attributes, { id });
expect(client.create).toHaveBeenCalled();
@@ -3063,17 +3102,17 @@ describe('SavedObjectsRepository', () => {
});
describe('returns', () => {
- it(`returns an empty object on success (delete)`, async () => {
+ it(`returns an empty namespaces array on success (delete)`, async () => {
const test = async (namespaces) => {
const result = await deleteFromNamespacesSuccess(type, id, namespaces, namespaces);
- expect(result).toEqual({});
+ expect(result).toEqual({ namespaces: [] });
client.delete.mockClear();
};
await test([namespace1]);
await test([namespace1, namespace2]);
});
- it(`returns an empty object on success (update)`, async () => {
+ it(`returns remaining namespaces on success (update)`, async () => {
const test = async (remaining) => {
const currentNamespaces = [namespace1].concat(remaining);
const result = await deleteFromNamespacesSuccess(
@@ -3082,7 +3121,7 @@ describe('SavedObjectsRepository', () => {
[namespace1],
currentNamespaces
);
- expect(result).toEqual({});
+ expect(result).toEqual({ namespaces: remaining });
client.delete.mockClear();
};
await test([namespace2]);
@@ -3093,7 +3132,7 @@ describe('SavedObjectsRepository', () => {
const namespaces = [namespace2];
const currentNamespaces = [namespace1];
const result = await deleteFromNamespacesSuccess(type, id, namespaces, currentNamespaces);
- expect(result).toEqual({});
+ expect(result).toEqual({ namespaces: currentNamespaces });
});
});
});
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index d7e1ecba0370b..28d409f7b65bb 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -52,7 +52,9 @@ import {
SavedObjectsBulkUpdateOptions,
SavedObjectsDeleteOptions,
SavedObjectsAddToNamespacesOptions,
+ SavedObjectsAddToNamespacesResponse,
SavedObjectsDeleteFromNamespacesOptions,
+ SavedObjectsDeleteFromNamespacesResponse,
} from '../saved_objects_client';
import {
SavedObject,
@@ -220,6 +222,7 @@ export class SavedObjectsRepository {
overwrite = false,
references = [],
refresh = DEFAULT_REFRESH_SETTING,
+ version,
} = options;
if (!this._allowedTypes.includes(type)) {
@@ -259,6 +262,7 @@ export class SavedObjectsRepository {
index: this.getIndexForType(type),
refresh,
body: raw._source,
+ ...(overwrite && version ? decodeRequestVersion(version) : {}),
};
const { body } =
@@ -345,7 +349,12 @@ export class SavedObjectsRepository {
let savedObjectNamespace;
let savedObjectNamespaces;
- const { esRequestIndex, object, method } = expectedBulkGetResult.value;
+ let versionProperties;
+ const {
+ esRequestIndex,
+ object: { version, ...object },
+ method,
+ } = expectedBulkGetResult.value;
if (esRequestIndex !== undefined) {
const indexFound = bulkGetResponse?.statusCode !== 404;
const actualResult = indexFound ? bulkGetResponse?.body.docs[esRequestIndex] : undefined;
@@ -362,12 +371,14 @@ export class SavedObjectsRepository {
};
}
savedObjectNamespaces = getSavedObjectNamespaces(namespace, docFound && actualResult);
+ versionProperties = getExpectedVersionProperties(version, actualResult);
} else {
if (this._registry.isSingleNamespace(object.type)) {
savedObjectNamespace = namespace;
} else if (this._registry.isMultiNamespace(object.type)) {
savedObjectNamespaces = getSavedObjectNamespaces(namespace);
}
+ versionProperties = getExpectedVersionProperties(version);
}
const expectedResult = {
@@ -392,6 +403,7 @@ export class SavedObjectsRepository {
[method]: {
_id: expectedResult.rawMigratedDoc._id,
_index: this.getIndexForType(object.type),
+ ...(overwrite && versionProperties),
},
},
expectedResult.rawMigratedDoc._source
@@ -937,7 +949,7 @@ export class SavedObjectsRepository {
id: string,
namespaces: string[],
options: SavedObjectsAddToNamespacesOptions = {}
- ): Promise<{}> {
+ ): Promise {
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
@@ -986,7 +998,7 @@ export class SavedObjectsRepository {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
- return {};
+ return { namespaces: doc.namespaces };
}
/**
@@ -999,7 +1011,7 @@ export class SavedObjectsRepository {
id: string,
namespaces: string[],
options: SavedObjectsDeleteFromNamespacesOptions = {}
- ): Promise<{}> {
+ ): Promise {
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
@@ -1053,7 +1065,7 @@ export class SavedObjectsRepository {
// see "404s from missing index" above
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
- return {};
+ return { namespaces: doc.namespaces };
} else {
// if there are no namespaces remaining, delete the saved object
const { body, statusCode } = await this.client.delete(
@@ -1070,7 +1082,7 @@ export class SavedObjectsRepository {
const deleted = body.result === 'deleted';
if (deleted) {
- return {};
+ return { namespaces: [] };
}
const deleteDocNotFound = body.result === 'not_found';
diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts
index e15a92c92772f..812669ee108a2 100644
--- a/src/core/server/saved_objects/service/saved_objects_client.ts
+++ b/src/core/server/saved_objects/service/saved_objects_client.ts
@@ -37,6 +37,11 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
id?: string;
/** Overwrite existing documents (defaults to false) */
overwrite?: boolean;
+ /**
+ * An opaque version number which changes on each successful write operation.
+ * Can be used in conjunction with `overwrite` for implementing optimistic concurrency control.
+ **/
+ version?: string;
/** {@inheritDoc SavedObjectsMigrationVersion} */
migrationVersion?: SavedObjectsMigrationVersion;
references?: SavedObjectReference[];
@@ -52,6 +57,7 @@ export interface SavedObjectsBulkCreateObject {
id?: string;
type: string;
attributes: T;
+ version?: string;
references?: SavedObjectReference[];
/** {@inheritDoc SavedObjectsMigrationVersion} */
migrationVersion?: SavedObjectsMigrationVersion;
@@ -129,6 +135,15 @@ export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOpti
refresh?: MutatingOperationRefreshSetting;
}
+/**
+ *
+ * @public
+ */
+export interface SavedObjectsAddToNamespacesResponse {
+ /** The namespaces the object exists in after this operation is complete. */
+ namespaces: string[];
+}
+
/**
*
* @public
@@ -138,6 +153,15 @@ export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBas
refresh?: MutatingOperationRefreshSetting;
}
+/**
+ *
+ * @public
+ */
+export interface SavedObjectsDeleteFromNamespacesResponse {
+ /** The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. */
+ namespaces: string[];
+}
+
/**
*
* @public
@@ -314,7 +338,7 @@ export class SavedObjectsClient {
id: string,
namespaces: string[],
options: SavedObjectsAddToNamespacesOptions = {}
- ): Promise<{}> {
+ ): Promise {
return await this._repository.addToNamespaces(type, id, namespaces, options);
}
@@ -331,7 +355,7 @@ export class SavedObjectsClient {
id: string,
namespaces: string[],
options: SavedObjectsDeleteFromNamespacesOptions = {}
- ): Promise<{}> {
+ ): Promise {
return await this._repository.deleteFromNamespaces(type, id, namespaces, options);
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 03545284e14fb..772be68f507d5 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -1059,6 +1059,7 @@ export class KibanaRequest(req: Request, routeSchemas?: RouteValidator | RouteValidatorFullConfig
, withoutSecretHeaders?: boolean): KibanaRequest
;
readonly headers: Headers;
+ readonly id: string;
readonly isSystemRequest: boolean;
// (undocumented)
readonly params: Params;
@@ -2022,6 +2023,11 @@ export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOpti
version?: string;
}
+// @public (undocumented)
+export interface SavedObjectsAddToNamespacesResponse {
+ namespaces: string[];
+}
+
// Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts
//
@@ -2044,6 +2050,8 @@ export interface SavedObjectsBulkCreateObject {
references?: SavedObjectReference[];
// (undocumented)
type: string;
+ // (undocumented)
+ version?: string;
}
// @public (undocumented)
@@ -2089,13 +2097,13 @@ export interface SavedObjectsBulkUpdateResponse {
export class SavedObjectsClient {
// @internal
constructor(repository: ISavedObjectsRepository);
- addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>;
+ addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise;
bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>;
bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>;
bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>;
create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
- deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>;
+ deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise;
// (undocumented)
static errors: typeof SavedObjectsErrorHelpers;
// (undocumented)
@@ -2178,6 +2186,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
// (undocumented)
references?: SavedObjectReference[];
refresh?: MutatingOperationRefreshSetting;
+ version?: string;
}
// @public (undocumented)
@@ -2190,6 +2199,11 @@ export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBas
refresh?: MutatingOperationRefreshSetting;
}
+// @public (undocumented)
+export interface SavedObjectsDeleteFromNamespacesResponse {
+ namespaces: string[];
+}
+
// @public (undocumented)
export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions {
refresh?: MutatingOperationRefreshSetting;
@@ -2488,7 +2502,7 @@ export interface SavedObjectsRawDoc {
// @public (undocumented)
export class SavedObjectsRepository {
- addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise<{}>;
+ addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise;
bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>;
bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>;
bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>;
@@ -2499,7 +2513,7 @@ export class SavedObjectsRepository {
static createRepository(migrator: KibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise;
- deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>;
+ deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise;
// (undocumented)
find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, }: SavedObjectsFindOptions): Promise>;
get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
diff --git a/src/dev/file.ts b/src/dev/file.ts
index 32998d3e776ef..1209cc323c4c3 100644
--- a/src/dev/file.ts
+++ b/src/dev/file.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { dirname, extname, join, relative, resolve, sep } from 'path';
+import { dirname, extname, join, relative, resolve, sep, basename } from 'path';
export class File {
private path: string;
@@ -38,6 +38,12 @@ export class File {
return this.relativePath;
}
+ public getWithoutExtension() {
+ const directory = dirname(this.path);
+ const stem = basename(this.path, this.ext);
+ return new File(resolve(directory, stem));
+ }
+
public isJs() {
return this.ext === '.js';
}
diff --git a/src/dev/i18n/tasks/extract_untracked_translations.ts b/src/dev/i18n/tasks/extract_untracked_translations.ts
index 21ab47641f2f0..928997aced801 100644
--- a/src/dev/i18n/tasks/extract_untracked_translations.ts
+++ b/src/dev/i18n/tasks/extract_untracked_translations.ts
@@ -47,7 +47,7 @@ export async function extractUntrackedMessagesTask({
'**/build/**',
'**/__fixtures__/**',
'**/packages/kbn-i18n/**',
- '**/packages/kbn-plugin-generator/sao_template/**',
+ '**/packages/kbn-plugin-generator/template/**',
'**/packages/kbn-ui-framework/generator-kui/**',
'**/target/**',
'**/test/**',
diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js
index d46b955f6668d..74e1ec5e2b4ed 100644
--- a/src/dev/jest/config.js
+++ b/src/dev/jest/config.js
@@ -78,7 +78,6 @@ export default {
setupFilesAfterEnv: [
'/src/dev/jest/setup/mocks.js',
'/src/dev/jest/setup/react_testing_library.js',
- '/src/dev/jest/setup/default_timeout.js',
],
coverageDirectory: '/target/kibana-coverage/jest',
coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'],
diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js
index 19d03e41ac5a9..e22dc03cf57aa 100644
--- a/src/dev/precommit_hook/casing_check_config.js
+++ b/src/dev/precommit_hook/casing_check_config.js
@@ -109,6 +109,14 @@ export const IGNORE_DIRECTORY_GLOBS = [
'packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack',
];
+/**
+ * These patterns identify files which should have the extension stripped
+ * to reveal the actual name that should be checked.
+ *
+ * @type {Array}
+ */
+export const REMOVE_EXTENSION = ['packages/kbn-plugin-generator/template/**/*.ejs'];
+
/**
* DO NOT ADD FILES TO THIS LIST!!
*
diff --git a/src/dev/precommit_hook/check_file_casing.js b/src/dev/precommit_hook/check_file_casing.js
index ec77d9a8bad3c..243818d35a90b 100644
--- a/src/dev/precommit_hook/check_file_casing.js
+++ b/src/dev/precommit_hook/check_file_casing.js
@@ -29,6 +29,7 @@ import {
IGNORE_FILE_GLOBS,
TEMPORARILY_IGNORED_PATHS,
KEBAB_CASE_DIRECTORY_GLOBS,
+ REMOVE_EXTENSION,
} from './casing_check_config';
const NON_SNAKE_CASE_RE = /[A-Z \-]/;
@@ -143,6 +144,10 @@ async function checkForSnakeCase(log, files) {
}
export async function checkFileCasing(log, files) {
+ files = files.map((f) =>
+ matchesAnyGlob(f.getRelativePath(), REMOVE_EXTENSION) ? f.getWithoutExtension() : f
+ );
+
await checkForKebabCase(log, files);
await checkForSnakeCase(log, files);
}
diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts
index 5d4cf14c1cd95..9eeaeb4da7042 100644
--- a/src/dev/typescript/run_type_check_cli.ts
+++ b/src/dev/typescript/run_type_check_cli.ts
@@ -88,7 +88,7 @@ export function runTypeCheckCli() {
}
execInProjects(log, projects, process.execPath, (project) => [
- ...(project.name.startsWith('x-pack') ? ['--max-old-space-size=4096'] : []),
+ '--max-old-space-size=4096',
require.resolve('typescript/bin/tsc'),
...['--project', project.tsConfigPath],
...tscArgs,
diff --git a/src/legacy/core_plugins/console_legacy/index.ts b/src/legacy/core_plugins/console_legacy/index.ts
deleted file mode 100644
index 82e00a99c6cfd..0000000000000
--- a/src/legacy/core_plugins/console_legacy/index.ts
+++ /dev/null
@@ -1,49 +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 { first } from 'rxjs/operators';
-import { head } from 'lodash';
-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: {
- injectDefaultVars: () => ({
- elasticsearchUrl: url.format(
- Object.assign(url.parse(head(_legacyEsConfig.hosts) as any), { auth: false })
- ),
- }),
- },
- } as any);
-}
diff --git a/src/legacy/core_plugins/console_legacy/package.json b/src/legacy/core_plugins/console_legacy/package.json
deleted file mode 100644
index b78807daed959..0000000000000
--- a/src/legacy/core_plugins/console_legacy/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "console_legacy",
- "version": "kibana"
-}
diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js
index 2e30bc5ce05ee..176c5386961a5 100644
--- a/src/legacy/core_plugins/kibana/index.js
+++ b/src/legacy/core_plugins/kibana/index.js
@@ -21,7 +21,6 @@ import Fs from 'fs';
import { promisify } from 'util';
import { getUiSettingDefaults } from './server/ui_setting_defaults';
-import { registerCspCollector } from './server/lib/csp_usage_collector';
const mkdirAsync = promisify(Fs.mkdir);
@@ -53,10 +52,5 @@ export default function (kibana) {
throw err;
}
},
-
- init: async function (server) {
- const { usageCollection } = server.newPlatform.setup.plugins;
- registerCspCollector(usageCollection, server);
- },
});
}
diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js
index 5ef1149146afe..b8d48b784dba7 100644
--- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js
+++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js
@@ -221,17 +221,6 @@ export const npSetup = {
registerFunction: sinon.fake(),
registerRenderer: sinon.fake(),
registerType: sinon.fake(),
- __LEGACY: {
- renderers: {
- register: () => undefined,
- get: () => null,
- },
- getExecutor: () => ({
- interpreter: {
- interpretAst: () => {},
- },
- }),
- },
},
data: {
autocomplete: {
diff --git a/src/legacy/ui/public/new_platform/set_services.ts b/src/legacy/ui/public/new_platform/set_services.ts
index ee92eda064aa8..036157a9f3fbc 100644
--- a/src/legacy/ui/public/new_platform/set_services.ts
+++ b/src/legacy/ui/public/new_platform/set_services.ts
@@ -41,7 +41,6 @@ interface NpStart {
export function setSetupServices(npSetup: NpSetup) {
// Services that need to be set in the legacy platform since the legacy data plugin
// which previously provided them has been removed.
- dataServices.setInjectedMetadata(npSetup.core.injectedMetadata);
visualizationsServices.setUISettings(npSetup.core.uiSettings);
visualizationsServices.setUsageCollector(npSetup.plugins.usageCollection);
}
@@ -49,7 +48,6 @@ export function setSetupServices(npSetup: NpSetup) {
export function setStartServices(npStart: NpStart) {
// Services that need to be set in the legacy platform since the legacy data plugin
// which previously provided them has been removed.
- dataServices.setHttp(npStart.core.http);
dataServices.setNotifications(npStart.core.notifications);
dataServices.setOverlays(npStart.core.overlays);
dataServices.setUiSettings(npStart.core.uiSettings);
diff --git a/src/plugins/kibana_utils/public/core/state.ts b/src/plugins/console/common/types/api_responses.ts
similarity index 71%
rename from src/plugins/kibana_utils/public/core/state.ts
rename to src/plugins/console/common/types/api_responses.ts
index 52c4d166d737e..1c8166bbe27f2 100644
--- a/src/plugins/kibana_utils/public/core/state.ts
+++ b/src/plugins/console/common/types/api_responses.ts
@@ -17,7 +17,13 @@
* under the License.
*/
-import { createGetterSetter } from '../../common';
-import { CoreStart } from '../../../../core/public';
-
-export const [getCoreStart, setCoreStart] = createGetterSetter('CoreStart');
+export interface EsConfigApiResponse {
+ /**
+ * This is the first host in the hosts array that Kibana is configured to use
+ * to communicate with ES.
+ *
+ * At the moment this is used to power the copy as cURL functionality in Console
+ * to complete the host portion of the URL.
+ */
+ host?: string;
+}
diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
index 880069d8ebc7a..fc88b31711b23 100644
--- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
+++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
@@ -67,9 +67,8 @@ const inputId = 'ConAppInputTextarea';
function EditorUI({ initialTextValue }: EditorProps) {
const {
- services: { history, notifications, settings: settingsService },
+ services: { history, notifications, settings: settingsService, esHostService },
docLinkVersion,
- elasticsearchUrl,
} = useServicesContext();
const { settings } = useEditorReadContext();
@@ -232,7 +231,7 @@ function EditorUI({ initialTextValue }: EditorProps) {
{
- return editorInstanceRef.current!.getRequestsAsCURL(elasticsearchUrl);
+ return editorInstanceRef.current!.getRequestsAsCURL(esHostService.getHost());
}}
getDocumentation={() => {
return getDocumentation(editorInstanceRef.current!, docLinkVersion);
diff --git a/src/plugins/console/public/application/contexts/services_context.mock.ts b/src/plugins/console/public/application/contexts/services_context.mock.ts
index ae8d15a890782..ba982d3f50cfb 100644
--- a/src/plugins/console/public/application/contexts/services_context.mock.ts
+++ b/src/plugins/console/public/application/contexts/services_context.mock.ts
@@ -17,21 +17,27 @@
* under the License.
*/
import { notificationServiceMock } from '../../../../../core/public/mocks';
+import { httpServiceMock } from '../../../../../core/public/mocks';
+
import { HistoryMock } from '../../services/history.mock';
import { SettingsMock } from '../../services/settings.mock';
import { StorageMock } from '../../services/storage.mock';
+import { createApi, createEsHostService } from '../lib';
import { ContextValue } from './services_context';
export const serviceContextMock = {
create: (): ContextValue => {
const storage = new StorageMock({} as any, 'test');
+ const http = httpServiceMock.createSetupContract();
+ const api = createApi({ http });
+ const esHostService = createEsHostService({ api });
(storage.keys as jest.Mock).mockImplementation(() => []);
return {
- elasticsearchUrl: 'test',
services: {
trackUiMetric: { count: () => {}, load: () => {} },
storage,
+ esHostService,
settings: new SettingsMock(storage),
history: new HistoryMock(storage),
notifications: notificationServiceMock.createSetupContract(),
diff --git a/src/plugins/console/public/application/contexts/services_context.tsx b/src/plugins/console/public/application/contexts/services_context.tsx
index 3d4ac3291c5ac..e2f01a152b27b 100644
--- a/src/plugins/console/public/application/contexts/services_context.tsx
+++ b/src/plugins/console/public/application/contexts/services_context.tsx
@@ -17,22 +17,25 @@
* under the License.
*/
-import React, { createContext, useContext } from 'react';
+import React, { createContext, useContext, useEffect } from 'react';
import { NotificationsSetup } from 'kibana/public';
-import { History, Storage, Settings } from '../../services';
+import { History, Settings, Storage } from '../../services';
import { ObjectStorageClient } from '../../../common/types';
import { MetricsTracker } from '../../types';
+import { EsHostService } from '../lib';
+
+interface ContextServices {
+ history: History;
+ storage: Storage;
+ settings: Settings;
+ notifications: NotificationsSetup;
+ objectStorageClient: ObjectStorageClient;
+ trackUiMetric: MetricsTracker;
+ esHostService: EsHostService;
+}
export interface ContextValue {
- services: {
- history: History;
- storage: Storage;
- settings: Settings;
- notifications: NotificationsSetup;
- objectStorageClient: ObjectStorageClient;
- trackUiMetric: MetricsTracker;
- };
- elasticsearchUrl: string;
+ services: ContextServices;
docLinkVersion: string;
}
@@ -44,6 +47,11 @@ interface ContextProps {
const ServicesContext = createContext(null as any);
export function ServicesContextProvider({ children, value }: ContextProps) {
+ useEffect(() => {
+ // Fire and forget, we attempt to init the host service once.
+ value.services.esHostService.init();
+ }, [value.services.esHostService]);
+
return {children} ;
}
diff --git a/src/plugins/console/public/application/hooks/README.md b/src/plugins/console/public/application/hooks/README.md
new file mode 100644
index 0000000000000..10057193560e9
--- /dev/null
+++ b/src/plugins/console/public/application/hooks/README.md
@@ -0,0 +1,5 @@
+## Notes
+
+* Do not add any code directly to this directory. This code should be moved to the neighbouring `lib` directory to be in line with future ES UI plugin patterns.
+
+* The `es.send` method uses $.ajax under the hood and needs to be refactored to use the new platform-provided http client.
\ No newline at end of file
diff --git a/src/plugins/console/public/application/index.tsx b/src/plugins/console/public/application/index.tsx
index 051eaea27a7de..0a5a502eb5062 100644
--- a/src/plugins/console/public/application/index.tsx
+++ b/src/plugins/console/public/application/index.tsx
@@ -19,19 +19,20 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
-import { NotificationsSetup } from 'src/core/public';
+import { HttpSetup, NotificationsSetup } from 'src/core/public';
import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts';
import { Main } from './containers';
import { createStorage, createHistory, createSettings } from '../services';
import * as localStorageObjectClient from '../lib/local_storage_object_client';
import { createUsageTracker } from '../services/tracker';
import { UsageCollectionSetup } from '../../../usage_collection/public';
+import { createApi, createEsHostService } from './lib';
export interface BootDependencies {
+ http: HttpSetup;
docLinkVersion: string;
I18nContext: any;
notifications: NotificationsSetup;
- elasticsearchUrl: string;
usageCollection?: UsageCollectionSetup;
element: HTMLElement;
}
@@ -40,9 +41,9 @@ export function renderApp({
I18nContext,
notifications,
docLinkVersion,
- elasticsearchUrl,
usageCollection,
element,
+ http,
}: BootDependencies) {
const trackUiMetric = createUsageTracker(usageCollection);
trackUiMetric.load('opened_app');
@@ -54,14 +55,16 @@ export function renderApp({
const history = createHistory({ storage });
const settings = createSettings({ storage });
const objectStorageClient = localStorageObjectClient.create(storage);
+ const api = createApi({ http });
+ const esHostService = createEsHostService({ api });
render(
) => {
- const savedObjectsClient: KUSavedObjectClient = {
- get: (...args) => getCoreStart().savedObjects.client.get(...args),
- };
+export type Api = ReturnType;
- return savedObjectsClient;
+export const createApi = ({ http }: Dependencies) => {
+ return {
+ getEsConfig: () => {
+ return sendRequest(http, {
+ path: '/api/console/es_config',
+ method: 'get',
+ });
+ },
+ };
};
diff --git a/src/plugins/kibana_utils/public/core/create_kibana_utils_core.test.ts b/src/plugins/console/public/application/lib/es_host_service.ts
similarity index 50%
rename from src/plugins/kibana_utils/public/core/create_kibana_utils_core.test.ts
rename to src/plugins/console/public/application/lib/es_host_service.ts
index c5b23bdf0055f..887f270a24687 100644
--- a/src/plugins/kibana_utils/public/core/create_kibana_utils_core.test.ts
+++ b/src/plugins/console/public/application/lib/es_host_service.ts
@@ -17,23 +17,38 @@
* under the License.
*/
-import { createKibanaUtilsCore } from './create_kibana_utils_core';
-import { CoreStart } from 'kibana/public';
+import { Api } from './api';
-describe('createKibanaUtilsCore', () => {
- it('should allows to work with multiple instances', () => {
- const core1 = {} as CoreStart;
- const core2 = {} as CoreStart;
+/**
+ * Very simple state for holding the current ES host.
+ *
+ * This is used to power the copy as cURL functionality.
+ */
+export class EsHostService {
+ private host = 'http://localhost:9200';
+
+ constructor(private readonly api: Api) {}
- const { setCoreStart: setCoreStart1, getCoreStart: getCoreStart1 } = createKibanaUtilsCore();
- const { setCoreStart: setCoreStart2, getCoreStart: getCoreStart2 } = createKibanaUtilsCore();
+ private setHost(host: string): void {
+ this.host = host;
+ }
- setCoreStart1(core1);
- setCoreStart2(core2);
+ /**
+ * Initialize the host value based on the value set on the server.
+ *
+ * This call is necessary because this value can only be retrieved at
+ * runtime.
+ */
+ public async init() {
+ const { data } = await this.api.getEsConfig();
+ if (data && data.host) {
+ this.setHost(data.host);
+ }
+ }
- expect(getCoreStart1()).toBe(core1);
- expect(getCoreStart2()).toBe(core2);
+ public getHost(): string {
+ return this.host;
+ }
+}
- expect(getCoreStart1() !== getCoreStart2()).toBeTruthy();
- });
-});
+export const createEsHostService = ({ api }: { api: Api }) => new EsHostService(api);
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/charts.js b/src/plugins/console/public/application/lib/index.ts
similarity index 84%
rename from src/plugins/vis_type_timeseries/public/application/components/lib/charts.js
rename to src/plugins/console/public/application/lib/index.ts
index 2495026304be8..1ba99cc607269 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/lib/charts.js
+++ b/src/plugins/console/public/application/lib/index.ts
@@ -17,6 +17,5 @@
* under the License.
*/
-import { uniq, map, size, flow } from 'lodash';
-
-export const areFieldsDifferent = (name) => (series) => flow(uniq, size)(map(series, name)) > 1;
+export { createApi, Api } from './api';
+export { createEsHostService, EsHostService } from './es_host_service';
diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts
index 851dc7a063d7b..03b65a8bd145c 100644
--- a/src/plugins/console/public/plugin.ts
+++ b/src/plugins/console/public/plugin.ts
@@ -25,7 +25,7 @@ import { AppSetupUIPluginDependencies } from './types';
export class ConsoleUIPlugin implements Plugin {
public setup(
- { notifications, getStartServices }: CoreSetup,
+ { notifications, getStartServices, http }: CoreSetup,
{ devTools, home, usageCollection }: AppSetupUIPluginDependencies
) {
home.featureCatalogue.register({
@@ -53,23 +53,17 @@ export class ConsoleUIPlugin implements Plugin ({
import { duration } from 'moment';
import { ProxyConfigCollection } from '../../lib';
-import { CreateHandlerDependencies } from '../../routes/api/console/proxy/create_handler';
-import { coreMock } from '../../../../../core/server/mocks';
+import { RouteDependencies, ProxyDependencies } from '../../routes';
+import { EsLegacyConfigService, SpecDefinitionsService } from '../../services';
+import { coreMock, httpServiceMock } from '../../../../../core/server/mocks';
-export const getProxyRouteHandlerDeps = ({
- proxyConfigCollection = new ProxyConfigCollection([]),
- pathFilters = [/.*/],
- readLegacyESConfig = () => ({
+const defaultProxyValue = Object.freeze({
+ readLegacyESConfig: async () => ({
requestTimeout: duration(30000),
customHeaders: {},
requestHeadersWhitelist: [],
hosts: ['http://localhost:9200'],
}),
- log = coreMock.createPluginInitializerContext().logger.get(),
-}: Partial): CreateHandlerDependencies => ({
- proxyConfigCollection,
- pathFilters,
- readLegacyESConfig,
- log,
+ pathFilters: [/.*/],
+ proxyConfigCollection: new ProxyConfigCollection([]),
});
+
+interface MockDepsArgument extends Partial> {
+ proxy?: Partial;
+}
+
+export const getProxyRouteHandlerDeps = ({
+ proxy,
+ log = coreMock.createPluginInitializerContext().logger.get(),
+ router = httpServiceMock.createSetupContract().createRouter(),
+}: MockDepsArgument): RouteDependencies => {
+ const services: RouteDependencies['services'] = {
+ esLegacyConfigService: new EsLegacyConfigService(),
+ specDefinitionService: new SpecDefinitionsService(),
+ };
+
+ return {
+ services,
+ router,
+ proxy: proxy
+ ? {
+ ...defaultProxyValue,
+ ...proxy,
+ }
+ : defaultProxyValue,
+ log,
+ };
+};
diff --git a/src/plugins/console/server/__tests__/proxy_route/params.test.ts b/src/plugins/console/server/__tests__/proxy_route/params.test.ts
index 1ab9c3ae789cc..e1c5295f6d30f 100644
--- a/src/plugins/console/server/__tests__/proxy_route/params.test.ts
+++ b/src/plugins/console/server/__tests__/proxy_route/params.test.ts
@@ -36,7 +36,7 @@ describe('Console Proxy Route', () => {
describe('no matches', () => {
it('rejects with 403', async () => {
handler = createHandler(
- getProxyRouteHandlerDeps({ pathFilters: [/^\/foo\//, /^\/bar\//] })
+ getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } })
);
const { status } = await handler(
@@ -51,7 +51,7 @@ describe('Console Proxy Route', () => {
describe('one match', () => {
it('allows the request', async () => {
handler = createHandler(
- getProxyRouteHandlerDeps({ pathFilters: [/^\/foo\//, /^\/bar\//] })
+ getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } })
);
(requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
@@ -68,7 +68,9 @@ describe('Console Proxy Route', () => {
});
describe('all match', () => {
it('allows the request', async () => {
- handler = createHandler(getProxyRouteHandlerDeps({ pathFilters: [/^\/foo\//] }));
+ handler = createHandler(
+ getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//] } })
+ );
(requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
diff --git a/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts b/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts
index b226bad11a01a..fc5233d0f833d 100644
--- a/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts
+++ b/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts
@@ -38,12 +38,14 @@ describe('Console Proxy Route', () => {
const handler = createHandler(
getProxyRouteHandlerDeps({
- readLegacyESConfig: () => ({
- requestTimeout: duration(30000),
- customHeaders: {},
- requestHeadersWhitelist: [],
- hosts: ['http://localhost:9201', 'http://localhost:9202', 'http://localhost:9203'],
- }),
+ proxy: {
+ readLegacyESConfig: async () => ({
+ requestTimeout: duration(30000),
+ customHeaders: {},
+ requestHeadersWhitelist: [],
+ hosts: ['http://localhost:9201', 'http://localhost:9202', 'http://localhost:9203'],
+ }),
+ },
})
);
diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts
index eedd1541e8898..a76a35f8146c9 100644
--- a/src/plugins/console/server/plugin.ts
+++ b/src/plugins/console/server/plugin.ts
@@ -19,13 +19,12 @@
import { first } from 'rxjs/operators';
import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server';
-import { readLegacyEsConfig } from '../../../legacy/core_plugins/console_legacy';
-
import { ProxyConfigCollection } from './lib';
-import { SpecDefinitionsService } from './services';
+import { SpecDefinitionsService, EsLegacyConfigService } from './services';
import { ConfigType } from './config';
-import { registerProxyRoute } from './routes/api/console/proxy';
-import { registerSpecDefinitionsRoute } from './routes/api/console/spec_definitions';
+
+import { registerRoutes } from './routes';
+
import { ESConfigForProxy, ConsoleSetup, ConsoleStart } from './types';
export class ConsoleServerPlugin implements Plugin {
@@ -33,11 +32,13 @@ export class ConsoleServerPlugin implements Plugin {
specDefinitionsService = new SpecDefinitionsService();
+ esLegacyConfigService = new EsLegacyConfigService();
+
constructor(private readonly ctx: PluginInitializerContext) {
this.log = this.ctx.logger.get();
}
- async setup({ http, capabilities, getStartServices }: CoreSetup) {
+ async setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) {
capabilities.registerProvider(() => ({
dev_tools: {
show: true,
@@ -46,30 +47,31 @@ export class ConsoleServerPlugin implements Plugin {
}));
const config = await this.ctx.config.create().pipe(first()).toPromise();
-
- const { elasticsearch } = await this.ctx.config.legacy.globalConfig$.pipe(first()).toPromise();
-
+ const globalConfig = await this.ctx.config.legacy.globalConfig$.pipe(first()).toPromise();
const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str));
+ this.esLegacyConfigService.setup(elasticsearch.legacy.config$);
+
const router = http.createRouter();
- registerProxyRoute({
+ registerRoutes({
+ router,
log: this.log,
- proxyConfigCollection: new ProxyConfigCollection(config.proxyConfig),
- readLegacyESConfig: (): ESConfigForProxy => {
- const legacyConfig = readLegacyEsConfig();
- return {
- ...elasticsearch,
- ...legacyConfig,
- };
+ services: {
+ esLegacyConfigService: this.esLegacyConfigService,
+ specDefinitionService: this.specDefinitionsService,
+ },
+ proxy: {
+ proxyConfigCollection: new ProxyConfigCollection(config.proxyConfig),
+ readLegacyESConfig: async (): Promise => {
+ const legacyConfig = await this.esLegacyConfigService.readConfig();
+ return {
+ ...globalConfig.elasticsearch,
+ ...legacyConfig,
+ };
+ },
+ pathFilters: proxyPathFilters,
},
- pathFilters: proxyPathFilters,
- router,
- });
-
- registerSpecDefinitionsRoute({
- router,
- services: { specDefinitions: this.specDefinitionsService },
});
return {
@@ -82,4 +84,8 @@ export class ConsoleServerPlugin implements Plugin {
...this.specDefinitionsService.start(),
};
}
+
+ stop() {
+ this.esLegacyConfigService.stop();
+ }
}
diff --git a/src/plugins/vis_type_metric/public/metric_vis_type.test.ts b/src/plugins/console/server/routes/api/console/es_config/index.ts
similarity index 62%
rename from src/plugins/vis_type_metric/public/metric_vis_type.test.ts
rename to src/plugins/console/server/routes/api/console/es_config/index.ts
index 636118c692aaa..a115a6b32ad01 100644
--- a/src/plugins/vis_type_metric/public/metric_vis_type.test.ts
+++ b/src/plugins/console/server/routes/api/console/es_config/index.ts
@@ -17,13 +17,17 @@
* under the License.
*/
-import { createMetricVisTypeDefinition } from './metric_vis_type';
-import { MetricVisComponent } from './components/metric_vis_component';
+import { EsConfigApiResponse } from '../../../../../common/types/api_responses';
+import { RouteDependencies } from '../../../';
-describe('metric_vis - createMetricVisTypeDefinition', () => {
- it('has metric vis component set', () => {
- const def = createMetricVisTypeDefinition();
+export const registerEsConfigRoute = ({ router, services }: RouteDependencies): void => {
+ router.get({ path: '/api/console/es_config', validate: false }, async (ctx, req, res) => {
+ const {
+ hosts: [host],
+ } = await services.esLegacyConfigService.readConfig();
- expect(def.visConfig.component).toBe(MetricVisComponent);
+ const body: EsConfigApiResponse = { host };
+
+ return res.ok({ body });
});
-});
+};
diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
index a16fb1dadfbcf..f6d9bcb77ddda 100644
--- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
@@ -21,7 +21,7 @@ import { Agent, IncomingMessage } from 'http';
import * as url from 'url';
import { pick, trimStart, trimEnd } from 'lodash';
-import { KibanaRequest, Logger, RequestHandler } from 'kibana/server';
+import { KibanaRequest, RequestHandler } from 'kibana/server';
import { ESConfigForProxy } from '../../../../types';
import {
@@ -31,19 +31,14 @@ import {
setHeaders,
} from '../../../../lib';
-import { Body, Query } from './validation_config';
-
// TODO: find a better way to get information from the request like remoteAddress and remotePort
// for forwarding.
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ensureRawRequest } from '../../../../../../../core/server/http/router';
-export interface CreateHandlerDependencies {
- log: Logger;
- readLegacyESConfig: () => ESConfigForProxy;
- pathFilters: RegExp[];
- proxyConfigCollection: ProxyConfigCollection;
-}
+import { RouteDependencies } from '../../../';
+
+import { Body, Query } from './validation_config';
function toURL(base: string, path: string) {
const urlResult = new url.URL(`${trimEnd(base, '/')}/${trimStart(path, '/')}`);
@@ -120,14 +115,8 @@ function getProxyHeaders(req: KibanaRequest) {
export const createHandler = ({
log,
- readLegacyESConfig,
- pathFilters,
- proxyConfigCollection,
-}: CreateHandlerDependencies): RequestHandler => async (
- ctx,
- request,
- response
-) => {
+ proxy: { readLegacyESConfig, pathFilters, proxyConfigCollection },
+}: RouteDependencies): RequestHandler => async (ctx, request, response) => {
const { body, query } = request;
const { path, method } = query;
@@ -140,7 +129,7 @@ export const createHandler = ({
});
}
- const legacyConfig = readLegacyESConfig();
+ const legacyConfig = await readLegacyESConfig();
const { hosts } = legacyConfig;
let esIncomingMessage: IncomingMessage;
diff --git a/src/plugins/console/server/routes/api/console/proxy/index.ts b/src/plugins/console/server/routes/api/console/proxy/index.ts
index 5f7df1d7cf66b..5841671c340bd 100644
--- a/src/plugins/console/server/routes/api/console/proxy/index.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/index.ts
@@ -17,17 +17,13 @@
* under the License.
*/
-import { IRouter } from 'kibana/server';
import { routeValidationConfig } from './validation_config';
-import { createHandler, CreateHandlerDependencies } from './create_handler';
+import { createHandler } from './create_handler';
-export const registerProxyRoute = (
- deps: {
- router: IRouter;
- } & CreateHandlerDependencies
-) => {
- const { router, ...handlerDeps } = deps;
- router.post(
+import { RouteDependencies } from '../../../';
+
+export const registerProxyRoute = (deps: RouteDependencies) => {
+ deps.router.post(
{
path: '/api/console/proxy',
options: {
@@ -39,6 +35,6 @@ export const registerProxyRoute = (
},
validate: routeValidationConfig,
},
- createHandler(handlerDeps)
+ createHandler(deps)
);
};
diff --git a/src/plugins/console/server/routes/api/console/spec_definitions/index.ts b/src/plugins/console/server/routes/api/console/spec_definitions/index.ts
index 5c7e679cd0d35..a179c36364e26 100644
--- a/src/plugins/console/server/routes/api/console/spec_definitions/index.ts
+++ b/src/plugins/console/server/routes/api/console/spec_definitions/index.ts
@@ -16,8 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { IRouter, RequestHandler } from 'kibana/server';
-import { SpecDefinitionsService } from '../../../../services';
+import { RequestHandler } from 'kibana/server';
+import { RouteDependencies } from '../../../';
interface SpecDefinitionsRouteResponse {
es: {
@@ -27,16 +27,10 @@ interface SpecDefinitionsRouteResponse {
};
}
-export const registerSpecDefinitionsRoute = ({
- router,
- services,
-}: {
- router: IRouter;
- services: { specDefinitions: SpecDefinitionsService };
-}) => {
+export const registerSpecDefinitionsRoute = ({ router, services }: RouteDependencies) => {
const handler: RequestHandler = async (ctx, request, response) => {
const specResponse: SpecDefinitionsRouteResponse = {
- es: services.specDefinitions.asJson(),
+ es: services.specDefinitionService.asJson(),
};
return response.ok({
diff --git a/src/plugins/console/server/routes/index.ts b/src/plugins/console/server/routes/index.ts
new file mode 100644
index 0000000000000..cbd1cef7b36e3
--- /dev/null
+++ b/src/plugins/console/server/routes/index.ts
@@ -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 { IRouter, Logger } from 'kibana/server';
+
+import { EsLegacyConfigService, SpecDefinitionsService } from '../services';
+import { ESConfigForProxy } from '../types';
+import { ProxyConfigCollection } from '../lib';
+
+import { registerEsConfigRoute } from './api/console/es_config';
+import { registerProxyRoute } from './api/console/proxy';
+import { registerSpecDefinitionsRoute } from './api/console/spec_definitions';
+
+export interface ProxyDependencies {
+ readLegacyESConfig: () => Promise;
+ pathFilters: RegExp[];
+ proxyConfigCollection: ProxyConfigCollection;
+}
+
+export interface RouteDependencies {
+ router: IRouter;
+ log: Logger;
+ proxy: ProxyDependencies;
+ services: {
+ esLegacyConfigService: EsLegacyConfigService;
+ specDefinitionService: SpecDefinitionsService;
+ };
+}
+
+export const registerRoutes = (dependencies: RouteDependencies) => {
+ registerEsConfigRoute(dependencies);
+ registerProxyRoute(dependencies);
+ registerSpecDefinitionsRoute(dependencies);
+};
diff --git a/src/plugins/console/server/services/es_legacy_config_service.ts b/src/plugins/console/server/services/es_legacy_config_service.ts
new file mode 100644
index 0000000000000..37928839b1846
--- /dev/null
+++ b/src/plugins/console/server/services/es_legacy_config_service.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { Observable, Subscription } from 'rxjs';
+import { first } from 'rxjs/operators';
+import { ElasticsearchConfig } from 'kibana/server';
+
+export class EsLegacyConfigService {
+ /**
+ * The elasticsearch config value at a given point in time.
+ */
+ private config?: ElasticsearchConfig;
+
+ /**
+ * An observable that emits elasticsearch config.
+ */
+ private config$?: Observable;
+
+ /**
+ * A reference to the subscription to the elasticsearch observable
+ */
+ private configSub?: Subscription;
+
+ setup(config$: Observable) {
+ this.config$ = config$;
+ this.configSub = this.config$.subscribe((config) => {
+ this.config = config;
+ });
+ }
+
+ stop() {
+ if (this.configSub) {
+ this.configSub.unsubscribe();
+ }
+ }
+
+ async readConfig(): Promise {
+ if (!this.config$) {
+ throw new Error('Could not read elasticsearch config, this service has not been setup!');
+ }
+
+ if (!this.config) {
+ return this.config$.pipe(first()).toPromise();
+ }
+
+ return this.config;
+ }
+}
diff --git a/src/plugins/console/server/services/index.ts b/src/plugins/console/server/services/index.ts
index c8dfeccd23070..c9d0b8b858150 100644
--- a/src/plugins/console/server/services/index.ts
+++ b/src/plugins/console/server/services/index.ts
@@ -17,4 +17,6 @@
* under the License.
*/
+export { EsLegacyConfigService } from './es_legacy_config_service';
+
export { SpecDefinitionsService } from './spec_definitions_service';
diff --git a/src/plugins/console/server/types.ts b/src/plugins/console/server/types.ts
index 4f026555ada7b..5dc8322c23ea9 100644
--- a/src/plugins/console/server/types.ts
+++ b/src/plugins/console/server/types.ts
@@ -38,8 +38,8 @@ export interface ESConfigForProxy {
requestTimeout: Duration;
ssl?: {
verificationMode: 'none' | 'certificate' | 'full';
- certificateAuthorities: string[] | string;
alwaysPresentCertificate: boolean;
+ certificateAuthorities?: string[];
certificate?: string;
key?: string;
keyPassphrase?: string;
diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx
new file mode 100644
index 0000000000000..9fa7fff9ad087
--- /dev/null
+++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx
@@ -0,0 +1,162 @@
+/*
+ * 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 { isErrorEmbeddable, IContainer, ReferenceOrValueEmbeddable } from '../../embeddable_plugin';
+import { DashboardContainer } from '../embeddable';
+import { getSampleDashboardInput } from '../test_helpers';
+import {
+ CONTACT_CARD_EMBEDDABLE,
+ ContactCardEmbeddableFactory,
+ ContactCardEmbeddable,
+ ContactCardEmbeddableInput,
+ ContactCardEmbeddableOutput,
+} from '../../embeddable_plugin_test_samples';
+import { coreMock } from '../../../../../core/public/mocks';
+import { CoreStart } from 'kibana/public';
+import { AddToLibraryAction } from '.';
+import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';
+import { ViewMode } from '../../../../embeddable/public';
+
+const { setup, doStart } = embeddablePluginMock.createInstance();
+setup.registerEmbeddableFactory(
+ CONTACT_CARD_EMBEDDABLE,
+ new ContactCardEmbeddableFactory((() => null) as any, {} as any)
+);
+const start = doStart();
+
+let container: DashboardContainer;
+let embeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable;
+let coreStart: CoreStart;
+beforeEach(async () => {
+ coreStart = coreMock.createStart();
+
+ const containerOptions = {
+ ExitFullScreenButton: () => null,
+ SavedObjectFinder: () => null,
+ application: {} as any,
+ embeddable: start,
+ inspector: {} as any,
+ notifications: {} as any,
+ overlays: coreStart.overlays,
+ savedObjectMetaData: {} as any,
+ uiActions: {} as any,
+ };
+
+ container = new DashboardContainer(getSampleDashboardInput(), containerOptions);
+
+ const contactCardEmbeddable = await container.addNewEmbeddable<
+ ContactCardEmbeddableInput,
+ ContactCardEmbeddableOutput,
+ ContactCardEmbeddable
+ >(CONTACT_CARD_EMBEDDABLE, {
+ firstName: 'Kibanana',
+ });
+
+ if (isErrorEmbeddable(contactCardEmbeddable)) {
+ throw new Error('Failed to create embeddable');
+ } else {
+ embeddable = embeddablePluginMock.mockRefOrValEmbeddable<
+ ContactCardEmbeddable,
+ ContactCardEmbeddableInput
+ >(contactCardEmbeddable, {
+ mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: contactCardEmbeddable.id },
+ mockedByValueInput: { firstName: 'Kibanana', id: contactCardEmbeddable.id },
+ });
+ embeddable.updateInput({ viewMode: ViewMode.EDIT });
+ }
+});
+
+test('Add to library is compatible when embeddable on dashboard has value type input', async () => {
+ const action = new AddToLibraryAction();
+ embeddable.updateInput(await embeddable.getInputAsValueType());
+ expect(await action.isCompatible({ embeddable })).toBe(true);
+});
+
+test('Add to library is not compatible when embeddable input is by reference', async () => {
+ const action = new AddToLibraryAction();
+ embeddable.updateInput(await embeddable.getInputAsRefType());
+ expect(await action.isCompatible({ embeddable })).toBe(false);
+});
+
+test('Add to library is not compatible when view mode is set to view', async () => {
+ const action = new AddToLibraryAction();
+ embeddable.updateInput(await embeddable.getInputAsRefType());
+ embeddable.updateInput({ viewMode: ViewMode.VIEW });
+ expect(await action.isCompatible({ embeddable })).toBe(false);
+});
+
+test('Add to library is not compatible when embeddable is not in a dashboard container', async () => {
+ let orphanContactCard = await container.addNewEmbeddable<
+ ContactCardEmbeddableInput,
+ ContactCardEmbeddableOutput,
+ ContactCardEmbeddable
+ >(CONTACT_CARD_EMBEDDABLE, {
+ firstName: 'Orphan',
+ });
+ orphanContactCard = embeddablePluginMock.mockRefOrValEmbeddable<
+ ContactCardEmbeddable,
+ ContactCardEmbeddableInput
+ >(orphanContactCard, {
+ mockedByReferenceInput: { savedObjectId: 'test', id: orphanContactCard.id },
+ mockedByValueInput: { firstName: 'Kibanana', id: orphanContactCard.id },
+ });
+ const action = new AddToLibraryAction();
+ expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false);
+});
+
+test('Add to library replaces embeddableId but retains panel count', async () => {
+ const dashboard = embeddable.getRoot() as IContainer;
+ const originalPanelCount = Object.keys(dashboard.getInput().panels).length;
+ const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
+ const action = new AddToLibraryAction();
+ await action.execute({ embeddable });
+ expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount);
+
+ const newPanelId = Object.keys(container.getInput().panels).find(
+ (key) => !originalPanelKeySet.has(key)
+ );
+ expect(newPanelId).toBeDefined();
+ const newPanel = container.getInput().panels[newPanelId!];
+ expect(newPanel.type).toEqual(embeddable.type);
+});
+
+test('Add to library returns reference type input', async () => {
+ const complicatedAttributes = {
+ attribute1: 'The best attribute',
+ attribute2: 22,
+ attribute3: ['array', 'of', 'strings'],
+ attribute4: { nestedattribute: 'hello from the nest' },
+ };
+
+ embeddable = embeddablePluginMock.mockRefOrValEmbeddable(embeddable, {
+ mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: embeddable.id },
+ mockedByValueInput: { attributes: complicatedAttributes, id: embeddable.id },
+ });
+ const dashboard = embeddable.getRoot() as IContainer;
+ const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
+ const action = new AddToLibraryAction();
+ await action.execute({ embeddable });
+ const newPanelId = Object.keys(container.getInput().panels).find(
+ (key) => !originalPanelKeySet.has(key)
+ );
+ expect(newPanelId).toBeDefined();
+ const newPanel = container.getInput().panels[newPanelId!];
+ expect(newPanel.type).toEqual(embeddable.type);
+ expect(newPanel.explicitInput.attributes).toBeUndefined();
+ expect(newPanel.explicitInput.savedObjectId).toBe('testSavedObjectId');
+});
diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx
new file mode 100644
index 0000000000000..3cc1a8a1dffe1
--- /dev/null
+++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx
@@ -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 { i18n } from '@kbn/i18n';
+import _ from 'lodash';
+import uuid from 'uuid';
+import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin';
+import { ViewMode, PanelState, IEmbeddable } from '../../embeddable_plugin';
+import {
+ PanelNotFoundError,
+ EmbeddableInput,
+ isReferenceOrValueEmbeddable,
+} from '../../../../embeddable/public';
+import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..';
+
+export const ACTION_ADD_TO_LIBRARY = 'addToFromLibrary';
+
+export interface AddToLibraryActionContext {
+ embeddable: IEmbeddable;
+}
+
+export class AddToLibraryAction implements ActionByType {
+ public readonly type = ACTION_ADD_TO_LIBRARY;
+ public readonly id = ACTION_ADD_TO_LIBRARY;
+ public order = 15;
+
+ constructor() {}
+
+ public getDisplayName({ embeddable }: AddToLibraryActionContext) {
+ if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) {
+ throw new IncompatibleActionError();
+ }
+ return i18n.translate('dashboard.panel.AddToLibrary', {
+ defaultMessage: 'Add to library',
+ });
+ }
+
+ public getIconType({ embeddable }: AddToLibraryActionContext) {
+ if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) {
+ throw new IncompatibleActionError();
+ }
+ return 'folderCheck';
+ }
+
+ public async isCompatible({ embeddable }: AddToLibraryActionContext) {
+ return Boolean(
+ embeddable.getInput()?.viewMode !== ViewMode.VIEW &&
+ embeddable.getRoot() &&
+ embeddable.getRoot().isContainer &&
+ embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE &&
+ isReferenceOrValueEmbeddable(embeddable) &&
+ !embeddable.inputIsRefType(embeddable.getInput())
+ );
+ }
+
+ public async execute({ embeddable }: AddToLibraryActionContext) {
+ if (!isReferenceOrValueEmbeddable(embeddable)) {
+ throw new IncompatibleActionError();
+ }
+
+ const newInput = await embeddable.getInputAsRefType();
+
+ embeddable.updateInput(newInput);
+
+ const dashboard = embeddable.getRoot() as DashboardContainer;
+ const panelToReplace = dashboard.getInput().panels[embeddable.id] as DashboardPanelState;
+ if (!panelToReplace) {
+ throw new PanelNotFoundError();
+ }
+
+ const newPanel: PanelState = {
+ type: embeddable.type,
+ explicitInput: { ...newInput, id: uuid.v4() },
+ };
+ dashboard.replacePanel(panelToReplace, newPanel);
+ }
+}
diff --git a/src/plugins/dashboard/public/application/actions/index.ts b/src/plugins/dashboard/public/application/actions/index.ts
index be183976c676f..4343a3409b696 100644
--- a/src/plugins/dashboard/public/application/actions/index.ts
+++ b/src/plugins/dashboard/public/application/actions/index.ts
@@ -33,7 +33,12 @@ export {
ACTION_CLONE_PANEL,
} from './clone_panel_action';
export {
+ AddToLibraryAction,
+ AddToLibraryActionContext,
+ ACTION_ADD_TO_LIBRARY,
+} from './add_to_library_action';
+export {
+ UnlinkFromLibraryAction,
UnlinkFromLibraryActionContext,
ACTION_UNLINK_FROM_LIBRARY,
- UnlinkFromLibraryAction,
} from './unlink_from_library_action';
diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx
index c2f529fe399f3..fe5f6a0c8e2bd 100644
--- a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx
+++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx
@@ -30,13 +30,21 @@ import {
SimpleSavedObject,
I18nStart,
NotificationsStart,
+ OverlayStart,
} from '../../../../core/public';
import {
SavedObjectSaveModal,
showSaveModal,
OnSaveProps,
SaveResult,
+ checkForDuplicateTitle,
} from '../../../saved_objects/public';
+import {
+ EmbeddableStart,
+ EmbeddableFactory,
+ EmbeddableFactoryNotFoundError,
+ Container,
+} from '../../../embeddable/public';
/**
* The attribute service is a shared, generic service that embeddables can use to provide the functionality
@@ -49,12 +57,22 @@ export class AttributeService<
ValType extends EmbeddableInput & { attributes: SavedObjectAttributes },
RefType extends SavedObjectEmbeddableInput
> {
+ private embeddableFactory: EmbeddableFactory;
+
constructor(
private type: string,
private savedObjectsClient: SavedObjectsClientContract,
+ private overlays: OverlayStart,
private i18nContext: I18nStart['Context'],
- private toasts: NotificationsStart['toasts']
- ) {}
+ private toasts: NotificationsStart['toasts'],
+ getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']
+ ) {
+ const factory = getEmbeddableFactory(this.type);
+ if (!factory) {
+ throw new EmbeddableFactoryNotFoundError(this.type);
+ }
+ this.embeddableFactory = factory;
+ }
public async unwrapAttributes(input: RefType | ValType): Promise {
if (this.inputIsRefType(input)) {
@@ -105,6 +123,15 @@ export class AttributeService<
return isSavedObjectEmbeddableInput(input);
};
+ public getExplicitInputFromEmbeddable(embeddable: IEmbeddable): ValType | RefType {
+ return embeddable.getRoot() &&
+ (embeddable.getRoot() as Container).getInput().panels[embeddable.id].explicitInput
+ ? ((embeddable.getRoot() as Container).getInput().panels[embeddable.id].explicitInput as
+ | ValType
+ | RefType)
+ : (embeddable.getInput() as ValType | RefType);
+ }
+
getInputAsValueType = async (input: ValType | RefType): Promise => {
if (!this.inputIsRefType(input)) {
return input;
@@ -124,16 +151,31 @@ export class AttributeService<
if (this.inputIsRefType(input)) {
return input;
}
-
return new Promise((resolve, reject) => {
const onSave = async (props: OnSaveProps): Promise => {
+ await checkForDuplicateTitle(
+ {
+ title: props.newTitle,
+ copyOnSave: false,
+ lastSavedTitle: '',
+ getEsType: () => this.type,
+ getDisplayName: this.embeddableFactory.getDisplayName,
+ },
+ props.isTitleDuplicateConfirmed,
+ props.onTitleDuplicate,
+ {
+ savedObjectsClient: this.savedObjectsClient,
+ overlays: this.overlays,
+ }
+ );
try {
- input.attributes.title = props.newTitle;
- const wrappedInput = (await this.wrapAttributes(input.attributes, true)) as RefType;
+ const newAttributes = { ...input.attributes };
+ newAttributes.title = props.newTitle;
+ const wrappedInput = (await this.wrapAttributes(newAttributes, true)) as RefType;
resolve(wrappedInput);
return { id: wrappedInput.savedObjectId };
} catch (error) {
- reject();
+ reject(error);
return { error };
}
};
diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx
index 2a36f2d801850..a788b06f91905 100644
--- a/src/plugins/dashboard/public/plugin.tsx
+++ b/src/plugins/dashboard/public/plugin.tsx
@@ -95,6 +95,11 @@ import { addEmbeddableToDashboardUrl } from './url_utils/url_helper';
import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder';
import { UrlGeneratorState } from '../../share/public';
import { AttributeService } from '.';
+import {
+ AddToLibraryAction,
+ ACTION_ADD_TO_LIBRARY,
+ AddToLibraryActionContext,
+} from './application/actions/add_to_library_action';
declare module '../../share/public' {
export interface UrlGeneratorStateMapping {
@@ -155,6 +160,7 @@ declare module '../../../plugins/ui_actions/public' {
[ACTION_EXPAND_PANEL]: ExpandPanelActionContext;
[ACTION_REPLACE_PANEL]: ReplacePanelActionContext;
[ACTION_CLONE_PANEL]: ClonePanelActionContext;
+ [ACTION_ADD_TO_LIBRARY]: AddToLibraryActionContext;
[ACTION_UNLINK_FROM_LIBRARY]: UnlinkFromLibraryActionContext;
}
}
@@ -406,6 +412,7 @@ export class DashboardPlugin
const {
uiActions,
data: { indexPatterns, search },
+ embeddable,
} = plugins;
const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings);
@@ -424,6 +431,9 @@ export class DashboardPlugin
uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id);
if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) {
+ const addToLibraryAction = new AddToLibraryAction();
+ uiActions.registerAction(addToLibraryAction);
+ uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id);
const unlinkFromLibraryAction = new UnlinkFromLibraryAction();
uiActions.registerAction(unlinkFromLibraryAction);
uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id);
@@ -452,8 +462,10 @@ export class DashboardPlugin
new AttributeService(
type,
core.savedObjects.client,
+ core.overlays,
core.i18n.Context,
- core.notifications.toasts
+ core.notifications.toasts,
+ embeddable.getEmbeddableFactory
),
};
}
diff --git a/src/plugins/data/common/es_query/es_query/get_es_query_config.ts b/src/plugins/data/common/es_query/es_query/get_es_query_config.ts
index ff8fc5b11b26e..87dbbcee03b81 100644
--- a/src/plugins/data/common/es_query/es_query/get_es_query_config.ts
+++ b/src/plugins/data/common/es_query/es_query/get_es_query_config.ts
@@ -18,10 +18,10 @@
*/
import { EsQueryConfig } from './build_es_query';
-import { UI_SETTINGS } from '../../';
+import { GetConfigFn, UI_SETTINGS } from '../../';
interface KibanaConfig {
- get(key: string): T;
+ get: GetConfigFn;
}
export function getEsQueryConfig(config: KibanaConfig) {
diff --git a/src/plugins/data/common/field_formats/converters/date_nanos_shared.ts b/src/plugins/data/common/field_formats/converters/date_nanos_shared.ts
index 89a63243c76f0..7a225cd7d1ae7 100644
--- a/src/plugins/data/common/field_formats/converters/date_nanos_shared.ts
+++ b/src/plugins/data/common/field_formats/converters/date_nanos_shared.ts
@@ -20,7 +20,8 @@
import { i18n } from '@kbn/i18n';
import { memoize, noop } from 'lodash';
import moment, { Moment } from 'moment';
-import { FieldFormat, FIELD_FORMAT_IDS, KBN_FIELD_TYPES, TextContextTypeConvert } from '../../';
+import { FieldFormat, FIELD_FORMAT_IDS, KBN_FIELD_TYPES } from '../../';
+import { TextContextTypeConvert } from '../types';
/**
* Analyse the given moment.js format pattern for the fractional sec part (S,SS,SSS...)
diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts
index 4b847ebc358d7..32f9f37b9ba53 100644
--- a/src/plugins/data/common/field_formats/field_formats_registry.ts
+++ b/src/plugins/data/common/field_formats/field_formats_registry.ts
@@ -32,7 +32,7 @@ import {
import { baseFormatters } from './constants/base_formatters';
import { FieldFormat } from './field_format';
import { SerializedFieldFormat } from '../../../expressions/common/types';
-import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../types';
+import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../kbn_field_types/types';
import { UI_SETTINGS } from '../constants';
export class FieldFormatsRegistry {
diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts
index 6f773378de08d..daa44b2b0f85b 100644
--- a/src/plugins/data/common/field_formats/types.ts
+++ b/src/plugins/data/common/field_formats/types.ts
@@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
+
+import { GetConfigFn } from '../types';
import { FieldFormat } from './field_format';
import { FieldFormatsRegistry } from './field_formats_registry';
@@ -72,7 +74,7 @@ export interface FieldFormatConfig {
es?: boolean;
}
-export type FieldFormatsGetConfigFn = (key: string, defaultOverride?: T) => T;
+export type FieldFormatsGetConfigFn = GetConfigFn;
export type IFieldFormat = PublicMethodsOf;
diff --git a/src/plugins/data/common/search/aggs/aggs_service.ts b/src/plugins/data/common/search/aggs/aggs_service.ts
index 59c54fcce6838..6f3e3904dbbd5 100644
--- a/src/plugins/data/common/search/aggs/aggs_service.ts
+++ b/src/plugins/data/common/search/aggs/aggs_service.ts
@@ -19,6 +19,7 @@
import { ExpressionsServiceSetup } from 'src/plugins/expressions/common';
import { UI_SETTINGS } from '../../../common';
+import { GetConfigFn } from '../../types';
import {
AggConfigs,
AggTypesRegistry,
@@ -48,7 +49,7 @@ export interface AggsCommonSetupDependencies {
/** @internal */
export interface AggsCommonStartDependencies {
- getConfig: (key: string) => T;
+ getConfig: GetConfigFn;
}
/**
diff --git a/src/plugins/data/common/search/es_search/types.ts b/src/plugins/data/common/search/es_search/types.ts
index 6fc0923768703..3184fbe341705 100644
--- a/src/plugins/data/common/search/es_search/types.ts
+++ b/src/plugins/data/common/search/es_search/types.ts
@@ -16,21 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { SearchParams, SearchResponse } from 'elasticsearch';
+import { SearchResponse } from 'elasticsearch';
+import { Search } from '@elastic/elasticsearch/api/requestParams';
import { IKibanaSearchRequest, IKibanaSearchResponse } from '../types';
export const ES_SEARCH_STRATEGY = 'es';
-export type ISearchRequestParams = {
+export type ISearchRequestParams> = {
trackTotalHits?: boolean;
-} & SearchParams;
+} & Search;
export interface IEsSearchRequest extends IKibanaSearchRequest {
params?: ISearchRequestParams;
indexType?: string;
}
-export interface IEsSearchResponse extends IKibanaSearchResponse {
+export interface IEsSearchResponse extends IKibanaSearchResponse {
/**
* Indicates whether async search is still in flight
*/
@@ -39,5 +40,5 @@ export interface IEsSearchResponse extends IKibanaSearchResponse {
* Indicates whether the results returned are complete or partial
*/
isPartial?: boolean;
- rawResponse: SearchResponse;
+ rawResponse: SearchResponse;
}
diff --git a/src/plugins/data/common/types.ts b/src/plugins/data/common/types.ts
index e2ec1a031b0ca..a1e1252b3112f 100644
--- a/src/plugins/data/common/types.ts
+++ b/src/plugins/data/common/types.ts
@@ -20,4 +20,15 @@
export * from './query/types';
export * from './kbn_field_types/types';
export * from './index_patterns/types';
-export { TextContextTypeConvert, IFieldFormatMetaParams } from './field_formats/types';
+
+/**
+ * If a service is being shared on both the client and the server, and
+ * the client code requires synchronous access to uiSettings, both client
+ * and server should wrap the core uiSettings services in a function
+ * matching this signature.
+ *
+ * This matches the signature of the public `core.uiSettings.get`, and
+ * should only be used in scenarios where async access to uiSettings is
+ * not possible.
+ */
+export type GetConfigFn = (key: string, defaultOverride?: T) => T;
diff --git a/src/plugins/data/public/field_formats/converters/date.ts b/src/plugins/data/public/field_formats/converters/date.ts
index 78ef8b293e8b9..8070633ef2dfe 100644
--- a/src/plugins/data/public/field_formats/converters/date.ts
+++ b/src/plugins/data/public/field_formats/converters/date.ts
@@ -20,12 +20,8 @@
import { i18n } from '@kbn/i18n';
import { memoize, noop } from 'lodash';
import moment from 'moment';
-import {
- FieldFormat,
- KBN_FIELD_TYPES,
- TextContextTypeConvert,
- FIELD_FORMAT_IDS,
-} from '../../../common';
+import { FieldFormat, KBN_FIELD_TYPES, FIELD_FORMAT_IDS } from '../../../common';
+import { TextContextTypeConvert } from '../../../common/field_formats/types';
export class DateFormat extends FieldFormat {
static id = FIELD_FORMAT_IDS.DATE;
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index ecf076aa517fb..eb5703f1c63c1 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -360,7 +360,6 @@ export {
SearchInterceptor,
SearchInterceptorDeps,
SearchRequest,
- SearchResponse,
SearchSourceFields,
SortDirection,
// expression functions and types
diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts
index 564c571b6ccd6..ee0b0714febc0 100644
--- a/src/plugins/data/public/plugin.ts
+++ b/src/plugins/data/public/plugin.ts
@@ -48,9 +48,7 @@ import {
} from './index_patterns';
import {
setFieldFormats,
- setHttp,
setIndexPatterns,
- setInjectedMetadata,
setNotifications,
setOverlays,
setQueryService,
@@ -164,11 +162,9 @@ export class DataPublicPlugin
public start(core: CoreStart, { uiActions }: DataStartDependencies): DataPublicPluginStart {
const { uiSettings, http, notifications, savedObjects, overlays, application } = core;
- setHttp(http);
setNotifications(notifications);
setOverlays(overlays);
setUiSettings(uiSettings);
- setInjectedMetadata(core.injectedMetadata);
const fieldFormats = this.fieldFormatsService.start();
setFieldFormats(fieldFormats);
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 58c2bd9957ab8..f8a108a5a4c58 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -34,7 +34,6 @@ import { InjectedIntl } from '@kbn/i18n/react';
import { ISearchSource as ISearchSource_2 } from 'src/plugins/data/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { IUiSettingsClient } from 'src/core/public';
-import { IUiSettingsClient as IUiSettingsClient_3 } from 'kibana/public';
import { KibanaClient } from '@elastic/elasticsearch/api/kibana';
import { KibanaConfigType } from 'src/core/server/kibana_config';
import { Location } from 'history';
@@ -61,8 +60,8 @@ import * as Rx from 'rxjs';
import { SavedObject } from 'src/core/server';
import { SavedObject as SavedObject_3 } from 'src/core/public';
import { SavedObjectsClientContract } from 'src/core/public';
-import { SearchParams } from 'elasticsearch';
-import { SearchResponse as SearchResponse_2 } from 'elasticsearch';
+import { Search } from '@elastic/elasticsearch/api/requestParams';
+import { SearchResponse } from 'elasticsearch';
import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common';
import { Subscription } from 'rxjs';
import { Toast } from 'kibana/public';
@@ -594,10 +593,11 @@ export const fieldFormats: {
// @public (undocumented)
export type FieldFormatsContentType = 'html' | 'text';
+// Warning: (ae-forgotten-export) The symbol "GetConfigFn" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "FieldFormatsGetConfigFn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export type FieldFormatsGetConfigFn = (key: string, defaultOverride?: T) => T;
+export type FieldFormatsGetConfigFn = GetConfigFn;
// Warning: (ae-missing-release-tag) "FieldList" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -727,12 +727,12 @@ export function getEsPreference(uiSettings: IUiSettingsClient_2, sessionId?: str
export const getKbnTypeNames: () => string[];
// Warning: (ae-forgotten-export) The symbol "ISearchRequestParams" needs to be exported by the entry point index.d.ts
-// Warning: (ae-missing-release-tag) "getSearchParamsFromRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+// Warning: (ae-incompatible-release-tags) The symbol "getSearchParamsFromRequest" is marked as @public, but its signature references "SearchRequest" which is marked as @internal
//
// @public (undocumented)
export function getSearchParamsFromRequest(searchRequest: SearchRequest, dependencies: {
- injectedMetadata: CoreStart['injectedMetadata'];
- uiSettings: IUiSettingsClient_3;
+ esShardTimeout: number;
+ getConfig: GetConfigFn;
}): ISearchRequestParams;
// Warning: (ae-missing-release-tag) "getTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -792,11 +792,11 @@ export interface IEsSearchRequest extends IKibanaSearchRequest {
// Warning: (ae-missing-release-tag) "IEsSearchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export interface IEsSearchResponse extends IKibanaSearchResponse {
+export interface IEsSearchResponse extends IKibanaSearchResponse {
isPartial?: boolean;
isRunning?: boolean;
// (undocumented)
- rawResponse: SearchResponse_2;
+ rawResponse: SearchResponse;
}
// Warning: (ae-missing-release-tag) "IFieldFormat" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -1241,7 +1241,7 @@ export type ISearch = (request: IKibanaSearchRequest, options?: ISearchOptions)
// Warning: (ae-missing-release-tag) "ISearchGeneric" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export type ISearchGeneric = (request: IEsSearchRequest, options?: ISearchOptions) => Observable;
+export type ISearchGeneric = (request: SearchStrategyRequest, options?: ISearchOptions) => Observable;
// Warning: (ae-missing-release-tag) "ISearchOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -1784,15 +1784,8 @@ export interface SearchInterceptorDeps {
usageCollector?: SearchUsageCollector;
}
-// Warning: (ae-missing-release-tag) "SearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
-//
-// @public (undocumented)
-export type SearchRequest = any;
-
-// Warning: (ae-missing-release-tag) "SearchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
-//
-// @public (undocumented)
-export type SearchResponse = any;
+// @internal
+export type SearchRequest = Record;
// Warning: (ae-missing-release-tag) "SearchSourceFields" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -1992,21 +1985,21 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:372:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:372:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:372:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:372:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:374:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:375:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:384:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:373:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:374:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:383:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:384:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:62:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:63:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts
index aaaac5ae6ff7c..315d4678cabf1 100644
--- a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts
+++ b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts
@@ -42,7 +42,7 @@ describe('Search Usage Collector', () => {
{} as any,
]);
mockUsageCollectionSetup = usageCollectionPluginMock.createSetupContract();
- usageCollector = createUsageCollector(mockCoreSetup, mockUsageCollectionSetup);
+ usageCollector = createUsageCollector(mockCoreSetup.getStartServices, mockUsageCollectionSetup);
});
test('tracks query timeouts', async () => {
diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.ts b/src/plugins/data/public/search/collectors/create_usage_collector.ts
index 7adb0c3caa675..321b2c5b99049 100644
--- a/src/plugins/data/public/search/collectors/create_usage_collector.ts
+++ b/src/plugins/data/public/search/collectors/create_usage_collector.ts
@@ -18,16 +18,16 @@
*/
import { first } from 'rxjs/operators';
-import { CoreSetup } from '../../../../../core/public';
+import { StartServicesAccessor } from '../../../../../core/public';
import { METRIC_TYPE, UsageCollectionSetup } from '../../../../usage_collection/public';
import { SEARCH_EVENT_TYPE, SearchUsageCollector } from './types';
export const createUsageCollector = (
- core: CoreSetup,
+ getStartServices: StartServicesAccessor,
usageCollection?: UsageCollectionSetup
): SearchUsageCollector => {
const getCurrentApp = async () => {
- const [{ application }] = await core.getStartServices();
+ const [{ application }] = await getStartServices();
return application.currentAppId$.pipe(first()).toPromise();
};
diff --git a/src/plugins/data/public/search/fetch/get_search_params.test.ts b/src/plugins/data/public/search/fetch/get_search_params.test.ts
index f9b62fdd4fc61..1ecb879b1602d 100644
--- a/src/plugins/data/public/search/fetch/get_search_params.test.ts
+++ b/src/plugins/data/public/search/fetch/get_search_params.test.ts
@@ -18,13 +18,10 @@
*/
import { getSearchParams } from './get_search_params';
-import { IUiSettingsClient } from 'kibana/public';
-import { UI_SETTINGS } from '../../../common';
+import { GetConfigFn, UI_SETTINGS } from '../../../common';
-function getConfigStub(config: any = {}) {
- return {
- get: (key) => config[key],
- } as IUiSettingsClient;
+function getConfigStub(config: any = {}): GetConfigFn {
+ return (key) => config[key];
}
describe('getSearchParams', () => {
diff --git a/src/plugins/data/public/search/fetch/get_search_params.ts b/src/plugins/data/public/search/fetch/get_search_params.ts
index 3246156b6b37e..5e0395189f647 100644
--- a/src/plugins/data/public/search/fetch/get_search_params.ts
+++ b/src/plugins/data/public/search/fetch/get_search_params.ts
@@ -17,37 +17,36 @@
* under the License.
*/
-import { IUiSettingsClient, CoreStart } from 'kibana/public';
-import { UI_SETTINGS, ISearchRequestParams } from '../../../common';
+import { UI_SETTINGS, ISearchRequestParams, GetConfigFn } from '../../../common';
import { SearchRequest } from './types';
const sessionId = Date.now();
-export function getSearchParams(config: IUiSettingsClient, esShardTimeout: number = 0) {
+export function getSearchParams(getConfig: GetConfigFn, esShardTimeout: number = 0) {
return {
rest_total_hits_as_int: true,
ignore_unavailable: true,
- ignore_throttled: getIgnoreThrottled(config),
- max_concurrent_shard_requests: getMaxConcurrentShardRequests(config),
- preference: getPreference(config),
+ ignore_throttled: getIgnoreThrottled(getConfig),
+ max_concurrent_shard_requests: getMaxConcurrentShardRequests(getConfig),
+ preference: getPreference(getConfig),
timeout: getTimeout(esShardTimeout),
};
}
-export function getIgnoreThrottled(config: IUiSettingsClient) {
- return !config.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN);
+export function getIgnoreThrottled(getConfig: GetConfigFn) {
+ return !getConfig(UI_SETTINGS.SEARCH_INCLUDE_FROZEN);
}
-export function getMaxConcurrentShardRequests(config: IUiSettingsClient) {
- const maxConcurrentShardRequests = config.get(UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS);
+export function getMaxConcurrentShardRequests(getConfig: GetConfigFn) {
+ const maxConcurrentShardRequests = getConfig(UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS);
return maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined;
}
-export function getPreference(config: IUiSettingsClient) {
- const setRequestPreference = config.get(UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE);
+export function getPreference(getConfig: GetConfigFn) {
+ const setRequestPreference = getConfig(UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE);
if (setRequestPreference === 'sessionId') return sessionId;
return setRequestPreference === 'custom'
- ? config.get(UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE)
+ ? getConfig(UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE)
: undefined;
}
@@ -55,13 +54,15 @@ export function getTimeout(esShardTimeout: number) {
return esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined;
}
+/** @public */
+// TODO: Could provide this on runtime contract with dependencies
+// already wired up.
export function getSearchParamsFromRequest(
searchRequest: SearchRequest,
- dependencies: { injectedMetadata: CoreStart['injectedMetadata']; uiSettings: IUiSettingsClient }
+ dependencies: { esShardTimeout: number; getConfig: GetConfigFn }
): ISearchRequestParams {
- const { injectedMetadata, uiSettings } = dependencies;
- const esShardTimeout = injectedMetadata.getInjectedVar('esShardTimeout') as number;
- const searchParams = getSearchParams(uiSettings, esShardTimeout);
+ const { esShardTimeout, getConfig } = dependencies;
+ const searchParams = getSearchParams(getConfig, esShardTimeout);
return {
index: searchRequest.index.title || searchRequest.index,
diff --git a/src/plugins/data/public/search/fetch/handle_response.test.ts b/src/plugins/data/public/search/fetch/handle_response.test.ts
index 10e6eda3de3d0..9a9d806bc9cf8 100644
--- a/src/plugins/data/public/search/fetch/handle_response.test.ts
+++ b/src/plugins/data/public/search/fetch/handle_response.test.ts
@@ -23,6 +23,7 @@ import { handleResponse } from './handle_response';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { notificationServiceMock } from '../../../../../core/public/notifications/notifications_service.mock';
import { setNotifications } from '../../services';
+import { SearchResponse } from 'elasticsearch';
jest.mock('@kbn/i18n', () => {
return {
@@ -44,7 +45,7 @@ describe('handleResponse', () => {
const request = { body: {} };
const response = {
timed_out: true,
- };
+ } as SearchResponse;
const result = handleResponse(request, response);
expect(result).toBe(response);
expect(notifications.toasts.addWarning).toBeCalled();
@@ -57,9 +58,12 @@ describe('handleResponse', () => {
const request = { body: {} };
const response = {
_shards: {
- failed: true,
+ failed: 1,
+ total: 2,
+ successful: 1,
+ skipped: 1,
},
- };
+ } as SearchResponse;
const result = handleResponse(request, response);
expect(result).toBe(response);
expect(notifications.toasts.addWarning).toBeCalled();
@@ -70,7 +74,7 @@ describe('handleResponse', () => {
test('returns the response', () => {
const request = {};
- const response = {};
+ const response = {} as SearchResponse;
const result = handleResponse(request, response);
expect(result).toBe(response);
});
diff --git a/src/plugins/data/public/search/fetch/handle_response.tsx b/src/plugins/data/public/search/fetch/handle_response.tsx
index 7905468f91c5f..14e9b59f49bfb 100644
--- a/src/plugins/data/public/search/fetch/handle_response.tsx
+++ b/src/plugins/data/public/search/fetch/handle_response.tsx
@@ -20,12 +20,13 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSpacer } from '@elastic/eui';
-import { ShardFailureOpenModalButton, ShardFailureRequest, ShardFailureResponse } from '../../ui';
+import { SearchResponse } from 'elasticsearch';
+import { ShardFailureOpenModalButton } from '../../ui';
import { toMountPoint } from '../../../../kibana_react/public';
import { getNotifications } from '../../services';
-import { SearchRequest, SearchResponse } from '..';
+import { SearchRequest } from '..';
-export function handleResponse(request: SearchRequest, response: SearchResponse) {
+export function handleResponse(request: SearchRequest, response: SearchResponse) {
if (response.timed_out) {
getNotifications().toasts.addWarning({
title: i18n.translate('data.search.searchSource.fetch.requestTimedOutNotificationMessage', {
@@ -53,11 +54,7 @@ export function handleResponse(request: SearchRequest, response: SearchResponse)
<>
{description}
-
+
>
);
diff --git a/src/plugins/data/public/search/fetch/request_error.ts b/src/plugins/data/public/search/fetch/request_error.ts
index 5e42a6fcf5b65..efaaafadf404e 100644
--- a/src/plugins/data/public/search/fetch/request_error.ts
+++ b/src/plugins/data/public/search/fetch/request_error.ts
@@ -17,8 +17,9 @@
* under the License.
*/
+import { SearchResponse } from 'elasticsearch';
import { KbnError } from '../../../../kibana_utils/common';
-import { SearchError, SearchResponse } from './types';
+import { SearchError } from './types';
/**
* Request Failure - When an entire multi request fails
@@ -26,8 +27,8 @@ import { SearchError, SearchResponse } from './types';
* @param {Object} resp - optional HTTP response
*/
export class RequestFailure extends KbnError {
- public resp: SearchResponse;
- constructor(err: SearchError | null = null, resp?: SearchResponse) {
+ public resp?: SearchResponse;
+ constructor(err: SearchError | null = null, resp?: SearchResponse) {
super(`Request to Elasticsearch failed: ${JSON.stringify(resp || err?.message)}`);
this.resp = resp;
diff --git a/src/plugins/data/public/search/fetch/types.ts b/src/plugins/data/public/search/fetch/types.ts
index dda66d6b5238d..670c4f731971a 100644
--- a/src/plugins/data/public/search/fetch/types.ts
+++ b/src/plugins/data/public/search/fetch/types.ts
@@ -17,11 +17,17 @@
* under the License.
*/
-import { IUiSettingsClient } from '../../../../../core/public';
+import { GetConfigFn } from '../../../common';
import { ISearchStartLegacy } from '../types';
-export type SearchRequest = any;
-export type SearchResponse = any;
+/**
+ * @internal
+ *
+ * This type is used when flattenning a SearchSource and passing it down to legacy search.
+ * Once legacy search is removed, this type should become internal to `SearchSource`,
+ * where `ISearchRequestParams` is used externally instead.
+ */
+export type SearchRequest = Record;
export interface FetchOptions {
abortSignal?: AbortSignal;
@@ -30,7 +36,7 @@ export interface FetchOptions {
export interface FetchHandlers {
legacySearchService: ISearchStartLegacy;
- config: IUiSettingsClient;
+ config: { get: GetConfigFn };
esShardTimeout: number;
}
diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts
index 32bcd8a279036..14eff13b378ee 100644
--- a/src/plugins/data/public/search/index.ts
+++ b/src/plugins/data/public/search/index.ts
@@ -34,13 +34,7 @@ export { getEsPreference } from './es_search';
export { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search';
-export {
- SearchError,
- FetchOptions,
- SearchRequest,
- SearchResponse,
- getSearchParamsFromRequest,
-} from './fetch';
+export { SearchError, FetchOptions, getSearchParamsFromRequest, SearchRequest } from './fetch';
export {
ISearchSource,
diff --git a/src/plugins/data/public/search/legacy/call_client.ts b/src/plugins/data/public/search/legacy/call_client.ts
index 4b12f517daf78..3dcf11f72a742 100644
--- a/src/plugins/data/public/search/legacy/call_client.ts
+++ b/src/plugins/data/public/search/legacy/call_client.ts
@@ -17,6 +17,7 @@
* under the License.
*/
+import { SearchResponse } from 'elasticsearch';
import { FetchOptions, FetchHandlers, handleResponse } from '../fetch';
import { defaultSearchStrategy } from './default_search_strategy';
import { SearchRequest } from '../index';
@@ -32,7 +33,7 @@ export function callClient(
FetchOptions
]> = searchRequests.map((request, i) => [request, requestsOptions[i]]);
const requestOptionsMap = new Map(requestOptionEntries);
- const requestResponseMap = new Map();
+ const requestResponseMap = new Map>>();
const { searching, abort } = defaultSearchStrategy.search({
searchRequests,
@@ -45,5 +46,5 @@ export function callClient(
if (abortSignal) abortSignal.addEventListener('abort', abort);
requestResponseMap.set(request, response);
});
- return searchRequests.map((request) => requestResponseMap.get(request));
+ return searchRequests.map((request) => requestResponseMap.get(request)!);
}
diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.ts b/src/plugins/data/public/search/legacy/default_search_strategy.ts
index 284768bc5a1cc..6ccb0a01cf898 100644
--- a/src/plugins/data/public/search/legacy/default_search_strategy.ts
+++ b/src/plugins/data/public/search/legacy/default_search_strategy.ts
@@ -42,7 +42,7 @@ function msearch({
index: index.title || index,
search_type: searchType,
ignore_unavailable: true,
- preference: getPreference(config),
+ preference: getPreference(config.get),
};
const inlineBody = {
...body,
@@ -52,7 +52,7 @@ function msearch({
});
const searching = es.msearch({
- ...getMSearchParams(config),
+ ...getMSearchParams(config.get),
body: `${inlineRequests.join('\n')}\n`,
});
diff --git a/src/plugins/data/public/search/legacy/es_client/get_es_client.ts b/src/plugins/data/public/search/legacy/es_client/get_es_client.ts
index 93d9d24920271..4367544ad9ff4 100644
--- a/src/plugins/data/public/search/legacy/es_client/get_es_client.ts
+++ b/src/plugins/data/public/search/legacy/es_client/get_es_client.ts
@@ -22,17 +22,20 @@ import { default as es } from 'elasticsearch-browser/elasticsearch';
import { CoreStart, PackageInfo } from 'kibana/public';
import { BehaviorSubject } from 'rxjs';
-export function getEsClient(
- injectedMetadata: CoreStart['injectedMetadata'],
- http: CoreStart['http'],
- packageInfo: PackageInfo
-) {
- const esRequestTimeout = injectedMetadata.getInjectedVar('esRequestTimeout') as number;
- const esApiVersion = injectedMetadata.getInjectedVar('esApiVersion') as string;
-
+export function getEsClient({
+ esRequestTimeout,
+ esApiVersion,
+ http,
+ packageVersion,
+}: {
+ esRequestTimeout: number;
+ esApiVersion: string;
+ http: CoreStart['http'];
+ packageVersion: PackageInfo['version'];
+}) {
// Use legacy es client for msearch.
const client = es.Client({
- host: getEsUrl(http, packageInfo),
+ host: getEsUrl(http, packageVersion),
log: 'info',
requestTimeout: esRequestTimeout,
apiVersion: esApiVersion,
@@ -78,7 +81,7 @@ function wrapEsClientMethod(esClient: any, method: string, loadingCount$: Behavi
};
}
-function getEsUrl(http: CoreStart['http'], packageInfo: PackageInfo) {
+function getEsUrl(http: CoreStart['http'], packageVersion: PackageInfo['version']) {
const a = document.createElement('a');
a.href = http.basePath.prepend('/elasticsearch');
const protocolPort = /https/.test(a.protocol) ? 443 : 80;
@@ -89,7 +92,7 @@ function getEsUrl(http: CoreStart['http'], packageInfo: PackageInfo) {
protocol: a.protocol,
pathname: a.pathname,
headers: {
- 'kbn-version': packageInfo.version,
+ 'kbn-version': packageVersion,
},
};
}
diff --git a/src/plugins/data/public/search/legacy/es_client/types.ts b/src/plugins/data/public/search/legacy/es_client/types.ts
index 7a56b9b0cb00a..2d35188322a4e 100644
--- a/src/plugins/data/public/search/legacy/es_client/types.ts
+++ b/src/plugins/data/public/search/legacy/es_client/types.ts
@@ -17,13 +17,14 @@
* under the License.
*/
-import { SearchRequest, SearchResponse } from '../../fetch';
+import { SearchResponse } from 'elasticsearch';
+import { SearchRequest } from '../../fetch';
export interface LegacyApiCaller {
search: (searchRequest: SearchRequest) => LegacyApiCallerResponse;
msearch: (searchRequest: SearchRequest) => LegacyApiCallerResponse;
}
-interface LegacyApiCallerResponse extends Promise {
+interface LegacyApiCallerResponse extends Promise> {
abort: () => void;
}
diff --git a/src/plugins/data/public/search/legacy/fetch_soon.test.ts b/src/plugins/data/public/search/legacy/fetch_soon.test.ts
index 61d3568350b6b..d7a85e65b475d 100644
--- a/src/plugins/data/public/search/legacy/fetch_soon.test.ts
+++ b/src/plugins/data/public/search/legacy/fetch_soon.test.ts
@@ -19,21 +19,28 @@
import { fetchSoon } from './fetch_soon';
import { callClient } from './call_client';
-import { IUiSettingsClient } from 'kibana/public';
import { FetchHandlers, FetchOptions } from '../fetch/types';
-import { SearchRequest, SearchResponse } from '../index';
-import { UI_SETTINGS } from '../../../common';
+import { SearchRequest } from '../index';
+import { SearchResponse } from 'elasticsearch';
+import { GetConfigFn, UI_SETTINGS } from '../../../common';
-function getConfigStub(config: any = {}) {
- return {
- get: (key) => config[key],
- } as IUiSettingsClient;
+function getConfigStub(config: any = {}): GetConfigFn {
+ return (key) => config[key];
}
-const mockResponses: Record = {
- foo: {},
- bar: {},
- baz: {},
+const mockResponses: Record> = {
+ foo: {
+ took: 1,
+ timed_out: false,
+ } as SearchResponse,
+ bar: {
+ took: 2,
+ timed_out: false,
+ } as SearchResponse,
+ baz: {
+ took: 3,
+ timed_out: false,
+ } as SearchResponse,
};
jest.useFakeTimers();
@@ -60,9 +67,9 @@ describe('fetchSoon', () => {
});
test('should execute asap if config is set to not batch searches', () => {
- const config = getConfigStub({
- [UI_SETTINGS.COURIER_BATCH_SEARCHES]: false,
- });
+ const config = {
+ get: getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: false }),
+ };
const request = {};
const options = {};
@@ -72,9 +79,9 @@ describe('fetchSoon', () => {
});
test('should delay by 50ms if config is set to batch searches', () => {
- const config = getConfigStub({
- [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true,
- });
+ const config = {
+ get: getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }),
+ };
const request = {};
const options = {};
@@ -88,9 +95,9 @@ describe('fetchSoon', () => {
});
test('should send a batch of requests to callClient', () => {
- const config = getConfigStub({
- [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true,
- });
+ const config = {
+ get: getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }),
+ };
const requests = [{ foo: 1 }, { foo: 2 }];
const options = [{ bar: 1 }, { bar: 2 }];
@@ -105,9 +112,9 @@ describe('fetchSoon', () => {
});
test('should return the response to the corresponding call for multiple batched requests', async () => {
- const config = getConfigStub({
- [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true,
- });
+ const config = {
+ get: getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }),
+ };
const requests = [{ _mockResponseId: 'foo' }, { _mockResponseId: 'bar' }];
const promises = requests.map((request) => {
@@ -120,9 +127,9 @@ describe('fetchSoon', () => {
});
test('should wait for the previous batch to start before starting a new batch', () => {
- const config = getConfigStub({
- [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true,
- });
+ const config = {
+ get: getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }),
+ };
const firstBatch = [{ foo: 1 }, { foo: 2 }];
const secondBatch = [{ bar: 1 }, { bar: 2 }];
diff --git a/src/plugins/data/public/search/legacy/fetch_soon.ts b/src/plugins/data/public/search/legacy/fetch_soon.ts
index 252682c7c8e58..16920a8a4dd97 100644
--- a/src/plugins/data/public/search/legacy/fetch_soon.ts
+++ b/src/plugins/data/public/search/legacy/fetch_soon.ts
@@ -17,9 +17,10 @@
* under the License.
*/
+import { SearchResponse } from 'elasticsearch';
import { callClient } from './call_client';
import { FetchHandlers, FetchOptions } from '../fetch/types';
-import { SearchRequest, SearchResponse } from '../index';
+import { SearchRequest } from '../index';
import { UI_SETTINGS } from '../../../common';
/**
@@ -53,7 +54,7 @@ let requestsToFetch: SearchRequest[] = [];
let requestOptions: FetchOptions[] = [];
// The in-progress fetch (if there is one)
-let fetchInProgress: Promise | null = null;
+let fetchInProgress: any = null;
/**
* Delay fetching for a given amount of time, while batching up the requests to be fetched.
@@ -67,7 +68,7 @@ async function delayedFetch(
options: FetchOptions,
fetchHandlers: FetchHandlers,
ms: number
-) {
+): Promise> {
if (ms === 0) {
return callClient([request], [options], fetchHandlers)[0];
}
@@ -75,7 +76,10 @@ async function delayedFetch(
const i = requestsToFetch.length;
requestsToFetch = [...requestsToFetch, request];
requestOptions = [...requestOptions, options];
- const responses: SearchResponse[] = await (fetchInProgress =
+
+ // Note: the typescript here only worked because `SearchResponse` was `any`
+ // Since this code is legacy, I'm leaving the any here.
+ const responses: any[] = await (fetchInProgress =
fetchInProgress ||
delay(() => {
const response = callClient(requestsToFetch, requestOptions, fetchHandlers);
diff --git a/src/plugins/data/public/search/legacy/get_msearch_params.test.ts b/src/plugins/data/public/search/legacy/get_msearch_params.test.ts
index dc61e19406631..d3206950174c8 100644
--- a/src/plugins/data/public/search/legacy/get_msearch_params.test.ts
+++ b/src/plugins/data/public/search/legacy/get_msearch_params.test.ts
@@ -18,13 +18,10 @@
*/
import { getMSearchParams } from './get_msearch_params';
-import { IUiSettingsClient } from '../../../../../core/public';
-import { UI_SETTINGS } from '../../../common';
+import { GetConfigFn, UI_SETTINGS } from '../../../common';
-function getConfigStub(config: any = {}) {
- return {
- get: (key) => config[key],
- } as IUiSettingsClient;
+function getConfigStub(config: any = {}): GetConfigFn {
+ return (key) => config[key];
}
describe('getMSearchParams', () => {
diff --git a/src/plugins/data/public/search/legacy/get_msearch_params.ts b/src/plugins/data/public/search/legacy/get_msearch_params.ts
index 48d13903c972f..c4f77b25078cd 100644
--- a/src/plugins/data/public/search/legacy/get_msearch_params.ts
+++ b/src/plugins/data/public/search/legacy/get_msearch_params.ts
@@ -17,13 +17,13 @@
* under the License.
*/
-import { IUiSettingsClient } from 'kibana/public';
+import { GetConfigFn } from '../../../common';
import { getIgnoreThrottled, getMaxConcurrentShardRequests } from '../fetch';
-export function getMSearchParams(config: IUiSettingsClient) {
+export function getMSearchParams(getConfig: GetConfigFn) {
return {
rest_total_hits_as_int: true,
- ignore_throttled: getIgnoreThrottled(config),
- max_concurrent_shard_requests: getMaxConcurrentShardRequests(config),
+ ignore_throttled: getIgnoreThrottled(getConfig),
+ max_concurrent_shard_requests: getMaxConcurrentShardRequests(getConfig),
};
}
diff --git a/src/plugins/data/public/search/legacy/types.ts b/src/plugins/data/public/search/legacy/types.ts
index 3812cec7a2aa2..ed17db464feff 100644
--- a/src/plugins/data/public/search/legacy/types.ts
+++ b/src/plugins/data/public/search/legacy/types.ts
@@ -17,8 +17,9 @@
* under the License.
*/
+import { SearchResponse } from 'elasticsearch';
import { FetchHandlers } from '../fetch';
-import { SearchRequest, SearchResponse } from '..';
+import { SearchRequest } from '..';
export interface SearchStrategySearchParams extends FetchHandlers {
searchRequests: SearchRequest[];
@@ -30,7 +31,7 @@ export interface SearchStrategyProvider {
search: (params: SearchStrategySearchParams) => SearchStrategyResponse;
}
-export interface SearchStrategyResponse {
- searching: Promise;
+export interface SearchStrategyResponse {
+ searching: Promise>>;
abort: () => void;
}
diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts
index 99fccda7fddf3..30e509edd4987 100644
--- a/src/plugins/data/public/search/search_interceptor.ts
+++ b/src/plugins/data/public/search/search_interceptor.ts
@@ -106,7 +106,7 @@ export class SearchInterceptor {
): Observable {
const { id, ...searchRequest } = request;
const path = trimEnd(`/internal/search/${strategy || ES_SEARCH_STRATEGY}/${id || ''}`, '/');
- const body = JSON.stringify(id != null ? {} : searchRequest);
+ const body = JSON.stringify(searchRequest);
return from(
this.deps.http.fetch({
method: 'POST',
diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts
index 4360a0caa7cde..738f1e8ffbca0 100644
--- a/src/plugins/data/public/search/search_service.test.ts
+++ b/src/plugins/data/public/search/search_service.test.ts
@@ -52,6 +52,7 @@ describe('Search service', () => {
} as any);
expect(start).toHaveProperty('aggs');
expect(start).toHaveProperty('search');
+ expect(start).toHaveProperty('searchSource');
});
});
});
diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts
index 04e1a46c84652..9a30a15936fe5 100644
--- a/src/plugins/data/public/search/search_service.ts
+++ b/src/plugins/data/public/search/search_service.ts
@@ -51,11 +51,22 @@ export class SearchService implements Plugin {
private usageCollector?: SearchUsageCollector;
public setup(
- core: CoreSetup,
- { packageInfo, usageCollection, expressions }: SearchServiceSetupDependencies
+ { http, getStartServices, injectedMetadata, notifications, uiSettings }: CoreSetup,
+ { expressions, packageInfo, usageCollection }: SearchServiceSetupDependencies
): ISearchSetup {
- this.usageCollector = createUsageCollector(core, usageCollection);
- this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo);
+ const esApiVersion = injectedMetadata.getInjectedVar('esApiVersion') as string;
+ const esRequestTimeout = injectedMetadata.getInjectedVar('esRequestTimeout') as number;
+ const packageVersion = packageInfo.version;
+
+ this.usageCollector = createUsageCollector(getStartServices, usageCollection);
+
+ this.esClient = getEsClient({
+ esRequestTimeout,
+ esApiVersion,
+ http,
+ packageVersion,
+ });
+
/**
* A global object that intercepts all searches and provides convenience methods for cancelling
* all pending search requests, as well as getting the number of pending search requests.
@@ -64,13 +75,13 @@ export class SearchService implements Plugin {
*/
this.searchInterceptor = new SearchInterceptor(
{
- toasts: core.notifications.toasts,
- http: core.http,
- uiSettings: core.uiSettings,
- startServices: core.getStartServices(),
+ toasts: notifications.toasts,
+ http,
+ uiSettings,
+ startServices: getStartServices(),
usageCollector: this.usageCollector!,
},
- core.injectedMetadata.getInjectedVar('esRequestTimeout') as number
+ esRequestTimeout
);
expressions.registerFunction(esdsl);
@@ -79,7 +90,7 @@ export class SearchService implements Plugin {
return {
aggs: this.aggsService.setup({
registerFunction: expressions.registerFunction,
- uiSettings: core.uiSettings,
+ uiSettings,
}),
usageCollector: this.usageCollector!,
__enhance: (enhancements: SearchEnhancements) => {
@@ -92,17 +103,17 @@ export class SearchService implements Plugin {
{ application, http, injectedMetadata, notifications, uiSettings }: CoreStart,
{ fieldFormats, indexPatterns }: SearchServiceStartDependencies
): ISearchStart {
- const search: ISearchGeneric = (request, options) => {
+ const search = ((request, options) => {
return this.searchInterceptor.search(request, options);
- };
+ }) as ISearchGeneric;
const legacySearch = {
esClient: this.esClient!,
};
const searchSourceDependencies: SearchSourceDependencies = {
- uiSettings,
- injectedMetadata,
+ getConfig: uiSettings.get.bind(uiSettings),
+ esShardTimeout: injectedMetadata.getInjectedVar('esShardTimeout') as number,
search,
legacySearch,
};
diff --git a/src/plugins/data/public/search/search_source/create_search_source.test.ts b/src/plugins/data/public/search/search_source/create_search_source.test.ts
index 23ab5979595af..56f6ca6c56270 100644
--- a/src/plugins/data/public/search/search_source/create_search_source.test.ts
+++ b/src/plugins/data/public/search/search_source/create_search_source.test.ts
@@ -16,27 +16,28 @@
* specific language governing permissions and limitations
* under the License.
*/
+
import { createSearchSource as createSearchSourceFactory } from './create_search_source';
+import { SearchSourceDependencies } from './search_source';
import { IIndexPattern } from '../../../common/index_patterns';
import { IndexPatternsContract } from '../../index_patterns/index_patterns';
import { Filter } from '../../../common/es_query/filters';
-import { coreMock } from '../../../../../core/public/mocks';
import { dataPluginMock } from '../../mocks';
describe('createSearchSource', () => {
const indexPatternMock: IIndexPattern = {} as IIndexPattern;
let indexPatternContractMock: jest.Mocked;
- let dependencies: any;
+ let dependencies: SearchSourceDependencies;
let createSearchSource: ReturnType;
beforeEach(() => {
- const core = coreMock.createStart();
const data = dataPluginMock.createStartContract();
dependencies = {
- searchService: data.search,
- uiSettings: core.uiSettings,
- injectedMetadata: core.injectedMetadata,
+ getConfig: jest.fn(),
+ search: jest.fn(),
+ legacySearch: data.search.__LEGACY,
+ esShardTimeout: 30000,
};
indexPatternContractMock = ({
diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/public/search/search_source/mocks.ts
index cf2d009e41b54..4e1c35557ffa6 100644
--- a/src/plugins/data/public/search/search_source/mocks.ts
+++ b/src/plugins/data/public/search/search_source/mocks.ts
@@ -17,10 +17,7 @@
* under the License.
*/
-import {
- injectedMetadataServiceMock,
- uiSettingsServiceMock,
-} from '../../../../../core/public/mocks';
+import { uiSettingsServiceMock } from '../../../../../core/public/mocks';
import { ISearchSource, SearchSource } from './search_source';
import { SearchSourceFields } from './types';
@@ -54,6 +51,8 @@ export const searchSourceMock = {
export const createSearchSourceMock = (fields?: SearchSourceFields) =>
new SearchSource(fields, {
+ getConfig: uiSettingsServiceMock.createStartContract().get,
+ esShardTimeout: 30000,
search: jest.fn(),
legacySearch: {
esClient: {
@@ -61,6 +60,4 @@ export const createSearchSourceMock = (fields?: SearchSourceFields) =>
msearch: jest.fn(),
},
},
- uiSettings: uiSettingsServiceMock.createStartContract(),
- injectedMetadata: injectedMetadataServiceMock.createStartContract(),
});
diff --git a/src/plugins/data/public/search/search_source/search_source.test.ts b/src/plugins/data/public/search/search_source/search_source.test.ts
index 6d53b8dfc4b4e..2f0fa0765e25a 100644
--- a/src/plugins/data/public/search/search_source/search_source.test.ts
+++ b/src/plugins/data/public/search/search_source/search_source.test.ts
@@ -16,13 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
+
import { Observable } from 'rxjs';
-import { SearchSource } from './search_source';
+import { GetConfigFn } from 'src/plugins/data/common';
+import { SearchSource, SearchSourceDependencies } from './search_source';
import { IndexPattern, SortDirection } from '../..';
import { fetchSoon } from '../legacy';
-import { IUiSettingsClient } from '../../../../../core/public';
import { dataPluginMock } from '../../../../data/public/mocks';
-import { coreMock } from '../../../../../core/public/mocks';
jest.mock('../legacy', () => ({
fetchSoon: jest.fn().mockResolvedValue({}),
@@ -51,10 +51,9 @@ const indexPattern2 = ({
describe('SearchSource', () => {
let mockSearchMethod: any;
- let searchSourceDependencies: any;
+ let searchSourceDependencies: SearchSourceDependencies;
beforeEach(() => {
- const core = coreMock.createStart();
const data = dataPluginMock.createStartContract();
mockSearchMethod = jest.fn(() => {
@@ -69,10 +68,10 @@ describe('SearchSource', () => {
});
searchSourceDependencies = {
+ getConfig: jest.fn(),
search: mockSearchMethod,
legacySearch: data.search.__LEGACY,
- injectedMetadata: core.injectedMetadata,
- uiSettings: core.uiSettings,
+ esShardTimeout: 30000,
};
});
@@ -184,16 +183,11 @@ describe('SearchSource', () => {
describe('#legacy fetch()', () => {
beforeEach(() => {
- const core = coreMock.createStart();
-
searchSourceDependencies = {
...searchSourceDependencies,
- uiSettings: {
- ...core.uiSettings,
- get: jest.fn(() => {
- return true; // batchSearches = true
- }),
- } as IUiSettingsClient,
+ getConfig: jest.fn(() => {
+ return true; // batchSearches = true
+ }) as GetConfigFn,
};
});
diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts
index 847dc8853d6ba..d2e3370762059 100644
--- a/src/plugins/data/public/search/search_source/search_source.ts
+++ b/src/plugins/data/public/search/search_source/search_source.ts
@@ -72,25 +72,47 @@
import { setWith } from '@elastic/safer-lodash-set';
import { uniqueId, uniq, extend, pick, difference, omit, isObject, keys, isFunction } from 'lodash';
import { map } from 'rxjs/operators';
-import { CoreStart } from 'kibana/public';
import { normalizeSortRequest } from './normalize_sort_request';
import { filterDocvalueFields } from './filter_docvalue_fields';
import { fieldWildcardFilter } from '../../../../kibana_utils/common';
-import { IIndexPattern, ISearchGeneric, SearchRequest } from '../..';
+import { IIndexPattern, ISearchGeneric } from '../..';
import { SearchSourceOptions, SearchSourceFields } from './types';
-import { FetchOptions, RequestFailure, handleResponse, getSearchParamsFromRequest } from '../fetch';
+import {
+ FetchOptions,
+ RequestFailure,
+ handleResponse,
+ getSearchParamsFromRequest,
+ SearchRequest,
+} from '../fetch';
import { getEsQueryConfig, buildEsQuery, Filter, UI_SETTINGS } from '../../../common';
import { getHighlightRequest } from '../../../common/field_formats';
+import { GetConfigFn } from '../../../common/types';
import { fetchSoon } from '../legacy';
import { extractReferences } from './extract_references';
import { ISearchStartLegacy } from '../types';
+/** @internal */
+export const searchSourceRequiredUiSettings = [
+ 'dateFormat:tz',
+ UI_SETTINGS.COURIER_BATCH_SEARCHES,
+ UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE,
+ UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX,
+ UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS,
+ UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE,
+ UI_SETTINGS.DOC_HIGHLIGHT,
+ UI_SETTINGS.META_FIELDS,
+ UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS,
+ UI_SETTINGS.QUERY_STRING_OPTIONS,
+ UI_SETTINGS.SEARCH_INCLUDE_FROZEN,
+ UI_SETTINGS.SORT_OPTIONS,
+];
+
export interface SearchSourceDependencies {
- uiSettings: CoreStart['uiSettings'];
+ getConfig: GetConfigFn;
search: ISearchGeneric;
legacySearch: ISearchStartLegacy;
- injectedMetadata: CoreStart['injectedMetadata'];
+ esShardTimeout: number;
}
/** @public **/
@@ -204,11 +226,11 @@ export class SearchSource {
* @return {Observable>}
*/
private fetch$(searchRequest: SearchRequest, signal?: AbortSignal) {
- const { search, injectedMetadata, uiSettings } = this.dependencies;
+ const { search, esShardTimeout, getConfig } = this.dependencies;
const params = getSearchParamsFromRequest(searchRequest, {
- injectedMetadata,
- uiSettings,
+ esShardTimeout,
+ getConfig,
});
return search({ params, indexType: searchRequest.indexType }, { signal }).pipe(
@@ -221,8 +243,7 @@ export class SearchSource {
* @return {Promise>}
*/
private async legacyFetch(searchRequest: SearchRequest, options: FetchOptions) {
- const { injectedMetadata, legacySearch, uiSettings } = this.dependencies;
- const esShardTimeout = injectedMetadata.getInjectedVar('esShardTimeout') as number;
+ const { esShardTimeout, legacySearch, getConfig } = this.dependencies;
return await fetchSoon(
searchRequest,
@@ -232,7 +253,7 @@ export class SearchSource {
},
{
legacySearchService: legacySearch,
- config: uiSettings,
+ config: { get: getConfig },
esShardTimeout,
}
);
@@ -243,20 +264,21 @@ export class SearchSource {
* @async
*/
async fetch(options: FetchOptions = {}) {
- const { uiSettings } = this.dependencies;
+ const { getConfig } = this.dependencies;
await this.requestIsStarting(options);
const searchRequest = await this.flatten();
this.history = [searchRequest];
let response;
- if (uiSettings.get(UI_SETTINGS.COURIER_BATCH_SEARCHES)) {
+ if (getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES)) {
response = await this.legacyFetch(searchRequest, options);
} else {
- response = this.fetch$(searchRequest, options.abortSignal).toPromise();
+ response = await this.fetch$(searchRequest, options.abortSignal).toPromise();
}
- if (response.error) {
+ // TODO: Remove casting when https://github.com/elastic/elasticsearch-js/issues/1287 is resolved
+ if ((response as any).error) {
throw new RequestFailure(null, response);
}
@@ -342,7 +364,7 @@ export class SearchSource {
}
};
- const { uiSettings } = this.dependencies;
+ const { getConfig } = this.dependencies;
switch (key) {
case 'filter':
@@ -364,7 +386,7 @@ export class SearchSource {
const sort = normalizeSortRequest(
val,
this.getField('index'),
- uiSettings.get(UI_SETTINGS.SORT_OPTIONS)
+ getConfig(UI_SETTINGS.SORT_OPTIONS)
);
return addToBody(key, sort);
default:
@@ -388,7 +410,7 @@ export class SearchSource {
return searchRequest;
}
- private getIndexType(index: IIndexPattern) {
+ private getIndexType(index?: IIndexPattern) {
if (this.searchStrategyId) {
return this.searchStrategyId === 'default' ? undefined : this.searchStrategyId;
} else {
@@ -418,14 +440,11 @@ export class SearchSource {
body._source = index.getSourceFiltering();
}
- const { uiSettings } = this.dependencies;
+ const { getConfig } = this.dependencies;
if (body._source) {
// exclude source fields for this index pattern specified by the user
- const filter = fieldWildcardFilter(
- body._source.excludes,
- uiSettings.get(UI_SETTINGS.META_FIELDS)
- );
+ const filter = fieldWildcardFilter(body._source.excludes, getConfig(UI_SETTINGS.META_FIELDS));
body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) =>
filter(docvalueField.field)
);
@@ -445,11 +464,11 @@ export class SearchSource {
);
}
- const esQueryConfigs = getEsQueryConfig(uiSettings);
+ const esQueryConfigs = getEsQueryConfig({ get: getConfig });
body.query = buildEsQuery(index, query, filters, esQueryConfigs);
if (highlightAll && body.query) {
- body.highlight = getHighlightRequest(body.query, uiSettings.get(UI_SETTINGS.DOC_HIGHLIGHT));
+ body.highlight = getHighlightRequest(body.query, getConfig(UI_SETTINGS.DOC_HIGHLIGHT));
delete searchRequest.highlightAll;
}
diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts
index d1a4437943402..55726e40f5a77 100644
--- a/src/plugins/data/public/search/types.ts
+++ b/src/plugins/data/public/search/types.ts
@@ -43,10 +43,13 @@ export type ISearch = (
options?: ISearchOptions
) => Observable;
-export type ISearchGeneric = (
- request: IEsSearchRequest,
+export type ISearchGeneric = <
+ SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest,
+ SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse
+>(
+ request: SearchStrategyRequest,
options?: ISearchOptions
-) => Observable;
+) => Observable;
export interface ISearchStartLegacy {
esClient: LegacyApiCaller;
diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts
index ba0b2de393bde..032bce6d8d2aa 100644
--- a/src/plugins/data/public/services.ts
+++ b/src/plugins/data/public/services.ts
@@ -31,8 +31,6 @@ export const [getUiSettings, setUiSettings] = createGetterSetter('Http');
-
export const [getFieldFormats, setFieldFormats] = createGetterSetter(
'FieldFormats'
);
@@ -47,10 +45,6 @@ export const [getQueryService, setQueryService] = createGetterSetter<
DataPublicPluginStart['query']
>('Query');
-export const [getInjectedMetadata, setInjectedMetadata] = createGetterSetter<
- CoreStart['injectedMetadata']
->('InjectedMetadata');
-
export const [getSearchService, setSearchService] = createGetterSetter<
DataPublicPluginStart['search']
>('Search');
diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts
index cb46a838a8c30..35b1bc50ddb1e 100644
--- a/src/plugins/data/public/ui/index.ts
+++ b/src/plugins/data/public/ui/index.ts
@@ -24,11 +24,7 @@ export { QueryStringInput } from './query_string_input/query_string_input';
export { SearchBar, SearchBarProps, StatefulSearchBarProps } from './search_bar';
// @internal
-export {
- ShardFailureOpenModalButton,
- ShardFailureRequest,
- ShardFailureResponse,
-} from './shard_failure_modal';
+export { ShardFailureOpenModalButton, ShardFailureRequest } from './shard_failure_modal';
// @internal
export { SavedQueryManagementComponent } from './saved_query_management';
diff --git a/src/plugins/data/public/ui/shard_failure_modal/__mocks__/shard_failure_response.ts b/src/plugins/data/public/ui/shard_failure_modal/__mocks__/shard_failure_response.ts
index 573aeefcdf469..6178fcf92a790 100644
--- a/src/plugins/data/public/ui/shard_failure_modal/__mocks__/shard_failure_response.ts
+++ b/src/plugins/data/public/ui/shard_failure_modal/__mocks__/shard_failure_response.ts
@@ -16,9 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { ShardFailureResponse } from '../shard_failure_types';
-export const shardFailureResponse = {
+import { SearchResponse } from 'elasticsearch';
+
+export const shardFailureResponse: SearchResponse = {
_shards: {
total: 2,
successful: 1,
@@ -43,4 +44,4 @@ export const shardFailureResponse = {
},
],
},
-} as ShardFailureResponse;
+} as any;
diff --git a/src/plugins/data/public/ui/shard_failure_modal/index.ts b/src/plugins/data/public/ui/shard_failure_modal/index.ts
index f4c2e26a756e3..e5af9633e73b7 100644
--- a/src/plugins/data/public/ui/shard_failure_modal/index.ts
+++ b/src/plugins/data/public/ui/shard_failure_modal/index.ts
@@ -17,5 +17,5 @@
* under the License.
*/
-export { ShardFailureRequest, ShardFailureResponse } from './shard_failure_types';
+export { ShardFailureRequest } from './shard_failure_types';
export { ShardFailureOpenModalButton } from './shard_failure_open_modal_button';
diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_description.test.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_description.test.tsx
index 49983c9926381..d40770fb74ef1 100644
--- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_description.test.tsx
+++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_description.test.tsx
@@ -24,7 +24,8 @@ import { ShardFailure } from './shard_failure_types';
describe('ShardFailureDescription', () => {
it('renders matching snapshot given valid properties', () => {
- const failure = shardFailureResponse._shards.failures[0] as ShardFailure;
+ // TODO: remove cast once https://github.com/elastic/elasticsearch-js/issues/1286 is resolved
+ const failure = (shardFailureResponse._shards as any).failures[0] as ShardFailure;
const component = shallowWithIntl( );
expect(component).toMatchSnapshot();
});
diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_modal.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_modal.tsx
index 535f63161966d..52f3cf702c2fb 100644
--- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_modal.tsx
+++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_modal.tsx
@@ -32,18 +32,24 @@ import {
EuiButtonEmpty,
EuiCallOut,
} from '@elastic/eui';
+import { SearchResponse } from 'elasticsearch';
import { ShardFailureTable } from './shard_failure_table';
-import { ShardFailureResponse, ShardFailureRequest } from './shard_failure_types';
+import { ShardFailureRequest } from './shard_failure_types';
export interface Props {
onClose: () => void;
request: ShardFailureRequest;
- response: ShardFailureResponse;
+ response: SearchResponse;
title: string;
}
export function ShardFailureModal({ request, response, title, onClose }: Props) {
- if (!response || !response._shards || !Array.isArray(response._shards.failures) || !request) {
+ if (
+ !response ||
+ !response._shards ||
+ !Array.isArray((response._shards as any).failures) ||
+ !request
+ ) {
// this should never ever happen, but just in case
return (
@@ -51,10 +57,9 @@ export function ShardFailureModal({ request, response, title, onClose }: Props)
);
}
-
+ const failures = (response._shards as any).failures;
const requestJSON = JSON.stringify(request, null, 2);
const responseJSON = JSON.stringify(response, null, 2);
- const failures = response._shards.failures;
const tabs = [
{
diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx
index fa42745da2e48..9d89dc4cb1a29 100644
--- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx
+++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx
@@ -20,14 +20,15 @@ import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiTextAlign } from '@elastic/eui';
+import { SearchResponse } from 'elasticsearch';
import { getOverlays } from '../../services';
import { toMountPoint } from '../../../../kibana_react/public';
import { ShardFailureModal } from './shard_failure_modal';
-import { ShardFailureResponse, ShardFailureRequest } from './shard_failure_types';
+import { ShardFailureRequest } from './shard_failure_types';
interface Props {
request: ShardFailureRequest;
- response: ShardFailureResponse;
+ response: SearchResponse;
title: string;
}
diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_table.test.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_table.test.tsx
index 9d00233d37f8c..22478ebd57393 100644
--- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_table.test.tsx
+++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_table.test.tsx
@@ -24,7 +24,7 @@ import { ShardFailure } from './shard_failure_types';
describe('ShardFailureTable', () => {
it('renders matching snapshot given valid properties', () => {
- const failures = shardFailureResponse._shards.failures as ShardFailure[];
+ const failures = (shardFailureResponse._shards as any).failures as ShardFailure[];
const component = shallowWithIntl( );
expect(component).toMatchSnapshot();
});
diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_types.ts b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_types.ts
index b1ce3f30c4278..a7a56d2de9621 100644
--- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_types.ts
+++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_types.ts
@@ -25,16 +25,6 @@ export interface ShardFailureRequest {
stored_fields: string[];
}
-export interface ShardFailureResponse {
- _shards: {
- failed: number;
- failures: ShardFailure[];
- skipped: number;
- successful: number;
- total: number;
- };
-}
-
export interface ShardFailure {
index: string;
node: string;
diff --git a/src/plugins/data/server/field_formats/converters/date_nanos_server.ts b/src/plugins/data/server/field_formats/converters/date_nanos_server.ts
index 299b2aac93d49..b99febf0c7e73 100644
--- a/src/plugins/data/server/field_formats/converters/date_nanos_server.ts
+++ b/src/plugins/data/server/field_formats/converters/date_nanos_server.ts
@@ -24,7 +24,7 @@ import {
DateNanosFormat,
formatWithNanos,
} from '../../../common/field_formats/converters/date_nanos_shared';
-import { TextContextTypeConvert } from '../../../common';
+import { TextContextTypeConvert } from '../../../common/field_formats/types';
class DateNanosFormatServer extends DateNanosFormat {
textConvert: TextContextTypeConvert = (val) => {
diff --git a/src/plugins/data/server/field_formats/converters/date_server.ts b/src/plugins/data/server/field_formats/converters/date_server.ts
index 85eb65dfc6a8d..19c4adc90bade 100644
--- a/src/plugins/data/server/field_formats/converters/date_server.ts
+++ b/src/plugins/data/server/field_formats/converters/date_server.ts
@@ -23,11 +23,13 @@ import moment from 'moment-timezone';
import {
FieldFormat,
KBN_FIELD_TYPES,
- TextContextTypeConvert,
FIELD_FORMAT_IDS,
FieldFormatsGetConfigFn,
- IFieldFormatMetaParams,
} from '../../../common';
+import {
+ IFieldFormatMetaParams,
+ TextContextTypeConvert,
+} from '../../../common/field_formats/types';
export class DateFormat extends FieldFormat {
static id = FIELD_FORMAT_IDS.DATE;
diff --git a/src/plugins/data/server/search/collectors/fetch.ts b/src/plugins/data/server/search/collectors/fetch.ts
index ee29607b63ede..3551767eab017 100644
--- a/src/plugins/data/server/search/collectors/fetch.ts
+++ b/src/plugins/data/server/search/collectors/fetch.ts
@@ -19,14 +19,18 @@
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
-import { LegacyAPICaller, SharedGlobalConfig } from '../../../../../core/server';
+import { LegacyAPICaller, SharedGlobalConfig } from 'kibana/server';
import { Usage } from './register';
+interface SearchTelemetrySavedObject {
+ 'search-telemetry': Usage;
+}
+
export function fetchProvider(config$: Observable) {
return async (callCluster: LegacyAPICaller): Promise => {
const config = await config$.pipe(first()).toPromise();
- const response = await callCluster('search', {
+ const response = await callCluster('search', {
index: config.kibana.index,
body: {
query: { term: { type: { value: 'search-telemetry' } } },
@@ -35,7 +39,7 @@ export function fetchProvider(config$: Observable) {
});
return response.hits.hits.length
- ? (response.hits.hits[0]._source as Usage)
+ ? response.hits.hits[0]._source['search-telemetry']
: {
successCount: 0,
errorCount: 0,
diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts
index 2888f9d9d20b5..c34c3a310814c 100644
--- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts
+++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts
@@ -23,18 +23,20 @@ import { esSearchStrategyProvider } from './es_search_strategy';
describe('ES search strategy', () => {
const mockLogger: any = {
- info: () => {},
+ debug: () => {},
};
const mockApiCaller = jest.fn().mockResolvedValue({
- _shards: {
- total: 10,
- failed: 1,
- skipped: 2,
- successful: 7,
+ body: {
+ _shards: {
+ total: 10,
+ failed: 1,
+ skipped: 2,
+ successful: 7,
+ },
},
});
const mockContext = {
- core: { elasticsearch: { legacy: { client: { callAsCurrentUser: mockApiCaller } } } },
+ core: { elasticsearch: { client: { asCurrentUser: { search: mockApiCaller } } } },
};
const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$;
@@ -55,8 +57,7 @@ describe('ES search strategy', () => {
await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params });
expect(mockApiCaller).toBeCalled();
- expect(mockApiCaller.mock.calls[0][0]).toBe('search');
- expect(mockApiCaller.mock.calls[0][1]).toEqual({
+ expect(mockApiCaller.mock.calls[0][0]).toEqual({
...params,
timeout: '0ms',
ignoreUnavailable: true,
@@ -71,8 +72,7 @@ describe('ES search strategy', () => {
await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params });
expect(mockApiCaller).toBeCalled();
- expect(mockApiCaller.mock.calls[0][0]).toBe('search');
- expect(mockApiCaller.mock.calls[0][1]).toEqual({
+ expect(mockApiCaller.mock.calls[0][0]).toEqual({
...params,
restTotalHitsAsInt: true,
});
diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts
index 234c30376d6db..eabbf3e3e2600 100644
--- a/src/plugins/data/server/search/es_search/es_search_strategy.ts
+++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts
@@ -20,6 +20,7 @@ import { first } from 'rxjs/operators';
import { SharedGlobalConfig, Logger } from 'kibana/server';
import { SearchResponse } from 'elasticsearch';
import { Observable } from 'rxjs';
+import { ApiResponse } from '@elastic/elasticsearch';
import { SearchUsage } from '../collectors/usage';
import { ISearchStrategy, getDefaultSearchParams, getTotalLoaded } from '..';
@@ -30,7 +31,7 @@ export const esSearchStrategyProvider = (
): ISearchStrategy => {
return {
search: async (context, request, options) => {
- logger.info(`search ${request.params?.index}`);
+ logger.debug(`search ${request.params?.index}`);
const config = await config$.pipe(first()).toPromise();
const defaultParams = getDefaultSearchParams(config);
@@ -46,11 +47,10 @@ export const esSearchStrategyProvider = (
};
try {
- const rawResponse = (await context.core.elasticsearch.legacy.client.callAsCurrentUser(
- 'search',
- params,
- options
- )) as SearchResponse;
+ const esResponse = (await context.core.elasticsearch.client.asCurrentUser.search(
+ params
+ )) as ApiResponse>;
+ const rawResponse = esResponse.body;
if (usage) usage.trackSuccess(rawResponse.took);
diff --git a/src/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes.test.ts
index 167bd5af5d51d..d91aeee1fe818 100644
--- a/src/plugins/data/server/search/routes.test.ts
+++ b/src/plugins/data/server/search/routes.test.ts
@@ -36,7 +36,7 @@ describe('Search service', () => {
const response = { id: 'yay' };
mockDataStart.search.search.mockResolvedValue(response);
const mockContext = {};
- const mockBody = { params: {} };
+ const mockBody = { id: undefined, params: {} };
const mockParams = { strategy: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({
body: mockBody,
@@ -67,7 +67,7 @@ describe('Search service', () => {
});
const mockContext = {};
- const mockBody = { params: {} };
+ const mockBody = { id: undefined, params: {} };
const mockParams = { strategy: 'foo' };
const mockRequest = httpServerMock.createKibanaRequest({
body: mockBody,
diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts
index 32d8f8c1b09e0..3d813f745305f 100644
--- a/src/plugins/data/server/search/routes.ts
+++ b/src/plugins/data/server/search/routes.ts
@@ -47,10 +47,14 @@ export function registerSearchRoute(core: CoreSetup): v
const [, , selfStart] = await core.getStartServices();
try {
- const response = await selfStart.search.search(context, id ? { id } : searchRequest, {
- signal,
- strategy,
- });
+ const response = await selfStart.search.search(
+ context,
+ { ...searchRequest, id },
+ {
+ signal,
+ strategy,
+ }
+ );
return res.ok({ body: response });
} catch (err) {
return res.customError({
diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts
index 030f37d0f7c46..7057c9c7ca15c 100644
--- a/src/plugins/data/server/search/search_service.test.ts
+++ b/src/plugins/data/server/search/search_service.test.ts
@@ -32,7 +32,7 @@ describe('Search service', () => {
beforeEach(() => {
const mockLogger: any = {
- info: () => {},
+ debug: () => {},
};
plugin = new SearchService(coreMock.createPluginInitializerContext({}), mockLogger);
mockCoreSetup = coreMock.createSetup();
diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts
index a8b1cdd608a84..cc23c455bed26 100644
--- a/src/plugins/data/server/search/search_service.ts
+++ b/src/plugins/data/server/search/search_service.ts
@@ -37,11 +37,12 @@ import { UsageCollectionSetup } from '../../../usage_collection/server';
import { registerUsageCollector } from './collectors/register';
import { usageProvider } from './collectors/usage';
import { searchTelemetry } from '../saved_objects';
-import { IEsSearchRequest } from '../../common';
+import { IEsSearchRequest, IEsSearchResponse } from '../../common';
-interface StrategyMap {
- [name: string]: ISearchStrategy;
-}
+type StrategyMap<
+ SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest,
+ SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse
+> = Record>;
/** @internal */
export interface SearchServiceSetupDependencies {
@@ -56,7 +57,7 @@ export interface SearchServiceStartDependencies {
export class SearchService implements Plugin {
private readonly aggsService = new AggsService();
- private searchStrategies: StrategyMap = {};
+ private searchStrategies: StrategyMap = {};
constructor(
private initializerContext: PluginInitializerContext,
@@ -125,13 +126,19 @@ export class SearchService implements Plugin {
this.aggsService.stop();
}
- private registerSearchStrategy = (name: string, strategy: ISearchStrategy) => {
- this.logger.info(`Register strategy ${name}`);
+ private registerSearchStrategy = <
+ SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest,
+ SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse
+ >(
+ name: string,
+ strategy: ISearchStrategy
+ ) => {
+ this.logger.debug(`Register strategy ${name}`);
this.searchStrategies[name] = strategy;
};
private getSearchStrategy = (name: string): ISearchStrategy => {
- this.logger.info(`Get strategy ${name}`);
+ this.logger.debug(`Get strategy ${name}`);
const strategy = this.searchStrategies[name];
if (!strategy) {
throw new Error(`Search strategy ${name} not found`);
diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts
index fe54975d76624..56f803512aa19 100644
--- a/src/plugins/data/server/search/types.ts
+++ b/src/plugins/data/server/search/types.ts
@@ -37,7 +37,13 @@ export interface ISearchSetup {
* Extension point exposed for other plugins to register their own search
* strategies.
*/
- registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void;
+ registerSearchStrategy: <
+ SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest,
+ SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse
+ >(
+ name: string,
+ strategy: ISearchStrategy
+ ) => void;
/**
* Used internally for telemetry
@@ -45,13 +51,18 @@ export interface ISearchSetup {
usage?: SearchUsage;
}
-export interface ISearchStart {
+export interface ISearchStart<
+ SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest,
+ SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse
+> {
aggs: AggsStart;
/**
* Get other registered search strategies. For example, if a new strategy needs to use the
* already-registered ES search strategy, it can use this function to accomplish that.
*/
- getSearchStrategy: (name: string) => ISearchStrategy;
+ getSearchStrategy: (
+ name: string
+ ) => ISearchStrategy;
search: (
context: RequestHandlerContext,
request: IKibanaSearchRequest,
@@ -63,11 +74,14 @@ export interface ISearchStart {
* Search strategy interface contains a search method that takes in a request and returns a promise
* that resolves to a response.
*/
-export interface ISearchStrategy {
+export interface ISearchStrategy<
+ SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest,
+ SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse
+> {
search: (
context: RequestHandlerContext,
- request: IEsSearchRequest,
+ request: SearchStrategyRequest,
options?: ISearchOptions
- ) => Promise;
+ ) => Promise;
cancel?: (context: RequestHandlerContext, id: string) => Promise;
}
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index 9c8a79f27a9db..f870030ae9562 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -131,6 +131,7 @@ import { RequestStatistics } from 'src/plugins/inspector/common';
import { SavedObject } from 'src/core/server';
import { SavedObjectsClientContract as SavedObjectsClientContract_2 } from 'src/core/server';
import { ScrollParams } from 'elasticsearch';
+import { Search } from '@elastic/elasticsearch/api/requestParams';
import { SearchParams } from 'elasticsearch';
import { SearchResponse } from 'elasticsearch';
import { SearchShardsParams } from 'elasticsearch';
@@ -418,10 +419,11 @@ export const fieldFormats: {
TruncateFormat: typeof TruncateFormat;
};
+// Warning: (ae-forgotten-export) The symbol "GetConfigFn" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "FieldFormatsGetConfigFn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export type FieldFormatsGetConfigFn = (key: string, defaultOverride?: T) => T;
+export type FieldFormatsGetConfigFn = GetConfigFn;
// Warning: (ae-missing-release-tag) "Filter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -496,11 +498,11 @@ export interface IEsSearchRequest extends IKibanaSearchRequest {
// Warning: (ae-missing-release-tag) "IEsSearchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export interface IEsSearchResponse extends IKibanaSearchResponse {
+export interface IEsSearchResponse extends IKibanaSearchResponse {
isPartial?: boolean;
isRunning?: boolean;
// (undocumented)
- rawResponse: SearchResponse;
+ rawResponse: SearchResponse;
}
// Warning: (ae-missing-release-tag) "IFieldFormatsRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -687,19 +689,19 @@ export interface ISearchSetup {
//
// (undocumented)
aggs: AggsSetup;
- registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void;
+ registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void;
usage?: SearchUsage;
}
// Warning: (ae-missing-release-tag) "ISearchStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export interface ISearchStart {
+export interface ISearchStart {
// Warning: (ae-forgotten-export) The symbol "AggsStart" needs to be exported by the entry point index.d.ts
//
// (undocumented)
aggs: AggsStart;
- getSearchStrategy: (name: string) => ISearchStrategy;
+ getSearchStrategy: (name: string) => ISearchStrategy;
// Warning: (ae-forgotten-export) The symbol "RequestHandlerContext" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@@ -709,11 +711,11 @@ export interface ISearchStart {
// Warning: (ae-missing-release-tag) "ISearchStrategy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
-export interface ISearchStrategy {
+export interface ISearchStrategy {
// (undocumented)
cancel?: (context: RequestHandlerContext, id: string) => Promise;
// (undocumented)
- search: (context: RequestHandlerContext, request: IEsSearchRequest, options?: ISearchOptions) => Promise;
+ search: (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise;
}
// @public (undocumented)
@@ -860,7 +862,7 @@ export class Plugin implements Plugin_2>;
fieldFormats: {
fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise;
};
diff --git a/src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts b/src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts
index 437898201863f..9a199ea4a62fc 100644
--- a/src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts
+++ b/src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts
@@ -18,7 +18,7 @@
*/
import { ISearchSource, EsQuerySortValue, SortDirection } from '../../../../../../../data/public';
import { convertTimeValueToIso } from './date_conversion';
-import { EsHitRecordList } from '../context';
+import { EsHitRecordList, EsHitRecord } from '../context';
import { IntervalValue } from './generate_intervals';
import { EsQuerySearchAfter } from './get_es_query_search_after';
@@ -76,5 +76,6 @@ export async function fetchHitsInInterval(
.setField('version', true)
.fetch();
- return response.hits ? response.hits.hits : [];
+ // TODO: There's a difference in the definition of SearchResponse and EsHitRecord
+ return ((response.hits?.hits as unknown) as EsHitRecord[]) || [];
}
diff --git a/src/plugins/discover/public/application/angular/directives/render_complete.ts b/src/plugins/discover/public/application/angular/directives/render_complete.ts
index 635cf68f12fcb..72f97138790b2 100644
--- a/src/plugins/discover/public/application/angular/directives/render_complete.ts
+++ b/src/plugins/discover/public/application/angular/directives/render_complete.ts
@@ -17,14 +17,14 @@
* under the License.
*/
import { IScope } from 'angular';
-import { RenderCompleteHelper } from '../../../../../kibana_utils/public';
+import { RenderCompleteListener } from '../../../../../kibana_utils/public';
export function createRenderCompleteDirective() {
return {
controller($scope: IScope, $element: JQLite) {
const el = $element[0];
- const renderCompleteHelper = new RenderCompleteHelper(el);
- $scope.$on('$destroy', renderCompleteHelper.destroy);
+ const renderCompleteListener = new RenderCompleteListener(el);
+ $scope.$on('$destroy', renderCompleteListener.destroy);
},
};
}
diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json
index c9694ad7b9423..6a8e6079232aa 100644
--- a/src/plugins/embeddable/kibana.json
+++ b/src/plugins/embeddable/kibana.json
@@ -12,6 +12,7 @@
],
"requiredBundles": [
"savedObjects",
- "kibanaReact"
+ "kibanaReact",
+ "kibanaUtils"
]
}
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
index fcecf117d7d52..ffe8a5bf6e7dc 100644
--- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
@@ -19,6 +19,8 @@
import { cloneDeep, isEqual } from 'lodash';
import * as Rx from 'rxjs';
+import { distinctUntilChanged, map } from 'rxjs/operators';
+import { RenderCompleteDispatcher } from '../../../../kibana_utils/public';
import { Adapters, ViewMode } from '../types';
import { IContainer } from '../containers';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
@@ -47,6 +49,8 @@ export abstract class Embeddable<
private readonly input$: Rx.BehaviorSubject;
private readonly output$: Rx.BehaviorSubject;
+ protected renderComplete = new RenderCompleteDispatcher();
+
// Listener to parent changes, if this embeddable exists in a parent, in order
// to update input when the parent changes.
private parentSubscription?: Rx.Subscription;
@@ -77,6 +81,15 @@ export abstract class Embeddable<
this.onResetInput(newInput);
});
}
+
+ this.getOutput$()
+ .pipe(
+ map(({ title }) => title || ''),
+ distinctUntilChanged()
+ )
+ .subscribe((title) => {
+ this.renderComplete.setTitle(title);
+ });
}
public getIsContainer(): this is IContainer {
@@ -105,8 +118,8 @@ export abstract class Embeddable<
return this.input;
}
- public getTitle() {
- return this.output.title;
+ public getTitle(): string {
+ return this.output.title || '';
}
/**
@@ -133,7 +146,10 @@ export abstract class Embeddable<
}
}
- public render(domNode: HTMLElement | Element): void {
+ public render(el: HTMLElement): void {
+ this.renderComplete.setEl(el);
+ this.renderComplete.setTitle(this.output.title || '');
+
if (this.destroyed) {
throw new Error('Embeddable has been destroyed');
}
diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
index 7628b1d41b452..9c4a1b5602c49 100644
--- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
@@ -22,6 +22,7 @@ import { Adapters } from '../types';
import { IContainer } from '../containers/i_container';
import { ViewMode } from '../types';
import { TriggerContextMapping } from '../../../../ui_actions/public';
+import type { TimeRange, Query, Filter } from '../../../../data/common';
export interface EmbeddableError {
name: string;
@@ -55,6 +56,21 @@ export interface EmbeddableInput {
*/
disableTriggers?: boolean;
+ /**
+ * Time range of the chart.
+ */
+ timeRange?: TimeRange;
+
+ /**
+ * Visualization query string used to narrow down results.
+ */
+ query?: Query;
+
+ /**
+ * Visualization filters used to narrow down results.
+ */
+ filters?: Filter[];
+
[key: string]: unknown;
}
diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts
index 42adb9d770e8a..ef79b18acd4f5 100644
--- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts
+++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts
@@ -19,7 +19,7 @@
import { coreMock, scopedHistoryMock } from '../../../../../core/public/mocks';
import { EmbeddableStateTransfer } from '.';
-import { ApplicationStart } from '../../../../../core/public';
+import { ApplicationStart, PublicAppInfo } from '../../../../../core/public';
function mockHistoryState(state: unknown) {
return scopedHistoryMock.create({ state });
@@ -37,6 +37,29 @@ describe('embeddable state transfer', () => {
stateTransfer = new EmbeddableStateTransfer(application.navigateToApp);
});
+ it('cannot fetch app name when given no app list', async () => {
+ expect(stateTransfer.getAppNameFromId('test')).toBeUndefined();
+ });
+
+ it('cannot fetch app name when app id is not in given app list', async () => {
+ const appsList = new Map([
+ ['testId', { title: 'State Transfer Test App Hello' } as PublicAppInfo],
+ ['testId2', { title: 'State Transfer Test App Goodbye' } as PublicAppInfo],
+ ]);
+ stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, undefined, appsList);
+ expect(stateTransfer.getAppNameFromId('kibanana')).toBeUndefined();
+ });
+
+ it('can fetch app titles when given app list', async () => {
+ const appsList = new Map([
+ ['testId', { title: 'State Transfer Test App Hello' } as PublicAppInfo],
+ ['testId2', { title: 'State Transfer Test App Goodbye' } as PublicAppInfo],
+ ]);
+ stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, undefined, appsList);
+ expect(stateTransfer.getAppNameFromId('testId')).toBe('State Transfer Test App Hello');
+ expect(stateTransfer.getAppNameFromId('testId2')).toBe('State Transfer Test App Goodbye');
+ });
+
it('can send an outgoing originating app state', async () => {
await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp } });
expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', {
diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts
index 8f70e5a66c478..780cff9f4be7e 100644
--- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts
+++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts
@@ -18,7 +18,12 @@
*/
import { cloneDeep } from 'lodash';
-import { ScopedHistory, ApplicationStart } from '../../../../../core/public';
+import {
+ ScopedHistory,
+ ApplicationStart,
+ PublicLegacyAppInfo,
+ PublicAppInfo,
+} from '../../../../../core/public';
import {
EmbeddableEditorState,
isEmbeddableEditorState,
@@ -35,9 +40,16 @@ import {
export class EmbeddableStateTransfer {
constructor(
private navigateToApp: ApplicationStart['navigateToApp'],
- private scopedHistory?: ScopedHistory
+ private scopedHistory?: ScopedHistory,
+ private appList?: ReadonlyMap | undefined
) {}
+ /**
+ * Fetches an internationalized app title when given an appId.
+ * @param appId - The id of the app to fetch the title for
+ */
+ public getAppNameFromId = (appId: string): string | undefined => this.appList?.get(appId)?.title;
+
/**
* Fetches an {@link EmbeddableEditorState | originating app} argument from the scoped
* history's location state.
diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx
index 7ec03ba659cda..2064236e9ae7f 100644
--- a/src/plugins/embeddable/public/mocks.tsx
+++ b/src/plugins/embeddable/public/mocks.tsx
@@ -25,6 +25,9 @@ import {
EmbeddableStateTransfer,
IEmbeddable,
EmbeddablePanel,
+ EmbeddableInput,
+ SavedObjectEmbeddableInput,
+ ReferenceOrValueEmbeddable,
} from '.';
import { EmbeddablePublicPlugin } from './plugin';
import { coreMock } from '../../../core/public/mocks';
@@ -35,7 +38,6 @@ import { dataPluginMock } from '../../data/public/mocks';
import { inspectorPluginMock } from '../../inspector/public/mocks';
import { uiActionsPluginMock } from '../../ui_actions/public/mocks';
-import { SavedObjectEmbeddableInput, ReferenceOrValueEmbeddable, EmbeddableInput } from './lib';
export type Setup = jest.Mocked;
export type Start = jest.Mocked;
diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx
index 3cbd49279564f..fb09729ab71c3 100644
--- a/src/plugins/embeddable/public/plugin.tsx
+++ b/src/plugins/embeddable/public/plugin.tsx
@@ -17,6 +17,7 @@
* under the License.
*/
import React from 'react';
+import { Subscription } from 'rxjs';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public';
import { getSavedObjectFinder } from '../../saved_objects/public';
import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public';
@@ -27,6 +28,8 @@ import {
CoreStart,
Plugin,
ScopedHistory,
+ PublicAppInfo,
+ PublicLegacyAppInfo,
} from '../../../core/public';
import { EmbeddableFactoryRegistry, EmbeddableFactoryProvider } from './types';
import { bootstrap } from './bootstrap';
@@ -89,6 +92,8 @@ export class EmbeddablePublicPlugin implements Plugin;
+ private appListSubscription?: Subscription;
constructor(initializerContext: PluginInitializerContext) {}
@@ -121,7 +126,15 @@ export class EmbeddablePublicPlugin implements Plugin {
+ this.appList = appList;
+ });
+
+ this.outgoingOnlyStateTransfer = new EmbeddableStateTransfer(
+ core.application.navigateToApp,
+ undefined,
+ this.appList
+ );
this.isRegistryReady = true;
const getEmbeddablePanelHoc = (stateTransfer?: EmbeddableStateTransfer) => ({
@@ -151,7 +164,7 @@ export class EmbeddablePublicPlugin implements Plugin {
return history
- ? new EmbeddableStateTransfer(core.application.navigateToApp, history)
+ ? new EmbeddableStateTransfer(core.application.navigateToApp, history, this.appList)
: this.outgoingOnlyStateTransfer;
},
EmbeddablePanel: getEmbeddablePanelHoc(),
@@ -159,7 +172,11 @@ export class EmbeddablePublicPlugin implements Plugin {
this.ensureFactoriesExist();
diff --git a/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/__tests__/json_xjson_translation_tools.test.ts b/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/__tests__/json_xjson_translation_tools.test.ts
index 419e80ad1608f..8c66a87adbaa1 100644
--- a/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/__tests__/json_xjson_translation_tools.test.ts
+++ b/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/__tests__/json_xjson_translation_tools.test.ts
@@ -23,6 +23,7 @@ import collapsingTests from './utils_string_collapsing.txt';
import expandingTests from './utils_string_expanding.txt';
import * as utils from '../index';
+import { extractJSONStringValues } from '../parser';
describe('JSON to XJSON conversion tools', () => {
it('will collapse multiline strings', () => {
@@ -34,6 +35,32 @@ describe('JSON to XJSON conversion tools', () => {
const multiline = '{ "foo": """bar\r\nbaz""" }';
expect(utils.collapseLiteralStrings(multiline)).toEqual('{ "foo": "bar\\r\\nbaz" }');
});
+
+ describe('JSON string values parser', () => {
+ test('correctly extracts JSON string values', () => {
+ const json = {
+ myString: 'string',
+ notAString: 1,
+ myStringArray: ['a', 1, 'test', { nestedString: 'string' }],
+ };
+ const jsonString = JSON.stringify(json);
+ const { stringValues } = extractJSONStringValues(jsonString);
+ expect(stringValues.length).toBe(4);
+
+ expect(jsonString.substring(stringValues[0].startIndex, stringValues[0].endIndex + 1)).toBe(
+ '"string"'
+ );
+ expect(jsonString.substring(stringValues[1].startIndex, stringValues[1].endIndex + 1)).toBe(
+ '"a"'
+ );
+ expect(jsonString.substring(stringValues[2].startIndex, stringValues[2].endIndex + 1)).toBe(
+ '"test"'
+ );
+ expect(jsonString.substring(stringValues[3].startIndex, stringValues[3].endIndex + 1)).toBe(
+ '"string"'
+ );
+ });
+ });
});
_.each(collapsingTests.split(/^=+$/m), function (fixture) {
diff --git a/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/__tests__/utils_string_expanding.txt b/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/__tests__/utils_string_expanding.txt
index 7de874c244e74..d157ed73817c2 100644
--- a/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/__tests__/utils_string_expanding.txt
+++ b/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/__tests__/utils_string_expanding.txt
@@ -1,8 +1,9 @@
+
==========
Scripts in requests
-------------------------------------
{
- "f": { "script": { "source": "\ntest\ntest\\\\\\\\\\\\\\\\2\n" } },
+ "f": { "script": { "source": "\ntest\ntest\\2\n" } },
"g": { "script": "second + \"\\\";" },
"a": "short with \\",
"\\\\h": 1,
@@ -12,7 +13,7 @@ Scripts in requests
{
"f": { "script": { "source": """
test
-test\\\\\\\\2
+test\2
""" } },
"g": { "script": """second + "\";""" },
"a": """short with \""",
@@ -23,11 +24,11 @@ test\\\\\\\\2
Preserve triple quotes
-------------------------------------
{
- "content\\\": "tri\"ple",
+ "content\\": "tri\"ple",
}
-------------------------------------
{
- "content\\\": """tri"ple""",
+ "content\\": """tri"ple""",
}
==========
Correctly parse with JSON embedded inside values
@@ -82,3 +83,13 @@ Single quotes escaped special case, end
{
"query": "test\""
}
+==========
+Strings in Arrays
+-------------------------------------
+{
+ "array": ["expand \\ me", "do not expand", "do expand \\"]
+}
+-------------------------------------
+{
+ "array": ["""expand \ me""", "do not expand", """do expand \"""]
+}
diff --git a/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/index.ts b/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/index.ts
index 28f1aca95efab..86fe9535a9619 100644
--- a/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/index.ts
+++ b/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/index.ts
@@ -17,6 +17,8 @@
* under the License.
*/
+import { extractJSONStringValues } from './parser';
+
export function collapseLiteralStrings(data: string) {
const splitData = data.split(`"""`);
for (let idx = 1; idx < splitData.length - 1; idx += 2) {
@@ -25,47 +27,60 @@ export function collapseLiteralStrings(data: string) {
return splitData.join('');
}
-/*
- The following regex describes global match on:
- 1. one colon followed by any number of space characters
- 2. one double quote (not escaped, special case for JSON in JSON).
- 3. greedily match any non double quote and non newline char OR any escaped double quote char (non-capturing).
- 4. handle a special case where an escaped slash may be the last character
- 5. one double quote
-
- For instance: `: "some characters \" here"`
- Will match and be expanded to: `"""some characters " here"""`
+// 5 megabytes
+const MAX_EXPANDABLE_JSON_SIZE = 5 * 1024 * 1024;
+/**
+ * Takes in a string representing some JSON data and expands strings,
+ * where needed, to a string literal representation.
+ *
+ * For example; given a value like: "{ "my_string": "\nhey!\n" }"
+ *
+ * Will return: "{ "my_string": """
+ * hey!
+ * """
+ * }"
*/
+export function expandLiteralStrings(data: string) {
+ // Assuming 1 byte per char
+ if (data.length > MAX_EXPANDABLE_JSON_SIZE) {
+ return data;
+ }
-const LITERAL_STRING_CANDIDATES = /((:[\s\r\n]*)([^\\])"(\\"|[^"\n])*\\?")/g;
+ const { stringValues } = extractJSONStringValues(data);
-export function expandLiteralStrings(data: string) {
- return data.replace(LITERAL_STRING_CANDIDATES, (match, string) => {
- // Expand to triple quotes if there are _any_ slashes
- if (string.match(/\\./)) {
- const firstDoubleQuoteIdx = string.indexOf('"');
- const lastDoubleQuoteIdx = string.lastIndexOf('"');
+ if (stringValues.length === 0) {
+ return data;
+ }
- // Handle a special case where we may have a value like "\"test\"". We don't
- // want to expand this to """"test"""" - so we terminate before processing the string
- // further if we detect this either at the start or end of the double quote section.
+ // Include JSON before our first string value
+ let result = data.substring(0, stringValues[0].startIndex);
- if (string[firstDoubleQuoteIdx + 1] === '\\' && string[firstDoubleQuoteIdx + 2] === '"') {
- return string;
- }
+ for (let x = 0; x < stringValues.length; x++) {
+ const { startIndex, endIndex } = stringValues[x];
+ const candidate = data.substring(startIndex, endIndex + 1);
- if (string[lastDoubleQuoteIdx - 1] === '"' && string[lastDoubleQuoteIdx - 2] === '\\') {
- return string;
- }
+ // Handle a special case where we may have a value like "\"test\"". We don't
+ // want to expand this to """"test"""" - so we terminate before processing the string
+ // further if we detect this either at the start or end of the double quote section.
+ const skip =
+ (candidate[1] === '\\' && candidate[2] === '"') ||
+ (candidate[candidate.length - 2] === '"' && candidate[candidate.length - 3] === '\\');
- const colonAndAnySpacing = string.slice(0, firstDoubleQuoteIdx);
- const rawStringifiedValue = string.slice(firstDoubleQuoteIdx, string.length);
- // Remove one level of JSON stringification
- const jsonValue = JSON.parse(rawStringifiedValue);
- return `${colonAndAnySpacing}"""${jsonValue}"""`;
+ if (!skip && candidate.match(/\\./)) {
+ result += `"""${JSON.parse(candidate)}"""`;
} else {
- return string;
+ result += candidate;
+ }
+
+ if (stringValues[x + 1]) {
+ // Add any JSON between string values
+ result += data.substring(endIndex + 1, stringValues[x + 1].startIndex);
}
- });
+ }
+
+ // Add any remaining JSON after all string values
+ result += data.substring(stringValues[stringValues.length - 1].endIndex + 1);
+
+ return result;
}
diff --git a/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/parser.ts b/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/parser.ts
new file mode 100644
index 0000000000000..2cbd886860a07
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/console_lang/lib/json_xjson_translation_tools/parser.ts
@@ -0,0 +1,97 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+type StringValues = Array<{ startIndex: number; endIndex: number }>;
+
+interface ParseResult {
+ stringValues: StringValues;
+}
+
+const JSON_COLON = ':';
+const JSON_STRING_DELIMITER = '"';
+const JSON_STRING_ESCAPE = '\\';
+
+/**
+ * Accepts JSON (as a string) and extracts the positions of all JSON string
+ * values.
+ *
+ * For example:
+ *
+ * '{ "my_string_value": "is this", "my_number_value": 42 }'
+ *
+ * Would extract one result:
+ *
+ * [ { startIndex: 21, endIndex: 29 } ]
+ *
+ * This result maps to `"is this"` from the example JSON.
+ *
+ */
+export const extractJSONStringValues = (input: string): ParseResult => {
+ let position = 0;
+ let currentStringStartPos: number;
+ let isInsideString = false;
+ const stringValues: StringValues = [];
+
+ function read() {
+ return input[position];
+ }
+
+ function peekNextNonWhitespace(): string | undefined {
+ let peekPosition = position + 1;
+
+ while (peekPosition < input.length) {
+ const peekChar = input[peekPosition];
+ if (peekChar.match(/[^\s\r\n]/)) {
+ return peekChar;
+ }
+ ++peekPosition;
+ }
+ }
+
+ function advance() {
+ ++position;
+ }
+
+ while (position < input.length) {
+ const char = read();
+ if (!isInsideString) {
+ if (char === JSON_STRING_DELIMITER) {
+ currentStringStartPos = position;
+ isInsideString = true;
+ }
+ // else continue scanning for JSON_STRING_DELIMITER
+ } else {
+ if (char === JSON_STRING_ESCAPE) {
+ // skip ahead - we are still inside of a string
+ advance();
+ } else if (char === JSON_STRING_DELIMITER) {
+ if (peekNextNonWhitespace() !== JSON_COLON) {
+ stringValues.push({
+ startIndex: currentStringStartPos!,
+ endIndex: position,
+ });
+ }
+ isInsideString = false;
+ }
+ }
+ advance();
+ }
+
+ return { stringValues };
+};
diff --git a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_context.tsx b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_context.tsx
index 39b91a2e20b53..7719e7748829d 100644
--- a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_context.tsx
+++ b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_context.tsx
@@ -147,7 +147,7 @@ export const FormWizardProvider = WithMultiContent>(function FormWiza
return nextState;
});
},
- [getStepIndex, validate, onSave, getData]
+ [getStepIndex, validate, onSave, getData, lastStep]
);
const value: Context = {
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx
index eead90d2f75b7..a55b2f0a8fa29 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx
@@ -98,7 +98,7 @@ describe(' ', () => {
useEffect(() => {
onForm(form);
- }, [form]);
+ }, [onForm, form]);
return (