From daa7cc92f46536be8f77e40c51c82c06014f895d Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 16 Nov 2020 11:54:19 +0100 Subject: [PATCH 01/69] [Uptime] Monitor status alert use url as instance (#81736) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/alert_instances.tsx | 14 +++++- .../lib/alerts/__tests__/status_check.test.ts | 8 ++-- .../uptime/server/lib/alerts/status_check.ts | 45 +++++++++++++++---- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index ed05d81646c4a..e0c4c663bc231 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -7,7 +7,7 @@ import React, { Fragment, useState } from 'react'; import moment, { Duration } from 'moment'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable, EuiHealth, EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { EuiBasicTable, EuiHealth, EuiSpacer, EuiSwitch, EuiToolTip } from '@elastic/eui'; // @ts-ignore import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services'; import { padStart, chunk } from 'lodash'; @@ -47,6 +47,13 @@ export const alertInstancesTableColumns = ( sortable: false, truncateText: true, 'data-test-subj': 'alertInstancesTableCell-instance', + render: (value: string) => { + return ( + + {value} + + ); + }, }, { field: 'status', @@ -54,6 +61,7 @@ export const alertInstancesTableColumns = ( 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.status', { defaultMessage: 'Status' } ), + width: '100px', render: (value: AlertInstanceListItemStatus, instance: AlertInstanceListItem) => { return ( @@ -67,6 +75,7 @@ export const alertInstancesTableColumns = ( }, { field: 'start', + width: '200px', render: (value: Date | undefined, instance: AlertInstanceListItem) => { return value ? moment(value).format('D MMM YYYY @ HH:mm:ss') : ''; }, @@ -88,11 +97,13 @@ export const alertInstancesTableColumns = ( { defaultMessage: 'Duration' } ), sortable: false, + width: '100px', 'data-test-subj': 'alertInstancesTableCell-duration', }, { field: '', align: RIGHT_ALIGNMENT, + width: '60px', name: i18n.translate( 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.mute', { defaultMessage: 'Mute' } @@ -180,6 +191,7 @@ export function AlertInstances({ })} columns={alertInstancesTableColumns(onMuteAction, readOnly)} data-test-subj="alertInstancesList" + tableLayout="fixed" /> ); diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index ccb1e5a40ad2d..4f795e2aaf29e 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -1205,10 +1205,10 @@ describe('status check alert', () => { it('creates a set of unique IDs from a list of composite unique objects', () => { expect(getUniqueIdsByLoc(downItems, availItems)).toEqual( new Set([ - 'firstharrisburg', - 'firstfairbanks', - 'secondharrisburg', - 'secondfairbanks', + 'first-harrisburg', + 'first-fairbanks', + 'second-harrisburg', + 'second-fairbanks', ]) ); }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index b1b3666b40dc6..577262c231977 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -31,11 +31,21 @@ import { UMServerLibs } from '../lib'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; +const getMonIdByLoc = (monitorId: string, location: string) => { + return monitorId + '-' + location; +}; + const uniqueDownMonitorIds = (items: GetMonitorStatusResult[]): Set => - items.reduce((acc, { monitorId, location }) => acc.add(monitorId + location), new Set()); + items.reduce( + (acc, { monitorId, location }) => acc.add(getMonIdByLoc(monitorId, location)), + new Set() + ); const uniqueAvailMonitorIds = (items: GetMonitorAvailabilityResult[]): Set => - items.reduce((acc, { monitorId, location }) => acc.add(monitorId + location), new Set()); + items.reduce( + (acc, { monitorId, location }) => acc.add(getMonIdByLoc(monitorId, location)), + new Set() + ); export const getUniqueIdsByLoc = ( downMonitorsByLocation: GetMonitorStatusResult[], @@ -157,6 +167,21 @@ export const getStatusMessage = ( return statusMessage + availabilityMessage; }; +const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => { + const normalizeText = (txt: string) => { + // replace url and name special characters with - + return txt.replace(/[^A-Z0-9]+/gi, '_').toLowerCase(); + }; + const urlText = normalizeText(monitorInfo.url?.full || ''); + + const monName = normalizeText(monitorInfo.monitor.name || ''); + + if (monName) { + return `${monName}_${urlText}_${monIdByLoc}`; + } + return `${urlText}_${monIdByLoc}`; +}; + export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => uptimeAlertWrapper({ id: 'xpack.uptime.alerts.monitorStatus', @@ -290,7 +315,9 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = for (const monitorLoc of downMonitorsByLocation) { const monitorInfo = monitorLoc.monitorInfo; - const alertInstance = alertInstanceFactory(MONITOR_STATUS.id + monitorLoc.location); + const alertInstance = alertInstanceFactory( + getInstanceId(monitorInfo, monitorLoc.location) + ); const monitorSummary = getMonitorSummary(monitorInfo); const statusMessage = getStatusMessage(monitorInfo); @@ -320,19 +347,21 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = const mergedIdsByLoc = getUniqueIdsByLoc(downMonitorsByLocation, availabilityResults); mergedIdsByLoc.forEach((monIdByLoc) => { - const alertInstance = alertInstanceFactory(MONITOR_STATUS.id + monIdByLoc); - const availMonInfo = availabilityResults.find( - ({ monitorId, location }) => monitorId + location === monIdByLoc + ({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc ); const downMonInfo = downMonitorsByLocation.find( - ({ monitorId, location }) => monitorId + location === monIdByLoc + ({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc )?.monitorInfo; - const monitorSummary = getMonitorSummary(downMonInfo || availMonInfo?.monitorInfo!); + const monitorInfo = downMonInfo || availMonInfo?.monitorInfo!; + + const monitorSummary = getMonitorSummary(monitorInfo); const statusMessage = getStatusMessage(downMonInfo!, availMonInfo!, availability); + const alertInstance = alertInstanceFactory(getInstanceId(monitorInfo, monIdByLoc)); + alertInstance.replaceState({ ...updateState(state, true), ...monitorSummary, From 66def097ad56df89470455963c4c9285dfd5da8d Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 16 Nov 2020 11:54:57 +0100 Subject: [PATCH 02/69] [APM] Ensure APM jest script can run (#83398) --- x-pack/plugins/apm/jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index 5be8ad141ffd0..ffd3a39e8afd1 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -29,7 +29,7 @@ module.exports = { roots: [`${rootDir}/common`, `${rootDir}/public`, `${rootDir}/server`], collectCoverage: true, collectCoverageFrom: [ - ...jestConfig.collectCoverageFrom, + ...(jestConfig.collectCoverageFrom ?? []), '**/*.{js,mjs,jsx,ts,tsx}', '!**/*.stories.{js,mjs,ts,tsx}', '!**/dev_docs/**', From d1abc866d403d177531969fca1de6706ff7ac5bb Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 16 Nov 2020 12:15:35 +0100 Subject: [PATCH 03/69] Migrate `/translations` route to core (#83280) * move i18n route to core * add FTR test for endpoint --- .../server/i18n/i18n_service.test.mocks.ts | 5 ++ src/core/server/i18n/i18n_service.test.ts | 26 +++++-- src/core/server/i18n/i18n_service.ts | 8 ++- src/core/server/i18n/routes/index.ts | 25 +++++++ src/core/server/i18n/routes/translations.ts | 69 +++++++++++++++++++ src/core/server/server.ts | 6 +- src/legacy/ui/ui_render/ui_render_mixin.js | 32 --------- test/api_integration/apis/core/compression.ts | 55 +++++++++++++++ test/api_integration/apis/core/index.js | 56 --------------- test/api_integration/apis/core/index.ts | 27 ++++++++ .../api_integration/apis/core/translations.ts | 42 +++++++++++ 11 files changed, 255 insertions(+), 96 deletions(-) create mode 100644 src/core/server/i18n/routes/index.ts create mode 100644 src/core/server/i18n/routes/translations.ts create mode 100644 test/api_integration/apis/core/compression.ts delete mode 100644 test/api_integration/apis/core/index.js create mode 100644 test/api_integration/apis/core/index.ts create mode 100644 test/api_integration/apis/core/translations.ts diff --git a/src/core/server/i18n/i18n_service.test.mocks.ts b/src/core/server/i18n/i18n_service.test.mocks.ts index 23f97a1404fff..d35141ecb111f 100644 --- a/src/core/server/i18n/i18n_service.test.mocks.ts +++ b/src/core/server/i18n/i18n_service.test.mocks.ts @@ -26,3 +26,8 @@ export const initTranslationsMock = jest.fn(); jest.doMock('./init_translations', () => ({ initTranslations: initTranslationsMock, })); + +export const registerRoutesMock = jest.fn(); +jest.doMock('./routes', () => ({ + registerRoutes: registerRoutesMock, +})); diff --git a/src/core/server/i18n/i18n_service.test.ts b/src/core/server/i18n/i18n_service.test.ts index 87de39a92ab26..e9deb96ccf88b 100644 --- a/src/core/server/i18n/i18n_service.test.ts +++ b/src/core/server/i18n/i18n_service.test.ts @@ -17,13 +17,18 @@ * under the License. */ -import { getKibanaTranslationFilesMock, initTranslationsMock } from './i18n_service.test.mocks'; +import { + getKibanaTranslationFilesMock, + initTranslationsMock, + registerRoutesMock, +} from './i18n_service.test.mocks'; import { BehaviorSubject } from 'rxjs'; import { I18nService } from './i18n_service'; import { configServiceMock } from '../config/mocks'; import { mockCoreContext } from '../core_context.mock'; +import { httpServiceMock } from '../http/http_service.mock'; const getConfigService = (locale = 'en') => { const configService = configServiceMock.create(); @@ -41,6 +46,7 @@ const getConfigService = (locale = 'en') => { describe('I18nService', () => { let service: I18nService; let configService: ReturnType; + let http: ReturnType; beforeEach(() => { jest.clearAllMocks(); @@ -48,6 +54,8 @@ describe('I18nService', () => { const coreContext = mockCoreContext.create({ configService }); service = new I18nService(coreContext); + + http = httpServiceMock.createInternalSetupContract(); }); describe('#setup', () => { @@ -55,7 +63,7 @@ describe('I18nService', () => { getKibanaTranslationFilesMock.mockResolvedValue([]); const pluginPaths = ['/pathA', '/pathB']; - await service.setup({ pluginPaths }); + await service.setup({ pluginPaths, http }); expect(getKibanaTranslationFilesMock).toHaveBeenCalledTimes(1); expect(getKibanaTranslationFilesMock).toHaveBeenCalledWith('en', pluginPaths); @@ -65,17 +73,27 @@ describe('I18nService', () => { const translationFiles = ['/path/to/file', 'path/to/another/file']; getKibanaTranslationFilesMock.mockResolvedValue(translationFiles); - await service.setup({ pluginPaths: [] }); + await service.setup({ pluginPaths: [], http }); expect(initTranslationsMock).toHaveBeenCalledTimes(1); expect(initTranslationsMock).toHaveBeenCalledWith('en', translationFiles); }); + it('calls `registerRoutesMock` with the correct parameters', async () => { + await service.setup({ pluginPaths: [], http }); + + expect(registerRoutesMock).toHaveBeenCalledTimes(1); + expect(registerRoutesMock).toHaveBeenCalledWith({ + locale: 'en', + router: expect.any(Object), + }); + }); + it('returns accessors for locale and translation files', async () => { const translationFiles = ['/path/to/file', 'path/to/another/file']; getKibanaTranslationFilesMock.mockResolvedValue(translationFiles); - const { getLocale, getTranslationFiles } = await service.setup({ pluginPaths: [] }); + const { getLocale, getTranslationFiles } = await service.setup({ pluginPaths: [], http }); expect(getLocale()).toEqual('en'); expect(getTranslationFiles()).toEqual(translationFiles); diff --git a/src/core/server/i18n/i18n_service.ts b/src/core/server/i18n/i18n_service.ts index fd32dd7fdd6ef..4a609ca5e2aea 100644 --- a/src/core/server/i18n/i18n_service.ts +++ b/src/core/server/i18n/i18n_service.ts @@ -21,11 +21,14 @@ import { take } from 'rxjs/operators'; import { Logger } from '../logging'; import { IConfigService } from '../config'; import { CoreContext } from '../core_context'; +import { InternalHttpServiceSetup } from '../http'; import { config as i18nConfigDef, I18nConfigType } from './i18n_config'; import { getKibanaTranslationFiles } from './get_kibana_translation_files'; import { initTranslations } from './init_translations'; +import { registerRoutes } from './routes'; interface SetupDeps { + http: InternalHttpServiceSetup; pluginPaths: string[]; } @@ -53,7 +56,7 @@ export class I18nService { this.configService = coreContext.configService; } - public async setup({ pluginPaths }: SetupDeps): Promise { + public async setup({ pluginPaths, http }: SetupDeps): Promise { const i18nConfig = await this.configService .atPath(i18nConfigDef.path) .pipe(take(1)) @@ -67,6 +70,9 @@ export class I18nService { this.log.debug(`Using translation files: [${translationFiles.join(', ')}]`); await initTranslations(locale, translationFiles); + const router = http.createRouter(''); + registerRoutes({ router, locale }); + return { getLocale: () => locale, getTranslationFiles: () => translationFiles, diff --git a/src/core/server/i18n/routes/index.ts b/src/core/server/i18n/routes/index.ts new file mode 100644 index 0000000000000..b0cce67b0aa4d --- /dev/null +++ b/src/core/server/i18n/routes/index.ts @@ -0,0 +1,25 @@ +/* + * 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 } from '../../http'; +import { registerTranslationsRoute } from './translations'; + +export const registerRoutes = ({ router, locale }: { router: IRouter; locale: string }) => { + registerTranslationsRoute(router, locale); +}; diff --git a/src/core/server/i18n/routes/translations.ts b/src/core/server/i18n/routes/translations.ts new file mode 100644 index 0000000000000..c5cc9525d54aa --- /dev/null +++ b/src/core/server/i18n/routes/translations.ts @@ -0,0 +1,69 @@ +/* + * 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 { createHash } from 'crypto'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { IRouter } from '../../http'; + +interface TranslationCache { + translations: string; + hash: string; +} + +export const registerTranslationsRoute = (router: IRouter, locale: string) => { + let translationCache: TranslationCache; + + router.get( + { + path: '/translations/{locale}.json', + validate: { + params: schema.object({ + locale: schema.string(), + }), + }, + options: { + authRequired: false, + }, + }, + (ctx, req, res) => { + if (req.params.locale.toLowerCase() !== locale.toLowerCase()) { + return res.notFound({ + body: `Unknown locale: ${req.params.locale}`, + }); + } + if (!translationCache) { + const translations = JSON.stringify(i18n.getTranslation()); + const hash = createHash('sha1').update(translations).digest('hex'); + translationCache = { + translations, + hash, + }; + } + return res.ok({ + headers: { + 'content-type': 'application/json', + 'cache-control': 'must-revalidate', + etag: translationCache.hash, + }, + body: translationCache.translations, + }); + } + ); +}; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 55ed88e55a9f5..0f7e8cced999c 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -131,9 +131,6 @@ export class Server { await ensureValidConfiguration(this.configService, legacyConfigSetup); } - // setup i18n prior to any other service, to have translations ready - const i18nServiceSetup = await this.i18n.setup({ pluginPaths }); - const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: // 1) Can access context from any KP plugin @@ -149,6 +146,9 @@ export class Server { context: contextServiceSetup, }); + // setup i18n prior to any other service, to have translations ready + const i18nServiceSetup = await this.i18n.setup({ http: httpSetup, pluginPaths }); + const capabilitiesSetup = this.capabilities.setup({ http: httpSetup }); const elasticsearchServiceSetup = await this.elasticsearch.setup({ diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index a02c2fca14c18..b8e80300957ba 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -17,9 +17,7 @@ * under the License. */ -import { createHash } from 'crypto'; import Boom from '@hapi/boom'; -import { i18n } from '@kbn/i18n'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { KibanaRequest } from '../../../core/server'; import { AppBootstrap } from './bootstrap'; @@ -37,36 +35,6 @@ import { getApmConfig } from '../apm'; * @param {KbnServer['config']} config */ export function uiRenderMixin(kbnServer, server, config) { - const translationsCache = { translations: null, hash: null }; - server.route({ - path: '/translations/{locale}.json', - method: 'GET', - config: { auth: false }, - handler(request, h) { - // Kibana server loads translations only for a single locale - // that is specified in `i18n.locale` config value. - const { locale } = request.params; - if (i18n.getLocale() !== locale.toLowerCase()) { - throw Boom.notFound(`Unknown locale: ${locale}`); - } - - // Stringifying thousands of labels and calculating hash on the resulting - // string can be expensive so it makes sense to do it once and cache. - if (translationsCache.translations == null) { - translationsCache.translations = JSON.stringify(i18n.getTranslation()); - translationsCache.hash = createHash('sha1') - .update(translationsCache.translations) - .digest('hex'); - } - - return h - .response(translationsCache.translations) - .header('cache-control', 'must-revalidate') - .header('content-type', 'application/json') - .etag(translationsCache.hash); - }, - }); - const authEnabled = !!server.auth.settings.default; server.route({ path: '/bootstrap.js', diff --git a/test/api_integration/apis/core/compression.ts b/test/api_integration/apis/core/compression.ts new file mode 100644 index 0000000000000..d7184e28ca3a4 --- /dev/null +++ b/test/api_integration/apis/core/compression.ts @@ -0,0 +1,55 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('compression', () => { + it(`uses compression when there isn't a referer`, async () => { + await supertest + .get('/app/kibana') + .set('accept-encoding', 'gzip') + .then((response) => { + expect(response.header).to.have.property('content-encoding', 'gzip'); + }); + }); + + it(`uses compression when there is a whitelisted referer`, async () => { + await supertest + .get('/app/kibana') + .set('accept-encoding', 'gzip') + .set('referer', 'https://some-host.com') + .then((response) => { + expect(response.header).to.have.property('content-encoding', 'gzip'); + }); + }); + + it(`doesn't use compression when there is a non-whitelisted referer`, async () => { + await supertest + .get('/app/kibana') + .set('accept-encoding', 'gzip') + .set('referer', 'https://other.some-host.com') + .then((response) => { + expect(response.header).not.to.have.property('content-encoding'); + }); + }); + }); +} diff --git a/test/api_integration/apis/core/index.js b/test/api_integration/apis/core/index.js deleted file mode 100644 index ab9bb8d33c2dc..0000000000000 --- a/test/api_integration/apis/core/index.js +++ /dev/null @@ -1,56 +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 expect from '@kbn/expect'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - - describe('core', () => { - describe('compression', () => { - it(`uses compression when there isn't a referer`, async () => { - await supertest - .get('/app/kibana') - .set('accept-encoding', 'gzip') - .then((response) => { - expect(response.headers).to.have.property('content-encoding', 'gzip'); - }); - }); - - it(`uses compression when there is a whitelisted referer`, async () => { - await supertest - .get('/app/kibana') - .set('accept-encoding', 'gzip') - .set('referer', 'https://some-host.com') - .then((response) => { - expect(response.headers).to.have.property('content-encoding', 'gzip'); - }); - }); - - it(`doesn't use compression when there is a non-whitelisted referer`, async () => { - await supertest - .get('/app/kibana') - .set('accept-encoding', 'gzip') - .set('referer', 'https://other.some-host.com') - .then((response) => { - expect(response.headers).not.to.have.property('content-encoding'); - }); - }); - }); - }); -} diff --git a/test/api_integration/apis/core/index.ts b/test/api_integration/apis/core/index.ts new file mode 100644 index 0000000000000..6a1d7db769df5 --- /dev/null +++ b/test/api_integration/apis/core/index.ts @@ -0,0 +1,27 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('core', () => { + loadTestFile(require.resolve('./compression')); + loadTestFile(require.resolve('./translations')); + }); +} diff --git a/test/api_integration/apis/core/translations.ts b/test/api_integration/apis/core/translations.ts new file mode 100644 index 0000000000000..865d3d070f39a --- /dev/null +++ b/test/api_integration/apis/core/translations.ts @@ -0,0 +1,42 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('translations', () => { + it(`returns the translations with the correct headers`, async () => { + await supertest.get('/translations/en.json').then((response) => { + expect(response.body.locale).to.eql('en'); + + expect(response.header).to.have.property('content-type', 'application/json; charset=utf-8'); + expect(response.header).to.have.property('cache-control', 'must-revalidate'); + expect(response.header).to.have.property('etag'); + }); + }); + + it(`returns a 404 when not using the correct locale`, async () => { + await supertest.get('/translations/foo.json').then((response) => { + expect(response.status).to.eql(404); + }); + }); + }); +} From f60abf368a53a4ab285c53868d2d1dd7fb011418 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 16 Nov 2020 13:11:23 +0100 Subject: [PATCH 04/69] [Search] Send to background UI (#81793) Also adds xpack.data_enhanced.search.sendToBackground.enabled config option --- src/dev/storybook/aliases.ts | 1 + .../data/common/search/session/mocks.ts | 3 +- .../plugins/data_enhanced/.storybook/main.js | 7 + x-pack/plugins/data_enhanced/config.ts | 17 ++ x-pack/plugins/data_enhanced/kibana.json | 2 +- x-pack/plugins/data_enhanced/public/index.ts | 5 +- x-pack/plugins/data_enhanced/public/plugin.ts | 22 +- .../data_enhanced/public/search/index.ts | 7 + .../background_session_indicator.scss | 23 ++ .../background_session_indicator.stories.tsx | 30 ++ .../background_session_indicator.test.tsx | 98 ++++++ .../background_session_indicator.tsx | 286 ++++++++++++++++++ .../ui/background_session_indicator/index.tsx | 23 ++ .../background_session_view_state.ts | 33 ++ ...cted_background_session_indicator.test.tsx | 37 +++ ...connected_background_session_indicator.tsx | 32 ++ .../index.ts | 11 + .../data_enhanced/public/search/ui/index.ts | 7 + x-pack/plugins/data_enhanced/server/index.ts | 12 +- 19 files changed, 649 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/data_enhanced/.storybook/main.js create mode 100644 x-pack/plugins/data_enhanced/config.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/index.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.scss create mode 100644 x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/index.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/background_session_view_state.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/ui/index.ts diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 153725fc48e7b..36c742dc40403 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -22,6 +22,7 @@ export const storybookAliases = { canvas: 'x-pack/plugins/canvas/storybook', codeeditor: 'src/plugins/kibana_react/public/code_editor/.storybook', dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/.storybook', + data_enhanced: 'x-pack/plugins/data_enhanced/.storybook', embeddable: 'src/plugins/embeddable/.storybook', infra: 'x-pack/plugins/infra/.storybook', security_solution: 'x-pack/plugins/security_solution/.storybook', diff --git a/src/plugins/data/common/search/session/mocks.ts b/src/plugins/data/common/search/session/mocks.ts index 2b64bbbd27565..370faaa640c56 100644 --- a/src/plugins/data/common/search/session/mocks.ts +++ b/src/plugins/data/common/search/session/mocks.ts @@ -17,6 +17,7 @@ * under the License. */ +import { BehaviorSubject } from 'rxjs'; import { ISessionService } from './types'; export function getSessionServiceMock(): jest.Mocked { @@ -25,6 +26,6 @@ export function getSessionServiceMock(): jest.Mocked { start: jest.fn(), restore: jest.fn(), getSessionId: jest.fn(), - getSession$: jest.fn(), + getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()), }; } diff --git a/x-pack/plugins/data_enhanced/.storybook/main.js b/x-pack/plugins/data_enhanced/.storybook/main.js new file mode 100644 index 0000000000000..1818aa44a9399 --- /dev/null +++ b/x-pack/plugins/data_enhanced/.storybook/main.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/x-pack/plugins/data_enhanced/config.ts b/x-pack/plugins/data_enhanced/config.ts new file mode 100644 index 0000000000000..9838f0959ef19 --- /dev/null +++ b/x-pack/plugins/data_enhanced/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + search: schema.object({ + sendToBackground: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), + }), +}); + +export type ConfigSchema = TypeOf; diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index 5ded0f8f0dec3..bc7c8410d3df1 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -12,5 +12,5 @@ "optionalPlugins": ["kibanaUtils", "usageCollection"], "server": true, "ui": true, - "requiredBundles": ["kibanaUtils"] + "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/x-pack/plugins/data_enhanced/public/index.ts b/x-pack/plugins/data_enhanced/public/index.ts index 22ac0c9883966..7fe34e21fde5c 100644 --- a/x-pack/plugins/data_enhanced/public/index.ts +++ b/x-pack/plugins/data_enhanced/public/index.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PluginInitializerContext } from 'kibana/public'; import { DataEnhancedPlugin, DataEnhancedSetup, DataEnhancedStart } from './plugin'; +import { ConfigSchema } from '../config'; -export const plugin = () => new DataEnhancedPlugin(); +export const plugin = (initializerContext: PluginInitializerContext) => + new DataEnhancedPlugin(initializerContext); export { DataEnhancedSetup, DataEnhancedStart }; diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index 43ad4a9ed9b8b..948858a5ed4c1 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -4,12 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import React from 'react'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; + import { setAutocompleteService } from './services'; import { setupKqlQuerySuggestionProvider, KUERY_LANGUAGE_NAME } from './autocomplete'; - import { EnhancedSearchInterceptor } from './search/search_interceptor'; +import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; +import { createConnectedBackgroundSessionIndicator } from './search'; +import { ConfigSchema } from '../config'; export interface DataEnhancedSetupDependencies { data: DataPublicPluginSetup; @@ -25,6 +29,8 @@ export class DataEnhancedPlugin implements Plugin { private enhancedSearchInterceptor!: EnhancedSearchInterceptor; + constructor(private initializerContext: PluginInitializerContext) {} + public setup( core: CoreSetup, { data }: DataEnhancedSetupDependencies @@ -52,6 +58,18 @@ export class DataEnhancedPlugin public start(core: CoreStart, plugins: DataEnhancedStartDependencies) { setAutocompleteService(plugins.data.autocomplete); + + if (this.initializerContext.config.get().search.sendToBackground.enabled) { + core.chrome.setBreadcrumbsAppendExtension({ + content: toMountPoint( + React.createElement( + createConnectedBackgroundSessionIndicator({ + sessionService: plugins.data.search.session, + }) + ) + ), + }); + } } public stop() { diff --git a/x-pack/plugins/data_enhanced/public/search/index.ts b/x-pack/plugins/data_enhanced/public/search/index.ts new file mode 100644 index 0000000000000..1a33812ca8566 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './ui'; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.scss b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.scss new file mode 100644 index 0000000000000..2d13d320ae78b --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.scss @@ -0,0 +1,23 @@ +.backgroundSessionIndicator { + padding: 0 $euiSizeXS; +} + +@include euiBreakpoint('xs', 's') { + .backgroundSessionIndicator__popoverContainer.euiFlexGroup--responsive .euiFlexItem { + margin-bottom: $euiSizeXS !important; + } +} + +.backgroundSessionIndicator__verticalDivider { + @include euiBreakpoint('xs', 's') { + margin-left: $euiSizeXS; + padding-left: $euiSizeXS; + } + + @include euiBreakpoint('m', 'l', 'xl') { + border-left: $euiBorderThin; + align-self: stretch; + margin-left: $euiSizeS; + padding-left: $euiSizeS; + } +} diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx new file mode 100644 index 0000000000000..9cef76c62279c --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { BackgroundSessionIndicator } from './background_session_indicator'; +import { BackgroundSessionViewState } from '../connected_background_session_indicator'; + +storiesOf('components/BackgroundSessionIndicator', module).add('default', () => ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +)); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx new file mode 100644 index 0000000000000..5b7ab2e4f9b1f --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import { screen, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { BackgroundSessionIndicator } from './background_session_indicator'; +import { BackgroundSessionViewState } from '../connected_background_session_indicator'; +import { IntlProvider } from 'react-intl'; + +function Container({ children }: { children?: ReactNode }) { + return {children}; +} + +test('Loading state', async () => { + const onCancel = jest.fn(); + render( + + + + ); + + await userEvent.click(screen.getByLabelText('Loading results')); + await userEvent.click(screen.getByText('Cancel')); + + expect(onCancel).toBeCalled(); +}); + +test('Completed state', async () => { + const onSave = jest.fn(); + render( + + + + ); + + await userEvent.click(screen.getByLabelText('Results loaded')); + await userEvent.click(screen.getByText('Save')); + + expect(onSave).toBeCalled(); +}); + +test('Loading in the background state', async () => { + const onCancel = jest.fn(); + render( + + + + ); + + await userEvent.click(screen.getByLabelText('Loading results in the background')); + await userEvent.click(screen.getByText('Cancel')); + + expect(onCancel).toBeCalled(); +}); + +test('BackgroundCompleted state', async () => { + const onViewSession = jest.fn(); + render( + + + + ); + + await userEvent.click(screen.getByLabelText('Results loaded in the background')); + await userEvent.click(screen.getByText('View background sessions')); + + expect(onViewSession).toBeCalled(); +}); + +test('Restored state', async () => { + const onRefresh = jest.fn(); + render( + + + + ); + + await userEvent.click(screen.getByLabelText('Results no longer current')); + await userEvent.click(screen.getByText('Refresh')); + + expect(onRefresh).toBeCalled(); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx new file mode 100644 index 0000000000000..b55bd6b655371 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiButtonEmpty, + EuiButtonEmptyProps, + EuiButtonIcon, + EuiButtonIconProps, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPopover, + EuiText, + EuiToolTip, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { BackgroundSessionViewState } from '../connected_background_session_indicator'; +import './background_session_indicator.scss'; + +export interface BackgroundSessionIndicatorProps { + state: BackgroundSessionViewState; + onContinueInBackground?: () => void; + onCancel?: () => void; + onViewBackgroundSessions?: () => void; + onSaveResults?: () => void; + onRefresh?: () => void; +} + +type ActionButtonProps = BackgroundSessionIndicatorProps & { buttonProps: EuiButtonEmptyProps }; + +const CancelButton = ({ onCancel = () => {}, buttonProps = {} }: ActionButtonProps) => ( + + + +); + +const ContinueInBackgroundButton = ({ + onContinueInBackground = () => {}, + buttonProps = {}, +}: ActionButtonProps) => ( + + + +); + +const ViewBackgroundSessionsButton = ({ + onViewBackgroundSessions = () => {}, + buttonProps = {}, +}: ActionButtonProps) => ( + // TODO: make this a link + + + +); + +const RefreshButton = ({ onRefresh = () => {}, buttonProps = {} }: ActionButtonProps) => ( + + + +); + +const SaveButton = ({ onSaveResults = () => {}, buttonProps = {} }: ActionButtonProps) => ( + + + +); + +const backgroundSessionIndicatorViewStateToProps: { + [state in BackgroundSessionViewState]: { + button: Pick & { tooltipText: string }; + popover: { + text: string; + primaryAction?: React.ComponentType; + secondaryAction?: React.ComponentType; + }; + }; +} = { + [BackgroundSessionViewState.Loading]: { + button: { + color: 'subdued', + iconType: 'clock', + 'aria-label': i18n.translate( + 'xpack.data.backgroundSessionIndicator.loadingResultsIconAriaLabel', + { defaultMessage: 'Loading results' } + ), + tooltipText: i18n.translate( + 'xpack.data.backgroundSessionIndicator.loadingResultsIconTooltipText', + { defaultMessage: 'Loading results' } + ), + }, + popover: { + text: i18n.translate('xpack.data.backgroundSessionIndicator.loadingResultsText', { + defaultMessage: 'Loading', + }), + primaryAction: CancelButton, + secondaryAction: ContinueInBackgroundButton, + }, + }, + [BackgroundSessionViewState.Completed]: { + button: { + color: 'subdued', + iconType: 'checkInCircleFilled', + 'aria-label': i18n.translate( + 'xpack.data.backgroundSessionIndicator.resultsLoadedIconAriaLabel', + { + defaultMessage: 'Results loaded', + } + ), + tooltipText: i18n.translate( + 'xpack.data.backgroundSessionIndicator.resultsLoadedIconTooltipText', + { + defaultMessage: 'Results loaded', + } + ), + }, + popover: { + text: i18n.translate('xpack.data.backgroundSessionIndicator.resultsLoadedText', { + defaultMessage: 'Results loaded', + }), + primaryAction: SaveButton, + secondaryAction: ViewBackgroundSessionsButton, + }, + }, + [BackgroundSessionViewState.BackgroundLoading]: { + button: { + iconType: EuiLoadingSpinner, + 'aria-label': i18n.translate( + 'xpack.data.backgroundSessionIndicator.loadingInTheBackgroundIconAriaLabel', + { + defaultMessage: 'Loading results in the background', + } + ), + tooltipText: i18n.translate( + 'xpack.data.backgroundSessionIndicator.loadingInTheBackgroundIconTooltipText', + { + defaultMessage: 'Loading results in the background', + } + ), + }, + popover: { + text: i18n.translate('xpack.data.backgroundSessionIndicator.loadingInTheBackgroundText', { + defaultMessage: 'Loading in the background', + }), + primaryAction: CancelButton, + secondaryAction: ViewBackgroundSessionsButton, + }, + }, + [BackgroundSessionViewState.BackgroundCompleted]: { + button: { + color: 'success', + iconType: 'checkInCircleFilled', + 'aria-label': i18n.translate( + 'xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundIconAraText', + { + defaultMessage: 'Results loaded in the background', + } + ), + tooltipText: i18n.translate( + 'xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundIconTooltipText', + { + defaultMessage: 'Results loaded in the background', + } + ), + }, + popover: { + text: i18n.translate( + 'xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundText', + { + defaultMessage: 'Results loaded', + } + ), + primaryAction: ViewBackgroundSessionsButton, + }, + }, + [BackgroundSessionViewState.Restored]: { + button: { + color: 'warning', + iconType: 'refresh', + 'aria-label': i18n.translate( + 'xpack.data.backgroundSessionIndicator.restoredResultsIconAriaLabel', + { + defaultMessage: 'Results no longer current', + } + ), + tooltipText: i18n.translate( + 'xpack.data.backgroundSessionIndicator.restoredResultsTooltipText', + { + defaultMessage: 'Results no longer current', + } + ), + }, + popover: { + text: i18n.translate('xpack.data.backgroundSessionIndicator.restoredText', { + defaultMessage: 'Results no longer current', + }), + primaryAction: RefreshButton, + secondaryAction: ViewBackgroundSessionsButton, + }, + }, +}; + +const VerticalDivider: React.FC = () => ( +
+); + +export const BackgroundSessionIndicator: React.FC = (props) => { + const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); + const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen); + const closePopover = () => setIsPopoverOpen(false); + + const { button, popover } = backgroundSessionIndicatorViewStateToProps[props.state]; + + return ( + + + + } + > + + + +

