diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 97099c6f87448..ee117d362d59b 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -110,6 +110,9 @@ pipeline { archiveArtifacts(allowEmptyArchive: true, artifacts: "${E2E_DIR}/kibana.log") } } + cleanup { + notifyBuildResult(notifyPRComment: false, analyzeFlakey: false, shouldNotify: false) + } } } diff --git a/.sass-lint.yml b/.sass-lint.yml index 50cbe81cc7da2..d6eaaf391de1a 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -3,6 +3,7 @@ files: - 'src/legacy/core_plugins/metrics/**/*.s+(a|c)ss' - 'src/plugins/timelion/**/*.s+(a|c)ss' - 'src/plugins/vis_type_vislib/**/*.s+(a|c)ss' + - 'src/plugins/vis_type_vega/**/*.s+(a|c)ss' - 'src/plugins/vis_type_xy/**/*.s+(a|c)ss' - 'x-pack/plugins/canvas/**/*.s+(a|c)ss' - 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss' diff --git a/src/core/server/elasticsearch/client/errors.test.ts b/src/core/server/elasticsearch/client/errors.test.ts new file mode 100644 index 0000000000000..35ad4ca71f48c --- /dev/null +++ b/src/core/server/elasticsearch/client/errors.test.ts @@ -0,0 +1,82 @@ +/* + * 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 { + ResponseError, + ConnectionError, + ConfigurationError, +} from '@elastic/elasticsearch/lib/errors'; +import { ApiResponse } from '@elastic/elasticsearch'; +import { isResponseError, isUnauthorizedError } from './errors'; + +const createApiResponseError = ({ + statusCode = 200, + headers = {}, + body = {}, +}: { + statusCode?: number; + headers?: Record; + body?: Record; +} = {}): ApiResponse => { + return { + body, + statusCode, + headers, + warnings: [], + meta: {} as any, + }; +}; + +describe('isResponseError', () => { + it('returns `true` when the input is a `ResponseError`', () => { + expect(isResponseError(new ResponseError(createApiResponseError()))).toBe(true); + }); + + it('returns `false` when the input is not a `ResponseError`', () => { + expect(isResponseError(new Error('foo'))).toBe(false); + expect(isResponseError(new ConnectionError('error', createApiResponseError()))).toBe(false); + expect(isResponseError(new ConfigurationError('foo'))).toBe(false); + }); +}); + +describe('isUnauthorizedError', () => { + it('returns true when the input is a `ResponseError` and statusCode === 401', () => { + expect( + isUnauthorizedError(new ResponseError(createApiResponseError({ statusCode: 401 }))) + ).toBe(true); + }); + + it('returns false when the input is a `ResponseError` and statusCode !== 401', () => { + expect( + isUnauthorizedError(new ResponseError(createApiResponseError({ statusCode: 200 }))) + ).toBe(false); + expect( + isUnauthorizedError(new ResponseError(createApiResponseError({ statusCode: 403 }))) + ).toBe(false); + expect( + isUnauthorizedError(new ResponseError(createApiResponseError({ statusCode: 500 }))) + ).toBe(false); + }); + + it('returns `false` when the input is not a `ResponseError`', () => { + expect(isUnauthorizedError(new Error('foo'))).toBe(false); + expect(isUnauthorizedError(new ConnectionError('error', createApiResponseError()))).toBe(false); + expect(isUnauthorizedError(new ConfigurationError('foo'))).toBe(false); + }); +}); diff --git a/src/core/server/elasticsearch/client/errors.ts b/src/core/server/elasticsearch/client/errors.ts new file mode 100644 index 0000000000000..31a27170e1155 --- /dev/null +++ b/src/core/server/elasticsearch/client/errors.ts @@ -0,0 +1,32 @@ +/* + * 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 { ResponseError } from '@elastic/elasticsearch/lib/errors'; + +export type UnauthorizedError = ResponseError & { + statusCode: 401; +}; + +export function isResponseError(error: any): error is ResponseError { + return Boolean(error.body && error.statusCode && error.headers); +} + +export function isUnauthorizedError(error: any): error is UnauthorizedError { + return isResponseError(error) && error.statusCode === 401; +} diff --git a/src/core/server/elasticsearch/client/mocks.test.ts b/src/core/server/elasticsearch/client/mocks.test.ts index b882f8d0c5d79..a6ce95155331e 100644 --- a/src/core/server/elasticsearch/client/mocks.test.ts +++ b/src/core/server/elasticsearch/client/mocks.test.ts @@ -49,6 +49,12 @@ describe('Mocked client', () => { expectMocked(client.close); }); + it('used EventEmitter functions should be mocked', () => { + expectMocked(client.on); + expectMocked(client.off); + expectMocked(client.once); + }); + it('`child` should be mocked and return a mocked Client', () => { expectMocked(client.child); diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index 34e83922d4d86..ec2885dfdf922 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -54,13 +54,20 @@ const createInternalClientMock = (): DeeplyMockedKeys => { mockify(client, omittedProps); - client.transport = { + // client got some read-only (getter) properties + // so we need to extend it to override the getter-only props. + const mock: any = { ...client }; + + mock.transport = { request: jest.fn(), }; - client.close = jest.fn().mockReturnValue(Promise.resolve()); - client.child = jest.fn().mockImplementation(() => createInternalClientMock()); + mock.close = jest.fn().mockReturnValue(Promise.resolve()); + mock.child = jest.fn().mockImplementation(() => createInternalClientMock()); + mock.on = jest.fn(); + mock.off = jest.fn(); + mock.once = jest.fn(); - return (client as unknown) as DeeplyMockedKeys; + return (mock as unknown) as DeeplyMockedKeys; }; export type ElasticSearchClientMock = DeeplyMockedKeys; diff --git a/src/core/server/http/integration_tests/core_service.test.mocks.ts b/src/core/server/http/integration_tests/core_service.test.mocks.ts index c23724b7d332f..515dad5383c01 100644 --- a/src/core/server/http/integration_tests/core_service.test.mocks.ts +++ b/src/core/server/http/integration_tests/core_service.test.mocks.ts @@ -18,10 +18,12 @@ */ import { elasticsearchServiceMock } from '../../elasticsearch/elasticsearch_service.mock'; -export const clusterClientMock = jest.fn(); -export const clusterClientInstanceMock = elasticsearchServiceMock.createLegacyScopedClusterClient(); +export const MockLegacyScopedClusterClient = jest.fn(); +export const legacyClusterClientInstanceMock = elasticsearchServiceMock.createLegacyScopedClusterClient(); jest.doMock('../../elasticsearch/legacy/scoped_cluster_client', () => ({ - LegacyScopedClusterClient: clusterClientMock.mockImplementation(() => clusterClientInstanceMock), + LegacyScopedClusterClient: MockLegacyScopedClusterClient.mockImplementation( + () => legacyClusterClientInstanceMock + ), })); jest.doMock('elasticsearch', () => { @@ -34,3 +36,12 @@ jest.doMock('elasticsearch', () => { }, }; }); + +export const MockElasticsearchClient = jest.fn(); +jest.doMock('@elastic/elasticsearch', () => { + const real = jest.requireActual('@elastic/elasticsearch'); + return { + ...real, + Client: MockElasticsearchClient, + }; +}); 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 3c5f22500e5e0..6338326626d54 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -17,14 +17,21 @@ * under the License. */ -import { clusterClientMock, clusterClientInstanceMock } from './core_service.test.mocks'; +import { + MockLegacyScopedClusterClient, + MockElasticsearchClient, + legacyClusterClientInstanceMock, +} from './core_service.test.mocks'; import Boom from 'boom'; import { Request } from 'hapi'; import { errors as esErrors } from 'elasticsearch'; import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy'; +import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import { InternalElasticsearchServiceStart } from '../../elasticsearch'; interface User { id: string; @@ -44,6 +51,17 @@ const cookieOptions = { }; describe('http service', () => { + let esClient: ReturnType; + + beforeEach(async () => { + esClient = elasticsearchClientMock.createInternalClient(); + MockElasticsearchClient.mockImplementation(() => esClient); + }, 30000); + + afterEach(async () => { + MockElasticsearchClient.mockClear(); + }); + describe('auth', () => { let root: ReturnType; beforeEach(async () => { @@ -200,7 +218,7 @@ describe('http service', () => { }, 30000); afterEach(async () => { - clusterClientMock.mockClear(); + MockLegacyScopedClusterClient.mockClear(); await root.shutdown(); }); @@ -363,7 +381,7 @@ describe('http service', () => { }, 30000); afterEach(async () => { - clusterClientMock.mockClear(); + MockLegacyScopedClusterClient.mockClear(); await root.shutdown(); }); @@ -386,7 +404,7 @@ describe('http service', () => { await kbnTestServer.request.get(root, '/new-platform/').expect(200); // client contains authHeaders for BWC with legacy platform. - const [client] = clusterClientMock.mock.calls; + const [client] = MockLegacyScopedClusterClient.mock.calls; const [, , clientHeaders] = client; expect(clientHeaders).toEqual(authHeaders); }); @@ -410,7 +428,7 @@ describe('http service', () => { .set('Authorization', authorizationHeader) .expect(200); - const [client] = clusterClientMock.mock.calls; + const [client] = MockLegacyScopedClusterClient.mock.calls; const [, , clientHeaders] = client; expect(clientHeaders).toEqual({ authorization: authorizationHeader }); }); @@ -426,7 +444,7 @@ describe('http service', () => { }) ); - clusterClientInstanceMock.callAsCurrentUser.mockRejectedValue(authenticationError); + legacyClusterClientInstanceMock.callAsCurrentUser.mockRejectedValue(authenticationError); const router = createRouter('/new-platform'); router.get({ path: '/', validate: false }, async (context, req, res) => { @@ -441,4 +459,91 @@ describe('http service', () => { expect(response.header['www-authenticate']).toEqual('authenticate header'); }); }); + + describe('elasticsearch client', () => { + let root: ReturnType; + + beforeEach(async () => { + root = kbnTestServer.createRoot({ plugins: { initialize: false } }); + }, 30000); + + afterEach(async () => { + MockElasticsearchClient.mockClear(); + await root.shutdown(); + }); + + it('forwards unauthorized errors from elasticsearch', async () => { + const { http } = await root.setup(); + const { createRouter } = http; + // eslint-disable-next-line prefer-const + let elasticsearch: InternalElasticsearchServiceStart; + + esClient.ping.mockImplementation(() => + elasticsearchClientMock.createClientError( + new ResponseError({ + statusCode: 401, + body: { + error: { + type: 'Unauthorized', + }, + }, + warnings: [], + headers: { + 'WWW-Authenticate': 'content', + }, + meta: {} as any, + }) + ) + ); + + const router = createRouter('/new-platform'); + router.get({ path: '/', validate: false }, async (context, req, res) => { + await elasticsearch.client.asScoped(req).asInternalUser.ping(); + return res.ok(); + }); + + const coreStart = await root.start(); + elasticsearch = coreStart.elasticsearch; + + const { header } = await kbnTestServer.request.get(root, '/new-platform/').expect(401); + + expect(header['www-authenticate']).toEqual('content'); + }); + + it('uses a default value for `www-authenticate` header when ES 401 does not specify it', async () => { + const { http } = await root.setup(); + const { createRouter } = http; + // eslint-disable-next-line prefer-const + let elasticsearch: InternalElasticsearchServiceStart; + + esClient.ping.mockImplementation(() => + elasticsearchClientMock.createClientError( + new ResponseError({ + statusCode: 401, + body: { + error: { + type: 'Unauthorized', + }, + }, + warnings: [], + headers: {}, + meta: {} as any, + }) + ) + ); + + const router = createRouter('/new-platform'); + router.get({ path: '/', validate: false }, async (context, req, res) => { + await elasticsearch.client.asScoped(req).asInternalUser.ping(); + return res.ok(); + }); + + const coreStart = await root.start(); + elasticsearch = coreStart.elasticsearch; + + const { header } = await kbnTestServer.request.get(root, '/new-platform/').expect(401); + + expect(header['www-authenticate']).toEqual('Basic realm="Authorization Required"'); + }); + }); }); diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 35eec746163ce..cc5279a396163 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -23,8 +23,17 @@ import Boom from 'boom'; import { isConfigSchema } from '@kbn/config-schema'; import { Logger } from '../../logging'; import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy/errors'; +import { + isUnauthorizedError as isElasticsearchUnauthorizedError, + UnauthorizedError as EsNotAuthorizedError, +} from '../../elasticsearch/client/errors'; import { KibanaRequest } from './request'; -import { KibanaResponseFactory, kibanaResponseFactory, IKibanaResponse } from './response'; +import { + KibanaResponseFactory, + kibanaResponseFactory, + IKibanaResponse, + ErrorHttpResponseOptions, +} from './response'; import { RouteConfig, RouteConfigOptions, RouteMethod, validBodyOutput } from './route'; import { HapiResponseAdapter } from './response_adapter'; import { RequestHandlerContext } from '../../../server'; @@ -264,7 +273,13 @@ export class Router implements IRouter { return hapiResponseAdapter.handle(kibanaResponse); } catch (e) { this.log.error(e); - // forward 401 (boom) error from ES + // forward 401 errors from ES client + if (isElasticsearchUnauthorizedError(e)) { + return hapiResponseAdapter.handle( + kibanaResponseFactory.unauthorized(convertEsUnauthorized(e)) + ); + } + // forward 401 (boom) errors from legacy ES client if (LegacyElasticsearchErrorHelpers.isNotAuthorizedError(e)) { return e; } @@ -273,6 +288,21 @@ export class Router implements IRouter { } } +const convertEsUnauthorized = (e: EsNotAuthorizedError): ErrorHttpResponseOptions => { + const getAuthenticateHeaderValue = () => { + const header = Object.entries(e.headers).find( + ([key]) => key.toLowerCase() === 'www-authenticate' + ); + return header ? header[1] : 'Basic realm="Authorization Required"'; + }; + return { + body: e.message, + headers: { + 'www-authenticate': getAuthenticateHeaderValue(), + }, + }; +}; + type WithoutHeadArgument = T extends (first: any, ...rest: infer Params) => infer Return ? (...rest: Params) => Return : never; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 38e0416233e25..a8868c07061c3 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -52,7 +52,6 @@ import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiComboBoxProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; -import { EventEmitter } from 'events'; import { ExclusiveUnion } from '@elastic/eui'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; @@ -148,7 +147,7 @@ import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; import { Reporter } from '@kbn/analytics'; import { RequestAdapter } from 'src/plugins/inspector/common'; -import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common'; +import { RequestStatistics } from 'src/plugins/inspector/common'; import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; import { SavedObject } from 'src/core/server'; diff --git a/src/plugins/data/public/search/aggs/buckets/terms.ts b/src/plugins/data/public/search/aggs/buckets/terms.ts index d3acd33d73d01..5c8483cf21369 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.ts @@ -129,7 +129,7 @@ export const getTermsBucketAgg = () => const response = await nestedSearchSource.fetch({ abortSignal }); request - .stats(getResponseInspectorStats(nestedSearchSource, response)) + .stats(getResponseInspectorStats(response, nestedSearchSource)) .ok({ json: response }); resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg()); } diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index b01f17762b2be..690f6b1df11c3 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -160,7 +160,7 @@ const handleCourierRequest = async ({ (searchSource as any).lastQuery = queryHash; - request.stats(getResponseInspectorStats(searchSource, response)).ok({ json: response }); + request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); (searchSource as any).rawResponse = response; } catch (e) { diff --git a/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts b/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts index 96d0aaa16f6ba..c933e8cd3e961 100644 --- a/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts +++ b/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts @@ -61,10 +61,11 @@ export function getRequestInspectorStats(searchSource: ISearchSource) { /** @public */ export function getResponseInspectorStats( - searchSource: ISearchSource, - resp: SearchResponse + resp: SearchResponse, + searchSource?: ISearchSource ) { - const lastRequest = searchSource.history && searchSource.history[searchSource.history.length - 1]; + const lastRequest = + searchSource?.history && searchSource.history[searchSource.history.length - 1]; const stats: RequestStatistics = {}; if (resp && resp.took) { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index c5d19fef9531e..99a77ff9aeb10 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -39,7 +39,6 @@ import { DeleteTemplateParams } from 'elasticsearch'; import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; import { ErrorToastOptions } from 'src/core/public/notifications'; -import { EventEmitter } from 'events'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; import { FieldStatsParams } from 'elasticsearch'; diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 9b8b32b51cfd8..c791bdd850151 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -874,7 +874,7 @@ function discoverController( } function onResults(resp) { - inspectorRequest.stats(getResponseInspectorStats($scope.searchSource, resp)).ok({ json: resp }); + inspectorRequest.stats(getResponseInspectorStats(resp, $scope.searchSource)).ok({ json: resp }); if (getTimeField()) { const tabifiedData = tabifyAggResponse($scope.vis.data.aggs, resp); diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 9a3dd0d310ff7..b621017677c58 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -307,7 +307,7 @@ export class SearchEmbeddable extends Embeddable this.updateOutput({ loading: false, error: undefined }); // Log response to inspector - inspectorRequest.stats(getResponseInspectorStats(searchSource, resp)).ok({ json: resp }); + inspectorRequest.stats(getResponseInspectorStats(resp, searchSource)).ok({ json: resp }); // Apply the changes to the angular scope this.searchScope.$apply(() => { diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index f42ee18965309..3533500a2fbc5 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -23,7 +23,7 @@ import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; import { Defer, now } from '../../../kibana_utils/common'; import { toPromise } from '../../../data/common/utils/abort_utils'; -import { RequestAdapter, DataAdapter } from '../../../inspector/common'; +import { RequestAdapter, DataAdapter, Adapters } from '../../../inspector/common'; import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { ExpressionAstExpression, @@ -70,7 +70,7 @@ export class Execution< ExtraContext extends Record = Record, Input = unknown, Output = unknown, - InspectorAdapters = ExtraContext['inspectorAdapters'] extends object + InspectorAdapters extends Adapters = ExtraContext['inspectorAdapters'] extends object ? ExtraContext['inspectorAdapters'] : DefaultInspectorAdapters > { diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index 51538394cd125..7c26e586fb790 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -18,7 +18,7 @@ */ import { ExpressionType } from '../expression_types'; -import { DataAdapter, RequestAdapter } from '../../../inspector/common'; +import { Adapters, DataAdapter, RequestAdapter } from '../../../inspector/common'; import { TimeRange, Query, Filter } from '../../../data/common'; import { SavedObject, SavedObjectAttributes } from '../../../../core/public'; @@ -26,7 +26,7 @@ import { SavedObject, SavedObjectAttributes } from '../../../../core/public'; * `ExecutionContext` is an object available to all functions during a single execution; * it provides various methods to perform side-effects. */ -export interface ExecutionContext { +export interface ExecutionContext { /** * Get initial input with which execution started. */ @@ -75,7 +75,7 @@ export interface ExecutionContext { /** * Adapters used to open the inspector. */ - adapters: Adapters; + adapters: TAdapters; /** * The title that the inspector is currently using e.g. a visualization name. */ diff --git a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap index 0e9560cbd7962..9ed4e60cac519 100644 --- a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap +++ b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap @@ -306,9 +306,11 @@ exports[`InspectorPanel should render as expected 1`] = ` - +
div { + display: flex; + flex-direction: column; + + > div { + flex-grow: 1; + } + } +} diff --git a/src/plugins/inspector/public/ui/inspector_panel.test.tsx b/src/plugins/inspector/public/ui/inspector_panel.test.tsx index c482b6fa8033b..23f698c23793b 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.test.tsx +++ b/src/plugins/inspector/public/ui/inspector_panel.test.tsx @@ -20,7 +20,8 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { InspectorPanel } from './inspector_panel'; -import { Adapters, InspectorViewDescription } from '../types'; +import { InspectorViewDescription } from '../types'; +import { Adapters } from '../../common'; describe('InspectorPanel', () => { let adapters: Adapters; diff --git a/src/plugins/inspector/public/ui/inspector_panel.tsx b/src/plugins/inspector/public/ui/inspector_panel.tsx index 85705b6b74f55..37a51257112d6 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.tsx +++ b/src/plugins/inspector/public/ui/inspector_panel.tsx @@ -17,11 +17,13 @@ * under the License. */ +import './inspector_panel.scss'; import { i18n } from '@kbn/i18n'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; -import { Adapters, InspectorViewDescription } from '../types'; +import { InspectorViewDescription } from '../types'; +import { Adapters } from '../../common'; import { InspectorViewChooser } from './inspector_view_chooser'; function hasAdaptersChanged(oldAdapters: Adapters, newAdapters: Adapters) { @@ -122,7 +124,9 @@ export class InspectorPanel extends Component - {this.renderSelectedPanel()} + + {this.renderSelectedPanel()} + ); } diff --git a/src/plugins/inspector/public/view_registry.test.ts b/src/plugins/inspector/public/view_registry.test.ts index 542328d4f48da..13e109f50243c 100644 --- a/src/plugins/inspector/public/view_registry.test.ts +++ b/src/plugins/inspector/public/view_registry.test.ts @@ -20,7 +20,7 @@ import { InspectorViewRegistry } from './view_registry'; import { InspectorViewDescription } from './types'; -import { Adapters } from './types'; +import { Adapters } from '../common'; function createMockView( params: { diff --git a/src/plugins/inspector/public/view_registry.ts b/src/plugins/inspector/public/view_registry.ts index 800d917af28ca..be84a62a11712 100644 --- a/src/plugins/inspector/public/view_registry.ts +++ b/src/plugins/inspector/public/view_registry.ts @@ -18,7 +18,8 @@ */ import { EventEmitter } from 'events'; -import { Adapters, InspectorViewDescription } from './types'; +import { InspectorViewDescription } from './types'; +import { Adapters } from '../common'; /** * @callback viewShouldShowFunc diff --git a/src/plugins/inspector/public/views/data/components/data_view.tsx b/src/plugins/inspector/public/views/data/components/data_view.tsx index e03c165d96a27..1a2b6f9922d2d 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.tsx +++ b/src/plugins/inspector/public/views/data/components/data_view.tsx @@ -30,7 +30,8 @@ import { } from '@elastic/eui'; import { DataTableFormat } from './data_table'; -import { InspectorViewProps, Adapters } from '../../../types'; +import { InspectorViewProps } from '../../../types'; +import { Adapters } from '../../../../common'; import { TabularLoaderOptions, TabularData, diff --git a/src/plugins/inspector/public/views/data/index.tsx b/src/plugins/inspector/public/views/data/index.tsx index 0cd88442bf8f8..b02e02bbe6b6b 100644 --- a/src/plugins/inspector/public/views/data/index.tsx +++ b/src/plugins/inspector/public/views/data/index.tsx @@ -20,7 +20,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { DataViewComponent } from './components/data_view'; -import { Adapters, InspectorViewDescription, InspectorViewProps } from '../../types'; +import { InspectorViewDescription, InspectorViewProps } from '../../types'; +import { Adapters } from '../../../common'; import { IUiSettingsClient } from '../../../../../core/public'; export const getDataViewDescription = ( diff --git a/src/plugins/inspector/public/views/requests/index.ts b/src/plugins/inspector/public/views/requests/index.ts index 741da76872710..00a223e1e30fa 100644 --- a/src/plugins/inspector/public/views/requests/index.ts +++ b/src/plugins/inspector/public/views/requests/index.ts @@ -19,7 +19,8 @@ import { i18n } from '@kbn/i18n'; import { RequestsViewComponent } from './components/requests_view'; -import { Adapters, InspectorViewDescription } from '../../types'; +import { InspectorViewDescription } from '../../types'; +import { Adapters } from '../../../common'; export const getRequestsViewDescription = (): InspectorViewDescription => ({ title: i18n.translate('inspector.requests.requestsTitle', { diff --git a/src/plugins/vis_type_markdown/public/markdown_vis.ts b/src/plugins/vis_type_markdown/public/markdown_vis.ts index 3309330d7527c..089e00bb44937 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/plugins/vis_type_markdown/public/markdown_vis.ts @@ -66,4 +66,5 @@ export const markdownVisDefinition = { }, requestHandler: 'none', responseHandler: 'none', + inspectorAdapters: {}, }; diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx index 52addb3c2d9d2..b4c90700b160f 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx @@ -62,6 +62,7 @@ export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) }, requestHandler: timelionRequestHandler, responseHandler: 'none', + inspectorAdapters: {}, options: { showIndexSelection: false, showQueryBar: false, diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 649ee765cc642..44b0334a37871 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -78,5 +78,6 @@ export const metricsVisDefinition = { showIndexSelection: false, }, requestHandler: metricsRequestHandler, + inspectorAdapters: {}, responseHandler: 'none', }; diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json index d7a92de627a99..7ba5f23f10564 100644 --- a/src/plugins/vis_type_vega/kibana.json +++ b/src/plugins/vis_type_vega/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions"], + "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions", "inspector"], "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/src/plugins/vis_type_vega/public/_vega_vis.scss b/src/plugins/vis_type_vega/public/_vega_vis.scss index 4fc6fbc326ec1..f9468d677eeed 100644 --- a/src/plugins/vis_type_vega/public/_vega_vis.scss +++ b/src/plugins/vis_type_vega/public/_vega_vis.scss @@ -17,6 +17,7 @@ // BUG #23514: Make sure Vega doesn't display the controls in two places .vega-bindings { + // sass-lint:disable no-important display: none !important; } } @@ -47,7 +48,7 @@ width: $euiSizeM * 10 - $euiSize; } - input[type="range"] { + input[type='range'] { width: $euiSizeM * 10; display: inline-block; vertical-align: middle; @@ -74,7 +75,7 @@ top: 0; width: 100%; margin: auto; - opacity: 0.8; + opacity: .8; z-index: 1; list-style: none; } @@ -115,25 +116,30 @@ @include euiTextTruncate; padding-top: $euiSizeXS; padding-bottom: $euiSizeXS; - } - td.key { - color: $euiColorMediumShade; - max-width: $euiSize * 10; - text-align: right; - padding-right: $euiSizeXS; - } - td.value { - max-width: $euiSizeL * 10; - text-align: left; - } + &.key { + color: $euiColorMediumShade; + max-width: $euiSize * 10; + text-align: right; + padding-right: $euiSizeXS; + } - @media only screen and (max-width: map-get($euiBreakpoints, 'm')){ - td.key { - max-width: $euiSize * 6; + &.value { + max-width: $euiSizeL * 10; + text-align: left; } - td.value { - max-width: $euiSize * 10; + } + + + @media only screen and (max-width: map-get($euiBreakpoints, 'm')) { + td { + &.key { + max-width: $euiSize * 6; + } + + &.value { + max-width: $euiSize * 10; + } } } } diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.ts b/src/plugins/vis_type_vega/public/data_model/search_api.ts index c2eecf13c2d51..18387a6ab0876 100644 --- a/src/plugins/vis_type_vega/public/data_model/search_api.ts +++ b/src/plugins/vis_type_vega/public/data_model/search_api.ts @@ -18,13 +18,17 @@ */ import { combineLatest } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { CoreStart, IUiSettingsClient } from 'kibana/public'; import { getSearchParamsFromRequest, SearchRequest, DataPublicPluginStart, + IEsSearchResponse, } from '../../../data/public'; +import { search as dataPluginSearch } from '../../../data/public'; +import { VegaInspectorAdapters } from '../vega_inspector'; +import { RequestResponder } from '../../../inspector/public'; export interface SearchAPIDependencies { uiSettings: IUiSettingsClient; @@ -35,26 +39,52 @@ export interface SearchAPIDependencies { export class SearchAPI { constructor( private readonly dependencies: SearchAPIDependencies, - private readonly abortSignal?: AbortSignal + private readonly abortSignal?: AbortSignal, + public readonly inspectorAdapters?: VegaInspectorAdapters ) {} search(searchRequests: SearchRequest[]) { const { search } = this.dependencies.search; + const requestResponders: any = {}; return combineLatest( searchRequests.map((request, index) => { + const requestId: number = index; const params = getSearchParamsFromRequest(request, { uiSettings: this.dependencies.uiSettings, injectedMetadata: this.dependencies.injectedMetadata, }); + if (this.inspectorAdapters) { + requestResponders[requestId] = this.inspectorAdapters.requests.start( + `#${requestId}`, + request + ); + requestResponders[requestId].json(params.body); + } + return search({ params }, { signal: this.abortSignal }).pipe( + tap((data) => this.inspectSearchResult(data, requestResponders[requestId])), map((data) => ({ - id: index, + id: requestId, rawResponse: data.rawResponse, })) ); }) ); } + + public resetSearchStats() { + if (this.inspectorAdapters) { + this.inspectorAdapters.requests.reset(); + } + } + + private inspectSearchResult(response: IEsSearchResponse, requestResponder: RequestResponder) { + if (requestResponder) { + requestResponder + .stats(dataPluginSearch.getResponseInspectorStats(response.rawResponse)) + .ok({ json: response.rawResponse }); + } + } } diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index 51aa4313a97b5..e29e16e3212f4 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -97,6 +97,7 @@ describe('VegaParser._resolveEsQueries', () => { search: jest.fn(() => ({ toPromise: jest.fn(() => Promise.resolve(data)), })), + resetSearchStats: jest.fn(), }; }); diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 17166e1540755..c867523d2b3b3 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -79,6 +79,7 @@ export class VegaParser { paddingHeight?: number; containerDir?: ControlsLocation | ControlsDirection; controlsDir?: ControlsLocation; + searchAPI: SearchAPI; constructor( spec: VegaSpec | string, @@ -92,10 +93,11 @@ export class VegaParser { this.error = undefined; this.warnings = []; + this.searchAPI = searchAPI; const onWarn = this._onWarning.bind(this); this._urlParsers = { - elasticsearch: new EsQueryParser(timeCache, searchAPI, filters, onWarn), + elasticsearch: new EsQueryParser(timeCache, this.searchAPI, filters, onWarn), emsfile: new EmsFileParser(serviceSettings), url: new UrlParser(onWarn), }; @@ -541,6 +543,8 @@ export class VegaParser { async _resolveDataUrls() { const pending: PendingType = {}; + this.searchAPI.resetSearchStats(); + this._findObjectDataUrls(this.spec!, (obj: Data) => { const url = obj.url; delete obj.url; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index c20a104736291..00c6b2e3c8d5b 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -18,8 +18,10 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; -import { Plugin as DataPublicPlugin } from '../../data/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; import { VisualizationsSetup } from '../../visualizations/public'; +import { Setup as InspectorSetup } from '../../inspector/public'; + import { setNotifications, setData, @@ -37,11 +39,13 @@ import { IServiceSettings } from '../../maps_legacy/public'; import './index.scss'; import { ConfigSchema } from '../config'; +import { getVegaInspectorView } from './vega_inspector'; + /** @internal */ export interface VegaVisualizationDependencies { core: CoreSetup; plugins: { - data: ReturnType; + data: DataPublicPluginSetup; }; serviceSettings: IServiceSettings; } @@ -50,13 +54,14 @@ export interface VegaVisualizationDependencies { export interface VegaPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; - data: ReturnType; + inspector: InspectorSetup; + data: DataPublicPluginSetup; mapsLegacy: any; } /** @internal */ export interface VegaPluginStartDependencies { - data: ReturnType; + data: DataPublicPluginStart; } /** @internal */ @@ -69,7 +74,7 @@ export class VegaPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies + { inspector, data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies ) { setInjectedVars({ enableExternalUrls: this.initializerContext.config.get().enableExternalUrls, @@ -88,6 +93,8 @@ export class VegaPlugin implements Plugin, void> { serviceSettings: mapsLegacy.serviceSettings, }; + inspector.registerView(getVegaInspectorView({ uiSettings: core.uiSettings })); + expressions.registerFunction(() => createVegaFn(visualizationDependencies)); visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies)); diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index d077aa7aee004..c109bb3c6e90c 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -19,9 +19,15 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public'; +import { + ExecutionContext, + ExpressionFunctionDefinition, + KibanaContext, + Render, +} from '../../expressions/public'; import { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; +import { VegaInspectorAdapters } from './vega_inspector/index'; import { TimeRange, Query } from '../../data/public'; import { VegaParser } from './data_model/vega_parser'; @@ -42,7 +48,13 @@ interface RenderValue { export const createVegaFn = ( dependencies: VegaVisualizationDependencies -): ExpressionFunctionDefinition<'vega', Input, Arguments, Output> => ({ +): ExpressionFunctionDefinition< + 'vega', + Input, + Arguments, + Output, + ExecutionContext +> => ({ name: 'vega', type: 'render', inputTypes: ['kibana_context', 'null'], @@ -57,7 +69,7 @@ export const createVegaFn = ( }, }, async fn(input, args, context) { - const vegaRequestHandler = createVegaRequestHandler(dependencies, context.abortSignal); + const vegaRequestHandler = createVegaRequestHandler(dependencies, context); const response = await vegaRequestHandler({ timeRange: get(input, 'timeRange') as TimeRange, diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx new file mode 100644 index 0000000000000..9b09a09eb05e0 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useState, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiComboBox, + EuiFlexGroup, + EuiComboBoxProps, + EuiFlexItem, + EuiSpacer, + CommonProps, +} from '@elastic/eui'; +import { VegaAdapter, InspectDataSets } from '../vega_adapter'; +import { InspectorDataGrid } from './inspector_data_grid'; + +interface DataViewerProps extends CommonProps { + vegaAdapter: VegaAdapter; +} + +const getDataGridArialabel = (view: InspectDataSets) => + i18n.translate('visTypeVega.inspector.dataViewer.gridAriaLabel', { + defaultMessage: '{name} data grid', + values: { + name: view.id, + }, + }); + +const dataSetAriaLabel = i18n.translate('visTypeVega.inspector.dataViewer.dataSetAriaLabel', { + defaultMessage: 'Data set', +}); + +export const DataViewer = ({ vegaAdapter, ...rest }: DataViewerProps) => { + const [inspectDataSets, setInspectDataSets] = useState([]); + const [selectedView, setSelectedView] = useState(); + const [dataGridAriaLabel, setDataGridAriaLabel] = useState(''); + + const onViewChange: EuiComboBoxProps['onChange'] = useCallback( + (selectedOptions) => { + const newView = inspectDataSets!.find((view) => view.id === selectedOptions[0].label); + + if (newView) { + setDataGridAriaLabel(getDataGridArialabel(newView)); + setSelectedView(newView); + } + }, + [inspectDataSets] + ); + + useEffect(() => { + const subscription = vegaAdapter.getDataSetsSubscription().subscribe((dataSets) => { + setInspectDataSets(dataSets); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [vegaAdapter]); + + useEffect(() => { + if (inspectDataSets) { + if (!selectedView) { + setSelectedView(inspectDataSets[0]); + } else { + setDataGridAriaLabel(getDataGridArialabel(selectedView)); + } + } + }, [selectedView, inspectDataSets]); + + if (!selectedView) { + return null; + } + + return ( + + + + ({ + label: item.id, + }))} + aria-label={dataSetAriaLabel} + onChange={onViewChange} + isClearable={false} + singleSelection={{ asPlainText: true }} + selectedOptions={[{ label: selectedView.id }]} + /> + + + + + + ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/index.ts b/src/plugins/vis_type_vega/public/vega_inspector/components/index.ts new file mode 100644 index 0000000000000..76e631f9ecd94 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { DataViewer } from './data_viewer'; +export { SignalViewer } from './signal_viewer'; +export { SpecViewer } from './spec_viewer'; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx new file mode 100644 index 0000000000000..00f24e03d8196 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx @@ -0,0 +1,144 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useState, useCallback, useMemo, useEffect } from 'react'; +import { EuiDataGrid, EuiDataGridSorting, EuiDataGridProps } from '@elastic/eui'; +import { VegaRuntimeData } from '../vega_adapter'; + +const DEFAULT_PAGE_SIZE = 15; + +interface InspectorDataGridProps extends VegaRuntimeData { + dataGridAriaLabel: string; +} + +export const InspectorDataGrid = ({ columns, data, dataGridAriaLabel }: InspectorDataGridProps) => { + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: DEFAULT_PAGE_SIZE }); + const onChangeItemsPerPage = useCallback( + (pageSize) => setPagination((p) => ({ ...p, pageSize, pageIndex: 0 })), + [setPagination] + ); + + const onChangePage = useCallback((pageIndex) => setPagination((p) => ({ ...p, pageIndex })), [ + setPagination, + ]); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState([]); + + useEffect( + () => { + setPagination({ + ...pagination, + pageIndex: 0, + }); + setVisibleColumns(columns.map((column) => column.id)); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [dataGridAriaLabel] + ); + + // Sorting + const [sortingColumns, setSortingColumns] = useState([]); + + const onSort = useCallback( + (newSortingColumns: EuiDataGridSorting['columns']) => { + setSortingColumns(newSortingColumns); + }, + [setSortingColumns] + ); + + let gridData = useMemo(() => { + return [...data].sort((a, b) => { + for (let i = 0; i < sortingColumns.length; i++) { + const column = sortingColumns[i]; + const aValue = a[column.id]; + const bValue = b[column.id]; + + if (aValue < bValue) return column.direction === 'asc' ? -1 : 1; + if (aValue > bValue) return column.direction === 'asc' ? 1 : -1; + } + return 0; + }); + }, [data, sortingColumns]); + + const renderCellValue = useMemo(() => { + return (({ rowIndex, columnId }) => { + let adjustedRowIndex = rowIndex; + + // If we are doing the pagination (instead of leaving that to the grid) + // then the row index must be adjusted as `data` has already been pruned to the page size + adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + return gridData.hasOwnProperty(adjustedRowIndex) + ? gridData[adjustedRowIndex][columnId] || null + : null; + }) as EuiDataGridProps['renderCellValue']; + }, [gridData, pagination.pageIndex, pagination.pageSize]); + + // Pagination + gridData = useMemo(() => { + const rowStart = pagination.pageIndex * pagination.pageSize; + const rowEnd = Math.min(rowStart + pagination.pageSize, gridData.length); + return gridData.slice(rowStart, rowEnd); + }, [gridData, pagination]); + + // Resize + const [columnsWidth, setColumnsWidth] = useState>({}); + + const onColumnResize: EuiDataGridProps['onColumnResize'] = useCallback( + ({ columnId, width }) => { + setColumnsWidth({ + ...columnsWidth, + [columnId]: width, + }); + }, + [columnsWidth] + ); + + return ( + { + if (columnsWidth[column.id]) { + return { + ...column, + initialWidth: columnsWidth[column.id], + }; + } + return column; + })} + columnVisibility={{ + visibleColumns, + setVisibleColumns, + }} + rowCount={data.length} + renderCellValue={renderCellValue} + sorting={{ columns: sortingColumns, onSort }} + toolbarVisibility={{ + showFullScreenSelector: false, + }} + onColumnResize={onColumnResize} + pagination={{ + ...pagination, + pageSizeOptions: [DEFAULT_PAGE_SIZE, 25, 50], + onChangeItemsPerPage, + onChangePage, + }} + /> + ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/signal_viewer.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/signal_viewer.tsx new file mode 100644 index 0000000000000..39df004f327a4 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/signal_viewer.tsx @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useEffect, useState } from 'react'; + +import { EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { VegaAdapter, InspectSignalsSets } from '../vega_adapter'; +import { InspectorDataGrid } from './inspector_data_grid'; + +interface SignalViewerProps { + vegaAdapter: VegaAdapter; +} + +const initialSignalColumnWidth = 150; + +const signalDataGridAriaLabel = i18n.translate('visTypeVega.inspector.signalViewer.gridAriaLabel', { + defaultMessage: 'Signal values data grid', +}); + +export const SignalViewer = ({ vegaAdapter }: SignalViewerProps) => { + const [inspectSignalsSets, setInspectSignalsSets] = useState(); + + useEffect(() => { + const subscription = vegaAdapter.getSignalsSetsSubscription().subscribe((signalSets) => { + if (signalSets) { + setInspectSignalsSets(signalSets); + } + }); + return () => { + subscription.unsubscribe(); + }; + }, [vegaAdapter]); + + if (!inspectSignalsSets) { + return null; + } + + return ( +
+ + { + if (index === 0) { + return { + ...column, + initialWidth: initialSignalColumnWidth, + }; + } + return column; + })} + data={inspectSignalsSets.data} + dataGridAriaLabel={signalDataGridAriaLabel} + /> +
+ ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx new file mode 100644 index 0000000000000..54f7974960aa2 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx @@ -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. + */ +import React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlexItem, + EuiFlexGroup, + EuiCopy, + EuiButtonEmpty, + EuiSpacer, + CommonProps, +} from '@elastic/eui'; +import { VegaAdapter } from '../vega_adapter'; +import { CodeEditor } from '../../../../kibana_react/public'; + +interface SpecViewerProps extends CommonProps { + vegaAdapter: VegaAdapter; +} + +const copyToClipboardLabel = i18n.translate( + 'visTypeVega.inspector.specViewer.copyToClipboardLabel', + { + defaultMessage: 'Copy to clipboard', + } +); + +export const SpecViewer = ({ vegaAdapter, ...rest }: SpecViewerProps) => { + const [spec, setSpec] = useState(); + + useEffect(() => { + const subscription = vegaAdapter.getSpecSubscription().subscribe((data) => { + if (data) { + setSpec(data); + } + }); + return () => { + subscription.unsubscribe(); + }; + }, [vegaAdapter]); + + if (!spec) { + return null; + } + + return ( + + + +
+ + {(copy) => ( + + {copyToClipboardLabel} + + )} + +
+
+ + {}} + options={{ + readOnly: true, + lineNumbers: 'off', + fontSize: 12, + minimap: { + enabled: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + automaticLayout: true, + }} + /> + +
+ ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/index.ts b/src/plugins/vis_type_vega/public/vega_inspector/index.ts new file mode 100644 index 0000000000000..24da27d2d742d --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + createInspectorAdapters, + getVegaInspectorView, + VegaInspectorAdapters, +} from './vega_inspector'; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_adapter.ts b/src/plugins/vis_type_vega/public/vega_inspector/vega_adapter.ts new file mode 100644 index 0000000000000..e4c536af40591 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_adapter.ts @@ -0,0 +1,148 @@ +/* + * 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, ReplaySubject, fromEventPattern, merge, timer } from 'rxjs'; +import { map, switchMap, filter, debounce } from 'rxjs/operators'; +import { View, Runtime, Spec } from 'vega'; +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; + +interface DebugValues { + view: View; + spec: Spec; +} + +export interface VegaRuntimeData { + columns: Array<{ + id: string; + }>; + data: Array>; +} + +export type InspectDataSets = Assign; +export type InspectSignalsSets = VegaRuntimeData; + +const vegaAdapterSignalLabel = i18n.translate('visTypeVega.inspector.vegaAdapter.signal', { + defaultMessage: 'Signal', +}); + +const vegaAdapterValueLabel = i18n.translate('visTypeVega.inspector.vegaAdapter.value', { + defaultMessage: 'Value', +}); + +/** Get Runtime Scope for Vega View + * @link https://vega.github.io/vega/docs/api/debugging/#scope + **/ +const getVegaRuntimeScope = (debugValues: DebugValues) => + (debugValues.view as any)._runtime as Runtime; + +const serializeColumns = (item: Record, columns: string[]) => { + const nonSerializableFieldLabel = '(..)'; + + return columns.reduce((row: Record, column) => { + try { + const cell = item[column]; + row[column] = typeof cell === 'object' ? JSON.stringify(cell) : `${cell}`; + } catch (e) { + row[column] = nonSerializableFieldLabel; + } + return row; + }, {}); +}; + +export class VegaAdapter { + private debugValuesSubject = new ReplaySubject(); + + bindInspectValues(debugValues: DebugValues) { + this.debugValuesSubject.next(debugValues); + } + + getDataSetsSubscription(): Observable { + return this.debugValuesSubject.pipe( + filter((debugValues) => Boolean(debugValues)), + map((debugValues) => { + const runtimeScope = getVegaRuntimeScope(debugValues); + + return Object.keys(runtimeScope.data || []).reduce((acc: InspectDataSets[], key) => { + const value = runtimeScope.data[key].values.value; + + if (value && value[0]) { + const columns = Object.keys(value[0]); + acc.push({ + id: key, + columns: columns.map((column) => ({ id: column, schema: 'json' })), + data: value.map((item: Record) => serializeColumns(item, columns)), + }); + } + return acc; + }, []); + }) + ); + } + + getSignalsSetsSubscription(): Observable { + const signalsListener = this.debugValuesSubject.pipe( + filter((debugValues) => Boolean(debugValues)), + switchMap((debugValues) => { + const runtimeScope = getVegaRuntimeScope(debugValues); + + return merge( + ...Object.keys(runtimeScope.signals).map((key: string) => + fromEventPattern( + (handler) => debugValues.view.addSignalListener(key, handler), + (handler) => debugValues.view.removeSignalListener(key, handler) + ) + ) + ).pipe( + debounce((val) => timer(350)), + map(() => debugValues) + ); + }) + ); + + return merge(this.debugValuesSubject, signalsListener).pipe( + filter((debugValues) => Boolean(debugValues)), + map((debugValues) => { + const runtimeScope = getVegaRuntimeScope(debugValues); + + return { + columns: [ + { id: vegaAdapterSignalLabel, schema: 'text' }, + { id: vegaAdapterValueLabel, schema: 'json' }, + ], + data: Object.keys(runtimeScope.signals).map((key: string) => + serializeColumns( + { + [vegaAdapterSignalLabel]: key, + [vegaAdapterValueLabel]: runtimeScope.signals[key].value, + }, + [vegaAdapterSignalLabel, vegaAdapterValueLabel] + ) + ), + }; + }) + ); + } + + getSpecSubscription(): Observable { + return this.debugValuesSubject.pipe( + filter((debugValues) => Boolean(debugValues)), + map((debugValues) => JSON.stringify(debugValues.spec, null, 2)) + ); + } +} diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.scss b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.scss new file mode 100644 index 0000000000000..487f505657d3b --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.scss @@ -0,0 +1,18 @@ +.vgaVegaDataInspector, +.vgaVegaDataInspector__specViewer { + height: 100%; +} + +.vgaVegaDataInspector { + // TODO: EUI needs to provide props to pass down from EuiTabbedContent to tabs and content + display: flex; + flex-direction: column; + + [role='tablist'] { + flex-shrink: 0; + } + + [role='tabpanel'] { + flex-grow: 1; + } +} diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx new file mode 100644 index 0000000000000..3b9427c96e62a --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx @@ -0,0 +1,74 @@ +/* + * 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 './vega_data_inspector.scss'; + +import React from 'react'; +import { EuiTabbedContent } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { VegaInspectorAdapters } from './vega_inspector'; +import { DataViewer, SignalViewer, SpecViewer } from './components'; +import { InspectorViewProps } from '../../../inspector/public'; + +export type VegaDataInspectorProps = InspectorViewProps; + +const dataSetsLabel = i18n.translate('visTypeVega.inspector.dataSetsLabel', { + defaultMessage: 'Data sets', +}); + +const signalValuesLabel = i18n.translate('visTypeVega.inspector.signalValuesLabel', { + defaultMessage: 'Signal values', +}); + +const specLabel = i18n.translate('visTypeVega.inspector.specLabel', { + defaultMessage: 'Spec', +}); + +export const VegaDataInspector = ({ adapters }: VegaDataInspectorProps) => { + const tabs = [ + { + id: 'data-viewer--id', + name: dataSetsLabel, + content: , + }, + { + id: 'signal-viewer--id', + name: signalValuesLabel, + content: , + }, + { + id: 'spec-viewer--id', + name: specLabel, + content: ( + + ), + }, + ]; + + return ( + + ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx b/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx new file mode 100644 index 0000000000000..83d9e467646a6 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { IUiSettingsClient } from 'kibana/public'; +import { VegaAdapter } from './vega_adapter'; +import { VegaDataInspector, VegaDataInspectorProps } from './vega_data_inspector'; +import { KibanaContextProvider } from '../../../kibana_react/public'; +import { Adapters, RequestAdapter, InspectorViewDescription } from '../../../inspector/public'; + +export interface VegaInspectorAdapters extends Adapters { + requests: RequestAdapter; + vega: VegaAdapter; +} + +const vegaDebugLabel = i18n.translate('visTypeVega.inspector.vegaDebugLabel', { + defaultMessage: 'Vega debug', +}); + +interface VegaInspectorViewDependencies { + uiSettings: IUiSettingsClient; +} + +export const getVegaInspectorView = (dependencies: VegaInspectorViewDependencies) => + ({ + title: vegaDebugLabel, + shouldShow(adapters) { + return Boolean(adapters.vega); + }, + component: (props) => ( + + + + ), + } as InspectorViewDescription); + +export const createInspectorAdapters = (): VegaInspectorAdapters => ({ + requests: new RequestAdapter(), + vega: new VegaAdapter(), +}); diff --git a/src/plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts index 997b1982d749a..c09a9466df602 100644 --- a/src/plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -25,6 +25,7 @@ import { TimeCache } from './data_model/time_cache'; import { VegaVisualizationDependencies } from './plugin'; import { VisParams } from './vega_fn'; import { getData, getInjectedMetadata } from './services'; +import { VegaInspectorAdapters } from './vega_inspector'; interface VegaRequestHandlerParams { query: Query; @@ -33,9 +34,14 @@ interface VegaRequestHandlerParams { visParams: VisParams; } +interface VegaRequestHandlerContext { + abortSignal?: AbortSignal; + inspectorAdapters?: VegaInspectorAdapters; +} + export function createVegaRequestHandler( { plugins: { data }, core: { uiSettings }, serviceSettings }: VegaVisualizationDependencies, - abortSignal?: AbortSignal + context: VegaRequestHandlerContext = {} ) { let searchAPI: SearchAPI; const { timefilter } = data.query.timefilter; @@ -54,7 +60,8 @@ export function createVegaRequestHandler( search: getData().search, injectedMetadata: getInjectedMetadata(), }, - abortSignal + context.abortSignal, + context.inspectorAdapters ); } diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 5825661f9001c..d69eb3cfba282 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -23,9 +23,10 @@ import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; import { createVegaRequestHandler } from './vega_request_handler'; -// @ts-ignore +// @ts-expect-error import { createVegaVisualization } from './vega_visualization'; import { getDefaultSpec } from './default_spec'; +import { createInspectorAdapters } from './vega_inspector'; export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependencies) => { const requestHandler = createVegaRequestHandler(dependencies); @@ -54,5 +55,6 @@ export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependen showFilterBar: true, }, stage: 'experimental', + inspectorAdapters: createInspectorAdapters, }; }; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 55c3606bf5e45..8f88d5c5b2056 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -364,6 +364,11 @@ export class VegaBaseView { * Set global debug variable to simplify vega debugging in console. Show info message first time */ setDebugValues(view, spec, vlspec) { + this._parser.searchAPI.inspectorAdapters?.vega.bindInspectValues({ + view, + spec: vlspec || spec, + }); + if (window) { if (window.VEGA_DEBUG === undefined && console) { console.log('%cWelcome to Kibana Vega Plugin!', 'font-size: 16px; font-weight: bold;'); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index 6908fd13a9ca1..78ae2efdbdda5 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -142,7 +142,7 @@ export class VegaMapView extends VegaBaseView { }); const vegaView = vegaMapLayer.getVegaView(); - this.setDebugValues(vegaView, this._parser.spec, this._parser.vlspec); await this.setView(vegaView); + this.setDebugValues(vegaView, this._parser.spec, this._parser.vlspec); } } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js index e3455b97b7fe2..98c972ef84ccb 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.js @@ -26,7 +26,6 @@ export class VegaView extends VegaBaseView { if (!this._$container) return; const view = new vega.View(vega.parse(this._parser.spec), this._vegaViewConfig); - this.setDebugValues(view, this._parser.spec, this._parser.vlspec); view.warn = this.onWarn.bind(this); view.error = this.onError.bind(this); @@ -36,5 +35,6 @@ export class VegaView extends VegaBaseView { if (this._parser.useHover) view.hover(); await this.setView(view); + this.setDebugValues(view, this._parser.spec, this._parser.vlspec); } } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 2f9cda32fccdc..749926e1abd00 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -34,6 +34,7 @@ import { EmbeddableOutput, Embeddable, IContainer, + Adapters, } from '../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../plugins/kibana_utils/public'; import { @@ -78,8 +79,6 @@ export interface VisualizeOutput extends EmbeddableOutput { type ExpressionLoader = InstanceType; -const visTypesWithoutInspector = ['markdown', 'input_control_vis', 'metrics', 'vega', 'timelion']; - export class VisualizeEmbeddable extends Embeddable { private handler?: ExpressionLoader; private timefilter: TimefilterContract; @@ -96,6 +95,7 @@ export class VisualizeEmbeddable extends Embeddable { - if (!this.handler || visTypesWithoutInspector.includes(this.vis.type.name)) { + if (!this.handler || (this.inspectorAdapters && !Object.keys(this.inspectorAdapters).length)) { return undefined; } return this.handler.inspect(); @@ -349,6 +356,7 @@ export class VisualizeEmbeddable extends Embeddable Adapters); } export class BaseVisType { @@ -63,6 +65,7 @@ export class BaseVisType { hierarchicalData: boolean | unknown; setup?: unknown; useCustomNoDataScreen: boolean; + inspectorAdapters?: Adapters | (() => Adapters); constructor(opts: BaseVisTypeOptions) { if (!opts.icon && !opts.image) { @@ -98,6 +101,7 @@ export class BaseVisType { this.requiresSearch = this.requestHandler !== 'none'; this.hierarchicalData = opts.hierarchicalData || false; this.useCustomNoDataScreen = opts.useCustomNoDataScreen || false; + this.inspectorAdapters = opts.inspectorAdapters; } public get schemas() { diff --git a/tasks/test.js b/tasks/test.js index 96ec4d91db325..09821b97fe2e8 100644 --- a/tasks/test.js +++ b/tasks/test.js @@ -48,7 +48,7 @@ module.exports = function (grunt) { grunt.task.run(['run:karmaTestServer', ...ciShardTasks]); }); - grunt.registerTask('test:coverage', ['run:testCoverageServer', 'karma:coverage']); + grunt.registerTask('test:coverage', ['run:karmaTestCoverageServer', 'karma:coverage']); grunt.registerTask('test:quick', [ 'checkPlugins', diff --git a/test/functional/apps/visualize/_vega_chart.js b/test/functional/apps/visualize/_vega_chart.js index 4442e1f969b4b..c530c6f823133 100644 --- a/test/functional/apps/visualize/_vega_chart.js +++ b/test/functional/apps/visualize/_vega_chart.js @@ -22,7 +22,6 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['timePicker', 'visualize', 'visChart', 'vegaChart']); const filterBar = getService('filterBar'); - const inspector = getService('inspector'); const log = getService('log'); describe('vega chart in visualize app', () => { @@ -35,10 +34,6 @@ export default function ({ getService, getPageObjects }) { describe('vega chart', () => { describe('initial render', () => { - it('should not have inspector enabled', async function () { - await inspector.expectIsNotEnabled(); - }); - it.skip('should have some initial vega spec text', async function () { const vegaSpec = await PageObjects.vegaChart.getSpec(); expect(vegaSpec).to.contain('{').and.to.contain('data'); diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index 53e128f94dfb6..89d578f27b118 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -80,7 +80,7 @@ export async function fetchSearchSourceAndRecordWithInspector({ inspectorRequest.json(body); }); resp = await searchSource.fetch({ abortSignal }); - inspectorRequest.stats(getResponseInspectorStats(searchSource, resp)).ok({ json: resp }); + inspectorRequest.stats(getResponseInspectorStats(resp, searchSource)).ok({ json: resp }); } catch (error) { inspectorRequest.error({ error }); throw error; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx index 24e82a8f95a6b..c64a1e6891a44 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx @@ -51,7 +51,7 @@ export const RISK_SCORE_DESCRIPTION = i18n.translate( export const RISK_SCORE_MAPPING_DESCRIPTION = i18n.translate( 'xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel', { - defaultMessage: 'Map a field from the source event (scaled 1-100) to risk score.', + defaultMessage: 'Use a source event value to override the default risk score.', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx index f0bfc5f4637ab..12653ec5806bb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx @@ -51,7 +51,7 @@ export const SEVERITY_DESCRIPTION = i18n.translate( export const SEVERITY_MAPPING_DESCRIPTION = i18n.translate( 'xpack.securitySolution.alerts.severityMapping.mappingDescriptionLabel', { - defaultMessage: 'Map a value from the source event to a specific severity.', + defaultMessage: 'Use source event values to override the default severity.', } );