{popover.text}

+
+
+ + + {popover.primaryAction && ( + + + + )} + {popover.primaryAction && popover.secondaryAction && } + {popover.secondaryAction && ( + + + + )} + + +
+
+ ); +}; + +// React.lazy() needs default: +// eslint-disable-next-line import/no-default-export +export default BackgroundSessionIndicator; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/index.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/index.tsx new file mode 100644 index 0000000000000..55c8c453dd5d2 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/index.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiDelayRender, EuiLoadingSpinner } from '@elastic/eui'; +import React from 'react'; +import type { BackgroundSessionIndicatorProps } from './background_session_indicator'; +export type { BackgroundSessionIndicatorProps }; + +const Fallback = () => ( + + + +); + +const LazyBackgroundSessionIndicator = React.lazy(() => import('./background_session_indicator')); +export const BackgroundSessionIndicator = (props: BackgroundSessionIndicatorProps) => ( + }> + + +); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/background_session_view_state.ts b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/background_session_view_state.ts new file mode 100644 index 0000000000000..b75c2a536f624 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/background_session_view_state.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum BackgroundSessionViewState { + /** + * Pending search request has not been sent to the background yet + */ + Loading = 'loading', + + /** + * No action was taken and the page completed loading without background session creation. + */ + Completed = 'completed', + + /** + * Search request was sent to the background. + * The page is loading in background. + */ + BackgroundLoading = 'backgroundLoading', + + /** + * Page load completed with background session created. + */ + BackgroundCompleted = 'backgroundCompleted', + + /** + * Revisiting the page after background completion + */ + Restored = 'restored', +} diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx new file mode 100644 index 0000000000000..b21081e10bbe1 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { createConnectedBackgroundSessionIndicator } from './connected_background_session_indicator'; +import { BehaviorSubject } from 'rxjs'; +import { ISessionService } from '../../../../../../../src/plugins/data/public'; + +const sessionService = dataPluginMock.createStartContract().search.session as jest.Mocked< + ISessionService +>; + +test("shouldn't show indicator in case no active search session", async () => { + const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ sessionService }); + const { getByTestId, container } = render(); + + // make sure `backgroundSessionIndicator` isn't appearing after some time (lazy-loading) + await expect( + waitFor(() => getByTestId('backgroundSessionIndicator'), { timeout: 100 }) + ).rejects.toThrow(); + expect(container).toMatchInlineSnapshot(`
`); +}); + +test('should show indicator in case there is an active search session', async () => { + const session$ = new BehaviorSubject('session_id'); + sessionService.getSession$.mockImplementation(() => session$); + sessionService.getSessionId.mockImplementation(() => session$.getValue()); + const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ sessionService }); + const { getByTestId } = render(); + + await waitFor(() => getByTestId('backgroundSessionIndicator')); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx new file mode 100644 index 0000000000000..d097a1aecb66a --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { distinctUntilChanged, map } from 'rxjs/operators'; +import { BackgroundSessionIndicator } from '../background_session_indicator'; +import { ISessionService } from '../../../../../../../src/plugins/data/public/'; +import { BackgroundSessionViewState } from './background_session_view_state'; + +export interface BackgroundSessionIndicatorDeps { + sessionService: ISessionService; +} + +export const createConnectedBackgroundSessionIndicator = ({ + sessionService, +}: BackgroundSessionIndicatorDeps): React.FC => { + const sessionId$ = sessionService.getSession$(); + const hasActiveSession$ = sessionId$.pipe( + map((sessionId) => !!sessionId), + distinctUntilChanged() + ); + + return () => { + const isSession = useObservable(hasActiveSession$, !!sessionService.getSessionId()); + if (!isSession) return null; + return ; + }; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts new file mode 100644 index 0000000000000..adbb6edbbfcf3 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + BackgroundSessionIndicatorDeps, + createConnectedBackgroundSessionIndicator, +} from './connected_background_session_indicator'; +export { BackgroundSessionViewState } from './background_session_view_state'; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/index.ts b/x-pack/plugins/data_enhanced/public/search/ui/index.ts new file mode 100644 index 0000000000000..04201325eb5db --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './connected_background_session_indicator'; diff --git a/x-pack/plugins/data_enhanced/server/index.ts b/x-pack/plugins/data_enhanced/server/index.ts index a0edd2e26ebef..c3907b3b67439 100644 --- a/x-pack/plugins/data_enhanced/server/index.ts +++ b/x-pack/plugins/data_enhanced/server/index.ts @@ -4,10 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'kibana/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'; import { EnhancedDataServerPlugin } from './plugin'; +import { configSchema, ConfigSchema } from '../config'; -export function plugin(initializerContext: PluginInitializerContext) { +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + search: true, + }, + schema: configSchema, +}; + +export function plugin(initializerContext: PluginInitializerContext) { return new EnhancedDataServerPlugin(initializerContext); } From 9d40dab794da7f00ab5179dd52c404b7e659233b Mon Sep 17 00:00:00 2001 From: kaisecheng <69120390+kaisecheng@users.noreply.github.com> Date: Mon, 16 Nov 2020 14:00:18 +0100 Subject: [PATCH 05/69] fix logstash central pipeline management test (#83281) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/functional/apps/logstash/pipeline_create.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/logstash/pipeline_create.js b/x-pack/test/functional/apps/logstash/pipeline_create.js index c3cf102c908a0..2b70dc1832367 100644 --- a/x-pack/test/functional/apps/logstash/pipeline_create.js +++ b/x-pack/test/functional/apps/logstash/pipeline_create.js @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { omit } from 'lodash'; export default function ({ getService, getPageObjects }) { const browser = getService('browser'); @@ -15,8 +16,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['logstash']); const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/83231 - describe.skip('pipeline create new', () => { + describe('pipeline create new', () => { let originalWindowSize; before(async () => { @@ -89,6 +89,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.logstash.gotoPipelineList(); await pipelineList.assertExists(); const originalRows = await pipelineList.readRows(); + const originalRowsWithoutTime = originalRows.map((row) => omit(row, 'lastModified')); await PageObjects.logstash.gotoNewPipelineEditor(); await pipelineEditor.clickCancel(); @@ -96,7 +97,8 @@ export default function ({ getService, getPageObjects }) { await retry.try(async () => { await pipelineList.assertExists(); const currentRows = await pipelineList.readRows(); - expect(originalRows).to.eql(currentRows); + const currentRowsWithoutTime = currentRows.map((row) => omit(row, 'lastModified')); + expect(originalRowsWithoutTime).to.eql(currentRowsWithoutTime); }); }); }); From 01b1710eb7c55d9c7a421f7d0e1b53d05a203ce0 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 16 Nov 2020 15:34:40 +0200 Subject: [PATCH 06/69] [Security Solution][Case] Change case connector minimum required license to basic (#83401) --- x-pack/plugins/case/server/connectors/case/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index f2f8f659f3a2c..dc647d288ec65 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -33,7 +33,7 @@ export function getActionType({ }: GetActionTypeParams): CaseActionType { return { id: CASE_ACTION_TYPE_ID, - minimumLicenseRequired: 'gold', + minimumLicenseRequired: 'basic', name: i18n.NAME, validate: { config: CaseConfigurationSchema, From c8b8a0ae9c27c43163a049994bccd0880f6d080f Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 16 Nov 2020 14:35:12 +0100 Subject: [PATCH 07/69] [Lens] Avoid unnecessary data fetching on dimension flyout open (#82957) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../operations/definitions/helpers.tsx | 30 +++++ .../definitions/ranges/advanced_editor.tsx | 9 +- .../definitions/ranges/range_editor.tsx | 16 +-- .../definitions/ranges/ranges.test.tsx | 111 +++++++++++------- .../terms/values_range_input.test.tsx | 20 ++-- .../definitions/terms/values_range_input.tsx | 6 +- 6 files changed, 129 insertions(+), 63 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx new file mode 100644 index 0000000000000..a5c08a93467af --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useRef } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; + +export const useDebounceWithOptions = ( + fn: Function, + { skipFirstRender }: { skipFirstRender: boolean } = { skipFirstRender: false }, + ms?: number | undefined, + deps?: React.DependencyList | undefined +) => { + const isFirstRender = useRef(true); + const newDeps = [...(deps || []), isFirstRender]; + + return useDebounce( + () => { + if (skipFirstRender && isFirstRender.current) { + isFirstRender.current = false; + return; + } + return fn(); + }, + ms, + newDeps + ); +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx index 2eb971aa03c55..95c7e3533ee09 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx @@ -8,7 +8,6 @@ import './advanced_editor.scss'; import React, { useState, MouseEventHandler } from 'react'; import { i18n } from '@kbn/i18n'; -import useDebounce from 'react-use/lib/useDebounce'; import { EuiFlexGroup, EuiFlexItem, @@ -31,6 +30,7 @@ import { DraggableBucketContainer, LabelInput, } from '../shared_components'; +import { useDebounceWithOptions } from '../helpers'; const generateId = htmlIdGenerator(); @@ -208,12 +208,13 @@ export const AdvancedRangeEditor = ({ const lastIndex = localRanges.length - 1; - // Update locally all the time, but bounce the parents prop function - // to aviod too many requests - useDebounce( + // Update locally all the time, but bounce the parents prop function to aviod too many requests + // Avoid to trigger on first render + useDebounceWithOptions( () => { setRanges(localRanges.map(({ id, ...rest }) => ({ ...rest }))); }, + { skipFirstRender: true }, TYPING_DEBOUNCE_TIME, [localRanges] ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx index a18c47f9dedd1..df955be6b490a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx @@ -6,7 +6,6 @@ import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import useDebounce from 'react-use/lib/useDebounce'; import { EuiButtonEmpty, EuiFormRow, @@ -21,6 +20,7 @@ import { IFieldFormat } from 'src/plugins/data/public'; import { RangeColumnParams, UpdateParamsFnType, MODES_TYPES } from './ranges'; import { AdvancedRangeEditor } from './advanced_editor'; import { TYPING_DEBOUNCE_TIME, MODES, MIN_HISTOGRAM_BARS } from './constants'; +import { useDebounceWithOptions } from '../helpers'; const BaseRangeEditor = ({ maxBars, @@ -37,10 +37,11 @@ const BaseRangeEditor = ({ }) => { const [maxBarsValue, setMaxBarsValue] = useState(String(maxBars)); - useDebounce( + useDebounceWithOptions( () => { onMaxBarsChange(Number(maxBarsValue)); }, + { skipFirstRender: true }, TYPING_DEBOUNCE_TIME, [maxBarsValue] ); @@ -151,13 +152,14 @@ export const RangeEditor = ({ }) => { const [isAdvancedEditor, toggleAdvancedEditor] = useState(params.type === MODES.Range); - // if the maxBars in the params is set to auto refresh it with the default value - // only on bootstrap + // if the maxBars in the params is set to auto refresh it with the default value only on bootstrap useEffect(() => { - if (params.maxBars !== maxBars) { - setParam('maxBars', maxBars); + if (!isAdvancedEditor) { + if (params.maxBars !== maxBars) { + setParam('maxBars', maxBars); + } } - }, [maxBars, params.maxBars, setParam]); + }, [maxBars, params.maxBars, setParam, isAdvancedEditor]); if (isAdvancedEditor) { return ( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index ce015284e544b..87dcdb45cf58f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -284,7 +284,11 @@ describe('ranges', () => { /> ); + // There's a useEffect in the component that updates the value on bootstrap + // because there's a debouncer, wait a bit before calling onChange act(() => { + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + instance.find(EuiRange).prop('onChange')!( { currentTarget: { @@ -293,26 +297,27 @@ describe('ranges', () => { } as React.ChangeEvent, true ); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + }); - expect(setStateSpy).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - ...state.layers.first.columns.col1, - params: { - ...state.layers.first.columns.col1.params, - maxBars: MAX_HISTOGRAM_VALUE, - }, + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + maxBars: MAX_HISTOGRAM_VALUE, }, }, }, }, - }); + }, }); }); @@ -330,59 +335,65 @@ describe('ranges', () => { /> ); + // There's a useEffect in the component that updates the value on bootstrap + // because there's a debouncer, wait a bit before calling onChange act(() => { + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); // minus button instance .find('[data-test-subj="lns-indexPattern-range-maxBars-minus"]') .find('button') .prop('onClick')!({} as ReactMouseEvent); jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + }); - expect(setStateSpy).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - ...state.layers.first.columns.col1, - params: { - ...state.layers.first.columns.col1.params, - maxBars: GRANULARITY_DEFAULT_VALUE - GRANULARITY_STEP, - }, + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + maxBars: GRANULARITY_DEFAULT_VALUE - GRANULARITY_STEP, }, }, }, }, - }); + }, + }); + act(() => { // plus button instance .find('[data-test-subj="lns-indexPattern-range-maxBars-plus"]') .find('button') .prop('onClick')!({} as ReactMouseEvent); jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + }); - expect(setStateSpy).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - ...state.layers.first.columns.col1, - params: { - ...state.layers.first.columns.col1.params, - maxBars: GRANULARITY_DEFAULT_VALUE, - }, + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + maxBars: GRANULARITY_DEFAULT_VALUE, }, }, }, }, - }); + }, }); + // }); }); }); @@ -749,6 +760,22 @@ describe('ranges', () => { ); }); + it('should not update the state on mount', () => { + const setStateSpy = jest.fn(); + + mount( + + ); + expect(setStateSpy.mock.calls.length).toBe(0); + }); + it('should not reset formatters when switching between custom ranges and auto histogram', () => { const setStateSpy = jest.fn(); // now set a format on the range operation diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.test.tsx index 18b9b5b1e8b98..759bda43efe67 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.test.tsx @@ -20,6 +20,13 @@ describe('ValuesRangeInput', () => { expect(instance.find(EuiRange).prop('value')).toEqual('5'); }); + it('should not run onChange function on mount', () => { + const onChangeSpy = jest.fn(); + shallow(); + + expect(onChangeSpy.mock.calls.length).toBe(0); + }); + it('should run onChange function on update', () => { const onChangeSpy = jest.fn(); const instance = shallow(); @@ -30,11 +37,10 @@ describe('ValuesRangeInput', () => { ); }); expect(instance.find(EuiRange).prop('value')).toEqual('7'); - // useDebounce runs on initialization and on change - expect(onChangeSpy.mock.calls.length).toBe(2); - expect(onChangeSpy.mock.calls[0][0]).toBe(5); - expect(onChangeSpy.mock.calls[1][0]).toBe(7); + expect(onChangeSpy.mock.calls.length).toBe(1); + expect(onChangeSpy.mock.calls[0][0]).toBe(7); }); + it('should not run onChange function on update when value is out of 1-100 range', () => { const onChangeSpy = jest.fn(); const instance = shallow(); @@ -46,9 +52,7 @@ describe('ValuesRangeInput', () => { }); instance.update(); expect(instance.find(EuiRange).prop('value')).toEqual('107'); - // useDebounce only runs on initialization - expect(onChangeSpy.mock.calls.length).toBe(2); - expect(onChangeSpy.mock.calls[0][0]).toBe(5); - expect(onChangeSpy.mock.calls[1][0]).toBe(100); + expect(onChangeSpy.mock.calls.length).toBe(1); + expect(onChangeSpy.mock.calls[0][0]).toBe(100); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.tsx index ef42f2d4a7175..7018ba3083f04 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_range_input.tsx @@ -5,9 +5,9 @@ */ import React, { useState } from 'react'; -import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import { EuiRange } from '@elastic/eui'; +import { useDebounceWithOptions } from '../helpers'; export const ValuesRangeInput = ({ value, @@ -20,7 +20,8 @@ export const ValuesRangeInput = ({ const MAX_NUMBER_OF_VALUES = 100; const [inputValue, setInputValue] = useState(String(value)); - useDebounce( + + useDebounceWithOptions( () => { if (inputValue === '') { return; @@ -28,6 +29,7 @@ export const ValuesRangeInput = ({ const inputNumber = Number(inputValue); onChange(Math.min(MAX_NUMBER_OF_VALUES, Math.max(inputNumber, MIN_NUMBER_OF_VALUES))); }, + { skipFirstRender: true }, 256, [inputValue] ); From 3ba7758a4fad65d0df62eafb4ffd03084195eed0 Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Mon, 16 Nov 2020 08:26:41 -0600 Subject: [PATCH 08/69] fix tall vislib charts in visualize (#83340) --- .../public/vislib/_vislib_vis_type.scss | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss b/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss index 843bb9d3f03eb..9e737fc87e895 100644 --- a/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss +++ b/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss @@ -29,3 +29,15 @@ min-height: 0; min-width: 0; } + +.vislib__wrapper { + position: relative; +} + +.vislib__container { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} From 3f51bf5e9f3be53cba23a4bc55d658ec0c227ddd Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 16 Nov 2020 09:23:28 -0600 Subject: [PATCH 09/69] Fix advanced settings category sorting (#83394) In the advanced settings categories, "Observability" and "Machine Learning" were using uppercase letters in their keys while everything else was using lowercase This caused them both to show up before the rest of the options in the dropdown and in the sorting in the advanced settings UI. Add keys for them to the get_category_name module in the advanced settings plugin and use those keys in the plugins that apply these categories. This also makes it so i18n keys are available for these items. Fixes #81974. --- .../public/management_app/lib/get_category_name.ts | 6 ++++++ x-pack/plugins/apm/server/ui_settings.ts | 4 ++-- x-pack/plugins/ml/server/lib/register_settings.ts | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/plugins/advanced_settings/public/management_app/lib/get_category_name.ts b/src/plugins/advanced_settings/public/management_app/lib/get_category_name.ts index 31df6875c97d9..c8fbe8009c9bb 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/get_category_name.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/get_category_name.ts @@ -25,6 +25,12 @@ const names: Record = { general: i18n.translate('advancedSettings.categoryNames.generalLabel', { defaultMessage: 'General', }), + machineLearning: i18n.translate('advancedSettings.categoryNames.machineLearningLabel', { + defaultMessage: 'Machine Learning', + }), + observability: i18n.translate('advancedSettings.categoryNames.observabilityLabel', { + defaultMessage: 'Observability', + }), timelion: i18n.translate('advancedSettings.categoryNames.timelionLabel', { defaultMessage: 'Timelion', }), diff --git a/x-pack/plugins/apm/server/ui_settings.ts b/x-pack/plugins/apm/server/ui_settings.ts index fe5b11d89d716..4932d9f79a383 100644 --- a/x-pack/plugins/apm/server/ui_settings.ts +++ b/x-pack/plugins/apm/server/ui_settings.ts @@ -17,7 +17,7 @@ import { */ export const uiSettings: Record> = { [enableCorrelations]: { - category: ['Observability'], + category: ['observability'], name: i18n.translate('xpack.apm.enableCorrelationsExperimentName', { defaultMessage: 'APM Correlations', }), @@ -32,7 +32,7 @@ export const uiSettings: Record> = { schema: schema.boolean(), }, [enableServiceOverview]: { - category: ['Observability'], + category: ['observability'], name: i18n.translate('xpack.apm.enableServiceOverviewExperimentName', { defaultMessage: 'APM Service overview', }), diff --git a/x-pack/plugins/ml/server/lib/register_settings.ts b/x-pack/plugins/ml/server/lib/register_settings.ts index a9ee24fbb5cea..0cdaaadf7f172 100644 --- a/x-pack/plugins/ml/server/lib/register_settings.ts +++ b/x-pack/plugins/ml/server/lib/register_settings.ts @@ -27,7 +27,7 @@ export function registerKibanaSettings(coreSetup: CoreSetup) { defaultMessage: 'Sets the file size limit when importing data in the File Data Visualizer. The highest supported value for this setting is 1GB.', }), - category: ['Machine Learning'], + category: ['machineLearning'], schema: schema.string(), validation: { regexString: '\\d+[mMgG][bB]', @@ -49,7 +49,7 @@ export function registerKibanaSettings(coreSetup: CoreSetup) { 'Use the default time filter in the Single Metric Viewer and Anomaly Explorer. If not enabled, the results for the full time range of the job are displayed.', } ), - category: ['Machine Learning'], + category: ['machineLearning'], }, [ANOMALY_DETECTION_DEFAULT_TIME_RANGE]: { name: i18n.translate('xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeName', { @@ -69,7 +69,7 @@ export function registerKibanaSettings(coreSetup: CoreSetup) { to: schema.string(), }), requiresPageReload: true, - category: ['Machine Learning'], + category: ['machineLearning'], }, }); } From b8b880662868e26c1264b6f68dcc968285436a08 Mon Sep 17 00:00:00 2001 From: Dan Panzarella Date: Mon, 16 Nov 2020 11:19:03 -0500 Subject: [PATCH 10/69] [Security Solution] Gracefully handle errors in detection rules install (#83306) --- .../routes/rules/add_prepackaged_rules_route.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index f885445c29b04..a3b378a6ef04a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -115,12 +115,15 @@ export const createPrepackagedRules = async ( ); } } - const result = await Promise.all([ - installPrepackagedRules(alertsClient, rulesToInstall, signalsIndex), - installPrepackagedTimelines(maxTimelineImportExportSize, frameworkRequest, true), - ]); + + await Promise.all(installPrepackagedRules(alertsClient, rulesToInstall, signalsIndex)); + const timeline = await installPrepackagedTimelines( + maxTimelineImportExportSize, + frameworkRequest, + true + ); const [prepackagedTimelinesResult, timelinesErrors] = validate( - result[1], + timeline, importTimelineResultSchema ); await updatePrepackagedRules(alertsClient, savedObjectsClient, rulesToUpdate, signalsIndex); From e1500bf86af5d4e1db0b2a8c92ea26504e138b98 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 16 Nov 2020 17:22:35 +0100 Subject: [PATCH 11/69] [Lens] Make the dimension flyout panel stay close on outside click (#83059) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../definitions/filters/filter_popover.tsx | 20 +++++++++++------- .../definitions/ranges/advanced_editor.tsx | 21 +++++++++++++------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx index b023a9a5a3ec5..b9d9d6306b9ae 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx @@ -32,9 +32,13 @@ export const FilterPopover = ({ const [isPopoverOpen, setIsPopoverOpen] = useState(false); const inputRef = React.useRef(); - const setPopoverOpen = (isOpen: boolean) => { - setIsPopoverOpen(isOpen); - setIsOpenByCreation(isOpen); + const closePopover = () => { + if (isOpenByCreation) { + setIsOpenByCreation(false); + } + if (isPopoverOpen) { + setIsPopoverOpen(false); + } }; const setFilterLabel = (label: string) => setFilter({ ...filter, label }); @@ -57,14 +61,14 @@ export const FilterPopover = ({ panelClassName="lnsIndexPatternDimensionEditor__filtersEditor" isOpen={isOpenByCreation || isPopoverOpen} ownFocus - closePopover={() => { - setPopoverOpen(false); - }} + closePopover={() => closePopover()} button={