From aeba68464e087cea6bb5d83e8d0e2eaae647c048 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 2 Sep 2020 17:23:45 +0200 Subject: [PATCH 01/25] Bump buffer from v5.2.1 to v5.6.0 (#76478) --- yarn.lock | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3be2599c64201d..9ab1de22af57ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7791,15 +7791,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.1.0, buffer@^5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6" - integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - -buffer@^5.5.0: +buffer@^5.1.0, buffer@^5.2.0, buffer@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== From 46689b33488fa76e17b5f2d0028d483af5833cf3 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 2 Sep 2020 17:24:12 +0200 Subject: [PATCH 02/25] Bump readable-stream to newest 2.x and 3.x versions (#76480) --- yarn.lock | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9ab1de22af57ce..a37ae19b15a06e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24179,7 +24179,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@1 || 2", readable-stream@^2.3.7, readable-stream@~2.3.3: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.3, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -24202,29 +24202,7 @@ readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0": isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" - integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.2: +"readable-stream@2 || 3", readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== From 34664360312f03c73bf0805115a27c41242e5066 Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 2 Sep 2020 08:53:23 -0700 Subject: [PATCH 03/25] [Enterprise Search] Add 404 pages (#76404) * Add new 404 NotFound shared view * Add new telemetry ui_error.not_found metric * Add check for licensed users who should be sent to support.elastic.co --- .../enterprise_search/common/constants.ts | 4 + .../public/applications/app_search/index.tsx | 4 + .../applications/shared/licensing/index.ts | 2 +- .../shared/licensing/license_checks.test.ts | 23 +++- .../shared/licensing/license_checks.ts | 8 +- .../not_found/assets/app_search_logo.tsx | 32 +++++ .../shared/not_found/assets/logo.scss | 21 ++++ .../assets/workplace_search_logo.tsx | 38 ++++++ .../applications/shared/not_found/index.ts | 7 ++ .../shared/not_found/not_found.test.tsx | 65 +++++++++++ .../shared/not_found/not_found.tsx | 110 ++++++++++++++++++ .../applications/workplace_search/index.tsx | 5 + .../collectors/app_search/telemetry.test.ts | 3 + .../server/collectors/app_search/telemetry.ts | 4 + .../workplace_search/telemetry.test.ts | 3 + .../collectors/workplace_search/telemetry.ts | 4 + .../schema/xpack_plugins.json | 6 + 17 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index c5839df4c603b3..05d27d7337a6ef 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -24,6 +24,7 @@ export const APP_SEARCH_PLUGIN = { 'Leverage dashboards, analytics, and APIs for advanced application search made simple.', }), URL: '/app/enterprise_search/app_search', + SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/app-search/', }; export const WORKPLACE_SEARCH_PLUGIN = { @@ -36,8 +37,11 @@ export const WORKPLACE_SEARCH_PLUGIN = { 'Search all documents, files, and sources available across your virtual workplace.', }), URL: '/app/enterprise_search/workplace_search', + SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/workplace-search/', }; +export const LICENSED_SUPPORT_URL = 'https://support.elastic.co'; + export const JSON_HEADER = { 'Content-Type': 'application/json' }; // This needs specific casing or Chrome throws a 415 error export const ENGINES_PAGE_SIZE = 10; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 6575e44f509ebc..c4a366930d22aa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -29,6 +29,7 @@ import { import { SetupGuide } from './components/setup_guide'; import { ErrorConnecting } from './components/error_connecting'; +import { NotFound } from '../shared/not_found'; import { EngineOverview } from './components/engine_overview'; export const AppSearch: React.FC = (props) => { @@ -73,6 +74,9 @@ export const AppSearchConfigured: React.FC = (props) => { + + + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts index 9c8c1417d48db2..29c11ffa1cef8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts @@ -5,4 +5,4 @@ */ export { LicenseContext, LicenseProvider, ILicenseContext } from './license_context'; -export { hasPlatinumLicense } from './license_checks'; +export { hasPlatinumLicense, hasGoldLicense } from './license_checks'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts index ad134e7d36b10c..40f0f6380c21cd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { hasPlatinumLicense } from './license_checks'; +import { hasPlatinumLicense, hasGoldLicense } from './license_checks'; describe('hasPlatinumLicense', () => { it('is true for platinum licenses', () => { @@ -31,3 +31,24 @@ describe('hasPlatinumLicense', () => { expect(hasPlatinumLicense({ isActive: true, type: 'gold' } as any)).toEqual(false); }); }); + +describe('hasGoldLicense', () => { + it('is true for gold+ and trial licenses', () => { + expect(hasGoldLicense({ isActive: true, type: 'gold' } as any)).toEqual(true); + expect(hasGoldLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true); + expect(hasGoldLicense({ isActive: true, type: 'enterprise' } as any)).toEqual(true); + expect(hasGoldLicense({ isActive: true, type: 'trial' } as any)).toEqual(true); + }); + + it('is false if the current license is expired', () => { + expect(hasGoldLicense({ isActive: false, type: 'gold' } as any)).toEqual(false); + expect(hasGoldLicense({ isActive: false, type: 'platinum' } as any)).toEqual(false); + expect(hasGoldLicense({ isActive: false, type: 'enterprise' } as any)).toEqual(false); + expect(hasGoldLicense({ isActive: false, type: 'trial' } as any)).toEqual(false); + }); + + it('is false for licenses below gold', () => { + expect(hasGoldLicense({ isActive: true, type: 'basic' } as any)).toEqual(false); + expect(hasGoldLicense({ isActive: false, type: 'standard' } as any)).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts index de4a17ce2bd3c1..d13d0909243be7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts @@ -7,5 +7,11 @@ import { ILicense } from '../../../../../licensing/public'; export const hasPlatinumLicense = (license?: ILicense) => { - return license?.isActive && ['platinum', 'enterprise', 'trial'].includes(license?.type as string); + const qualifyingLicenses = ['platinum', 'enterprise', 'trial']; + return license?.isActive && qualifyingLicenses.includes(license?.type as string); +}; + +export const hasGoldLicense = (license?: ILicense) => { + const qualifyingLicenses = ['gold', 'platinum', 'enterprise', 'trial']; + return license?.isActive && qualifyingLicenses.includes(license?.type as string); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx new file mode 100644 index 00000000000000..6bd0486532576d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.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'; + +export const AppSearchLogo: React.FC = () => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss new file mode 100644 index 00000000000000..322e6a4182c5c1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss @@ -0,0 +1,21 @@ +/* + * 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. + */ + +.logo404 { + width: $euiSize * 8; + height: $euiSize * 8; + + fill: $euiColorEmptyShade; + stroke: $euiColorLightShade; + + &__light { + fill: $euiColorLightShade; + } + + &__dark { + fill: $euiColorMediumShade; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx new file mode 100644 index 00000000000000..30bcf9ad2d7fda --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx @@ -0,0 +1,38 @@ +/* + * 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'; + +export const WorkplaceSearchLogo: React.FC = () => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts new file mode 100644 index 00000000000000..dcc7762b932141 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/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 { NotFound } from './not_found'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx new file mode 100644 index 00000000000000..ce9071ad7b9d0c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx @@ -0,0 +1,65 @@ +/* + * 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 '../../__mocks__/shallow_usecontext.mock'; + +import React, { useContext } from 'react'; +import { shallow } from 'enzyme'; + +import { EuiButton as EuiButtonExternal, EuiEmptyPrompt } from '@elastic/eui'; + +import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../common/constants'; +import { AppSearchLogo } from './assets/app_search_logo'; +import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; + +import { NotFound } from './'; + +describe('NotFound', () => { + const basicLicense = { isActive: true, type: 'basic' }; + const goldLicense = { isActive: true, type: 'gold' }; + + beforeEach(() => { + (useContext as jest.Mock).mockImplementation(() => ({ license: basicLicense })); + }); + + it('renders an App Search 404 view', () => { + const wrapper = shallow(); + const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); + + expect(prompt.find('h2').text()).toEqual('404 error'); + expect(prompt.find(EuiButtonExternal).prop('href')).toEqual(APP_SEARCH_PLUGIN.SUPPORT_URL); + + const logo = prompt.find(AppSearchLogo).dive().shallow(); + expect(logo.type()).toEqual('svg'); + }); + + it('renders a Workplace Search 404 view', () => { + const wrapper = shallow(); + const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); + + expect(prompt.find('h2').text()).toEqual('404 error'); + expect(prompt.find(EuiButtonExternal).prop('href')).toEqual( + WORKPLACE_SEARCH_PLUGIN.SUPPORT_URL + ); + + const logo = prompt.find(WorkplaceSearchLogo).dive().shallow(); + expect(logo.type()).toEqual('svg'); + }); + + it('changes the support URL if the user has a gold+ license', () => { + (useContext as jest.Mock).mockImplementation(() => ({ license: goldLicense })); + const wrapper = shallow(); + const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); + + expect(prompt.find(EuiButtonExternal).prop('href')).toEqual('https://support.elastic.co'); + }); + + it('does not render anything without a valid product', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx new file mode 100644 index 00000000000000..bd988854225fbd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx @@ -0,0 +1,110 @@ +/* + * 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, { useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPageContent, + EuiEmptyPrompt, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButton as EuiButtonExternal, +} from '@elastic/eui'; + +import { + APP_SEARCH_PLUGIN, + WORKPLACE_SEARCH_PLUGIN, + LICENSED_SUPPORT_URL, +} from '../../../../common/constants'; + +import { EuiButton } from '../react_router_helpers'; +import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome'; +import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry'; +import { LicenseContext, ILicenseContext, hasGoldLicense } from '../licensing'; + +import { AppSearchLogo } from './assets/app_search_logo'; +import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; +import './assets/logo.scss'; + +interface NotFoundProps { + // Expects product plugin constants (@see common/constants.ts) + product: { + ID: string; + SUPPORT_URL: string; + }; +} + +export const NotFound: React.FC = ({ product = {} }) => { + const { license } = useContext(LicenseContext) as ILicenseContext; + const supportUrl = hasGoldLicense(license) ? LICENSED_SUPPORT_URL : product.SUPPORT_URL; + + let Logo; + let SetPageChrome; + let SendTelemetry; + + switch (product.ID) { + case APP_SEARCH_PLUGIN.ID: + Logo = AppSearchLogo; + SetPageChrome = SetAppSearchChrome; + SendTelemetry = SendAppSearchTelemetry; + break; + case WORKPLACE_SEARCH_PLUGIN.ID: + Logo = WorkplaceSearchLogo; + SetPageChrome = SetWorkplaceSearchChrome; + SendTelemetry = SendWorkplaceSearchTelemetry; + break; + default: + return null; + } + + return ( + <> + + + + + } + body={ + <> + +

+ {i18n.translate('xpack.enterpriseSearch.notFound.title', { + defaultMessage: '404 error', + })} +

+
+

+ {i18n.translate('xpack.enterpriseSearch.notFound.description', { + defaultMessage: 'The page you’re looking for was not found.', + })} +

+ + } + actions={ + + + + {i18n.translate('xpack.enterpriseSearch.notFound.action1', { + defaultMessage: 'Back to your dashboard', + })} + + + + + {i18n.translate('xpack.enterpriseSearch.notFound.action2', { + defaultMessage: 'Contact support', + })} + + + + } + /> +
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 23e24e343937d4..6a51b49869eafb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -8,6 +8,7 @@ import React, { useContext, useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; import { useActions, useValues } from 'kea'; +import { WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; import { IInitialAppData } from '../../../common/types'; import { KibanaContext, IKibanaContext } from '../index'; import { HttpLogic } from '../shared/http'; @@ -19,6 +20,7 @@ import { SETUP_GUIDE_PATH } from './routes'; import { SetupGuide } from './views/setup_guide'; import { ErrorState } from './views/error_state'; +import { NotFound } from '../shared/not_found'; import { Overview } from './views/overview'; export const WorkplaceSearch: React.FC = (props) => { @@ -53,6 +55,9 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { {/* Will replace with groups component subsequent PR */}
+ + + )} diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts index 189f8278f1b070..6532e06f7051cf 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts @@ -22,6 +22,7 @@ describe('App Search Telemetry Usage Collector', () => { 'ui_viewed.setup_guide': 10, 'ui_viewed.engines_overview': 20, 'ui_error.cannot_connect': 3, + 'ui_error.not_found': 7, 'ui_clicked.create_first_engine_button': 40, 'ui_clicked.header_launch_button': 50, 'ui_clicked.engine_table_link': 60, @@ -60,6 +61,7 @@ describe('App Search Telemetry Usage Collector', () => { }, ui_error: { cannot_connect: 3, + not_found: 7, }, ui_clicked: { create_first_engine_button: 40, @@ -86,6 +88,7 @@ describe('App Search Telemetry Usage Collector', () => { }, ui_error: { cannot_connect: 0, + not_found: 0, }, ui_clicked: { create_first_engine_button: 0, diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts index f700088cb67a03..ede776b6e0dce1 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts @@ -17,6 +17,7 @@ interface ITelemetry { }; ui_error: { cannot_connect: number; + not_found: number; }; ui_clicked: { create_first_engine_button: number; @@ -47,6 +48,7 @@ export const registerTelemetryUsageCollector = ( }, ui_error: { cannot_connect: { type: 'long' }, + not_found: { type: 'long' }, }, ui_clicked: { create_first_engine_button: { type: 'long' }, @@ -77,6 +79,7 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log }, ui_error: { cannot_connect: 0, + not_found: 0, }, ui_clicked: { create_first_engine_button: 0, @@ -97,6 +100,7 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log }, ui_error: { cannot_connect: get(savedObjectAttributes, 'ui_error.cannot_connect', 0), + not_found: get(savedObjectAttributes, 'ui_error.not_found', 0), }, ui_clicked: { create_first_engine_button: get( diff --git a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts index 8960d6fa9b67b4..f7d73cb232b8dd 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts @@ -22,6 +22,7 @@ describe('Workplace Search Telemetry Usage Collector', () => { 'ui_viewed.setup_guide': 10, 'ui_viewed.overview': 20, 'ui_error.cannot_connect': 3, + 'ui_error.not_found': 7, 'ui_clicked.header_launch_button': 30, 'ui_clicked.org_name_change_button': 40, 'ui_clicked.onboarding_card_button': 50, @@ -61,6 +62,7 @@ describe('Workplace Search Telemetry Usage Collector', () => { }, ui_error: { cannot_connect: 3, + not_found: 7, }, ui_clicked: { header_launch_button: 30, @@ -88,6 +90,7 @@ describe('Workplace Search Telemetry Usage Collector', () => { }, ui_error: { cannot_connect: 0, + not_found: 0, }, ui_clicked: { header_launch_button: 0, diff --git a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts index 892de5cfee35e0..cc08febf41f454 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts @@ -17,6 +17,7 @@ interface ITelemetry { }; ui_error: { cannot_connect: number; + not_found: number; }; ui_clicked: { header_launch_button: number; @@ -48,6 +49,7 @@ export const registerTelemetryUsageCollector = ( }, ui_error: { cannot_connect: { type: 'long' }, + not_found: { type: 'long' }, }, ui_clicked: { header_launch_button: { type: 'long' }, @@ -79,6 +81,7 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log }, ui_error: { cannot_connect: 0, + not_found: 0, }, ui_clicked: { header_launch_button: 0, @@ -100,6 +103,7 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log }, ui_error: { cannot_connect: get(savedObjectAttributes, 'ui_error.cannot_connect', 0), + not_found: get(savedObjectAttributes, 'ui_error.not_found', 0), }, ui_clicked: { header_launch_button: get(savedObjectAttributes, 'ui_clicked.header_launch_button', 0), diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 5280f094e3a5d2..2435d8a9aaf04c 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -30,6 +30,9 @@ "properties": { "cannot_connect": { "type": "long" + }, + "not_found": { + "type": "long" } } }, @@ -64,6 +67,9 @@ "properties": { "cannot_connect": { "type": "long" + }, + "not_found": { + "type": "long" } } }, From 7b7b013b977ec75a8c379b50ae42bb729c66b384 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Wed, 2 Sep 2020 08:59:58 -0700 Subject: [PATCH 04/25] [DOCS] Adds cloud icon to alerting settings (#76516) --- docs/settings/alert-action-settings.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index e02c7f212277ec..88858c36643ec8 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -37,12 +37,12 @@ You can configure the following settings in the `kibana.yml` file. [cols="2*<"] |=== -| `xpack.actions.whitelistedHosts` +| `xpack.actions.whitelistedHosts` {ess-icon} | A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly whitelisted. An empty list `[]` can be used to block built-in actions from making any external connections. + + Note that hosts associated with built-in actions, such as Slack and PagerDuty, are not automatically whitelisted. If you are not using the default `[*]` setting, you must ensure that the corresponding endpoints are whitelisted as well. -| `xpack.actions.enabledActionTypes` +| `xpack.actions.enabledActionTypes` {ess-icon} | A list of action types that are enabled. It defaults to `[*]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, and `.webhook`. An empty list `[]` will disable all action types. + + Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function. From 71b9dedfc4eadd97db2b6b0dc038d0f640babf7b Mon Sep 17 00:00:00 2001 From: John Schulz Date: Wed, 2 Sep 2020 12:07:32 -0400 Subject: [PATCH 05/25] [Ingest Manager] Remove success: true as top-level API response (#73223) * Big bang commit removing top-level success property in API response Left in check-permissions and in array of objects returned by delete package configs * Remove success property from mocks * Resolve conflict from upstream changes * Remove success property (after upstream merge) * Remove more 'success'es after merging in upstream * Remove success from some tests * Remove success from OpenAPI spec * Revert prior try/catch. Use res.ok Co-authored-by: Elastic Machine --- .../common/openapi/spec_oas3.json | 94 ++++--------------- .../common/types/rest_spec/agent.ts | 20 ++-- .../common/types/rest_spec/agent_policy.ts | 7 -- .../types/rest_spec/enrollment_api_key.ts | 4 - .../common/types/rest_spec/epm.ts | 6 -- .../common/types/rest_spec/output.ts | 3 - .../common/types/rest_spec/package_policy.ts | 3 - .../common/types/rest_spec/settings.ts | 2 - .../components/agent_policy_copy_provider.tsx | 6 +- .../agent_policy_delete_provider.tsx | 6 +- .../components/settings/index.tsx | 2 +- .../components/create_agent_policy.tsx | 2 +- .../scripts/dev_agent/script.ts | 2 +- .../server/routes/agent/acks_handlers.test.ts | 1 - .../server/routes/agent/acks_handlers.ts | 1 - .../routes/agent/actions_handlers.test.ts | 1 - .../server/routes/agent/actions_handlers.ts | 1 - .../server/routes/agent/handlers.ts | 13 +-- .../server/routes/agent/unenroll_handler.ts | 4 +- .../server/routes/agent_policy/handlers.ts | 8 +- .../routes/enrollment_api_key/handler.ts | 8 +- .../server/routes/epm/handlers.ts | 6 -- .../server/routes/output/handler.ts | 3 - .../server/routes/package_policy/handlers.ts | 6 +- .../server/routes/settings/index.ts | 2 - .../server/services/agent_policy.ts | 1 - .../store/mock_endpoint_result_list.ts | 3 - .../store/policy_list/test_mock_utils.ts | 2 - .../apis/agent_policy/agent_policy.ts | 7 +- .../apis/fleet/agents/acks.ts | 1 - .../apis/fleet/agents/actions.ts | 1 - .../apis/fleet/agents/checkin.ts | 1 - .../apis/fleet/agents/complete_flow.ts | 14 +-- .../apis/fleet/agents/delete.ts | 2 - .../apis/fleet/agents/enroll.ts | 3 +- .../apis/fleet/agents/list.ts | 6 +- .../apis/fleet/agents/unenroll.ts | 10 +- .../apis/fleet/enrollment_api_keys/crud.ts | 7 +- .../apis/package_policy/create.ts | 5 +- .../apis/package_policy/get.ts | 7 +- .../apis/package_policy/update.ts | 5 +- .../apis/artifacts/index.ts | 1 - 42 files changed, 58 insertions(+), 229 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json index 8c9ede1fde9ba1..d75a914e080d73 100644 --- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json +++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json @@ -43,12 +43,9 @@ }, "perPage": { "type": "number" - }, - "success": { - "type": "boolean" } }, - "required": ["items", "total", "page", "perPage", "success"] + "required": ["items", "total", "page", "perPage"] }, "examples": { "success": { @@ -71,8 +68,7 @@ ], "total": 1, "page": 1, - "perPage": 50, - "success": true + "perPage": 50 } } } @@ -107,9 +103,6 @@ "properties": { "item": { "$ref": "#/components/schemas/AgentPolicy" - }, - "success": { - "type": "boolean" } } } @@ -159,12 +152,9 @@ "properties": { "item": { "$ref": "#/components/schemas/AgentPolicy" - }, - "success": { - "type": "boolean" } }, - "required": ["item", "success"] + "required": ["item"] }, "examples": { "success": { @@ -707,8 +697,7 @@ "revision": 2, "updated_on": "2020-05-06T17:32:21.905Z", "updated_by": "system" - }, - "success": true + } } } } @@ -733,12 +722,9 @@ "properties": { "item": { "$ref": "#/components/schemas/AgentPolicy" - }, - "success": { - "type": "boolean" } }, - "required": ["item", "success"] + "required": ["item"] }, "examples": { "example-1": { @@ -751,8 +737,7 @@ "updated_on": "Fri Feb 28 2020 16:22:31 GMT-0500 (Eastern Standard Time)", "updated_by": "elastic", "packagePolicies": [] - }, - "success": true + } } } } @@ -810,12 +795,9 @@ "properties": { "item": { "$ref": "#/components/schemas/AgentPolicy" - }, - "success": { - "type": "boolean" } }, - "required": ["item", "success"] + "required": ["item"] } } } @@ -948,12 +930,9 @@ }, "perPage": { "type": "number" - }, - "success": { - "type": "boolean" } }, - "required": ["items", "success"] + "required": ["items"] }, "examples": { "example-1": { @@ -1157,8 +1136,7 @@ ], "total": 6, "page": 1, - "perPage": 20, - "success": true + "perPage": 20 } } } @@ -1251,12 +1229,9 @@ "properties": { "item": { "$ref": "#/components/schemas/PackagePolicy" - }, - "success": { - "type": "boolean" } }, - "required": ["item", "success"] + "required": ["item"] } } } @@ -1415,9 +1390,6 @@ "properties": { "response": { "$ref": "#/components/schemas/PackageInfo" - }, - "success": { - "type": "boolean" } } }, @@ -1709,8 +1681,7 @@ }, "references": [] } - }, - "success": true + } } }, "required-package": { @@ -1899,8 +1870,7 @@ }, "references": [] } - }, - "success": true + } } } } @@ -1950,12 +1920,9 @@ }, "required": ["id", "type"] } - }, - "success": { - "type": "boolean" } }, - "required": ["response", "success"] + "required": ["response"] } } } @@ -1994,12 +1961,9 @@ }, "required": ["id", "type"] } - }, - "success": { - "type": "boolean" } }, - "required": ["response", "success"] + "required": ["response"] } } } @@ -2678,8 +2642,7 @@ "version": "1.0.0", "status": "not_installed" } - ], - "success": true + ] } } } @@ -2743,9 +2706,6 @@ "type": "object" } }, - "success": { - "type": "boolean" - }, "total": { "type": "number" }, @@ -2756,7 +2716,7 @@ "type": "number" } }, - "required": ["list", "success", "total", "page", "perPage"] + "required": ["list", "total", "page", "perPage"] }, "examples": { "example-1": { @@ -2793,7 +2753,6 @@ "status": "online" } ], - "success": true, "total": 1, "page": 1, "perPage": 20 @@ -2836,12 +2795,9 @@ "properties": { "item": { "type": "object" - }, - "success": { - "type": "string" } }, - "required": ["item", "success"] + "required": ["item"] } } } @@ -2916,9 +2872,6 @@ "type": "string", "enum": ["checkin"] }, - "success": { - "type": "string" - }, "actions": { "type": "array", "items": { @@ -2950,7 +2903,6 @@ "success": { "value": { "action": "checkin", - "success": true, "actions": [ { "agent_id": "a6f14bd2-1a2a-481c-9212-9494d064ffdf", @@ -3346,21 +3298,17 @@ "schema": { "type": "object", "properties": { - "success": { - "type": "boolean" - }, "action": { "type": "string", "enum": ["acks"] } }, - "required": ["success", "action"] + "required": ["action"] }, "examples": { "success": { "value": { - "action": "checkin", - "success": true + "action": "checkin" } } } @@ -3417,9 +3365,6 @@ "action": { "type": "string" }, - "success": { - "type": "boolean" - }, "item": { "$ref": "#/components/schemas/Agent" } @@ -3429,7 +3374,6 @@ "success": { "value": { "action": "created", - "success": true, "item": { "id": "8086fb1a-72ca-4a67-8533-09300c1639fa", "active": true, diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index 4a50938049a7b2..cf8d3ab1c908ac 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -28,7 +28,6 @@ export interface GetAgentsResponse { total: number; page: number; perPage: number; - success: boolean; } export interface GetOneAgentRequest { @@ -39,7 +38,6 @@ export interface GetOneAgentRequest { export interface GetOneAgentResponse { item: Agent; - success: boolean; } export interface PostAgentCheckinRequest { @@ -55,7 +53,7 @@ export interface PostAgentCheckinRequest { export interface PostAgentCheckinResponse { action: string; - success: boolean; + actions: AgentAction[]; } @@ -72,7 +70,7 @@ export interface PostAgentEnrollRequest { export interface PostAgentEnrollResponse { action: string; - success: boolean; + item: Agent & { status: AgentStatus }; } @@ -87,7 +85,6 @@ export interface PostAgentAcksRequest { export interface PostAgentAcksResponse { action: string; - success: boolean; } export interface PostNewAgentActionRequest { @@ -100,7 +97,6 @@ export interface PostNewAgentActionRequest { } export interface PostNewAgentActionResponse { - success: boolean; item: AgentAction; } @@ -110,9 +106,8 @@ export interface PostAgentUnenrollRequest { }; } -export interface PostAgentUnenrollResponse { - success: boolean; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PostAgentUnenrollResponse {} export interface PutAgentReassignRequest { params: { @@ -121,9 +116,8 @@ export interface PutAgentReassignRequest { body: { policy_id: string }; } -export interface PutAgentReassignResponse { - success: boolean; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PutAgentReassignResponse {} export interface GetOneAgentEventsRequest { params: { @@ -141,7 +135,6 @@ export interface GetOneAgentEventsResponse { total: number; page: number; perPage: number; - success: boolean; } export interface DeleteAgentRequest { @@ -166,7 +159,6 @@ export interface GetAgentStatusRequest { } export interface GetAgentStatusResponse { - success: boolean; results: { events: number; total: number; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts index 4683727ed51ac3..aa9fbc20fc0b03 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts @@ -19,7 +19,6 @@ export interface GetAgentPoliciesResponse { total: number; page: number; perPage: number; - success: boolean; } export interface GetOneAgentPolicyRequest { @@ -30,7 +29,6 @@ export interface GetOneAgentPolicyRequest { export interface GetOneAgentPolicyResponse { item: AgentPolicy; - success: boolean; } export interface CreateAgentPolicyRequest { @@ -39,7 +37,6 @@ export interface CreateAgentPolicyRequest { export interface CreateAgentPolicyResponse { item: AgentPolicy; - success: boolean; } export type UpdateAgentPolicyRequest = GetOneAgentPolicyRequest & { @@ -48,7 +45,6 @@ export type UpdateAgentPolicyRequest = GetOneAgentPolicyRequest & { export interface UpdateAgentPolicyResponse { item: AgentPolicy; - success: boolean; } export interface CopyAgentPolicyRequest { @@ -57,7 +53,6 @@ export interface CopyAgentPolicyRequest { export interface CopyAgentPolicyResponse { item: AgentPolicy; - success: boolean; } export interface DeleteAgentPolicyRequest { @@ -68,7 +63,6 @@ export interface DeleteAgentPolicyRequest { export interface DeleteAgentPolicyResponse { id: string; - success: boolean; } export interface GetFullAgentPolicyRequest { @@ -79,5 +73,4 @@ export interface GetFullAgentPolicyRequest { export interface GetFullAgentPolicyResponse { item: FullAgentPolicy; - success: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/enrollment_api_key.ts index 1929955cac84c9..1d1c2f79bf6cb7 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/enrollment_api_key.ts @@ -19,7 +19,6 @@ export interface GetEnrollmentAPIKeysResponse { total: number; page: number; perPage: number; - success: boolean; } export interface GetOneEnrollmentAPIKeyRequest { @@ -30,7 +29,6 @@ export interface GetOneEnrollmentAPIKeyRequest { export interface GetOneEnrollmentAPIKeyResponse { item: EnrollmentAPIKey; - success: boolean; } export interface DeleteEnrollmentAPIKeyRequest { @@ -41,7 +39,6 @@ export interface DeleteEnrollmentAPIKeyRequest { export interface DeleteEnrollmentAPIKeyResponse { action: string; - success: boolean; } export interface PostEnrollmentAPIKeyRequest { @@ -55,5 +52,4 @@ export interface PostEnrollmentAPIKeyRequest { export interface PostEnrollmentAPIKeyResponse { action: string; item: EnrollmentAPIKey; - success: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts index 1901b8c0c70394..5fb718f91b8762 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts @@ -20,7 +20,6 @@ export interface GetCategoriesRequest { export interface GetCategoriesResponse { response: CategorySummaryList; - success: boolean; } export interface GetPackagesRequest { @@ -39,12 +38,10 @@ export interface GetPackagesResponse { > > >; - success: boolean; } export interface GetLimitedPackagesResponse { response: string[]; - success: boolean; } export interface GetFileRequest { @@ -62,7 +59,6 @@ export interface GetInfoRequest { export interface GetInfoResponse { response: PackageInfo; - success: boolean; } export interface InstallPackageRequest { @@ -73,7 +69,6 @@ export interface InstallPackageRequest { export interface InstallPackageResponse { response: AssetReference[]; - success: boolean; } export interface DeletePackageRequest { @@ -84,5 +79,4 @@ export interface DeletePackageRequest { export interface DeletePackageResponse { response: AssetReference[]; - success: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts index 41620603633818..87e8a0977e3ba7 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts @@ -7,7 +7,6 @@ import { Output } from '../models'; export interface GetOneOutputResponse { item: Output; - success: boolean; } export interface GetOneOutputRequest { @@ -28,7 +27,6 @@ export interface PutOutputRequest { export interface PutOutputResponse { item: Output; - success: boolean; } export interface GetOutputsResponse { @@ -36,5 +34,4 @@ export interface GetOutputsResponse { total: number; page: number; perPage: number; - success: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts index e5bba3d6deab3e..61669ab876d802 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts @@ -18,7 +18,6 @@ export interface GetPackagePoliciesResponse { total: number; page: number; perPage: number; - success: boolean; } export interface GetOnePackagePolicyRequest { @@ -29,7 +28,6 @@ export interface GetOnePackagePolicyRequest { export interface GetOnePackagePolicyResponse { item: PackagePolicy; - success: boolean; } export interface CreatePackagePolicyRequest { @@ -38,7 +36,6 @@ export interface CreatePackagePolicyRequest { export interface CreatePackagePolicyResponse { item: PackagePolicy; - success: boolean; } export type UpdatePackagePolicyRequest = GetOnePackagePolicyRequest & { diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts index c02a5e5878ee91..90d623b8a4be94 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts @@ -7,7 +7,6 @@ import { Settings } from '../models'; export interface GetSettingsResponse { item: Settings; - success: boolean; } export interface PutSettingsRequest { @@ -16,5 +15,4 @@ export interface PutSettingsRequest { export interface PutSettingsResponse { item: Settings; - success: boolean; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_copy_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_copy_provider.tsx index 8a91cabe78d00f..fefbe1fa82a0c5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_copy_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_copy_provider.tsx @@ -61,7 +61,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr try { const { data } = await sendCopyAgentPolicy(agentPolicy!.id, newAgentPolicy!); - if (data?.success) { + if (data) { notifications.toasts.addSuccess( i18n.translate('xpack.ingestManager.copyAgentPolicy.successNotificationTitle', { defaultMessage: 'Agent policy copied', @@ -70,9 +70,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr if (onSuccessCallback.current) { onSuccessCallback.current(data.item); } - } - - if (!data?.success) { + } else { notifications.toasts.addDanger( i18n.translate('xpack.ingestManager.copyAgentPolicy.failureNotificationTitle', { defaultMessage: "Error copying agent policy '{id}'", diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx index 08367bf2a97bfc..15f71689ecdfa1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx @@ -59,7 +59,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil agentPolicyId: agentPolicy!, }); - if (data?.success) { + if (data) { notifications.toasts.addSuccess( i18n.translate('xpack.ingestManager.deleteAgentPolicy.successSingleNotificationTitle', { defaultMessage: "Deleted agent policy '{id}'", @@ -69,9 +69,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil if (onSuccessCallback.current) { onSuccessCallback.current(agentPolicy!); } - } - - if (!data?.success) { + } else { notifications.toasts.addDanger( i18n.translate('xpack.ingestManager.deleteAgentPolicy.failureSingleNotificationTitle', { defaultMessage: "Error deleting agent policy '{id}'", diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/settings/index.tsx index 766bdc2e2c1930..b8fbb4f1835a6a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/settings/index.tsx @@ -82,7 +82,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( namespace, monitoring_enabled, }); - if (data?.success) { + if (data) { notifications.toasts.addSuccess( i18n.translate('xpack.ingestManager.editAgentPolicy.successNotificationTitle', { defaultMessage: "Successfully updated '{name}' settings", diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx index b072990727e3cc..e3e2975777e17b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx @@ -116,7 +116,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({ try { const { data, error } = await createAgentPolicy(); setIsLoading(false); - if (data?.success) { + if (data) { notifications.toasts.addSuccess( i18n.translate( 'xpack.ingestManager.createAgentPolicy.successNotificationTitle', diff --git a/x-pack/plugins/ingest_manager/scripts/dev_agent/script.ts b/x-pack/plugins/ingest_manager/scripts/dev_agent/script.ts index c7b8edd0c332ef..65375a076e9a48 100644 --- a/x-pack/plugins/ingest_manager/scripts/dev_agent/script.ts +++ b/x-pack/plugins/ingest_manager/scripts/dev_agent/script.ts @@ -122,7 +122,7 @@ async function enroll(kibanaURL: string, apiKey: string, log: ToolingLog): Promi }); const obj: PostAgentEnrollResponse = await res.json(); - if (!obj.success) { + if (!res.ok) { log.error(JSON.stringify(obj, null, 2)); throw new Error('unable to enroll'); } diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts index aaed189ae3dddf..33b9dc617075b3 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts @@ -88,7 +88,6 @@ describe('test acks handlers', () => { await postAgentAcksHandler(({} as unknown) as RequestHandlerContext, mockRequest, mockResponse); expect(mockResponse.ok.mock.calls[0][0]?.body as PostAgentAcksResponse).toEqual({ action: 'acks', - success: true, }); }); }); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts index 0b719d8a67df74..564f5d03e94502 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts @@ -46,7 +46,6 @@ export const postAgentAcksHandlerBuilder = function ( const body: PostAgentAcksResponse = { action: 'acks', - success: true, }; return response.ok({ body }); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts index bcb9a7797f26ab..5445a46fbe2b47 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts @@ -97,6 +97,5 @@ describe('test actions handlers', () => { ?.body as unknown) as PostNewAgentActionResponse; expect(expectedAgentActionResponse.item).toEqual(agentAction); - expect(expectedAgentActionResponse.success).toEqual(true); }); }); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts index 81893b1e78338c..b81d44c40f8eb8 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts @@ -35,7 +35,6 @@ export const postNewAgentActionHandlerBuilder = function ( }); const body: PostNewAgentActionResponse = { - success: true, item: savedAgentAction, }; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 3e80a74dc2b11f..2bce8daa6637e4 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -43,7 +43,6 @@ export const getAgentHandler: RequestHandler ({ agent_id: agent.id, type: a.type, @@ -248,7 +243,6 @@ export const postAgentEnrollHandler: RequestHandler< ); const body: PostAgentEnrollResponse = { action: 'created', - success: true, item: { ...agent, status: AgentService.getAgentStatus(agent), @@ -289,7 +283,6 @@ export const getAgentsHandler: RequestHandler< ...agent, status: AgentService.getAgentStatus(agent), })), - success: true, total, page, perPage, @@ -312,9 +305,7 @@ export const putAgentsReassignHandler: RequestHandler< try { await AgentService.reassignAgent(soClient, request.params.agentId, request.body.policy_id); - const body: PutAgentReassignResponse = { - success: true, - }; + const body: PutAgentReassignResponse = {}; return response.ok({ body }); } catch (e) { return response.customError({ @@ -336,7 +327,7 @@ export const getAgentStatusForAgentPolicyHandler: RequestHandler< request.query.policyId ); - const body: GetAgentStatusResponse = { results, success: true }; + const body: GetAgentStatusResponse = { results }; return response.ok({ body }); } catch (e) { diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts b/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts index d1e54fe4fb3a17..5df695d248f5b6 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts @@ -23,9 +23,7 @@ export const postAgentsUnenrollHandler: RequestHandler< await AgentService.unenrollAgent(soClient, request.params.agentId); } - const body: PostAgentUnenrollResponse = { - success: true, - }; + const body: PostAgentUnenrollResponse = {}; return response.ok({ body }); } catch (e) { return response.customError({ diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts index cc178e1038e2ea..7eb3df23461068 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts @@ -49,7 +49,6 @@ export const getAgentPoliciesHandler: RequestHandler< total, page, perPage, - success: true, }; await bluebird.map( @@ -82,7 +81,6 @@ export const getOneAgentPolicyHandler: RequestHandler { return { response: epmPackages, - success: true, }; }, @@ -114,7 +113,6 @@ const endpointListApiPathHandlerMocks = ({ return { items: [agentPolicy], total: 10, - success: true, perPage: 10, page: 1, }; @@ -132,7 +130,6 @@ const endpointListApiPathHandlerMocks = ({ page: 1, perPage: 10, total: endpointPackagePolicies?.length, - success: true, }; }, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts index 5ef01c00dbf166..1093aed0608d56 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -64,7 +64,6 @@ export const mockPolicyResultList: (options?: { total, page: requestPageIndex, perPage: requestPageSize, - success: true, }; return mock; }; @@ -81,7 +80,6 @@ export const policyListApiPathHandlers = (totalPolicies: number = 1) => { [INGEST_API_EPM_PACKAGES]: (): GetPackagesResponse => { return { response: [generator.generateEpmPackage()], - success: true, }; }, }; diff --git a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts index b55da0798c5f06..410b0fe0930024 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts @@ -14,7 +14,7 @@ export default function ({ getService }: FtrProviderContext) { describe('ingest_manager_agent_policies', () => { describe('POST /api/ingest_manager/agent_policies', () => { it('should work with valid values', async () => { - const { body: apiResponse } = await supertest + await supertest .post(`/api/ingest_manager/agent_policies`) .set('kbn-xsrf', 'xxxx') .send({ @@ -22,8 +22,6 @@ export default function ({ getService }: FtrProviderContext) { namespace: 'default', }) .expect(200); - - expect(apiResponse.success).to.be(true); }); it('should return a 400 with an empty namespace', async () => { @@ -61,7 +59,7 @@ export default function ({ getService }: FtrProviderContext) { it('should work with valid values', async () => { const { - body: { success, item }, + body: { item }, } = await supertest .post(`/api/ingest_manager/agent_policies/${TEST_POLICY_ID}/copy`) .set('kbn-xsrf', 'xxxx') @@ -73,7 +71,6 @@ export default function ({ getService }: FtrProviderContext) { // eslint-disable-next-line @typescript-eslint/naming-convention const { id, updated_at, ...newPolicy } = item; - expect(success).to.be(true); expect(newPolicy).to.eql({ name: 'Copied policy', description: 'Test', diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/acks.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/acks.ts index c9fa80c88762bf..1613ca9d11ee66 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/acks.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/acks.ts @@ -90,7 +90,6 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); expect(apiResponse.action).to.be('acks'); - expect(apiResponse.success).to.be(true); const { body: eventResponse } = await supertest .get(`/api/ingest_manager/fleet/agents/agent1/events`) .set('kbn-xsrf', 'xx') diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/actions.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/actions.ts index 8dc4e5c232b800..2b4bb335dfc5c9 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/actions.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/actions.ts @@ -33,7 +33,6 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(apiResponse.success).to.be(true); expect(apiResponse.item.data).to.eql({ data: 'action_data' }); expect(apiResponse.item.sent_at).to.be('2020-03-18T19:45:02.620Z'); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/checkin.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/checkin.ts index 79f6cfae175e19..fbfc94a0840b04 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/checkin.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/checkin.ts @@ -102,7 +102,6 @@ export default function (providerContext: FtrProviderContext) { .expect(200); expect(apiResponse.action).to.be('checkin'); - expect(apiResponse.success).to.be(true); }); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/complete_flow.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/complete_flow.ts index 2d14a7a10e6685..1d5b682d71c7ab 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/complete_flow.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/complete_flow.ts @@ -62,7 +62,6 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(200); - expect(enrollmentResponse.success).to.eql(true); const agentAccessAPIKey = enrollmentResponse.item.access_api_key; @@ -76,14 +75,13 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(checkinApiResponse.success).to.eql(true); expect(checkinApiResponse.actions).length(1); expect(checkinApiResponse.actions[0].type).be('CONFIG_CHANGE'); const policyChangeAction = checkinApiResponse.actions[0]; const defaultOutputApiKey = policyChangeAction.data.config.outputs.default.api_key; // Ack actions - const { body: ackApiResponse } = await supertestWithoutAuth + await supertestWithoutAuth .post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/acks`) .set('Authorization', `ApiKey ${agentAccessAPIKey}`) .set('kbn-xsrf', 'xx') @@ -102,7 +100,6 @@ export default function (providerContext: FtrProviderContext) { ], }) .expect(200); - expect(ackApiResponse.success).to.eql(true); // Second agent checkin const { body: secondCheckinApiResponse } = await supertestWithoutAuth @@ -113,7 +110,6 @@ export default function (providerContext: FtrProviderContext) { events: [], }) .expect(200); - expect(secondCheckinApiResponse.success).to.eql(true); expect(secondCheckinApiResponse.actions).length(0); // Get agent @@ -121,18 +117,16 @@ export default function (providerContext: FtrProviderContext) { .get(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}`) .expect(200); - expect(getAgentApiResponse.success).to.eql(true); expect(getAgentApiResponse.item.packages).to.contain( 'system', "Agent should run the 'system' package" ); // Unenroll agent - const { body: unenrollResponse } = await supertest + await supertest .post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/unenroll`) .set('kbn-xsrf', 'xx') .expect(200); - expect(unenrollResponse.success).to.eql(true); // Checkin after unenrollment const { body: checkinAfterUnenrollResponse } = await supertestWithoutAuth @@ -144,13 +138,12 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(checkinAfterUnenrollResponse.success).to.eql(true); expect(checkinAfterUnenrollResponse.actions).length(1); expect(checkinAfterUnenrollResponse.actions[0].type).be('UNENROLL'); const unenrollAction = checkinAfterUnenrollResponse.actions[0]; // ack unenroll actions - const { body: ackUnenrollApiResponse } = await supertestWithoutAuth + await supertestWithoutAuth .post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/acks`) .set('Authorization', `ApiKey ${agentAccessAPIKey}`) .set('kbn-xsrf', 'xx') @@ -168,7 +161,6 @@ export default function (providerContext: FtrProviderContext) { ], }) .expect(200); - expect(ackUnenrollApiResponse.success).to.eql(true); // Checkin after unenrollment acknowledged await supertestWithoutAuth diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/delete.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/delete.ts index dc05b7a4dd792e..65fbdabe8bd790 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/delete.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/delete.ts @@ -68,7 +68,6 @@ export default function ({ getService }: FtrProviderContext) { .expect(404); expect(apiResponse).not.to.eql({ - success: true, action: 'deleted', }); }); @@ -88,7 +87,6 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xx') .expect(200); expect(apiResponse).to.eql({ - success: true, action: 'deleted', }); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/enroll.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/enroll.ts index 93f656ea952d22..eef0d3beb69d07 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/enroll.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/enroll.ts @@ -136,7 +136,6 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(200); - expect(apiResponse.success).to.eql(true); expect(apiResponse.item).to.have.keys('id', 'active', 'access_api_key', 'type', 'policy_id'); }); @@ -158,7 +157,7 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(200); - expect(apiResponse.success).to.eql(true); + const { body: privileges } = await getEsClientForAPIKey( providerContext, apiResponse.item.access_api_key diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/list.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/list.ts index 23563c6f43bbec..1ee00ed7201692 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/list.ts @@ -78,8 +78,7 @@ export default function ({ getService }: FtrProviderContext) { .auth(users.fleet_admin.username, users.fleet_admin.password) .expect(200); - expect(apiResponse).to.have.keys('success', 'page', 'total', 'list'); - expect(apiResponse.success).to.eql(true); + expect(apiResponse).to.have.keys('page', 'total', 'list'); expect(apiResponse.total).to.eql(4); }); it('should return the list of agents when requesting as a user with fleet read permissions', async () => { @@ -87,8 +86,7 @@ export default function ({ getService }: FtrProviderContext) { .get(`/api/ingest_manager/fleet/agents`) .auth(users.fleet_user.username, users.fleet_user.password) .expect(200); - expect(apiResponse).to.have.keys('success', 'page', 'total', 'list'); - expect(apiResponse.success).to.eql(true); + expect(apiResponse).to.have.keys('page', 'total', 'list'); expect(apiResponse.total).to.eql(4); }); it('should not return the list of agents when requesting as a user without fleet permissions', async () => { diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts index d1ff8731183ba4..d24e438fa13eab 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts @@ -65,20 +65,17 @@ export default function (providerContext: FtrProviderContext) { }); it('allow to unenroll using a list of ids', async () => { - const { body } = await supertest + await supertest .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') .send({ force: true, }) .expect(200); - - expect(body).to.have.keys('success'); - expect(body.success).to.be(true); }); it('should invalidate related API keys', async () => { - const { body } = await supertest + await supertest .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') .send({ @@ -86,9 +83,6 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(body).to.have.keys('success'); - expect(body.success).to.be(true); - const { body: { api_keys: accessAPIKeys }, } = await esClient.security.getApiKey({ id: accessAPIKeyId }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts index 275462072919bb..6c5d552a51eb9c 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts @@ -67,13 +67,11 @@ export default function (providerContext: FtrProviderContext) { }); it('should invalide an existing api keys', async () => { - const { body: apiResponse } = await supertest + await supertest .delete(`/api/ingest_manager/fleet/enrollment-api-keys/${keyId}`) .set('kbn-xsrf', 'xxx') .expect(200); - expect(apiResponse.success).to.eql(true); - const { body: { api_keys: apiKeys }, } = await es.security.getApiKey({ id: esApiKeyId }); @@ -113,7 +111,6 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(apiResponse.success).to.eql(true); expect(apiResponse.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id'); }); @@ -125,7 +122,7 @@ export default function (providerContext: FtrProviderContext) { policy_id: 'policy1', }) .expect(200); - expect(apiResponse.success).to.eql(true); + const { body: privileges } = await getEsClientForAPIKey( providerContext, apiResponse.item.api_key diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_policy/create.ts b/x-pack/test/ingest_manager_api_integration/apis/package_policy/create.ts index c88f03de59615b..113fbeca494d8f 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/package_policy/create.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { warnAndSkipTest } from '../../helpers'; @@ -34,7 +33,7 @@ export default function ({ getService }: FtrProviderContext) { it('should work with valid values', async function () { if (server.enabled) { - const { body: apiResponse } = await supertest + await supertest .post(`/api/ingest_manager/package_policies`) .set('kbn-xsrf', 'xxxx') .send({ @@ -52,8 +51,6 @@ export default function ({ getService }: FtrProviderContext) { }, }) .expect(200); - - expect(apiResponse.success).to.be(true); } else { warnAndSkipTest(this, log); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_policy/get.ts b/x-pack/test/ingest_manager_api_integration/apis/package_policy/get.ts index 756eaa8ea49bab..53a400d2fd9ebf 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/package_policy/get.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/package_policy/get.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -75,11 +74,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should succeed with a valid id', async function () { - const { body: apiResponse } = await supertest - .get(`/api/ingest_manager/package_policies/${packagePolicyId}`) - .expect(200); - - expect(apiResponse.success).to.be(true); + await supertest.get(`/api/ingest_manager/package_policies/${packagePolicyId}`).expect(200); }); it('should return a 404 with an invalid id', async function () { diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_policy/update.ts b/x-pack/test/ingest_manager_api_integration/apis/package_policy/update.ts index e74d88f3538e4a..3c4cb93ee3ff3d 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/package_policy/update.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/package_policy/update.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -77,7 +76,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should work with valid values', async function () { - const { body: apiResponse } = await supertest + await supertest .put(`/api/ingest_manager/package_policies/${packagePolicyId}`) .set('kbn-xsrf', 'xxxx') .send({ @@ -95,8 +94,6 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(200); - - expect(apiResponse.success).to.be(true); }); it('should return a 500 if there is another package policy with the same name', async function () { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts index d5106f55499241..17a4182fe9371d 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts @@ -54,7 +54,6 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(200); - expect(enrollmentResponse.success).to.eql(true); agentAccessAPIKey = enrollmentResponse.item.access_api_key; }); From 1d4a56f7056f6627b823bcfaaea7de4e2d19c1bb Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 2 Sep 2020 11:15:01 -0700 Subject: [PATCH 06/25] [esArchiver] stop importing stream helpers from src (#76519) Co-authored-by: spalger --- .../src/lib/streams/concat_stream.test.js | 74 ++++++++++ .../src/lib/streams/concat_stream.ts | 41 ++++++ .../streams/concat_stream_providers.test.js | 66 +++++++++ .../lib/streams/concat_stream_providers.ts | 60 ++++++++ .../src/lib/streams/filter_stream.test.ts | 77 ++++++++++ .../{streams.ts => streams/filter_stream.ts} | 15 +- .../kbn-es-archiver/src/lib/streams/index.ts | 29 ++++ .../lib/streams/intersperse_stream.test.js | 54 +++++++ .../src/lib/streams/intersperse_stream.ts | 61 ++++++++ .../src/lib/streams/list_stream.test.js | 44 ++++++ .../src/lib/streams/list_stream.ts | 41 ++++++ .../src/lib/streams/map_stream.test.js | 61 ++++++++ .../src/lib/streams/map_stream.ts | 36 +++++ .../lib/streams/promise_from_streams.test.js | 136 ++++++++++++++++++ .../src/lib/streams/promise_from_streams.ts | 64 +++++++++ .../src/lib/streams/reduce_stream.test.js | 84 +++++++++++ .../src/lib/streams/reduce_stream.ts | 77 ++++++++++ .../src/lib/streams/replace_stream.test.js | 130 +++++++++++++++++ .../src/lib/streams/replace_stream.ts | 84 +++++++++++ .../src/lib/streams/split_stream.test.js | 71 +++++++++ .../src/lib/streams/split_stream.ts | 71 +++++++++ 21 files changed, 1375 insertions(+), 1 deletion(-) create mode 100644 packages/kbn-es-archiver/src/lib/streams/concat_stream.test.js create mode 100644 packages/kbn-es-archiver/src/lib/streams/concat_stream.ts create mode 100644 packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.test.js create mode 100644 packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.ts create mode 100644 packages/kbn-es-archiver/src/lib/streams/filter_stream.test.ts rename packages/kbn-es-archiver/src/lib/{streams.ts => streams/filter_stream.ts} (71%) create mode 100644 packages/kbn-es-archiver/src/lib/streams/index.ts create mode 100644 packages/kbn-es-archiver/src/lib/streams/intersperse_stream.test.js create mode 100644 packages/kbn-es-archiver/src/lib/streams/intersperse_stream.ts create mode 100644 packages/kbn-es-archiver/src/lib/streams/list_stream.test.js create mode 100644 packages/kbn-es-archiver/src/lib/streams/list_stream.ts create mode 100644 packages/kbn-es-archiver/src/lib/streams/map_stream.test.js create mode 100644 packages/kbn-es-archiver/src/lib/streams/map_stream.ts create mode 100644 packages/kbn-es-archiver/src/lib/streams/promise_from_streams.test.js create mode 100644 packages/kbn-es-archiver/src/lib/streams/promise_from_streams.ts create mode 100644 packages/kbn-es-archiver/src/lib/streams/reduce_stream.test.js create mode 100644 packages/kbn-es-archiver/src/lib/streams/reduce_stream.ts create mode 100644 packages/kbn-es-archiver/src/lib/streams/replace_stream.test.js create mode 100644 packages/kbn-es-archiver/src/lib/streams/replace_stream.ts create mode 100644 packages/kbn-es-archiver/src/lib/streams/split_stream.test.js create mode 100644 packages/kbn-es-archiver/src/lib/streams/split_stream.ts diff --git a/packages/kbn-es-archiver/src/lib/streams/concat_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/concat_stream.test.js new file mode 100644 index 00000000000000..1498334013d1ab --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/concat_stream.test.js @@ -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 { createListStream, createPromiseFromStreams, createConcatStream } from './'; + +describe('concatStream', () => { + test('accepts an initial value', async () => { + const output = await createPromiseFromStreams([ + createListStream([1, 2, 3]), + createConcatStream([0]), + ]); + + expect(output).toEqual([0, 1, 2, 3]); + }); + + describe(`combines using the previous value's concat method`, () => { + test('works with strings', async () => { + const output = await createPromiseFromStreams([ + createListStream(['a', 'b', 'c']), + createConcatStream(), + ]); + expect(output).toEqual('abc'); + }); + + test('works with arrays', async () => { + const output = await createPromiseFromStreams([ + createListStream([[1], [2, 3, 4], [10]]), + createConcatStream(), + ]); + expect(output).toEqual([1, 2, 3, 4, 10]); + }); + + test('works with a mixture, starting with array', async () => { + const output = await createPromiseFromStreams([ + createListStream([[], 1, 2, 3, 4, [5, 6, 7]]), + createConcatStream(), + ]); + expect(output).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + test('fails when the value does not have a concat method', async () => { + let promise; + try { + promise = createPromiseFromStreams([createListStream([1, '1']), createConcatStream()]); + } catch (err) { + throw new Error('createPromiseFromStreams() should not fail synchronously'); + } + + try { + await promise; + throw new Error('Promise should have rejected'); + } catch (err) { + expect(err).toBeInstanceOf(Error); + expect(err.message).toContain('concat'); + } + }); + }); +}); diff --git a/packages/kbn-es-archiver/src/lib/streams/concat_stream.ts b/packages/kbn-es-archiver/src/lib/streams/concat_stream.ts new file mode 100644 index 00000000000000..03dd894067afc5 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/concat_stream.ts @@ -0,0 +1,41 @@ +/* + * 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 { createReduceStream } from './reduce_stream'; + +/** + * Creates a Transform stream that consumes all provided + * values and concatenates them using each values `concat` + * method. + * + * Concatenate strings: + * createListStream(['f', 'o', 'o']) + * .pipe(createConcatStream()) + * .on('data', console.log) + * // logs "foo" + * + * Concatenate values into an array: + * createListStream([1,2,3]) + * .pipe(createConcatStream([])) + * .on('data', console.log) + * // logs "[1,2,3]" + */ +export function createConcatStream(initial: any) { + return createReduceStream((acc, chunk) => acc.concat(chunk), initial); +} diff --git a/packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.test.js b/packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.test.js new file mode 100644 index 00000000000000..b742a770b70c81 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.test.js @@ -0,0 +1,66 @@ +/* + * 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 { Readable } from 'stream'; + +import { concatStreamProviders } from './concat_stream_providers'; +import { createListStream } from './list_stream'; +import { createConcatStream } from './concat_stream'; +import { createPromiseFromStreams } from './promise_from_streams'; + +describe('concatStreamProviders() helper', () => { + test('writes the data from an array of stream providers into a destination stream in order', async () => { + const results = await createPromiseFromStreams([ + concatStreamProviders([ + () => createListStream(['foo', 'bar']), + () => createListStream(['baz']), + () => createListStream(['bug']), + ]), + createConcatStream(''), + ]); + + expect(results).toBe('foobarbazbug'); + }); + + test('emits the errors from a sub-stream to the destination', async () => { + const dest = concatStreamProviders([ + () => createListStream(['foo', 'bar']), + () => + new Readable({ + read() { + this.emit('error', new Error('foo')); + }, + }), + ]); + + const errorListener = jest.fn(); + dest.on('error', errorListener); + + await expect(createPromiseFromStreams([dest])).rejects.toThrowErrorMatchingInlineSnapshot( + `"foo"` + ); + expect(errorListener.mock.calls).toMatchInlineSnapshot(` +Array [ + Array [ + [Error: foo], + ], +] +`); + }); +}); diff --git a/packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.ts b/packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.ts new file mode 100644 index 00000000000000..4794d76cc7f846 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.ts @@ -0,0 +1,60 @@ +/* + * 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 { PassThrough, TransformOptions } from 'stream'; + +/** + * Write the data and errors from a list of stream providers + * to a single stream in order. Stream providers are only + * called right before they will be consumed, and only one + * provider will be active at a time. + */ +export function concatStreamProviders( + sourceProviders: Array<() => NodeJS.ReadableStream>, + options: TransformOptions = {} +) { + const destination = new PassThrough(options); + const queue = sourceProviders.slice(); + + (function pipeNext() { + const provider = queue.shift(); + + if (!provider) { + return; + } + + const source = provider(); + const isLast = !queue.length; + + // if there are more sources to pipe, hook + // into the source completion + if (!isLast) { + source.once('end', pipeNext); + } + + source + // proxy errors from the source to the destination + .once('error', (error) => destination.emit('error', error)) + // pipe the source to the destination but only proxy the + // end event if this is the last source + .pipe(destination, { end: isLast }); + })(); + + return destination; +} diff --git a/packages/kbn-es-archiver/src/lib/streams/filter_stream.test.ts b/packages/kbn-es-archiver/src/lib/streams/filter_stream.test.ts new file mode 100644 index 00000000000000..28b7f2588628e7 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/filter_stream.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { + createConcatStream, + createFilterStream, + createListStream, + createPromiseFromStreams, +} from './'; + +describe('createFilterStream()', () => { + test('calls the function with each item in the source stream', async () => { + const filter = jest.fn().mockReturnValue(true); + + await createPromiseFromStreams([createListStream(['a', 'b', 'c']), createFilterStream(filter)]); + + expect(filter).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + "a", + ], + Array [ + "b", + ], + Array [ + "c", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": true, + }, + Object { + "type": "return", + "value": true, + }, + Object { + "type": "return", + "value": true, + }, + ], + } + `); + }); + + test('send the filtered values on the output stream', async () => { + const result = await createPromiseFromStreams([ + createListStream([1, 2, 3]), + createFilterStream((n) => n % 2 === 0), + createConcatStream([]), + ]); + + expect(result).toMatchInlineSnapshot(` + Array [ + 2, + ] + `); + }); +}); diff --git a/packages/kbn-es-archiver/src/lib/streams.ts b/packages/kbn-es-archiver/src/lib/streams/filter_stream.ts similarity index 71% rename from packages/kbn-es-archiver/src/lib/streams.ts rename to packages/kbn-es-archiver/src/lib/streams/filter_stream.ts index a90afbe0c4d254..738b9d5793d064 100644 --- a/packages/kbn-es-archiver/src/lib/streams.ts +++ b/packages/kbn-es-archiver/src/lib/streams/filter_stream.ts @@ -17,4 +17,17 @@ * under the License. */ -export * from '../../../../src/legacy/utils/streams'; +import { Transform } from 'stream'; + +export function createFilterStream(fn: (obj: T) => boolean) { + return new Transform({ + objectMode: true, + async transform(obj, _, done) { + const canPushDownStream = fn(obj); + if (canPushDownStream) { + this.push(obj); + } + done(); + }, + }); +} diff --git a/packages/kbn-es-archiver/src/lib/streams/index.ts b/packages/kbn-es-archiver/src/lib/streams/index.ts new file mode 100644 index 00000000000000..447d1ed5b1c535 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { concatStreamProviders } from './concat_stream_providers'; +export { createIntersperseStream } from './intersperse_stream'; +export { createSplitStream } from './split_stream'; +export { createListStream } from './list_stream'; +export { createReduceStream } from './reduce_stream'; +export { createPromiseFromStreams } from './promise_from_streams'; +export { createConcatStream } from './concat_stream'; +export { createMapStream } from './map_stream'; +export { createReplaceStream } from './replace_stream'; +export { createFilterStream } from './filter_stream'; diff --git a/packages/kbn-es-archiver/src/lib/streams/intersperse_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/intersperse_stream.test.js new file mode 100644 index 00000000000000..e11b36d77106a6 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/intersperse_stream.test.js @@ -0,0 +1,54 @@ +/* + * 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 { + createPromiseFromStreams, + createListStream, + createIntersperseStream, + createConcatStream, +} from './'; + +describe('intersperseStream', () => { + test('places the intersperse value between each provided value', async () => { + expect( + await createPromiseFromStreams([ + createListStream(['to', 'be', 'or', 'not', 'to', 'be']), + createIntersperseStream(' '), + createConcatStream(), + ]) + ).toBe('to be or not to be'); + }); + + test('emits values as soon as possible, does not needlessly buffer', async () => { + const str = createIntersperseStream('y'); + const onData = jest.fn(); + str.on('data', onData); + + str.write('a'); + expect(onData).toHaveBeenCalledTimes(1); + expect(onData.mock.calls[0]).toEqual(['a']); + onData.mockClear(); + + str.write('b'); + expect(onData).toHaveBeenCalledTimes(2); + expect(onData.mock.calls[0]).toEqual(['y']); + expect(onData).toHaveBeenCalledTimes(2); + expect(onData.mock.calls[1]).toEqual(['b']); + }); +}); diff --git a/packages/kbn-es-archiver/src/lib/streams/intersperse_stream.ts b/packages/kbn-es-archiver/src/lib/streams/intersperse_stream.ts new file mode 100644 index 00000000000000..eb2e3d3087d4ad --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/intersperse_stream.ts @@ -0,0 +1,61 @@ +/* + * 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 { Transform } from 'stream'; + +/** + * Create a Transform stream that receives values in object mode, + * and intersperses a chunk between each object received. + * + * This is useful for writing lists: + * + * createListStream(['foo', 'bar']) + * .pipe(createIntersperseStream('\n')) + * .pipe(process.stdout) // outputs "foo\nbar" + * + * Combine with a concat stream to get "join" like functionality: + * + * await createPromiseFromStreams([ + * createListStream(['foo', 'bar']), + * createIntersperseStream(' '), + * createConcatStream() + * ]) // produces a single value "foo bar" + */ +export function createIntersperseStream(intersperseChunk: any) { + let first = true; + + return new Transform({ + writableObjectMode: true, + readableObjectMode: true, + transform(chunk, _, callback) { + try { + if (first) { + first = false; + } else { + this.push(intersperseChunk); + } + + this.push(chunk); + callback(undefined); + } catch (err) { + callback(err); + } + }, + }); +} diff --git a/packages/kbn-es-archiver/src/lib/streams/list_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/list_stream.test.js new file mode 100644 index 00000000000000..12e20696b05101 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/list_stream.test.js @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createListStream } from './'; + +describe('listStream', () => { + test('provides the values in the initial list', async () => { + const str = createListStream([1, 2, 3, 4]); + const onData = jest.fn(); + str.on('data', onData); + + await new Promise((resolve) => str.on('end', resolve)); + + expect(onData).toHaveBeenCalledTimes(4); + expect(onData.mock.calls[0]).toEqual([1]); + expect(onData.mock.calls[1]).toEqual([2]); + expect(onData.mock.calls[2]).toEqual([3]); + expect(onData.mock.calls[3]).toEqual([4]); + }); + + test('does not modify the list passed', async () => { + const list = [1, 2, 3, 4]; + const str = createListStream(list); + str.resume(); + await new Promise((resolve) => str.on('end', resolve)); + expect(list).toEqual([1, 2, 3, 4]); + }); +}); diff --git a/packages/kbn-es-archiver/src/lib/streams/list_stream.ts b/packages/kbn-es-archiver/src/lib/streams/list_stream.ts new file mode 100644 index 00000000000000..c061b969b3c099 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/list_stream.ts @@ -0,0 +1,41 @@ +/* + * 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 { Readable } from 'stream'; + +/** + * Create a Readable stream that provides the items + * from a list as objects to subscribers + */ +export function createListStream(items: any | any[] = []) { + const queue: any[] = [].concat(items); + + return new Readable({ + objectMode: true, + read(size) { + queue.splice(0, size).forEach((item) => { + this.push(item); + }); + + if (!queue.length) { + this.push(null); + } + }, + }); +} diff --git a/packages/kbn-es-archiver/src/lib/streams/map_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/map_stream.test.js new file mode 100644 index 00000000000000..d86da178f0c1b3 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/map_stream.test.js @@ -0,0 +1,61 @@ +/* + * 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 { delay } from 'bluebird'; + +import { createPromiseFromStreams } from './promise_from_streams'; +import { createListStream } from './list_stream'; +import { createMapStream } from './map_stream'; +import { createConcatStream } from './concat_stream'; + +describe('createMapStream()', () => { + test('calls the function with each item in the source stream', async () => { + const mapper = jest.fn(); + + await createPromiseFromStreams([createListStream(['a', 'b', 'c']), createMapStream(mapper)]); + + expect(mapper).toHaveBeenCalledTimes(3); + expect(mapper).toHaveBeenCalledWith('a', 0); + expect(mapper).toHaveBeenCalledWith('b', 1); + expect(mapper).toHaveBeenCalledWith('c', 2); + }); + + test('send the return value from the mapper on the output stream', async () => { + const result = await createPromiseFromStreams([ + createListStream([1, 2, 3]), + createMapStream((n) => n * 100), + createConcatStream([]), + ]); + + expect(result).toEqual([100, 200, 300]); + }); + + test('supports async mappers', async () => { + const result = await createPromiseFromStreams([ + createListStream([1, 2, 3]), + createMapStream(async (n, i) => { + await delay(n); + return n * i; + }), + createConcatStream([]), + ]); + + expect(result).toEqual([0, 2, 6]); + }); +}); diff --git a/packages/kbn-es-archiver/src/lib/streams/map_stream.ts b/packages/kbn-es-archiver/src/lib/streams/map_stream.ts new file mode 100644 index 00000000000000..e88c512a386535 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/map_stream.ts @@ -0,0 +1,36 @@ +/* + * 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 { Transform } from 'stream'; + +export function createMapStream(fn: (chunk: any, i: number) => T | Promise) { + let i = 0; + + return new Transform({ + objectMode: true, + async transform(value, _, done) { + try { + this.push(await fn(value, i++)); + done(); + } catch (err) { + done(err); + } + }, + }); +} diff --git a/packages/kbn-es-archiver/src/lib/streams/promise_from_streams.test.js b/packages/kbn-es-archiver/src/lib/streams/promise_from_streams.test.js new file mode 100644 index 00000000000000..e4d9835106f12c --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/promise_from_streams.test.js @@ -0,0 +1,136 @@ +/* + * 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 { Readable, Writable, Duplex, Transform } from 'stream'; + +import { createListStream, createPromiseFromStreams, createReduceStream } from './'; + +describe('promiseFromStreams', () => { + test('pipes together an array of streams', async () => { + const str1 = createListStream([1, 2, 3]); + const str2 = createReduceStream((acc, n) => acc + n, 0); + const sumPromise = new Promise((resolve) => str2.once('data', resolve)); + createPromiseFromStreams([str1, str2]); + await new Promise((resolve) => str2.once('end', resolve)); + expect(await sumPromise).toBe(6); + }); + + describe('last stream is writable', () => { + test('waits for the last stream to finish writing', async () => { + let written = ''; + + await createPromiseFromStreams([ + createListStream(['a']), + new Writable({ + write(chunk, enc, cb) { + setTimeout(() => { + written += chunk; + cb(); + }, 100); + }, + }), + ]); + + expect(written).toBe('a'); + }); + + test('resolves to undefined', async () => { + const result = await createPromiseFromStreams([ + createListStream(['a']), + new Writable({ + write(chunk, enc, cb) { + cb(); + }, + }), + ]); + + expect(result).toBe(undefined); + }); + }); + + describe('last stream is readable', () => { + test(`resolves to it's final value`, async () => { + const result = await createPromiseFromStreams([createListStream(['a', 'b', 'c'])]); + + expect(result).toBe('c'); + }); + }); + + describe('last stream is duplex', () => { + test('waits for writing and resolves to final value', async () => { + let written = ''; + + const duplexReadQueue = []; + const duplexItemsToPush = ['foo', 'bar', null]; + const result = await createPromiseFromStreams([ + createListStream(['a', 'b', 'c']), + new Duplex({ + async read() { + const result = await duplexReadQueue.shift(); + this.push(result); + }, + + write(chunk, enc, cb) { + duplexReadQueue.push( + new Promise((resolve) => { + setTimeout(() => { + written += chunk; + cb(); + resolve(duplexItemsToPush.shift()); + }, 50); + }) + ); + }, + }).setEncoding('utf8'), + ]); + + expect(written).toEqual('abc'); + expect(result).toBe('bar'); + }); + }); + + describe('error handling', () => { + test('read stream gets destroyed when transform stream fails', async () => { + let destroyCalled = false; + const readStream = new Readable({ + read() { + this.push('a'); + this.push('b'); + this.push('c'); + this.push(null); + }, + destroy() { + destroyCalled = true; + }, + }); + const transformStream = new Transform({ + transform(chunk, enc, done) { + done(new Error('Test error')); + }, + }); + try { + await createPromiseFromStreams([readStream, transformStream]); + throw new Error('Should fail'); + } catch (e) { + expect(e.message).toBe('Test error'); + expect(destroyCalled).toBe(true); + } + }); + }); +}); diff --git a/packages/kbn-es-archiver/src/lib/streams/promise_from_streams.ts b/packages/kbn-es-archiver/src/lib/streams/promise_from_streams.ts new file mode 100644 index 00000000000000..fefb18be147805 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/promise_from_streams.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Take an array of streams, pipe the output + * from each one into the next, listening for + * errors from any of the streams, and then resolve + * the promise once the final stream has finished + * writing/reading. + * + * If the last stream is readable, it's final value + * will be provided as the promise value. + * + * Errors emitted from any stream will cause + * the promise to be rejected with that error. + */ + +import { pipeline, Writable } from 'stream'; +import { promisify } from 'util'; + +const asyncPipeline = promisify(pipeline); + +export async function createPromiseFromStreams(streams: any): Promise { + let finalChunk: any; + const last = streams[streams.length - 1]; + if (typeof last.read !== 'function' && streams.length === 1) { + // For a nicer error than what stream.pipeline throws + throw new Error('A minimum of 2 streams is required when a non-readable stream is given'); + } + if (typeof last.read === 'function') { + // We are pushing a writable stream to capture the last chunk + streams.push( + new Writable({ + // Use object mode even when "last" stream isn't. This allows to + // capture the last chunk as-is. + objectMode: true, + write(chunk, _, done) { + finalChunk = chunk; + done(); + }, + }) + ); + } + + await asyncPipeline(...(streams as [any])); + + return finalChunk; +} diff --git a/packages/kbn-es-archiver/src/lib/streams/reduce_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/reduce_stream.test.js new file mode 100644 index 00000000000000..2c073f67f82a89 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/reduce_stream.test.js @@ -0,0 +1,84 @@ +/* + * 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 { createReduceStream, createPromiseFromStreams, createListStream } from './'; + +const promiseFromEvent = (name, emitter) => + new Promise((resolve) => emitter.on(name, () => resolve(name))); + +describe('reduceStream', () => { + test('calls the reducer for each item provided', async () => { + const stub = jest.fn(); + await createPromiseFromStreams([ + createListStream([1, 2, 3]), + createReduceStream((val, chunk, enc) => { + stub(val, chunk, enc); + return chunk; + }, 0), + ]); + expect(stub).toHaveBeenCalledTimes(3); + expect(stub.mock.calls[0]).toEqual([0, 1, 'utf8']); + expect(stub.mock.calls[1]).toEqual([1, 2, 'utf8']); + expect(stub.mock.calls[2]).toEqual([2, 3, 'utf8']); + }); + + test('provides the return value of the last iteration of the reducer', async () => { + const result = await createPromiseFromStreams([ + createListStream('abcdefg'.split('')), + createReduceStream((acc) => acc + 1, 0), + ]); + expect(result).toBe(7); + }); + + test('emits an error if an iteration fails', async () => { + const reduce = createReduceStream((acc, i) => expect(i).toBe(1), 0); + const errorEvent = promiseFromEvent('error', reduce); + + reduce.write(1); + reduce.write(2); + reduce.resume(); + await errorEvent; + }); + + test('stops calling the reducer if an iteration fails, emits no data', async () => { + const reducer = jest.fn((acc, i) => { + if (i < 100) return acc + i; + else throw new Error(i); + }); + const reduce$ = createReduceStream(reducer, 0); + + const dataStub = jest.fn(); + const errorStub = jest.fn(); + reduce$.on('data', dataStub); + reduce$.on('error', errorStub); + const endEvent = promiseFromEvent('end', reduce$); + + reduce$.write(1); + reduce$.write(2); + reduce$.write(300); + reduce$.write(400); + reduce$.write(1000); + reduce$.end(); + + await endEvent; + expect(reducer).toHaveBeenCalledTimes(3); + expect(dataStub).toHaveBeenCalledTimes(0); + expect(errorStub).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/kbn-es-archiver/src/lib/streams/reduce_stream.ts b/packages/kbn-es-archiver/src/lib/streams/reduce_stream.ts new file mode 100644 index 00000000000000..d9458e9a11c33a --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/reduce_stream.ts @@ -0,0 +1,77 @@ +/* + * 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 { Transform } from 'stream'; + +/** + * Create a transform stream that consumes each chunk it receives + * and passes it to the reducer, which will return the new value + * for the stream. Once all chunks have been received the reduce + * stream provides the result of final call to the reducer to + * subscribers. + */ +export function createReduceStream( + reducer: (acc: any, chunk: any, env: string) => any, + initial: any +) { + let i = -1; + let value = initial; + + // if the reducer throws an error then the value is + // considered invalid and the stream will never provide + // it to subscribers. We will also stop calling the + // reducer for any new data that is provided to us + let failed = false; + + if (typeof reducer !== 'function') { + throw new TypeError('reducer must be a function'); + } + + return new Transform({ + readableObjectMode: true, + writableObjectMode: true, + async transform(chunk, enc, callback) { + try { + if (failed) { + return callback(); + } + + i += 1; + if (i === 0 && initial === undefined) { + value = chunk; + } else { + value = await reducer(value, chunk, enc); + } + + callback(); + } catch (err) { + failed = true; + callback(err); + } + }, + + flush(callback) { + if (!failed) { + this.push(value); + } + + callback(); + }, + }); +} diff --git a/packages/kbn-es-archiver/src/lib/streams/replace_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/replace_stream.test.js new file mode 100644 index 00000000000000..01b89f93e5af06 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/replace_stream.test.js @@ -0,0 +1,130 @@ +/* + * 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 { + createReplaceStream, + createConcatStream, + createPromiseFromStreams, + createListStream, + createMapStream, +} from './'; + +async function concatToString(streams) { + return await createPromiseFromStreams([ + ...streams, + createMapStream((buff) => buff.toString('utf8')), + createConcatStream(''), + ]); +} + +describe('replaceStream', () => { + test('produces buffers when it receives buffers', async () => { + const chunks = await createPromiseFromStreams([ + createListStream([Buffer.from('foo'), Buffer.from('bar')]), + createReplaceStream('o', '0'), + createConcatStream([]), + ]); + + chunks.forEach((chunk) => { + expect(chunk).toBeInstanceOf(Buffer); + }); + }); + + test('produces buffers when it receives strings', async () => { + const chunks = await createPromiseFromStreams([ + createListStream(['foo', 'bar']), + createReplaceStream('o', '0'), + createConcatStream([]), + ]); + + chunks.forEach((chunk) => { + expect(chunk).toBeInstanceOf(Buffer); + }); + }); + + test('expects toReplace to be a string', () => { + expect(() => createReplaceStream(Buffer.from('foo'))).toThrowError(/be a string/); + }); + + test('replaces multiple single-char instances in a single chunk', async () => { + expect( + await concatToString([ + createListStream([Buffer.from('f00 bar')]), + createReplaceStream('0', 'o'), + ]) + ).toBe('foo bar'); + }); + + test('replaces multiple single-char instances in multiple chunks', async () => { + expect( + await concatToString([ + createListStream([Buffer.from('f0'), Buffer.from('0 bar')]), + createReplaceStream('0', 'o'), + ]) + ).toBe('foo bar'); + }); + + test('replaces single multi-char instances in single chunks', async () => { + expect( + await concatToString([ + createListStream([Buffer.from('f0'), Buffer.from('0 bar')]), + createReplaceStream('0', 'o'), + ]) + ).toBe('foo bar'); + }); + + test('replaces multiple multi-char instances in single chunks', async () => { + expect( + await concatToString([ + createListStream([Buffer.from('foo ba'), Buffer.from('r b'), Buffer.from('az bar')]), + createReplaceStream('bar', '*'), + ]) + ).toBe('foo * baz *'); + }); + + test('replaces multi-char instance that stretches multiple chunks', async () => { + expect( + await concatToString([ + createListStream([ + Buffer.from('foo supe'), + Buffer.from('rcalifra'), + Buffer.from('gilistic'), + Buffer.from('expialid'), + Buffer.from('ocious bar'), + ]), + createReplaceStream('supercalifragilisticexpialidocious', '*'), + ]) + ).toBe('foo * bar'); + }); + + test('ignores missing multi-char instance', async () => { + expect( + await concatToString([ + createListStream([ + Buffer.from('foo supe'), + Buffer.from('rcalifra'), + Buffer.from('gili stic'), + Buffer.from('expialid'), + Buffer.from('ocious bar'), + ]), + createReplaceStream('supercalifragilisticexpialidocious', '*'), + ]) + ).toBe('foo supercalifragili sticexpialidocious bar'); + }); +}); diff --git a/packages/kbn-es-archiver/src/lib/streams/replace_stream.ts b/packages/kbn-es-archiver/src/lib/streams/replace_stream.ts new file mode 100644 index 00000000000000..fe2ba1fcdf31c9 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/replace_stream.ts @@ -0,0 +1,84 @@ +/* + * 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 { Transform } from 'stream'; + +export function createReplaceStream(toReplace: string, replacement: string) { + if (typeof toReplace !== 'string') { + throw new TypeError('toReplace must be a string'); + } + + let buffer = Buffer.alloc(0); + return new Transform({ + objectMode: false, + async transform(value, _, done) { + try { + buffer = Buffer.concat([buffer, value], buffer.length + value.length); + + while (true) { + // try to find the next instance of `toReplace` in buffer + const index = buffer.indexOf(toReplace); + + // if there is no next instance, break + if (index === -1) { + break; + } + + // flush everything to the left of the next instance + // of `toReplace` + this.push(buffer.slice(0, index)); + + // then flush an instance of `replacement` + this.push(replacement); + + // and finally update the buffer to include everything + // to the right of `toReplace`, dropping to replace from the buffer + buffer = buffer.slice(index + toReplace.length); + } + + // until now we have only flushed data that is to the left + // of a discovered instance of `toReplace`. If `toReplace` is + // never found this would lead to us buffering the entire stream. + // + // Instead, we only keep enough buffer to complete a potentially + // partial instance of `toReplace` + if (buffer.length > toReplace.length) { + // the entire buffer except the last `toReplace.length` bytes + // so that if all but one byte from `toReplace` is in the buffer, + // and the next chunk delivers the necessary byte, the buffer will then + // contain a complete `toReplace` token. + this.push(buffer.slice(0, buffer.length - toReplace.length)); + buffer = buffer.slice(-toReplace.length); + } + + done(); + } catch (err) { + done(err); + } + }, + + flush(callback) { + if (buffer.length) { + this.push(buffer); + } + + callback(); + }, + }); +} diff --git a/packages/kbn-es-archiver/src/lib/streams/split_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/split_stream.test.js new file mode 100644 index 00000000000000..e0736d220ba5ca --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/split_stream.test.js @@ -0,0 +1,71 @@ +/* + * 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 { createSplitStream, createConcatStream, createPromiseFromStreams } from './'; + +async function split(stream, input) { + const concat = createConcatStream(); + concat.write([]); + stream.pipe(concat); + const output = createPromiseFromStreams([concat]); + + input.forEach((i) => { + stream.write(i); + }); + stream.end(); + + return await output; +} + +describe('splitStream', () => { + test('splits buffers, produces strings', async () => { + const output = await split(createSplitStream('&'), [Buffer.from('foo&bar')]); + expect(output).toEqual(['foo', 'bar']); + }); + + test('supports mixed input', async () => { + const output = await split(createSplitStream('&'), [Buffer.from('foo&b'), 'ar']); + expect(output).toEqual(['foo', 'bar']); + }); + + test('supports buffer split chunks', async () => { + const output = await split(createSplitStream(Buffer.from('&')), ['foo&b', 'ar']); + expect(output).toEqual(['foo', 'bar']); + }); + + test('splits provided values by a delimiter', async () => { + const output = await split(createSplitStream('&'), ['foo&b', 'ar']); + expect(output).toEqual(['foo', 'bar']); + }); + + test('handles multi-character delimiters', async () => { + const output = await split(createSplitStream('oo'), ['foo&b', 'ar']); + expect(output).toEqual(['f', '&bar']); + }); + + test('handles delimiters that span multiple chunks', async () => { + const output = await split(createSplitStream('ba'), ['foo&b', 'ar']); + expect(output).toEqual(['foo&', 'r']); + }); + + test('produces an empty chunk if the split char is at the end of the input', async () => { + const output = await split(createSplitStream('&bar'), ['foo&b', 'ar']); + expect(output).toEqual(['foo', '']); + }); +}); diff --git a/packages/kbn-es-archiver/src/lib/streams/split_stream.ts b/packages/kbn-es-archiver/src/lib/streams/split_stream.ts new file mode 100644 index 00000000000000..1c9b59449bd927 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/split_stream.ts @@ -0,0 +1,71 @@ +/* + * 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 { Transform } from 'stream'; + +/** + * Creates a Transform stream that consumes a stream of Buffers + * and produces a stream of strings (in object mode) by splitting + * the received bytes using the splitChunk. + * + * Ways this is behaves like String#split: + * - instances of splitChunk are removed from the input + * - splitChunk can be on any size + * - if there are no bytes found after the last splitChunk + * a final empty chunk is emitted + * + * Ways this deviates from String#split: + * - splitChunk cannot be a regexp + * - an empty string or Buffer will not produce a stream of individual + * bytes like `string.split('')` would + */ +export function createSplitStream(splitChunk: string) { + let unsplitBuffer = Buffer.alloc(0); + + return new Transform({ + writableObjectMode: false, + readableObjectMode: true, + transform(chunk, _, callback) { + try { + let i; + let toSplit = Buffer.concat([unsplitBuffer, chunk]); + while ((i = toSplit.indexOf(splitChunk)) !== -1) { + const slice = toSplit.slice(0, i); + toSplit = toSplit.slice(i + splitChunk.length); + this.push(slice.toString('utf8')); + } + + unsplitBuffer = toSplit; + callback(undefined); + } catch (err) { + callback(err); + } + }, + + flush(callback) { + try { + this.push(unsplitBuffer.toString('utf8')); + + callback(undefined); + } catch (err) { + callback(err); + } + }, + }); +} From de3ddbc6e1a986229ad2033b24af7bba6b353497 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 2 Sep 2020 14:22:55 -0400 Subject: [PATCH 07/25] [ML] DF Analytics jobs list: improve job details analysis stats view in expanded row (#76196) * move analysis stats to own section in expanded row * fix type errors * move stats to separate tab in expanded row * handle parameters and hyperparameters not defined --- .../analytics_list/expanded_row.tsx | 78 ++++++++++++++++--- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index 645c6c276a6f9f..95204f9ba09fcd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; import { DataFrameAnalyticsListRow } from './common'; -import { ExpandedRowDetailsPane, SectionConfig } from './expanded_row_details_pane'; +import { ExpandedRowDetailsPane, SectionConfig, SectionItem } from './expanded_row_details_pane'; import { ExpandedRowJsonPane } from './expanded_row_json_pane'; import { ProgressBar } from './progress_bar'; import { @@ -28,6 +28,7 @@ import { getTaskStateBadge } from './use_columns'; import { getDataFrameAnalyticsProgressPhase, isCompletedAnalyticsJob } from './common'; import { isRegressionAnalysis, + getAnalysisType, ANALYSIS_CONFIG_TYPE, REGRESSION_STATS, isRegressionEvaluateResponse, @@ -76,6 +77,7 @@ export const ExpandedRow: FC = ({ item }) => { const resultsField = item.config.dest.results_field; const jobIsCompleted = isCompletedAnalyticsJob(item.stats); const isRegressionJob = isRegressionAnalysis(item.config.analysis); + const analysisType = getAnalysisType(item.config.analysis); const loadData = async () => { setIsLoadingGeneralization(true); @@ -160,25 +162,34 @@ export const ExpandedRow: FC = ({ item }) => { const stateValues: any = { ...item.stats }; + const analysisStatsValues = stateValues.analysis_stats + ? stateValues.analysis_stats[`${analysisType}_stats`] + : undefined; + if (item.config?.description) { stateValues.description = item.config.description; } delete stateValues.progress; - const state: SectionConfig = { - title: i18n.translate('xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state', { - defaultMessage: 'State', - }), - items: Object.entries(stateValues).map(([stateKey, stateValue]) => { + const stateItems = Object.entries(stateValues) + .map(([stateKey, stateValue]) => { const title = stateKey.toString(); if (title === 'state') { return { title, description: getTaskStateBadge(getItemDescription(stateValue)), }; + } else if (title !== 'analysis_stats') { + return { title, description: getItemDescription(stateValue) }; } - return { title, description: getItemDescription(stateValue) }; + }) + .filter((stateItem) => stateItem !== undefined); + + const state: SectionConfig = { + title: i18n.translate('xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state', { + defaultMessage: 'State', }), + items: stateItems as SectionItem[], position: 'left', }; @@ -204,7 +215,7 @@ export const ExpandedRow: FC = ({ item }) => { }; }), ], - position: 'left', + position: 'right', }; const stats: SectionConfig = { @@ -221,9 +232,39 @@ export const ExpandedRow: FC = ({ item }) => { { title: 'model_memory_limit', description: item.config.model_memory_limit }, { title: 'version', description: item.config.version }, ], - position: 'right', + position: 'left', }; + const analysisStats: SectionConfig | undefined = analysisStatsValues + ? { + title: i18n.translate( + 'xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.analysisStats', + { + defaultMessage: 'Analysis stats', + } + ), + items: [ + { + title: 'timestamp', + description: formatHumanReadableDateTimeSeconds( + moment(analysisStatsValues.timestamp).unix() * 1000 + ), + }, + { + title: 'timing_stats', + description: getItemDescription(analysisStatsValues.timing_stats), + }, + ...Object.entries( + analysisStatsValues.parameters || analysisStatsValues.hyperparameters || {} + ).map(([stateKey, stateValue]) => { + const title = stateKey.toString(); + return { title, description: getItemDescription(stateValue) }; + }), + ], + position: 'right', + } + : undefined; + if (jobIsCompleted && isRegressionJob) { stats.items.push( { @@ -309,13 +350,30 @@ export const ExpandedRow: FC = ({ item }) => { ); } + const detailsSections: SectionConfig[] = [state, progress]; + const statsSections: SectionConfig[] = [stats]; + + if (analysisStats !== undefined) { + statsSections.push(analysisStats); + } + const tabs = [ { id: 'ml-analytics-job-details', name: i18n.translate('xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettingsLabel', { defaultMessage: 'Job details', }), - content: , + content: , + }, + { + id: 'ml-analytics-job-stats', + name: i18n.translate( + 'xpack.ml.dataframe.analyticsList.analyticsDetails.tabs.analyticsStatsLabel', + { + defaultMessage: 'Job stats', + } + ), + content: , }, { id: 'ml-analytics-job-json', From b8822da21e48a944ffb5248c800f09c904b91add Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 2 Sep 2020 20:45:52 +0200 Subject: [PATCH 08/25] Bump end-of-stream from v1.4.1 to v1.4.4 (#76479) --- packages/kbn-pm/dist/index.js | 11 +++++++++-- yarn.lock | 9 +-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index eb2d0d2581a347..9a3bb1c687032d 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -27651,6 +27651,7 @@ var eos = function(stream, opts, callback) { var rs = stream._readableState; var readable = opts.readable || (opts.readable !== false && stream.readable); var writable = opts.writable || (opts.writable !== false && stream.writable); + var cancelled = false; var onlegacyfinish = function() { if (!stream.writable) onfinish(); @@ -27675,8 +27676,13 @@ var eos = function(stream, opts, callback) { }; var onclose = function() { - if (readable && !(rs && rs.ended)) return callback.call(stream, new Error('premature close')); - if (writable && !(ws && ws.ended)) return callback.call(stream, new Error('premature close')); + process.nextTick(onclosenexttick); + }; + + var onclosenexttick = function() { + if (cancelled) return; + if (readable && !(rs && (rs.ended && !rs.destroyed))) return callback.call(stream, new Error('premature close')); + if (writable && !(ws && (ws.ended && !ws.destroyed))) return callback.call(stream, new Error('premature close')); }; var onrequest = function() { @@ -27701,6 +27707,7 @@ var eos = function(stream, opts, callback) { stream.on('close', onclose); return function() { + cancelled = true; stream.removeListener('complete', onfinish); stream.removeListener('abort', onclose); stream.removeListener('request', onrequest); diff --git a/yarn.lock b/yarn.lock index a37ae19b15a06e..56530a0ab4fdc0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11392,14 +11392,7 @@ encoding@^0.1.11: dependencies: iconv-lite "~0.4.13" -end-of-stream@^1.0.0, end-of-stream@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" - integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== - dependencies: - once "^1.4.0" - -end-of-stream@^1.4.1, end-of-stream@^1.4.4: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== From 535eb4069e24d3f725286affedd72f06997330d0 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Wed, 2 Sep 2020 14:53:35 -0400 Subject: [PATCH 09/25] [SECURITY_SOLUTION][ENDPOINT] Trusted app delete api (#76435) * Trusted app delete api --- .../common/endpoint/constants.ts | 1 + .../common/endpoint/schema/trusted_apps.ts | 6 +++ .../endpoint/routes/trusted_apps/handlers.ts | 29 ++++++++++++ .../endpoint/routes/trusted_apps/index.ts | 18 +++++++- .../routes/trusted_apps/trusted_apps.test.ts | 45 ++++++++++++++++++- .../endpoint/routes/trusted_apps/types.ts | 10 +++++ 6 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index b72a52f0a0eb72..366bf7a1df1f24 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -14,3 +14,4 @@ export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100; export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps'; export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps'; +export const TRUSTED_APPS_DELETE_API = '/api/endpoint/trusted_apps/{id}'; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 7535b23a10e8af..7c0de84b637c9a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -6,6 +6,12 @@ import { schema } from '@kbn/config-schema'; +export const DeleteTrustedAppsRequestSchema = { + params: schema.object({ + id: schema.string(), + }), +}; + export const GetTrustedAppsRequestSchema = { query: schema.object({ page: schema.maybe(schema.number({ defaultValue: 1, min: 1 })), diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts index 977683ab55495a..a3e6f54f3eee80 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts @@ -13,6 +13,35 @@ import { import { EndpointAppContext } from '../../types'; import { exceptionItemToTrustedAppItem, newTrustedAppItemToExceptionItem } from './utils'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; +import { DeleteTrustedAppsRequestParams } from './types'; + +export const getTrustedAppsDeleteRouteHandler = ( + endpointAppContext: EndpointAppContext +): RequestHandler => { + const logger = endpointAppContext.logFactory.get('trusted_apps'); + + return async (context, req, res) => { + const exceptionsListService = endpointAppContext.service.getExceptionsList(); + + try { + const { id } = req.params; + const response = await exceptionsListService.deleteExceptionListItem({ + id, + itemId: undefined, + namespaceType: 'agnostic', + }); + + if (response === null) { + return res.notFound({ body: `trusted app id [${id}] not found` }); + } + + return res.ok(); + } catch (error) { + logger.error(error); + return res.internalError({ body: error }); + } + }; +}; export const getTrustedAppsListRouteHandler = ( endpointAppContext: EndpointAppContext diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts index 1302b10533ccf6..5f502555ee153d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts @@ -6,20 +6,36 @@ import { IRouter } from 'kibana/server'; import { + DeleteTrustedAppsRequestSchema, GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema, } from '../../../../common/endpoint/schema/trusted_apps'; import { TRUSTED_APPS_CREATE_API, + TRUSTED_APPS_DELETE_API, TRUSTED_APPS_LIST_API, } from '../../../../common/endpoint/constants'; -import { getTrustedAppsCreateRouteHandler, getTrustedAppsListRouteHandler } from './handlers'; +import { + getTrustedAppsCreateRouteHandler, + getTrustedAppsDeleteRouteHandler, + getTrustedAppsListRouteHandler, +} from './handlers'; import { EndpointAppContext } from '../../types'; export const registerTrustedAppsRoutes = ( router: IRouter, endpointAppContext: EndpointAppContext ) => { + // DELETE one + router.delete( + { + path: TRUSTED_APPS_DELETE_API, + validate: DeleteTrustedAppsRequestSchema, + options: { authRequired: true }, + }, + getTrustedAppsDeleteRouteHandler(endpointAppContext) + ); + // GET list router.get( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts index 488c8390411b04..2325036ef40aeb 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts @@ -9,11 +9,12 @@ import { createMockEndpointAppContext, createMockEndpointAppContextServiceStartContract, } from '../../mocks'; -import { IRouter, RequestHandler } from 'kibana/server'; +import { IRouter, KibanaRequest, RequestHandler } from 'kibana/server'; import { httpServerMock, httpServiceMock } from '../../../../../../../src/core/server/mocks'; import { registerTrustedAppsRoutes } from './index'; import { TRUSTED_APPS_CREATE_API, + TRUSTED_APPS_DELETE_API, TRUSTED_APPS_LIST_API, } from '../../../../common/endpoint/constants'; import { @@ -26,6 +27,7 @@ import { EndpointAppContext } from '../../types'; import { ExceptionListClient } from '../../../../../lists/server'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response'; +import { DeleteTrustedAppsRequestParams } from './types'; describe('when invoking endpoint trusted apps route handlers', () => { let routerMock: jest.Mocked; @@ -220,4 +222,45 @@ describe('when invoking endpoint trusted apps route handlers', () => { expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); }); }); + + describe('when deleting a trusted app', () => { + let routeHandler: RequestHandler; + let request: KibanaRequest; + + beforeEach(() => { + [, routeHandler] = routerMock.delete.mock.calls.find(([{ path }]) => + path.startsWith(TRUSTED_APPS_DELETE_API) + )!; + + request = httpServerMock.createKibanaRequest({ + path: TRUSTED_APPS_DELETE_API.replace('{id}', '123'), + method: 'delete', + }); + }); + + it('should return 200 on successful delete', async () => { + await routeHandler(context, request, response); + expect(exceptionsListClient.deleteExceptionListItem).toHaveBeenCalledWith({ + id: request.params.id, + itemId: undefined, + namespaceType: 'agnostic', + }); + expect(response.ok).toHaveBeenCalled(); + }); + + it('should return 404 if item does not exist', async () => { + exceptionsListClient.deleteExceptionListItem.mockResolvedValueOnce(null); + await routeHandler(context, request, response); + expect(response.notFound).toHaveBeenCalled(); + }); + + it('should log unexpected error if one occurs', async () => { + exceptionsListClient.deleteExceptionListItem.mockImplementation(() => { + throw new Error('expected error for delete'); + }); + await routeHandler(context, request, response); + expect(response.internalError).toHaveBeenCalled(); + expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts new file mode 100644 index 00000000000000..13c8bcfc207932 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts @@ -0,0 +1,10 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { DeleteTrustedAppsRequestSchema } from '../../../../common/endpoint/schema/trusted_apps'; + +export type DeleteTrustedAppsRequestParams = TypeOf; From 6f94bf7d2aaac6f329456357b0f4164a6a8cbe30 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 2 Sep 2020 13:55:02 -0500 Subject: [PATCH 10/25] [Enterprise Search] Migrate shared components used in Workplace Search Groups (#76345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add shared images and export file * Migrate SourceIcon component * Add util to format routes We need a way to format routes using parametrized routes in Workplace Search. We used to the `react-router-named-routes` library, but it is 5 years old has has no typings. I extracted the function we needed into a util. * Update recent_activity to use formatRoute For the 7.9 Kibana MVP, we used a temporary function to generate this route. Aligning this component to use the helper other parts of the app will use after migration. * Add types and constants Various types and constants migrated for use in groups component * Migrate SourceRow component Also adds a mock for contentSources. For now the array only has one item needed for this test, but in future tests, more sources will be added to that array. * Migrate SourcesTable component * Migrate TablePaginationBar component * Migrate UserIcon component * Migrate UserRow component * Refactor format_route Uses Object.entries instead of for…in. This is more modern and allows for 100% test coverage. Also removed `toString()` as `encodeURIComponent()` seems to automatically cast to string * Remove unused import * Use forEach instead of map We’re not returning anything so map is not needed * Minify images * Remove formatRoute in favor of generatePath Unike formatRoute, generatePath adds the trailing slash automatically * Stop renaming methods Co-authored-by: Elastic Machine --- .../__mocks__/content_sources.mock.ts | 36 ++++ .../workplace_search/__mocks__/users.mock.ts | 17 ++ .../components/shared/assets/box.svg | 1 + .../components/shared/assets/confluence.svg | 1 + .../shared/assets/connection_illustration.svg | 1 + .../components/shared/assets/crawler.svg | 1 + .../components/shared/assets/custom.svg | 1 + .../components/shared/assets/drive.svg | 1 + .../components/shared/assets/dropbox.svg | 1 + .../components/shared/assets/github.svg | 1 + .../components/shared/assets/gmail.svg | 1 + .../components/shared/assets/google.svg | 1 + .../components/shared/assets/google_drive.svg | 1 + .../components/shared/assets/index.ts | 53 ++++++ .../components/shared/assets/jira.svg | 1 + .../components/shared/assets/jira_server.svg | 1 + .../shared/assets/loading_small.svg | 1 + .../components/shared/assets/office365.svg | 1 + .../components/shared/assets/one_drive.svg | 1 + .../components/shared/assets/outlook.svg | 1 + .../components/shared/assets/people.svg | 1 + .../components/shared/assets/salesforce.svg | 1 + .../components/shared/assets/service_now.svg | 1 + .../components/shared/assets/share_point.svg | 1 + .../components/shared/assets/slack.svg | 1 + .../components/shared/assets/zendesk.svg | 1 + .../components/shared/source_icon/index.ts | 7 + .../shared/source_icon/source_icon.test.tsx | 27 +++ .../shared/source_icon/source_icon.tsx | 34 ++++ .../components/shared/source_row/index.ts | 7 + .../shared/source_row/source_row.test.tsx | 83 +++++++++ .../shared/source_row/source_row.tsx | 171 ++++++++++++++++++ .../components/shared/sources_table/index.ts | 7 + .../sources_table/sources_table.test.tsx | 35 ++++ .../shared/sources_table/sources_table.tsx | 44 +++++ .../shared/table_pagination_bar/index.ts | 7 + .../table_pagination_bar.test.tsx | 35 ++++ .../table_pagination_bar.tsx | 82 +++++++++ .../components/shared/user_icon/index.ts | 7 + .../shared/user_icon/user_icon.test.tsx | 42 +++++ .../components/shared/user_icon/user_icon.tsx | 19 ++ .../components/shared/user_row/index.ts | 7 + .../shared/user_row/user_row.test.tsx | 29 +++ .../components/shared/user_row/user_row.tsx | 23 +++ .../workplace_search/constants.ts | 19 ++ .../workplace_search/routes.test.tsx | 38 ++++ .../applications/workplace_search/routes.ts | 8 +- .../applications/workplace_search/types.ts | 40 ++++ .../views/overview/recent_activity.tsx | 4 +- 49 files changed, 901 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/users.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/confluence.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/connection_illustration.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/crawler.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/custom.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/drive.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/dropbox.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/github.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/gmail.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google_drive.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira_server.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/loading_small.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/office365.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/one_drive.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/outlook.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/people.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/salesforce.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/service_now.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/share_point.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/slack.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/zendesk.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts new file mode 100644 index 00000000000000..7789d0caba3458 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -0,0 +1,36 @@ +/* + * 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 const contentSources = [ + { + id: '123', + serviceType: 'custom', + searchable: true, + supportedByLicense: true, + status: 'foo', + statusMessage: 'bar', + name: 'source', + documentCount: '123', + isFederatedSource: false, + errorReason: 0, + allowsReauth: true, + boost: 1, + }, + { + id: '123', + serviceType: 'jira', + searchable: true, + supportedByLicense: true, + status: 'synced', + statusMessage: 'all green', + name: 'Jira', + documentCount: '34234', + isFederatedSource: false, + errorReason: 0, + allowsReauth: true, + boost: 0.5, + }, +]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/users.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/users.mock.ts new file mode 100644 index 00000000000000..0294803042ada7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/users.mock.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. + */ + +export const users = [ + { + id: '1z1z', + name: 'John Does', + pictureUrl: 'http://google.cats', + color: '#ededed', + initials: 'JD', + email: 'jd@elastic.co', + groupIds: [], + }, +]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg new file mode 100644 index 00000000000000..1e7324d9581a78 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/confluence.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/confluence.svg new file mode 100644 index 00000000000000..23eff13915401d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/confluence.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/connection_illustration.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/connection_illustration.svg new file mode 100644 index 00000000000000..c1c3c3b3ee9eeb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/connection_illustration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/crawler.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/crawler.svg new file mode 100644 index 00000000000000..d241989f1aff16 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/crawler.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/custom.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/custom.svg new file mode 100644 index 00000000000000..f8f6415dea22b9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/custom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/drive.svg new file mode 100644 index 00000000000000..40b65df3a1ce3a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/dropbox.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/dropbox.svg new file mode 100644 index 00000000000000..d16f293fde6dc6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/dropbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/github.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/github.svg new file mode 100644 index 00000000000000..c4b4176560d5b1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/gmail.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/gmail.svg new file mode 100644 index 00000000000000..ee824f730aca67 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/gmail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google.svg new file mode 100644 index 00000000000000..22630f533dcbf2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google_drive.svg new file mode 100644 index 00000000000000..59469d814e35fd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google_drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/index.ts new file mode 100644 index 00000000000000..5f93694da09b8f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/index.ts @@ -0,0 +1,53 @@ +/* + * 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 box from './box.svg'; +import confluence from './confluence.svg'; +import crawler from './crawler.svg'; +import custom from './custom.svg'; +import drive from './drive.svg'; +import dropbox from './dropbox.svg'; +import github from './github.svg'; +import gmail from './gmail.svg'; +import google from './google.svg'; +import googleDrive from './google_drive.svg'; +import jira from './jira.svg'; +import jiraServer from './jira_server.svg'; +import loadingSmall from './loading_small.svg'; +import office365 from './office365.svg'; +import oneDrive from './one_drive.svg'; +import outlook from './outlook.svg'; +import people from './people.svg'; +import salesforce from './salesforce.svg'; +import serviceNow from './service_now.svg'; +import sharePoint from './share_point.svg'; +import slack from './slack.svg'; +import zendesk from './zendesk.svg'; + +export const images = { + box, + confluence, + crawler, + custom, + drive, + dropbox, + github, + gmail, + googleDrive, + google, + jira, + jiraServer, + loadingSmall, + office365, + oneDrive, + outlook, + people, + salesforce, + serviceNow, + sharePoint, + slack, + zendesk, +} as { [key: string]: string }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira.svg new file mode 100644 index 00000000000000..224bb822a581ca --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira_server.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira_server.svg new file mode 100644 index 00000000000000..71750fb6e25a01 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira_server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/loading_small.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/loading_small.svg new file mode 100644 index 00000000000000..159408ea029386 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/loading_small.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/office365.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/office365.svg new file mode 100644 index 00000000000000..fdce5d02da3cda --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/office365.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/one_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/one_drive.svg new file mode 100644 index 00000000000000..1856e5e3ce1af8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/one_drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/outlook.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/outlook.svg new file mode 100644 index 00000000000000..2680bc99cc3677 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/outlook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/people.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/people.svg new file mode 100644 index 00000000000000..4500c494c23b77 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/people.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/salesforce.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/salesforce.svg new file mode 100644 index 00000000000000..510c438a281951 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/salesforce.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/service_now.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/service_now.svg new file mode 100644 index 00000000000000..2d0c09db4e1c31 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/service_now.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/share_point.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/share_point.svg new file mode 100644 index 00000000000000..8724be9da88cf0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/share_point.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/slack.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/slack.svg new file mode 100644 index 00000000000000..14dbd0289da848 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/zendesk.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/zendesk.svg new file mode 100644 index 00000000000000..f7bc1fda0c9ac7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/zendesk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/index.ts new file mode 100644 index 00000000000000..da0d11cc1fdc5c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/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 { SourceIcon } from './source_icon'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx new file mode 100644 index 00000000000000..dd37ba9b6d8590 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx @@ -0,0 +1,27 @@ +/* + * 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 '../../../../__mocks__/shallow_usecontext.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SourceIcon } from './'; + +describe('SourceIcon', () => { + it('renders unwrapped icon', () => { + const wrapper = shallow(); + + expect(wrapper.find('img')).toHaveLength(1); + expect(wrapper.find('.user-group-source')).toHaveLength(0); + }); + + it('renders wrapped icon', () => { + const wrapper = shallow(); + + expect(wrapper.find('.user-group-source')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx new file mode 100644 index 00000000000000..ddbe327a40a305 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx @@ -0,0 +1,34 @@ +/* + * 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 _camelCase from 'lodash/camelCase'; + +import { images } from '../assets'; + +interface ISourceIconProps { + serviceType: string; + name: string; + className?: string; + wrapped?: boolean; +} + +export const SourceIcon: React.FC = ({ + name, + serviceType, + className, + wrapped, +}) => { + const icon = {name}; + return wrapped ? ( +
+ {icon} +
+ ) : ( + <>{icon} + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/index.ts new file mode 100644 index 00000000000000..f7a99b59837a5d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/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 { SourceRow, ISourceRow } from './source_row'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx new file mode 100644 index 00000000000000..2bde3b70f82b50 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx @@ -0,0 +1,83 @@ +/* + * 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 '../../../../__mocks__/shallow_usecontext.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiTableRow, EuiSwitch, EuiIcon } from '@elastic/eui'; +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import { SourceIcon } from '../source_icon'; + +import { SourceRow } from './'; + +const onToggle = jest.fn(); + +describe('SourceRow', () => { + it('renders with no "Fix" link', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTableRow)).toHaveLength(1); + expect(wrapper.contains('Fix')).toBeFalsy(); + expect(wrapper.find(SourceIcon).prop('serviceType')).toEqual('custom'); + }); + + it('calls handler on click', () => { + const wrapper = shallow(); + wrapper.find(EuiSwitch).simulate('change', { target: { checked: true } }); + + expect(onToggle).toHaveBeenCalled(); + }); + + it('renders "Fix" link', () => { + const source = { + ...contentSources[0], + status: 'error', + errorReason: 1, + }; + const wrapper = shallow(); + + expect(wrapper.contains('Fix')).toBeTruthy(); + }); + + it('renders loading icon when indexing', () => { + const source = { + ...contentSources[0], + status: 'indexing', + }; + const wrapper = shallow(); + + expect(wrapper.find(SourceIcon).prop('serviceType')).toEqual('loadingSmall'); + }); + + it('renders warning dot when more config needed', () => { + const source = { + ...contentSources[0], + status: 'need-more-config', + }; + const wrapper = shallow(); + + expect(wrapper.find(EuiIcon).prop('color')).toEqual('warning'); + }); + + it('renders remote tooltip when source is federated', () => { + const source = { + ...contentSources[0], + isFederatedSource: true, + }; + const wrapper = shallow(); + + expect(wrapper.find('.source-row__document-count').contains('Remote')).toBeTruthy(); + }); + + it('renders details link', () => { + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="SourceDetailsLink"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx new file mode 100644 index 00000000000000..17ca8e58a80fab --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx @@ -0,0 +1,171 @@ +/* + * 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 classNames from 'classnames'; +import _kebabCase from 'lodash/kebabCase'; +import { Link } from 'react-router-dom'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSwitch, + EuiSwitchEvent, + EuiTableRow, + EuiTableRowCell, + EuiText, + EuiToolTip, +} from '@elastic/eui'; + +import { SOURCE_STATUSES as statuses } from '../../../constants'; +import { IContentSourceDetails } from '../../../types'; +import { ADD_SOURCE_PATH, SOURCE_DETAILS_PATH, getContentSourcePath } from '../../../routes'; + +import { SourceIcon } from '../source_icon'; + +const CREDENTIALS_INVALID_ERROR_REASON = 1; + +export interface ISourceRow { + showDetails?: boolean; + isOrganization?: boolean; + onSearchableToggle?(sourceId: string, isSearchable: boolean): void; +} + +interface ISourceRowProps extends ISourceRow { + source: IContentSourceDetails; +} + +export const SourceRow: React.FC = ({ + source: { + id, + serviceType, + searchable, + supportedByLicense, + status, + statusMessage, + name, + documentCount, + isFederatedSource, + errorReason, + allowsReauth, + }, + onSearchableToggle, + isOrganization, + showDetails, +}) => { + const isIndexing = status === statuses.INDEXING; + const hasError = status === statuses.ERROR || status === statuses.DISCONNECTED; + const showFix = + isOrganization && hasError && allowsReauth && errorReason === CREDENTIALS_INVALID_ERROR_REASON; + + const rowClass = classNames( + 'source-row', + { 'content-section--disabled': !searchable }, + { 'source-row source-row--error': hasError } + ); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const imageClass = classNames('source-row__icon', { 'source-row__icon--loading': isIndexing }); + + const fixLink = ( + + Fix + + ); + + const remoteTooltip = ( + <> + Remote + + + + + ); + + return ( + + + + + + + + {name} + + + + + + {status === 'need-more-config' && ( + + + + )} + + + {statusMessage} + + + + + + {isFederatedSource ? remoteTooltip : parseInt(documentCount, 10).toLocaleString('en-US')} + + {onSearchableToggle && ( + + onSearchableToggle(id, e.target.checked)} + disabled={!supportedByLicense} + compressed + label="Source Searchable Toggle" + showLabel={false} + data-test-subj="SourceSearchableToggle" + /> + + )} + + + {showFix && {fixLink}} + + {showDetails && ( + + Details + + )} + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/index.ts new file mode 100644 index 00000000000000..bb5f490ed472b1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/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 { SourcesTable } from './sources_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx new file mode 100644 index 00000000000000..dfac8525471b49 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx @@ -0,0 +1,35 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { EuiTable } from '@elastic/eui'; +import { TableHeader } from '../../../../shared/table_header/table_header'; +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import { SourceRow } from '../source_row'; + +import { SourcesTable } from './'; + +const onToggle = jest.fn(); + +describe('SourcesTable', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(SourceRow)).toHaveLength(2); + }); + + it('renders "Searchable" header item when toggle fn present', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(TableHeader).prop('headerItems')).toContain('Searchable'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx new file mode 100644 index 00000000000000..184aadc1dad848 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx @@ -0,0 +1,44 @@ +/* + * 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 { EuiTable, EuiTableBody } from '@elastic/eui'; + +import { TableHeader } from '../../../../shared/table_header/table_header'; +import { SourceRow, ISourceRow } from '../source_row'; +import { IContentSourceDetails } from '../../../types'; + +interface ISourcesTableProps extends ISourceRow { + sources: IContentSourceDetails[]; +} + +export const SourcesTable: React.FC = ({ + sources, + showDetails, + isOrganization, + onSearchableToggle, +}) => { + const headerItems = ['Source', 'Status', 'Documents']; + if (onSearchableToggle) headerItems.push('Searchable'); + + return ( + + + + {sources.map((source) => ( + + ))} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/index.ts new file mode 100644 index 00000000000000..1598ee760c059a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/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 { TablePaginationBar } from './table_pagination_bar'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx new file mode 100644 index 00000000000000..14d4dfecd0094d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx @@ -0,0 +1,35 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { EuiFlexGroup, EuiTablePagination } from '@elastic/eui'; + +import { TablePaginationBar } from './'; + +const onChange = jest.fn(); + +describe('TablePaginationBar', () => { + it('renders', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiTablePagination)).toHaveLength(1); + expect(wrapper.find(EuiFlexGroup)).toHaveLength(2); + expect(wrapper.find(EuiTablePagination).prop('itemsPerPage')).toEqual(10); + expect(wrapper.find(EuiTablePagination).prop('pageCount')).toEqual(1); + }); + + it('passes max page count when showAllPages false', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiTablePagination).prop('pageCount')).toEqual(100); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.tsx new file mode 100644 index 00000000000000..04f5b9a477058a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.tsx @@ -0,0 +1,82 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiTablePagination } from '@elastic/eui'; + +interface ITablePaginationBarProps { + itemLabel?: string; + itemsPerPage?: number; + totalPages: number; + totalItems: number; + activePage?: number; + hidePerPageOptions?: boolean; + hideLabelCount?: boolean; + clearFiltersLink?: React.ReactElement; + onChangePage(nextPage: number): void; +} + +const MAX_PAGES = 100; + +export const TablePaginationBar: React.FC = ({ + itemLabel = 'Items', + itemsPerPage = 10, + totalPages, + totalItems, + activePage = 1, + hidePerPageOptions = true, + hideLabelCount = false, + onChangePage, + clearFiltersLink, +}) => { + // EUI component starts page at 0. API starts at 1. + const currentPage = activePage - 1; + const showAllPages = totalPages < MAX_PAGES; + + const pageRangeText = () => { + const rangeEnd = activePage === totalPages ? totalItems : itemsPerPage * activePage; + const rangeStart = currentPage * itemsPerPage + 1; + const rangeEl = ( + + {rangeStart}-{rangeEnd} + + ); + const totalEl = {totalItems.toLocaleString()}; + + return ( + + +
+ Showing {rangeEl} + {showAllPages && ( + <> + {' '} + of {totalEl} {itemLabel} + + )} +
+
+ {clearFiltersLink} +
+ ); + }; + + return ( + + {!hideLabelCount && {pageRangeText()}} + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/index.ts new file mode 100644 index 00000000000000..e9ee92bed68cad --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/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 { UserIcon } from './user_icon'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx new file mode 100644 index 00000000000000..f8c1d00047825f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx @@ -0,0 +1,42 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { users } from '../../../__mocks__/users.mock'; + +import { UserIcon } from './user_icon'; + +describe('SourcesTable', () => { + it('renders with picture', () => { + const wrapper = shallow(); + + expect(wrapper.find('.avatar')).toHaveLength(1); + expect(wrapper.find('.avatar__image')).toHaveLength(1); + }); + + it('renders without picture', () => { + const user = { + ...users[0], + pictureUrl: null, + }; + const wrapper = shallow(); + + expect(wrapper.find('.avatar')).toHaveLength(1); + expect(wrapper.find('.avatar__text')).toHaveLength(1); + }); + + it('renders fallback "alt" value when name not present', () => { + const user = { + ...users[0], + name: null, + }; + const wrapper = shallow(); + + expect(wrapper.find('img').prop('alt')).toEqual(user.email); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.tsx new file mode 100644 index 00000000000000..28ea303d14eaf9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.tsx @@ -0,0 +1,19 @@ +/* + * 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 { IUser } from '../../../types'; + +export const UserIcon: React.FC = ({ name, pictureUrl, color, initials, email }) => ( +
+ {pictureUrl ? ( + {name + ) : ( + {initials} + )} +
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/index.ts new file mode 100644 index 00000000000000..cc4650d70dad03 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/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 { UserRow } from './user_row'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx new file mode 100644 index 00000000000000..20d2732e8c82a3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx @@ -0,0 +1,29 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { EuiTableRow } from '@elastic/eui'; + +import { users } from '../../../__mocks__/users.mock'; + +import { UserRow } from './'; + +describe('SourcesTable', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTableRow)).toHaveLength(1); + expect(wrapper.find('span')).toHaveLength(0); + }); + + it('renders with email visible', () => { + const wrapper = shallow(); + + expect(wrapper.find('span')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.tsx new file mode 100644 index 00000000000000..45866c10a5b217 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.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 React from 'react'; + +import { EuiTableRow, EuiTableRowCell } from '@elastic/eui'; + +import { IUser } from '../../../types'; + +interface IUserRowProps { + user: IUser; + showEmail?: boolean; +} + +export const UserRow: React.FC = ({ user: { name, email }, showEmail }) => ( + + {name} + {showEmail && {email}} + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts new file mode 100644 index 00000000000000..9f313a6995ad58 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -0,0 +1,19 @@ +/* + * 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 const MAX_TABLE_ROW_ICONS = 3; + +export const SOURCE_STATUSES = { + INDEXING: 'indexing', + SYNCED: 'synced', + SYNCING: 'syncing', + AWAITING_USER_ACTION: 'awaiting_user_action', + ERROR: 'error', + DISCONNECTED: 'disconnected', + ALWAYS_SYNCED: 'always_synced', +}; + +export const CUSTOM_SERVICE_TYPE = 'custom'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx new file mode 100644 index 00000000000000..d03c0abb441b98 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { EuiLink } from '@elastic/eui'; + +import { + getContentSourcePath, + SOURCES_PATH, + ORG_SOURCES_PATH, + SOURCE_DETAILS_PATH, +} from './routes'; + +const TestComponent = ({ id, isOrg }: { id: string; isOrg?: boolean }) => { + const href = getContentSourcePath(SOURCE_DETAILS_PATH, id, !!isOrg); + return test; +}; + +describe('getContentSourcePath', () => { + it('should format org route', () => { + const wrapper = shallow(); + const path = wrapper.find(EuiLink).prop('href'); + + expect(path).toEqual(`${ORG_SOURCES_PATH}/123`); + }); + + it('should format user route', () => { + const wrapper = shallow(); + const path = wrapper.find(EuiLink).prop('href'); + + expect(path).toEqual(`${SOURCES_PATH}/123`); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 993a1a378e7385..e833dde4c1b728 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { generatePath } from 'react-router-dom'; + import { CURRENT_MAJOR_VERSION } from '../../../common/version'; export const SETUP_GUIDE_PATH = '/setup_guide'; @@ -107,4 +109,8 @@ export const EDIT_SLACK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/slack/edit`; export const EDIT_ZENDESK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/zendesk/edit`; export const EDIT_CUSTOM_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/custom/edit`; -export const getSourcePath = (sourceId: string): string => `${ORG_SOURCES_PATH}/${sourceId}`; +export const getContentSourcePath = ( + path: string, + sourceId: string, + isOrganization: boolean +): string => generatePath(isOrganization ? ORG_PATH + path : path, { sourceId }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index a8348a6f69a394..3866da738cbb61 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -7,3 +7,43 @@ export * from '../../../common/types/workplace_search'; export type TSpacerSize = 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'; + +export interface IGroup { + id: string; + name: string; + createdAt: string; + updatedAt: string; + contentSources: IContentSource[]; + users: IUser[]; + usersCount: number; + color?: string; +} + +export interface IUser { + id: string; + name: string | null; + initials: string; + pictureUrl: string | null; + color: string; + email: string; + role?: string; + groupIds: string[]; +} + +export interface IContentSource { + id: string; + serviceType: string; + name: string; +} + +export interface IContentSourceDetails extends IContentSource { + status: string; + statusMessage: string; + documentCount: string; + isFederatedSource: boolean; + searchable: boolean; + supportedByLicense: boolean; + errorReason: number; + allowsReauth: boolean; + boost: number; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index 3c476be8d10e66..441f45a947a493 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -15,7 +15,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ContentSection } from '../../components/shared/content_section'; import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; -import { getSourcePath } from '../../routes'; +import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes'; import { OverviewLogic } from './overview_logic'; @@ -107,7 +107,7 @@ export const RecentActivityItem: React.FC = ({ const linkProps = { onClick, target: '_blank', - href: getWorkplaceSearchUrl(getSourcePath(sourceId)), + href: getWorkplaceSearchUrl(getContentSourcePath(SOURCE_DETAILS_PATH, sourceId, true)), external: true, color: status === 'error' ? 'danger' : 'primary', 'data-test-subj': 'viewSourceDetailsLink', From d9dc47ef369ff91c5e569ebbc534969f78c950be Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 2 Sep 2020 12:17:23 -0700 Subject: [PATCH 11/25] skip flaky suite (#76551) --- .../reporting_api_integration/reporting_and_security/spaces.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts index 0145ca2a180927..6a68bd530cf63b 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts @@ -27,7 +27,8 @@ export default function ({ getService }: FtrProviderContext) { ); }; - describe('Exports from Non-default Space', () => { + // FLAKY: https://github.com/elastic/kibana/issues/76551 + describe.skip('Exports from Non-default Space', () => { before(async () => { await esArchiver.load('reporting/ecommerce'); await esArchiver.load('reporting/ecommerce_kibana_spaces'); // dashboard in non default space From aac84240b237ec33aa72d986137f9decc5f64c38 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Wed, 2 Sep 2020 14:52:10 -0500 Subject: [PATCH 12/25] [ML] Extend MlUrlGenerator to support other ML pages (#75696) Co-authored-by: Dima Arnautov --- .../ml/common/constants/ml_url_generator.ts | 38 +++ .../ml/common/types/ml_url_generator.ts | 187 +++++++++++++ .../application/contexts/kibana/index.ts | 1 + .../contexts/kibana/use_create_url.ts | 20 ++ .../contexts/kibana/use_navigate_to_path.ts | 25 +- .../back_to_list_panel/back_to_list_panel.tsx | 13 +- .../view_results_panel/view_results_panel.tsx | 19 +- .../explorer/explorer_dashboard_service.ts | 19 +- .../ml/public/application/routing/router.tsx | 1 - .../application/routing/routes/explorer.tsx | 3 +- .../anomaly_detection_urls_generator.ts | 163 +++++++++++ .../ml/public/ml_url_generator/common.ts | 55 ++++ .../data_frame_analytics_urls_generator.ts | 70 +++++ .../data_visualizer_urls_generator.ts | 29 ++ .../ml/public/ml_url_generator/index.ts | 6 + .../ml_url_generator/ml_url_generator.test.ts | 262 ++++++++++++++++++ .../ml_url_generator/ml_url_generator.ts | 91 ++++++ x-pack/plugins/ml/public/plugin.ts | 2 +- .../open_in_anomaly_explorer_action.tsx | 30 +- .../plugins/ml/public/url_generator.test.ts | 34 --- x-pack/plugins/ml/public/url_generator.ts | 118 -------- .../lib/sample_data_sets/sample_data_sets.ts | 4 +- 22 files changed, 982 insertions(+), 208 deletions(-) create mode 100644 x-pack/plugins/ml/common/constants/ml_url_generator.ts create mode 100644 x-pack/plugins/ml/common/types/ml_url_generator.ts create mode 100644 x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/common.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/index.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts delete mode 100644 x-pack/plugins/ml/public/url_generator.test.ts delete mode 100644 x-pack/plugins/ml/public/url_generator.ts diff --git a/x-pack/plugins/ml/common/constants/ml_url_generator.ts b/x-pack/plugins/ml/common/constants/ml_url_generator.ts new file mode 100644 index 00000000000000..44f33aa329e7ae --- /dev/null +++ b/x-pack/plugins/ml/common/constants/ml_url_generator.ts @@ -0,0 +1,38 @@ +/* + * 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 const ML_APP_URL_GENERATOR = 'ML_APP_URL_GENERATOR'; + +export const ML_PAGES = { + ANOMALY_DETECTION_JOBS_MANAGE: 'jobs', + ANOMALY_EXPLORER: 'explorer', + SINGLE_METRIC_VIEWER: 'timeseriesexplorer', + DATA_FRAME_ANALYTICS_JOBS_MANAGE: 'data_frame_analytics', + DATA_FRAME_ANALYTICS_EXPLORATION: 'data_frame_analytics/exploration', + /** + * Page: Data Visualizer + */ + DATA_VISUALIZER: 'datavisualizer', + /** + * Page: Data Visualizer + * Open data visualizer by selecting a Kibana index pattern or saved search + */ + DATA_VISUALIZER_INDEX_SELECT: 'datavisualizer_index_select', + /** + * Page: Data Visualizer + * Open data visualizer by importing data from a log file + */ + DATA_VISUALIZER_FILE: 'filedatavisualizer', + /** + * Page: Data Visualizer + * Open index data visualizer viewer page + */ + DATA_VISUALIZER_INDEX_VIEWER: 'jobs/new_job/datavisualizer', + ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: `jobs/new_job/step/job_type`, + SETTINGS: 'settings', + CALENDARS_MANAGE: 'settings/calendars_list', + FILTER_LISTS_MANAGE: 'settings/filter_lists', +} as const; diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts new file mode 100644 index 00000000000000..234be8b6faf909 --- /dev/null +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -0,0 +1,187 @@ +/* + * 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 { RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common/query'; +import { JobId } from '../../../reporting/common/types'; +import { ML_PAGES } from '../constants/ml_url_generator'; + +type OptionalPageState = object | undefined; + +export type MLPageState = PageState extends OptionalPageState + ? { page: PageType; pageState?: PageState } + : PageState extends object + ? { page: PageType; pageState: PageState } + : { page: PageType }; + +export const ANALYSIS_CONFIG_TYPE = { + OUTLIER_DETECTION: 'outlier_detection', + REGRESSION: 'regression', + CLASSIFICATION: 'classification', +} as const; + +type DataFrameAnalyticsType = typeof ANALYSIS_CONFIG_TYPE[keyof typeof ANALYSIS_CONFIG_TYPE]; + +export interface MlCommonGlobalState { + time?: TimeRange; +} +export interface MlCommonAppState { + [key: string]: any; +} + +export interface MlIndexBasedSearchState { + index?: string; + savedSearchId?: string; +} + +export interface MlGenericUrlPageState extends MlIndexBasedSearchState { + globalState?: MlCommonGlobalState; + appState?: MlCommonAppState; + [key: string]: any; +} + +export interface MlGenericUrlState { + page: + | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER + | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE; + pageState: MlGenericUrlPageState; +} + +export interface AnomalyDetectionQueryState { + jobId?: JobId; + groupIds?: string[]; +} + +export type AnomalyDetectionUrlState = MLPageState< + typeof ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + AnomalyDetectionQueryState | undefined +>; +export interface ExplorerAppState { + mlExplorerSwimlane: { + selectedType?: string; + selectedLanes?: string[]; + selectedTimes?: number[]; + showTopFieldValues?: boolean; + viewByFieldName?: string; + viewByPerPage?: number; + viewByFromPage?: number; + }; + mlExplorerFilter: { + influencersFilterQuery?: unknown; + filterActive?: boolean; + filteredFields?: string[]; + queryString?: string; + }; + query?: any; +} +export interface ExplorerGlobalState { + ml: { jobIds: JobId[] }; + time?: TimeRange; + refreshInterval?: RefreshInterval; +} + +export interface ExplorerUrlPageState { + /** + * Job IDs + */ + jobIds: JobId[]; + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval; + /** + * Optionally set the query. + */ + query?: any; + /** + * Optional state for the swim lane + */ + mlExplorerSwimlane?: ExplorerAppState['mlExplorerSwimlane']; + mlExplorerFilter?: ExplorerAppState['mlExplorerFilter']; +} + +export type ExplorerUrlState = MLPageState; + +export interface TimeSeriesExplorerGlobalState { + ml: { + jobIds: JobId[]; + }; + time?: TimeRange; + refreshInterval?: RefreshInterval; +} + +export interface TimeSeriesExplorerAppState { + zoom?: { + from?: string; + to?: string; + }; + mlTimeSeriesExplorer?: { + detectorIndex?: number; + entities?: Record; + }; + query?: any; +} + +export interface TimeSeriesExplorerPageState + extends Pick, + Pick { + jobIds: JobId[]; + timeRange?: TimeRange; + detectorIndex?: number; + entities?: Record; +} + +export type TimeSeriesExplorerUrlState = MLPageState< + typeof ML_PAGES.SINGLE_METRIC_VIEWER, + TimeSeriesExplorerPageState +>; + +export interface DataFrameAnalyticsQueryState { + jobId?: JobId | JobId[]; + groupIds?: string[]; +} + +export type DataFrameAnalyticsUrlState = MLPageState< + typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + DataFrameAnalyticsQueryState | undefined +>; + +export interface DataVisualizerUrlState { + page: + | typeof ML_PAGES.DATA_VISUALIZER + | typeof ML_PAGES.DATA_VISUALIZER_FILE + | typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT; +} + +export interface DataFrameAnalyticsExplorationQueryState { + ml: { + jobId: JobId; + analysisType: DataFrameAnalyticsType; + }; +} + +export type DataFrameAnalyticsExplorationUrlState = MLPageState< + typeof ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + { + jobId: JobId; + analysisType: DataFrameAnalyticsType; + } +>; + +/** + * Union type of ML URL state based on page + */ +export type MlUrlGeneratorState = + | AnomalyDetectionUrlState + | ExplorerUrlState + | TimeSeriesExplorerUrlState + | DataFrameAnalyticsUrlState + | DataFrameAnalyticsExplorationUrlState + | DataVisualizerUrlState + | MlGenericUrlState; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts index 8a43ae12deb21c..44979a1b9c60ae 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts @@ -9,3 +9,4 @@ export { useNavigateToPath, NavigateToPath } from './use_navigate_to_path'; export { useUiSettings } from './use_ui_settings_context'; export { useTimefilter } from './use_timefilter'; export { useNotifications } from './use_notifications_context'; +export { useMlUrlGenerator } from './use_create_url'; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts new file mode 100644 index 00000000000000..48385ad3ae6a82 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts @@ -0,0 +1,20 @@ +/* + * 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 { useMlKibana } from './kibana_context'; +import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator'; + +export const useMlUrlGenerator = () => { + const { + services: { + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = useMlKibana(); + + return getUrlGenerator(ML_APP_URL_GENERATOR); +}; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts index f2db970bf5057e..96d41be03a142e 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts @@ -21,14 +21,19 @@ export const useNavigateToPath = () => { const location = useLocation(); - return useMemo( - () => (path: string | undefined, preserveSearch = false) => { - navigateToUrl( - getUrlForApp(PLUGIN_ID, { - path: `${path}${preserveSearch === true ? location.search : ''}`, - }) - ); - }, - [location] - ); + return useMemo(() => { + return (path: string | undefined, preserveSearch = false) => { + if (path === undefined) return; + const modifiedPath = `${path}${preserveSearch === true ? location.search : ''}`; + /** + * Handle urls generated by MlUrlGenerator where '/app/ml' is automatically prepended + */ + const url = modifiedPath.includes('/app/ml') + ? modifiedPath + : getUrlForApp(PLUGIN_ID, { + path: modifiedPath, + }); + navigateToUrl(url); + }; + }, [location]); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx index babb5571052702..0560c3150a424c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx @@ -7,13 +7,20 @@ import React, { FC, Fragment } from 'react'; import { EuiCard, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useNavigateToPath } from '../../../../../contexts/kibana'; +import { useMlKibana, useMlUrlGenerator } from '../../../../../contexts/kibana'; +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; export const BackToListPanel: FC = () => { - const navigateToPath = useNavigateToPath(); + const urlGenerator = useMlUrlGenerator(); + const { + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); const redirectToAnalyticsManagementPage = async () => { - await navigateToPath('/data_frame_analytics'); + const url = await urlGenerator.createUrl({ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE }); + await navigateToUrl(url); }; return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/view_results_panel/view_results_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/view_results_panel/view_results_panel.tsx index 23a16ba84ef923..b832bfb2549c6d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/view_results_panel/view_results_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/view_results_panel/view_results_panel.tsx @@ -7,20 +7,27 @@ import React, { FC, Fragment } from 'react'; import { EuiCard, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useNavigateToPath } from '../../../../../contexts/kibana'; -import { getResultsUrl } from '../../../analytics_management/components/analytics_list/common'; +import { useMlUrlGenerator } from '../../../../../contexts/kibana'; import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; - +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; +import { useNavigateToPath } from '../../../../../contexts/kibana'; interface Props { jobId: string; analysisType: ANALYSIS_CONFIG_TYPE; } export const ViewResultsPanel: FC = ({ jobId, analysisType }) => { + const urlGenerator = useMlUrlGenerator(); const navigateToPath = useNavigateToPath(); - const redirectToAnalyticsManagementPage = async () => { - const path = getResultsUrl(jobId, analysisType); + const redirectToAnalyticsExplorationPage = async () => { + const path = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + pageState: { + jobId, + analysisType, + }, + }); await navigateToPath(path); }; @@ -38,7 +45,7 @@ export const ViewResultsPanel: FC = ({ jobId, analysisType }) => { defaultMessage: 'View results for the analytics job.', } )} - onClick={redirectToAnalyticsManagementPage} + onClick={redirectToAnalyticsExplorationPage} data-test-subj="analyticsWizardViewResultsCard" /> diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts index 4d697bcda1a065..e0ed2ea6cf5e08 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts @@ -21,6 +21,7 @@ import { ExplorerChartsData } from './explorer_charts/explorer_charts_container_ import { EXPLORER_ACTION } from './explorer_constants'; import { AppStateSelectedCells, TimeRangeBounds } from './explorer_utils'; import { explorerReducer, getExplorerDefaultState, ExplorerState } from './reducers'; +import { ExplorerAppState } from '../../../common/types/ml_url_generator'; export const ALLOW_CELL_RANGE_SELECTION = true; @@ -49,24 +50,6 @@ const explorerState$: Observable = explorerFilteredAction$.pipe( shareReplay(1) ); -export interface ExplorerAppState { - mlExplorerSwimlane: { - selectedType?: string; - selectedLanes?: string[]; - selectedTimes?: number[]; - showTopFieldValues?: boolean; - viewByFieldName?: string; - viewByPerPage?: number; - viewByFromPage?: number; - }; - mlExplorerFilter: { - influencersFilterQuery?: unknown; - filterActive?: boolean; - filteredFields?: string[]; - queryString?: string; - }; -} - const explorerAppState$: Observable = explorerState$.pipe( map( (state: ExplorerState): ExplorerAppState => { diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index 56c9a19723fba1..22a17c4ea089a9 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -75,7 +75,6 @@ const MlRoutes: FC<{ pageDeps: PageDependencies; }> = ({ pageDeps }) => { const navigateToPath = useNavigateToPath(); - return ( <> {Object.entries(routes).map(([name, routeFactory]) => { diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 62d7e82a214b57..2f2fc77283ef79 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -22,7 +22,8 @@ import { useSelectedCells } from '../../explorer/hooks/use_selected_cells'; import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; import { useExplorerData } from '../../explorer/actions'; -import { ExplorerAppState, explorerService } from '../../explorer/explorer_dashboard_service'; +import { ExplorerAppState } from '../../../../common/types/ml_url_generator'; +import { explorerService } from '../../explorer/explorer_dashboard_service'; import { getDateFormatTz } from '../../explorer/explorer_utils'; import { useJobSelection } from '../../components/job_selector/use_job_selection'; import { useShowCharts } from '../../components/controls/checkbox_showcharts'; diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts new file mode 100644 index 00000000000000..c4aebb108e7b92 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts @@ -0,0 +1,163 @@ +/* + * 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 isEmpty from 'lodash/isEmpty'; +import { + AnomalyDetectionQueryState, + AnomalyDetectionUrlState, + ExplorerAppState, + ExplorerGlobalState, + ExplorerUrlState, + MlGenericUrlState, + TimeSeriesExplorerAppState, + TimeSeriesExplorerGlobalState, + TimeSeriesExplorerUrlState, +} from '../../common/types/ml_url_generator'; +import { ML_PAGES } from '../../common/constants/ml_url_generator'; +import { createIndexBasedMlUrl } from './common'; +import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; +/** + * Creates URL to the Anomaly Detection Job management page + */ +export function createAnomalyDetectionJobManagementUrl( + appBasePath: string, + params: AnomalyDetectionUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE}`; + if (!params || isEmpty(params)) { + return url; + } + const { jobId, groupIds } = params; + const queryState: AnomalyDetectionQueryState = { + jobId, + groupIds, + }; + + url = setStateToKbnUrl( + 'mlManagement', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + return url; +} + +export function createAnomalyDetectionCreateJobSelectType( + appBasePath: string, + pageState: MlGenericUrlState['pageState'] +): string { + return createIndexBasedMlUrl( + appBasePath, + ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, + pageState + ); +} + +/** + * Creates URL to the Anomaly Explorer page + */ +export function createExplorerUrl( + appBasePath: string, + params: ExplorerUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.ANOMALY_EXPLORER}`; + + if (!params) { + return url; + } + const { + refreshInterval, + timeRange, + jobIds, + query, + mlExplorerSwimlane = {}, + mlExplorerFilter = {}, + } = params; + const appState: Partial = { + mlExplorerSwimlane, + mlExplorerFilter, + }; + if (query) appState.query = query; + + if (jobIds) { + const queryState: Partial = { + ml: { + jobIds, + }, + }; + + if (timeRange) queryState.time = timeRange; + if (refreshInterval) queryState.refreshInterval = refreshInterval; + + url = setStateToKbnUrl>( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + url = setStateToKbnUrl>( + '_a', + appState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + return url; +} + +/** + * Creates URL to the SingleMetricViewer page + */ +export function createSingleMetricViewerUrl( + appBasePath: string, + params: TimeSeriesExplorerUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.SINGLE_METRIC_VIEWER}`; + if (!params) { + return url; + } + const { timeRange, jobIds, refreshInterval, zoom, query, detectorIndex, entities } = params; + + const queryState: TimeSeriesExplorerGlobalState = { + ml: { + jobIds, + }, + refreshInterval, + time: timeRange, + }; + + const appState: Partial = {}; + const mlTimeSeriesExplorer: Partial = {}; + + if (detectorIndex !== undefined) { + mlTimeSeriesExplorer.detectorIndex = detectorIndex; + } + if (entities !== undefined) { + mlTimeSeriesExplorer.entities = entities; + } + appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer; + + if (zoom) appState.zoom = zoom; + if (query) + appState.query = { + query_string: query, + }; + url = setStateToKbnUrl( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + url = setStateToKbnUrl( + '_a', + appState, + { useHash: false, storeInHashQuery: false }, + url + ); + + return url; +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/common.ts b/x-pack/plugins/ml/public/ml_url_generator/common.ts new file mode 100644 index 00000000000000..57cfc52045282b --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/common.ts @@ -0,0 +1,55 @@ +/* + * 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 isEmpty from 'lodash/isEmpty'; +import { MlGenericUrlState } from '../../common/types/ml_url_generator'; +import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; + +export function extractParams(urlState: UrlState) { + // page should be guaranteed to exist here but is unknown + // @ts-ignore + const { page, ...params } = urlState; + return { page, params }; +} + +/** + * Creates generic index based search ML url + * e.g. `jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a` + */ +export function createIndexBasedMlUrl( + appBasePath: string, + page: MlGenericUrlState['page'], + pageState: MlGenericUrlState['pageState'] +): string { + const { globalState, appState, index, savedSearchId, ...restParams } = pageState; + let url = `${appBasePath}/${page}`; + + if (index !== undefined && savedSearchId === undefined) { + url = `${url}?index=${index}`; + } + if (index === undefined && savedSearchId !== undefined) { + url = `${url}?savedSearchId=${savedSearchId}`; + } + + if (!isEmpty(restParams)) { + Object.keys(restParams).forEach((key) => { + url = setStateToKbnUrl( + key, + restParams[key], + { useHash: false, storeInHashQuery: false }, + url + ); + }); + } + + if (globalState) { + url = setStateToKbnUrl('_g', globalState, { useHash: false, storeInHashQuery: false }, url); + } + if (appState) { + url = setStateToKbnUrl('_a', appState, { useHash: false, storeInHashQuery: false }, url); + } + return url; +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts new file mode 100644 index 00000000000000..8cf10a2acb64f2 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -0,0 +1,70 @@ +/* + * 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. + */ + +/** + * Creates URL to the DataFrameAnalytics page + */ +import { + DataFrameAnalyticsExplorationQueryState, + DataFrameAnalyticsExplorationUrlState, + DataFrameAnalyticsQueryState, + DataFrameAnalyticsUrlState, +} from '../../common/types/ml_url_generator'; +import { ML_PAGES } from '../../common/constants/ml_url_generator'; +import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; + +export function createDataFrameAnalyticsJobManagementUrl( + appBasePath: string, + mlUrlGeneratorState: DataFrameAnalyticsUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE}`; + + if (mlUrlGeneratorState) { + const { jobId, groupIds } = mlUrlGeneratorState; + const queryState: Partial = { + jobId, + groupIds, + }; + + url = setStateToKbnUrl>( + 'mlManagement', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + return url; +} + +/** + * Creates URL to the DataFrameAnalytics Exploration page + */ +export function createDataFrameAnalyticsExplorationUrl( + appBasePath: string, + mlUrlGeneratorState: DataFrameAnalyticsExplorationUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION}`; + + if (mlUrlGeneratorState) { + const { jobId, analysisType } = mlUrlGeneratorState; + const queryState: DataFrameAnalyticsExplorationQueryState = { + ml: { + jobId, + analysisType, + }, + }; + + url = setStateToKbnUrl( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + return url; +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts new file mode 100644 index 00000000000000..24693df5025d9d --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/** + * Creates URL to the Data Visualizer page + */ +import { DataVisualizerUrlState, MlGenericUrlState } from '../../common/types/ml_url_generator'; +import { createIndexBasedMlUrl } from './common'; +import { ML_PAGES } from '../../common/constants/ml_url_generator'; + +export function createDataVisualizerUrl( + appBasePath: string, + { page }: DataVisualizerUrlState +): string { + return `${appBasePath}/${page}`; +} + +/** + * Creates URL to the Index Data Visualizer + */ +export function createIndexDataVisualizerUrl( + appBasePath: string, + pageState: MlGenericUrlState['pageState'] +): string { + return createIndexBasedMlUrl(appBasePath, ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, pageState); +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/index.ts b/x-pack/plugins/ml/public/ml_url_generator/index.ts new file mode 100644 index 00000000000000..1579b187278c4f --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/index.ts @@ -0,0 +1,6 @@ +/* + * 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 { MlUrlGenerator, registerUrlGenerator } from './ml_url_generator'; diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts new file mode 100644 index 00000000000000..55bc6d3668de78 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts @@ -0,0 +1,262 @@ +/* + * 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 { MlUrlGenerator } from './ml_url_generator'; +import { ML_PAGES } from '../../common/constants/ml_url_generator'; +import { ANALYSIS_CONFIG_TYPE } from '../../common/types/ml_url_generator'; + +describe('MlUrlGenerator', () => { + const urlGenerator = new MlUrlGenerator({ + appBasePath: '/app/ml', + useHash: false, + }); + + describe('AnomalyDetection', () => { + describe('Job Management Page', () => { + it('should generate valid URL for the Anomaly Detection job management page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + }); + expect(url).toBe('/app/ml/jobs'); + }); + + it('should generate valid URL for the Anomaly Detection job management page for job', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState: { + jobId: 'fq_single_1', + }, + }); + expect(url).toBe('/app/ml/jobs?mlManagement=(jobId:fq_single_1)'); + }); + + it('should generate valid URL for the Anomaly Detection job management page for groupIds', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState: { + groupIds: ['farequote', 'categorization'], + }, + }); + expect(url).toBe('/app/ml/jobs?mlManagement=(groupIds:!(farequote,categorization))'); + }); + + it('should generate valid URL for the page for selecting the type of anomaly detection job to create', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, + pageState: { + index: `3da93760-e0af-11ea-9ad3-3bcfc330e42a`, + globalState: { + time: { + from: 'now-30m', + to: 'now', + }, + }, + }, + }); + expect(url).toBe( + '/app/ml/jobs/new_job/step/job_type?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_g=(time:(from:now-30m,to:now))' + ); + }); + }); + + describe('Anomaly Explorer Page', () => { + it('should generate valid URL for the Anomaly Explorer page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_EXPLORER, + pageState: { + jobIds: ['fq_single_1'], + mlExplorerSwimlane: { viewByFromPage: 2, viewByPerPage: 20 }, + refreshInterval: { + pause: false, + value: 0, + }, + timeRange: { + from: '2019-02-07T00:00:00.000Z', + to: '2020-08-13T17:15:00.000Z', + mode: 'absolute', + }, + query: { + analyze_wildcard: true, + query: '*', + }, + }, + }); + expect(url).toBe( + "/app/ml/explorer?_g=(ml:(jobIds:!(fq_single_1)),refreshInterval:(pause:!f,value:0),time:(from:'2019-02-07T00:00:00.000Z',mode:absolute,to:'2020-08-13T17:15:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20),query:(analyze_wildcard:!t,query:'*'))" + ); + }); + it('should generate valid URL for the Anomaly Explorer page for multiple jobIds', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_EXPLORER, + pageState: { + jobIds: ['fq_single_1', 'logs_categorization_1'], + timeRange: { + from: '2019-02-07T00:00:00.000Z', + to: '2020-08-13T17:15:00.000Z', + mode: 'absolute', + }, + }, + }); + expect(url).toBe( + "/app/ml/explorer?_g=(ml:(jobIds:!(fq_single_1,logs_categorization_1)),time:(from:'2019-02-07T00:00:00.000Z',mode:absolute,to:'2020-08-13T17:15:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:())" + ); + }); + }); + + describe('Single Metric Viewer Page', () => { + it('should generate valid URL for the Single Metric Viewer page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { + jobIds: ['logs_categorization_1'], + refreshInterval: { + pause: false, + value: 0, + }, + timeRange: { + from: '2020-07-12T00:39:02.912Z', + to: '2020-07-22T15:52:18.613Z', + mode: 'absolute', + }, + query: { + analyze_wildcard: true, + query: '*', + }, + }, + }); + expect(url).toBe( + "/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(logs_categorization_1)),refreshInterval:(pause:!f,value:0),time:(from:'2020-07-12T00:39:02.912Z',mode:absolute,to:'2020-07-22T15:52:18.613Z'))&_a=(mlTimeSeriesExplorer:(),query:(query_string:(analyze_wildcard:!t,query:'*')))" + ); + }); + + it('should generate valid URL for the Single Metric Viewer page with extra settings', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { + jobIds: ['logs_categorization_1'], + detectorIndex: 0, + entities: { mlcategory: '2' }, + refreshInterval: { + pause: false, + value: 0, + }, + timeRange: { + from: '2020-07-12T00:39:02.912Z', + to: '2020-07-22T15:52:18.613Z', + mode: 'absolute', + }, + zoom: { + from: '2020-07-20T23:58:29.367Z', + to: '2020-07-21T11:00:13.173Z', + }, + query: { + analyze_wildcard: true, + query: '*', + }, + }, + }); + expect(url).toBe( + "/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(logs_categorization_1)),refreshInterval:(pause:!f,value:0),time:(from:'2020-07-12T00:39:02.912Z',mode:absolute,to:'2020-07-22T15:52:18.613Z'))&_a=(mlTimeSeriesExplorer:(detectorIndex:0,entities:(mlcategory:'2')),query:(query_string:(analyze_wildcard:!t,query:'*')),zoom:(from:'2020-07-20T23:58:29.367Z',to:'2020-07-21T11:00:13.173Z'))" + ); + }); + }); + }); + + describe('DataFrameAnalytics', () => { + describe('JobManagement Page', () => { + it('should generate valid URL for the Data Frame Analytics job management page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + }); + expect(url).toBe('/app/ml/data_frame_analytics'); + }); + it('should generate valid URL for the Data Frame Analytics job management page with jobId', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + pageState: { + jobId: 'grid_regression_1', + }, + }); + expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(jobId:grid_regression_1)'); + }); + + it('should generate valid URL for the Data Frame Analytics job management page with groupIds', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + pageState: { + groupIds: ['group_1', 'group_2'], + }, + }); + expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(groupIds:!(group_1,group_2))'); + }); + }); + + describe('ExplorationPage', () => { + it('should generate valid URL for the Data Frame Analytics exploration page for job', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + pageState: { + jobId: 'grid_regression_1', + analysisType: ANALYSIS_CONFIG_TYPE.REGRESSION, + }, + }); + expect(url).toBe( + '/app/ml/data_frame_analytics/exploration?_g=(ml:(analysisType:regression,jobId:grid_regression_1))' + ); + }); + }); + }); + + describe('DataVisualizer', () => { + it('should generate valid URL for the Data Visualizer page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER, + }); + expect(url).toBe('/app/ml/datavisualizer'); + }); + + it('should generate valid URL for the File Data Visualizer import page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_FILE, + }); + expect(url).toBe('/app/ml/filedatavisualizer'); + }); + + it('should generate valid URL for the Index Data Visualizer select index pattern or saved search page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_INDEX_SELECT, + }); + expect(url).toBe('/app/ml/datavisualizer_index_select'); + }); + + it('should generate valid URL for the Index Data Visualizer Viewer page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, + pageState: { + index: '3da93760-e0af-11ea-9ad3-3bcfc330e42a', + globalState: { + time: { + from: 'now-30m', + to: 'now', + }, + }, + }, + }); + expect(url).toBe( + '/app/ml/jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_g=(time:(from:now-30m,to:now))' + ); + }); + }); + + it('should throw an error in case the page is not provided', async () => { + expect.assertions(1); + + // @ts-ignore + await urlGenerator.createUrl({ jobIds: ['test-job'] }).catch((e) => { + expect(e.message).toEqual('Page type is not provided or unknown'); + }); + }); +}); diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts new file mode 100644 index 00000000000000..b69260d8d4157b --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts @@ -0,0 +1,91 @@ +/* + * 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 { CoreSetup } from 'kibana/public'; +import { + SharePluginSetup, + UrlGeneratorsDefinition, + UrlGeneratorState, +} from '../../../../../src/plugins/share/public'; +import { MlStartDependencies } from '../plugin'; +import { ML_PAGES, ML_APP_URL_GENERATOR } from '../../common/constants/ml_url_generator'; +import { MlUrlGeneratorState } from '../../common/types/ml_url_generator'; +import { + createAnomalyDetectionJobManagementUrl, + createAnomalyDetectionCreateJobSelectType, + createExplorerUrl, + createSingleMetricViewerUrl, +} from './anomaly_detection_urls_generator'; +import { + createDataFrameAnalyticsJobManagementUrl, + createDataFrameAnalyticsExplorationUrl, +} from './data_frame_analytics_urls_generator'; +import { + createIndexDataVisualizerUrl, + createDataVisualizerUrl, +} from './data_visualizer_urls_generator'; + +declare module '../../../../../src/plugins/share/public' { + export interface UrlGeneratorStateMapping { + [ML_APP_URL_GENERATOR]: UrlGeneratorState; + } +} + +interface Params { + appBasePath: string; + useHash: boolean; +} + +export class MlUrlGenerator implements UrlGeneratorsDefinition { + constructor(private readonly params: Params) {} + + public readonly id = ML_APP_URL_GENERATOR; + + public readonly createUrl = async (mlUrlGeneratorState: MlUrlGeneratorState): Promise => { + const appBasePath = this.params.appBasePath; + switch (mlUrlGeneratorState.page) { + case ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE: + return createAnomalyDetectionJobManagementUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.ANOMALY_EXPLORER: + return createExplorerUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: + return createAnomalyDetectionCreateJobSelectType( + appBasePath, + mlUrlGeneratorState.pageState + ); + case ML_PAGES.SINGLE_METRIC_VIEWER: + return createSingleMetricViewerUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE: + return createDataFrameAnalyticsJobManagementUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION: + return createDataFrameAnalyticsExplorationUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.DATA_VISUALIZER: + case ML_PAGES.DATA_VISUALIZER_FILE: + case ML_PAGES.DATA_VISUALIZER_INDEX_SELECT: + return createDataVisualizerUrl(appBasePath, mlUrlGeneratorState); + case ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER: + return createIndexDataVisualizerUrl(appBasePath, mlUrlGeneratorState.pageState); + default: + throw new Error('Page type is not provided or unknown'); + } + }; +} + +/** + * Registers the URL generator + */ +export function registerUrlGenerator( + share: SharePluginSetup, + core: CoreSetup +) { + const baseUrl = core.http.basePath.prepend('/app/ml'); + share.urlGenerators.registerUrlGenerator( + new MlUrlGenerator({ + appBasePath: baseUrl, + useHash: core.uiSettings.get('state:storeInSessionStorage'), + }) + ); +} diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 214b393a0fda92..3e8ab99e341ad5 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -34,7 +34,7 @@ import { registerFeature } from './register_feature'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { registerMlUiActions } from './ui_actions'; import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; -import { registerUrlGenerator } from './url_generator'; +import { registerUrlGenerator } from './ml_url_generator'; import { isFullLicense, isMlEnabled } from '../common/license'; import { registerEmbeddables } from './embeddables'; diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index e327befcf72938..e4d7cfa32c2cd5 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ActionContextMapping, createAction } from '../../../../../src/plugins/ui_actions/public'; import { MlCoreSetup } from '../plugin'; -import { ML_APP_URL_GENERATOR } from '../url_generator'; +import { ML_APP_URL_GENERATOR } from '../../common/constants/ml_url_generator'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, SwimLaneDrilldownContext } from '../embeddables'; export const OPEN_IN_ANOMALY_EXPLORER_ACTION = 'openInAnomalyExplorerAction'; @@ -32,19 +32,21 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta return urlGenerator.createUrl({ page: 'explorer', - jobIds, - timeRange, - mlExplorerSwimlane: { - viewByFromPage: fromPage, - viewByPerPage: perPage, - viewByFieldName: viewBy, - ...(data - ? { - selectedType: data.type, - selectedTimes: data.times, - selectedLanes: data.lanes, - } - : {}), + pageState: { + jobIds, + timeRange, + mlExplorerSwimlane: { + viewByFromPage: fromPage, + viewByPerPage: perPage, + viewByFieldName: viewBy, + ...(data + ? { + selectedType: data.type, + selectedTimes: data.times, + selectedLanes: data.lanes, + } + : {}), + }, }, }); }, diff --git a/x-pack/plugins/ml/public/url_generator.test.ts b/x-pack/plugins/ml/public/url_generator.test.ts deleted file mode 100644 index 21dde124049577..00000000000000 --- a/x-pack/plugins/ml/public/url_generator.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { MlUrlGenerator } from './url_generator'; - -describe('MlUrlGenerator', () => { - const urlGenerator = new MlUrlGenerator({ - appBasePath: '/app/ml', - useHash: false, - }); - - it('should generate valid URL for the Anomaly Explorer page', async () => { - const url = await urlGenerator.createUrl({ - page: 'explorer', - jobIds: ['test-job'], - mlExplorerSwimlane: { viewByFromPage: 2, viewByPerPage: 20 }, - }); - expect(url).toBe( - '/app/ml/explorer#?_g=(ml:(jobIds:!(test-job)))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20))' - ); - }); - - it('should throw an error in case the page is not provided', async () => { - expect.assertions(1); - - // @ts-ignore - await urlGenerator.createUrl({ jobIds: ['test-job'] }).catch((e) => { - expect(e.message).toEqual('Page type is not provided or unknown'); - }); - }); -}); diff --git a/x-pack/plugins/ml/public/url_generator.ts b/x-pack/plugins/ml/public/url_generator.ts deleted file mode 100644 index 4e08c57c0b2e0b..00000000000000 --- a/x-pack/plugins/ml/public/url_generator.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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 { CoreSetup } from 'kibana/public'; -import { - SharePluginSetup, - UrlGeneratorsDefinition, - UrlGeneratorState, -} from '../../../../src/plugins/share/public'; -import { TimeRange } from '../../../../src/plugins/data/public'; -import { setStateToKbnUrl } from '../../../../src/plugins/kibana_utils/public'; -import { JobId } from '../../reporting/common/types'; -import { ExplorerAppState } from './application/explorer/explorer_dashboard_service'; -import { MlStartDependencies } from './plugin'; - -declare module '../../../../src/plugins/share/public' { - export interface UrlGeneratorStateMapping { - [ML_APP_URL_GENERATOR]: UrlGeneratorState; - } -} - -export const ML_APP_URL_GENERATOR = 'ML_APP_URL_GENERATOR'; - -export interface ExplorerUrlState { - /** - * ML App Page - */ - page: 'explorer'; - /** - * Job IDs - */ - jobIds: JobId[]; - /** - * Optionally set the time range in the time picker. - */ - timeRange?: TimeRange; - /** - * Optional state for the swim lane - */ - mlExplorerSwimlane?: ExplorerAppState['mlExplorerSwimlane']; - mlExplorerFilter?: ExplorerAppState['mlExplorerFilter']; -} - -/** - * Union type of ML URL state based on page - */ -export type MlUrlGeneratorState = ExplorerUrlState; - -export interface ExplorerQueryState { - ml: { jobIds: JobId[] }; - time?: TimeRange; -} - -interface Params { - appBasePath: string; - useHash: boolean; -} - -export class MlUrlGenerator implements UrlGeneratorsDefinition { - constructor(private readonly params: Params) {} - - public readonly id = ML_APP_URL_GENERATOR; - - public readonly createUrl = async ({ page, ...params }: MlUrlGeneratorState): Promise => { - if (page === 'explorer') { - return this.createExplorerUrl(params); - } - throw new Error('Page type is not provided or unknown'); - }; - - /** - * Creates URL to the Anomaly Explorer page - */ - private createExplorerUrl({ - timeRange, - jobIds, - mlExplorerSwimlane = {}, - mlExplorerFilter = {}, - }: Omit): string { - const appState: ExplorerAppState = { - mlExplorerSwimlane, - mlExplorerFilter, - }; - - const queryState: ExplorerQueryState = { - ml: { - jobIds, - }, - }; - - if (timeRange) queryState.time = timeRange; - - let url = `${this.params.appBasePath}/explorer`; - url = setStateToKbnUrl('_g', queryState, { useHash: false }, url); - url = setStateToKbnUrl('_a', appState, { useHash: false }, url); - - return url; - } -} - -/** - * Registers the URL generator - */ -export function registerUrlGenerator( - share: SharePluginSetup, - core: CoreSetup -) { - const baseUrl = core.http.basePath.prepend('/app/ml'); - share.urlGenerators.registerUrlGenerator( - new MlUrlGenerator({ - appBasePath: baseUrl, - useHash: core.uiSettings.get('state:storeInSessionStorage'), - }) - ); -} diff --git a/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts b/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts index 902cc907a0eff1..28f12ead7924ba 100644 --- a/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts +++ b/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts @@ -18,7 +18,7 @@ export function initSampleDataSets(mlLicense: MlLicense, plugins: PluginsSetup) addAppLinksToSampleDataset('ecommerce', [ { path: - '/app/ml#/modules/check_view_or_create?id=sample_data_ecommerce&index=ff959d40-b880-11e8-a6d9-e546fe2bba5f', + '/app/ml/modules/check_view_or_create?id=sample_data_ecommerce&index=ff959d40-b880-11e8-a6d9-e546fe2bba5f', label: sampleDataLinkLabel, icon: 'machineLearningApp', }, @@ -27,7 +27,7 @@ export function initSampleDataSets(mlLicense: MlLicense, plugins: PluginsSetup) addAppLinksToSampleDataset('logs', [ { path: - '/app/ml#/modules/check_view_or_create?id=sample_data_weblogs&index=90943e30-9a47-11e8-b64d-95841ca0b247', + '/app/ml/modules/check_view_or_create?id=sample_data_weblogs&index=90943e30-9a47-11e8-b64d-95841ca0b247', label: sampleDataLinkLabel, icon: 'machineLearningApp', }, From c46e77712ae6ae0ae563d0ebc9b6ca53ef2bccbd Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 2 Sep 2020 16:44:42 -0400 Subject: [PATCH 13/25] [Security Solution][Exceptions] - Adds sort order for exception items (#76537) ## Summary **Components affected:** ExceptionsViewer **Current behavior:** - when a user edits an exception item, the order of the exception items in the viewer changes. This creates confusion and looks like updates weren't applied (even though they were, just item order changed) **New behavior:** - when a user edits an exception item, the order of the exception items in the viewer don't change. Sort order is now based on `created_at` --- x-pack/plugins/lists/public/exceptions/api.test.ts | 10 ++++++++++ x-pack/plugins/lists/public/exceptions/api.ts | 2 ++ 2 files changed, 12 insertions(+) diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 7e79b212f69cb8..b02a82f98af91e 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -375,6 +375,8 @@ describe('Exceptions Lists API', () => { namespace_type: 'single,single', page: '1', per_page: '20', + sort_field: 'created_at', + sort_order: 'desc', }, signal: abortCtrl.signal, }); @@ -406,6 +408,8 @@ describe('Exceptions Lists API', () => { namespace_type: 'single', page: '1', per_page: '20', + sort_field: 'created_at', + sort_order: 'desc', }, signal: abortCtrl.signal, }); @@ -437,6 +441,8 @@ describe('Exceptions Lists API', () => { namespace_type: 'agnostic', page: '1', per_page: '20', + sort_field: 'created_at', + sort_order: 'desc', }, signal: abortCtrl.signal, }); @@ -468,6 +474,8 @@ describe('Exceptions Lists API', () => { namespace_type: 'agnostic', page: '1', per_page: '20', + sort_field: 'created_at', + sort_order: 'desc', }, signal: abortCtrl.signal, }); @@ -500,6 +508,8 @@ describe('Exceptions Lists API', () => { namespace_type: 'agnostic', page: '1', per_page: '20', + sort_field: 'created_at', + sort_order: 'desc', }, signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index 203c84b2943fd5..3f5ec803205035 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -288,6 +288,8 @@ export const fetchExceptionListsItemsByListIds = async ({ namespace_type: namespaceTypes.join(','), page: pagination.page ? `${pagination.page}` : '1', per_page: pagination.perPage ? `${pagination.perPage}` : '20', + sort_field: 'created_at', + sort_order: 'desc', ...(filters.trim() !== '' ? { filter: filters } : {}), }; const [validatedRequest, errorsRequest] = validate(query, findExceptionListItemSchema); From 5467f3164034784f10e60c084dbcc856ec0b4b36 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Wed, 2 Sep 2020 16:00:23 -0500 Subject: [PATCH 14/25] Remove dependencies from index pattern field and index pattern list (#75185) * Remove dependencies from index pattern field and index pattern list --- ...ins-data-public.fieldlist._constructor_.md | 23 --- ...lugin-plugins-data-public.fieldlist.add.md | 11 - ...in-plugins-data-public.fieldlist.getall.md | 11 - ...plugins-data-public.fieldlist.getbyname.md | 11 - ...plugins-data-public.fieldlist.getbytype.md | 11 - ...na-plugin-plugins-data-public.fieldlist.md | 27 +-- ...in-plugins-data-public.fieldlist.remove.md | 11 - ...plugins-data-public.fieldlist.removeall.md | 11 - ...lugins-data-public.fieldlist.replaceall.md | 11 - ...in-plugins-data-public.fieldlist.tospec.md | 25 --- ...in-plugins-data-public.fieldlist.update.md | 11 - ...a-plugin-plugins-data-public.ifieldtype.md | 2 +- ...n-plugins-data-public.ifieldtype.tospec.md | 4 +- ...gins-data-public.iindexpatternfieldlist.md | 1 + ...ta-public.iindexpatternfieldlist.tospec.md | 24 +++ ...-public.indexpatternfield._constructor_.md | 4 +- ...ns-data-public.indexpatternfield.format.md | 11 - ...a-public.indexpatternfield.indexpattern.md | 11 - ...n-plugins-data-public.indexpatternfield.md | 6 +- ...ns-data-public.indexpatternfield.tospec.md | 21 +- .../kibana-plugin-plugins-data-public.md | 2 +- ...a-plugin-plugins-data-server.ifieldtype.md | 2 +- ...n-plugins-data-server.ifieldtype.tospec.md | 4 +- src/core/public/public.api.md | 3 + src/core/server/server.api.md | 3 + .../es_query/filters/get_display_value.ts | 30 ++- .../common/field_formats/converters/source.ts | 4 +- .../field_formats/field_formats_registry.ts | 6 +- .../data/common/field_formats/types.ts | 1 + .../data/common/index_patterns/errors.ts | 29 +++ .../index_pattern_field.test.ts.snap | 8 +- .../index_patterns/fields/field_list.ts | 143 +++++++------ .../fields/index_pattern_field.test.ts | 46 ++--- .../fields/index_pattern_field.ts | 39 ++-- .../common/index_patterns/fields/types.ts | 4 +- .../data/common/index_patterns/index.ts | 2 + .../__snapshots__/index_pattern.test.ts.snap | 189 +++++++++++++++--- .../index_patterns/format_hit.ts | 4 +- .../index_patterns/index_pattern.test.ts | 12 +- .../index_patterns/index_pattern.ts | 86 ++++++-- .../index_patterns/index_patterns.ts | 10 - .../common/search/aggs/agg_config.test.ts | 6 +- .../data/common/search/aggs/agg_type.test.ts | 3 + .../data/common/search/aggs/agg_type.ts | 4 +- .../buckets/_terms_other_bucket_helper.ts | 8 +- .../buckets/create_filter/histogram.test.ts | 1 + .../aggs/buckets/create_filter/range.test.ts | 2 +- .../aggs/buckets/create_filter/terms.ts | 2 +- .../common/search/aggs/buckets/date_range.ts | 4 +- .../common/search/aggs/buckets/ip_range.ts | 4 +- .../common/search/aggs/buckets/range.test.ts | 13 +- .../data/common/search/aggs/buckets/range.ts | 4 +- .../data/common/search/aggs/buckets/terms.ts | 4 +- .../aggs/metrics/parent_pipeline.test.ts | 6 +- .../aggs/metrics/sibling_pipeline.test.ts | 3 + .../create_filters_from_value_click.test.ts | 6 +- src/plugins/data/public/index.ts | 2 +- src/plugins/data/public/public.api.md | 64 ++---- .../search/expressions/create_filter.test.ts | 1 + src/plugins/data/server/server.api.md | 5 +- .../sidebar/discover_field.test.tsx | 4 +- .../components/sidebar/discover_sidebar.tsx | 4 +- .../sidebar/lib/field_calculator.js | 6 +- .../sidebar/lib/field_calculator.test.ts | 12 +- .../components/sidebar/lib/get_details.ts | 8 +- .../lib/get_index_pattern_field_list.ts | 1 + .../indexed_fields_table.test.tsx.snap | 19 +- .../indexed_fields_table.test.tsx | 8 +- .../indexed_fields_table.tsx | 1 - .../edit_index_pattern/tabs/tabs.tsx | 4 +- .../edit_index_pattern/tabs/utils.ts | 4 +- .../field_editor/field_editor.test.tsx | 6 +- .../public/control/control.ts | 5 +- src/test_utils/public/stub_index_pattern.js | 4 +- .../classes/sources/es_source/es_source.js | 2 +- .../classes/tooltips/es_tooltip_property.ts | 9 +- .../services/field_format_service.ts | 6 +- .../rules/description_step/helpers.test.tsx | 6 +- 78 files changed, 578 insertions(+), 547 deletions(-) delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist._constructor_.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.add.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getall.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbyname.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbytype.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.remove.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.removeall.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.replaceall.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.tospec.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.update.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md create mode 100644 src/plugins/data/common/index_patterns/errors.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist._constructor_.md deleted file mode 100644 index 9f9613a5a68f7c..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist._constructor_.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [(constructor)](./kibana-plugin-plugins-data-public.fieldlist._constructor_.md) - -## FieldList.(constructor) - -Constructs a new instance of the `FieldList` class - -Signature: - -```typescript -constructor(indexPattern: IndexPattern, specs?: FieldSpec[], shortDotsEnable?: boolean, onNotification?: OnNotification); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| indexPattern | IndexPattern | | -| specs | FieldSpec[] | | -| shortDotsEnable | boolean | | -| onNotification | OnNotification | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.add.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.add.md deleted file mode 100644 index ae3d82f0cc3ea1..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.add.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [add](./kibana-plugin-plugins-data-public.fieldlist.add.md) - -## FieldList.add property - -Signature: - -```typescript -readonly add: (field: FieldSpec) => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getall.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getall.md deleted file mode 100644 index da29a4de9acc8f..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getall.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [getAll](./kibana-plugin-plugins-data-public.fieldlist.getall.md) - -## FieldList.getAll property - -Signature: - -```typescript -readonly getAll: () => IndexPatternField[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbyname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbyname.md deleted file mode 100644 index af368d003423a5..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbyname.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [getByName](./kibana-plugin-plugins-data-public.fieldlist.getbyname.md) - -## FieldList.getByName property - -Signature: - -```typescript -readonly getByName: (name: IndexPatternField['name']) => IndexPatternField | undefined; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbytype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbytype.md deleted file mode 100644 index 16bae3ee7c555c..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbytype.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [getByType](./kibana-plugin-plugins-data-public.fieldlist.getbytype.md) - -## FieldList.getByType property - -Signature: - -```typescript -readonly getByType: (type: IndexPatternField['type']) => any[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.md index 012b0694302903..79bcaf9700cf02 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.md @@ -1,32 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [fieldList](./kibana-plugin-plugins-data-public.fieldlist.md) -## FieldList class +## fieldList variable Signature: ```typescript -export declare class FieldList extends Array implements IIndexPatternFieldList +fieldList: (specs?: FieldSpec[], shortDotsEnable?: boolean) => IIndexPatternFieldList ``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(indexPattern, specs, shortDotsEnable, onNotification)](./kibana-plugin-plugins-data-public.fieldlist._constructor_.md) | | Constructs a new instance of the FieldList class | - -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [add](./kibana-plugin-plugins-data-public.fieldlist.add.md) | | (field: FieldSpec) => void | | -| [getAll](./kibana-plugin-plugins-data-public.fieldlist.getall.md) | | () => IndexPatternField[] | | -| [getByName](./kibana-plugin-plugins-data-public.fieldlist.getbyname.md) | | (name: IndexPatternField['name']) => IndexPatternField | undefined | | -| [getByType](./kibana-plugin-plugins-data-public.fieldlist.getbytype.md) | | (type: IndexPatternField['type']) => any[] | | -| [remove](./kibana-plugin-plugins-data-public.fieldlist.remove.md) | | (field: IFieldType) => void | | -| [removeAll](./kibana-plugin-plugins-data-public.fieldlist.removeall.md) | | () => void | | -| [replaceAll](./kibana-plugin-plugins-data-public.fieldlist.replaceall.md) | | (specs: FieldSpec[]) => void | | -| [toSpec](./kibana-plugin-plugins-data-public.fieldlist.tospec.md) | | () => {
count: number;
script: string | undefined;
lang: string | undefined;
conflictDescriptions: Record<string, string[]> | undefined;
name: string;
type: string;
esTypes: string[] | undefined;
scripted: boolean;
searchable: boolean;
aggregatable: boolean;
readFromDocValues: boolean;
subType: import("../types").IFieldSubType | undefined;
format: any;
}[] | | -| [update](./kibana-plugin-plugins-data-public.fieldlist.update.md) | | (field: FieldSpec) => void | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.remove.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.remove.md deleted file mode 100644 index 149410adb3550a..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.remove.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [remove](./kibana-plugin-plugins-data-public.fieldlist.remove.md) - -## FieldList.remove property - -Signature: - -```typescript -readonly remove: (field: IFieldType) => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.removeall.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.removeall.md deleted file mode 100644 index 92a45349ad0051..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.removeall.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [removeAll](./kibana-plugin-plugins-data-public.fieldlist.removeall.md) - -## FieldList.removeAll property - -Signature: - -```typescript -readonly removeAll: () => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.replaceall.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.replaceall.md deleted file mode 100644 index 5330440e6b96a1..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.replaceall.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [replaceAll](./kibana-plugin-plugins-data-public.fieldlist.replaceall.md) - -## FieldList.replaceAll property - -Signature: - -```typescript -readonly replaceAll: (specs: FieldSpec[]) => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.tospec.md deleted file mode 100644 index e646339feb4955..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.tospec.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [toSpec](./kibana-plugin-plugins-data-public.fieldlist.tospec.md) - -## FieldList.toSpec property - -Signature: - -```typescript -readonly toSpec: () => { - count: number; - script: string | undefined; - lang: string | undefined; - conflictDescriptions: Record | undefined; - name: string; - type: string; - esTypes: string[] | undefined; - scripted: boolean; - searchable: boolean; - aggregatable: boolean; - readFromDocValues: boolean; - subType: import("../types").IFieldSubType | undefined; - format: any; - }[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.update.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.update.md deleted file mode 100644 index c718e47b31b50a..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.update.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [update](./kibana-plugin-plugins-data-public.fieldlist.update.md) - -## FieldList.update property - -Signature: - -```typescript -readonly update: (field: FieldSpec) => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md index 6f42fb32fdb7be..3ff2afafcc5145 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md @@ -28,7 +28,7 @@ export interface IFieldType | [searchable](./kibana-plugin-plugins-data-public.ifieldtype.searchable.md) | boolean | | | [sortable](./kibana-plugin-plugins-data-public.ifieldtype.sortable.md) | boolean | | | [subType](./kibana-plugin-plugins-data-public.ifieldtype.subtype.md) | IFieldSubType | | -| [toSpec](./kibana-plugin-plugins-data-public.ifieldtype.tospec.md) | () => FieldSpec | | +| [toSpec](./kibana-plugin-plugins-data-public.ifieldtype.tospec.md) | (options?: {
getFormatterForField?: IndexPattern['getFormatterForField'];
}) => FieldSpec | | | [type](./kibana-plugin-plugins-data-public.ifieldtype.type.md) | string | | | [visualizable](./kibana-plugin-plugins-data-public.ifieldtype.visualizable.md) | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md index 1fb4084c25d343..52238ea2a00ca4 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md @@ -7,5 +7,7 @@ Signature: ```typescript -toSpec?: () => FieldSpec; +toSpec?: (options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }) => FieldSpec; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.md index b068c4804c0ddd..b1e13ffaabd077 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.md @@ -21,5 +21,6 @@ export interface IIndexPatternFieldList extends Array | [remove(field)](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.remove.md) | | | [removeAll()](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.removeall.md) | | | [replaceAll(specs)](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.replaceall.md) | | +| [toSpec(options)](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md) | | | [update(field)](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.update.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md new file mode 100644 index 00000000000000..fd20f2944c5be3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPatternFieldList](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.md) > [toSpec](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md) + +## IIndexPatternFieldList.toSpec() method + +Signature: + +```typescript +toSpec(options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }): FieldSpec[]; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | {
getFormatterForField?: IndexPattern['getFormatterForField'];
} | | + +Returns: + +`FieldSpec[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md index 10b65bdccdf87e..5d467a7a9cbcee 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md @@ -9,15 +9,13 @@ Constructs a new instance of the `IndexPatternField` class Signature: ```typescript -constructor(indexPattern: IndexPattern, spec: FieldSpec, displayName: string, onNotification: OnNotification); +constructor(spec: FieldSpec, displayName: string); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| indexPattern | IndexPattern | | | spec | FieldSpec | | | displayName | string | | -| onNotification | OnNotification | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md deleted file mode 100644 index f28d5b1bca7e56..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [format](./kibana-plugin-plugins-data-public.indexpatternfield.format.md) - -## IndexPatternField.format property - -Signature: - -```typescript -get format(): FieldFormat; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md deleted file mode 100644 index 3d145cce9d07df..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) - -## IndexPatternField.indexPattern property - -Signature: - -```typescript -readonly indexPattern: IndexPattern; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index 713b29ea3a3d30..215188ffa26073 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -14,7 +14,7 @@ export declare class IndexPatternField implements IFieldType | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(indexPattern, spec, displayName, onNotification)](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) | | Constructs a new instance of the IndexPatternField class | +| [(constructor)(spec, displayName)](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) | | Constructs a new instance of the IndexPatternField class | ## Properties @@ -26,8 +26,6 @@ export declare class IndexPatternField implements IFieldType | [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | undefined | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | -| [format](./kibana-plugin-plugins-data-public.indexpatternfield.format.md) | | FieldFormat | | -| [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) | | IndexPattern | | | [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | undefined | | | [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) | | string | | | [readFromDocValues](./kibana-plugin-plugins-data-public.indexpatternfield.readfromdocvalues.md) | | boolean | | @@ -45,5 +43,5 @@ export declare class IndexPatternField implements IFieldType | Method | Modifiers | Description | | --- | --- | --- | | [toJSON()](./kibana-plugin-plugins-data-public.indexpatternfield.tojson.md) | | | -| [toSpec()](./kibana-plugin-plugins-data-public.indexpatternfield.tospec.md) | | | +| [toSpec({ getFormatterForField, })](./kibana-plugin-plugins-data-public.indexpatternfield.tospec.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md index 5037cb0049e829..1d80c90991f553 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md @@ -7,7 +7,9 @@ Signature: ```typescript -toSpec(): { +toSpec({ getFormatterForField, }?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }): { count: number; script: string | undefined; lang: string | undefined; @@ -20,9 +22,19 @@ toSpec(): { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; - format: any; + format: { + id: any; + params: any; + } | undefined; }; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { getFormatterForField, } | {
getFormatterForField?: IndexPattern['getFormatterForField'];
} | | + Returns: `{ @@ -38,6 +50,9 @@ toSpec(): { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; - format: any; + format: { + id: any; + params: any; + } | undefined; }` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 09702df4fdb545..0ab86e0f4dab20 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -10,7 +10,6 @@ | --- | --- | | [AggParamType](./kibana-plugin-plugins-data-public.aggparamtype.md) | | | [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) | | -| [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) | | | [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) | | | [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) | | | [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) | | @@ -103,6 +102,7 @@ | [expandShorthand](./kibana-plugin-plugins-data-public.expandshorthand.md) | | | [extractSearchSourceReferences](./kibana-plugin-plugins-data-public.extractsearchsourcereferences.md) | | | [fieldFormats](./kibana-plugin-plugins-data-public.fieldformats.md) | | +| [fieldList](./kibana-plugin-plugins-data-public.fieldlist.md) | | | [FilterBar](./kibana-plugin-plugins-data-public.filterbar.md) | | | [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {Array} | | [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md index 77a2954428f8d4..d106f3a35a91c7 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md @@ -28,7 +28,7 @@ export interface IFieldType | [searchable](./kibana-plugin-plugins-data-server.ifieldtype.searchable.md) | boolean | | | [sortable](./kibana-plugin-plugins-data-server.ifieldtype.sortable.md) | boolean | | | [subType](./kibana-plugin-plugins-data-server.ifieldtype.subtype.md) | IFieldSubType | | -| [toSpec](./kibana-plugin-plugins-data-server.ifieldtype.tospec.md) | () => FieldSpec | | +| [toSpec](./kibana-plugin-plugins-data-server.ifieldtype.tospec.md) | (options?: {
getFormatterForField?: IndexPattern['getFormatterForField'];
}) => FieldSpec | | | [type](./kibana-plugin-plugins-data-server.ifieldtype.type.md) | string | | | [visualizable](./kibana-plugin-plugins-data-server.ifieldtype.visualizable.md) | boolean | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md index d1863bebce4f00..6f8ee9d9eebf03 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md @@ -7,5 +7,7 @@ Signature: ```typescript -toSpec?: () => FieldSpec; +toSpec?: (options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }) => FieldSpec; ``` diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index bacbd6e757114a..cc37a3c7c09246 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -7,6 +7,7 @@ import { Action } from 'history'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import Boom from 'boom'; +import { ErrorToastOptions as ErrorToastOptions_2 } from 'src/core/public/notifications'; import { EuiBreadcrumb } from '@elastic/eui'; import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; @@ -27,7 +28,9 @@ import { PublicUiSettingsParams as PublicUiSettingsParams_2 } from 'src/core/ser import React from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; import * as Rx from 'rxjs'; +import { SavedObject as SavedObject_2 } from 'src/core/server'; import { ShallowPromise } from '@kbn/utility-types'; +import { ToastInputFields as ToastInputFields_2 } from 'src/core/public/notifications'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 05afad5a4f7a41..2128eb077211fb 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -40,6 +40,7 @@ import { DeleteScriptParams } from 'elasticsearch'; import { DeleteTemplateParams } from 'elasticsearch'; import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; +import { ErrorToastOptions } from 'src/core/public/notifications'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; import { FieldStatsParams } from 'elasticsearch'; @@ -118,6 +119,7 @@ import { RenderSearchTemplateParams } from 'elasticsearch'; import { Request } from 'hapi'; import { ResponseObject } from 'hapi'; import { ResponseToolkit } from 'hapi'; +import { SavedObject as SavedObject_2 } from 'src/core/server'; import { SchemaTypeError } from '@kbn/config-schema'; import { ScrollParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch'; @@ -141,6 +143,7 @@ import { TasksCancelParams } from 'elasticsearch'; import { TasksGetParams } from 'elasticsearch'; import { TasksListParams } from 'elasticsearch'; import { TermvectorsParams } from 'elasticsearch'; +import { ToastInputFields } from 'src/core/public/notifications'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; diff --git a/src/plugins/data/common/es_query/filters/get_display_value.ts b/src/plugins/data/common/es_query/filters/get_display_value.ts index 10b4dab3f46efa..28ba0ab629e8f1 100644 --- a/src/plugins/data/common/es_query/filters/get_display_value.ts +++ b/src/plugins/data/common/es_query/filters/get_display_value.ts @@ -17,30 +17,26 @@ * under the License. */ -import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { IIndexPattern, IFieldType } from '../..'; +import { IIndexPattern } from '../..'; import { getIndexPatternFromFilter } from './get_index_pattern_from_filter'; import { Filter } from '../filters'; function getValueFormatter(indexPattern?: IIndexPattern, key?: string) { - if (!indexPattern || !key) return; + // checking getFormatterForField exists because there is at least once case where an index pattern + // is an object rather than an IndexPattern class + if (!indexPattern || !indexPattern.getFormatterForField || !key) return; - let format = get(indexPattern, ['fields', 'byName', key, 'format']); - if (!format && (indexPattern.fields as any).getByName) { - // TODO: Why is indexPatterns sometimes a map and sometimes an array? - const field: IFieldType = (indexPattern.fields as any).getByName(key); - if (!field) { - throw new Error( - i18n.translate('data.filter.filterBar.fieldNotFound', { - defaultMessage: 'Field {key} not found in index pattern {indexPattern}', - values: { key, indexPattern: indexPattern.title }, - }) - ); - } - format = field.format; + const field = indexPattern.fields.find((f) => f.name === key); + if (!field) { + throw new Error( + i18n.translate('data.filter.filterBar.fieldNotFound', { + defaultMessage: 'Field {key} not found in index pattern {indexPattern}', + values: { key, indexPattern: indexPattern.title }, + }) + ); } - return format; + return indexPattern.getFormatterForField(field); } export function getDisplayValueFromFilter(filter: Filter, indexPatterns: IIndexPattern[]): string { diff --git a/src/plugins/data/common/field_formats/converters/source.ts b/src/plugins/data/common/field_formats/converters/source.ts index 9c81bb011e127e..e7dce82c725d2b 100644 --- a/src/plugins/data/common/field_formats/converters/source.ts +++ b/src/plugins/data/common/field_formats/converters/source.ts @@ -60,7 +60,7 @@ export class SourceFormat extends FieldFormat { textConvert: TextContextTypeConvert = (value) => JSON.stringify(value); htmlConvert: HtmlContextTypeConvert = (value, options = {}) => { - const { field, hit } = options; + const { field, hit, indexPattern } = options; if (!field) { const converter = this.getConverterFor('text') as Function; @@ -69,7 +69,7 @@ export class SourceFormat extends FieldFormat { } const highlights = (hit && hit.highlight) || {}; - const formatted = field.indexPattern.formatHit(hit); + const formatted = indexPattern.formatHit(hit); const highlightPairs: any[] = []; const sourcePairs: any[] = []; const isShortDots = this.getConfig!(UI_SETTINGS.SHORT_DOTS_ENABLE); diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index 4b46adf399363c..dbc3693c99779a 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -181,11 +181,11 @@ export class FieldFormatsRegistry { * @param {ES_FIELD_TYPES[]} esTypes * @return {FieldFormat} */ - getDefaultInstancePlain( + getDefaultInstancePlain = ( fieldType: KBN_FIELD_TYPES, esTypes?: ES_FIELD_TYPES[], params: Record = {} - ): FieldFormat { + ): FieldFormat => { const conf = this.getDefaultConfig(fieldType, esTypes); const instanceParams = { ...conf.params, @@ -193,7 +193,7 @@ export class FieldFormatsRegistry { }; return this.getInstance(conf.id, instanceParams); - } + }; /** * Returns a cache key built by the given variables for caching in memoized * Where esType contains fieldType, fieldType is returned diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index daa44b2b0f85be..af956a20c0dc51 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -27,6 +27,7 @@ export type FieldFormatsContentType = 'html' | 'text'; /** @internal **/ export interface HtmlContextTypeOptions { field?: any; + indexPattern?: any; hit?: Record; } diff --git a/src/plugins/data/common/index_patterns/errors.ts b/src/plugins/data/common/index_patterns/errors.ts new file mode 100644 index 00000000000000..3d92bae1968fb5 --- /dev/null +++ b/src/plugins/data/common/index_patterns/errors.ts @@ -0,0 +1,29 @@ +/* + * 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 { FieldSpec } from './types'; + +export class FieldTypeUnknownError extends Error { + public readonly fieldSpec: FieldSpec; + constructor(message: string, spec: FieldSpec) { + super(message); + this.name = 'FieldTypeUnknownError'; + this.fieldSpec = spec; + } +} diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap index e61593f6bfb27b..4279dd320ad621 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap @@ -14,7 +14,7 @@ Object { }, "count": 1, "esTypes": Array [ - "type", + "text", ], "lang": "lang", "name": "name", @@ -30,7 +30,7 @@ Object { "path": "path", }, }, - "type": "type", + "type": "string", } `; @@ -48,7 +48,7 @@ Object { }, "count": 1, "esTypes": Array [ - "type", + "text", ], "format": Object { "id": "number", @@ -70,6 +70,6 @@ Object { "path": "path", }, }, - "type": "type", + "type": "string", } `; diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts index d2489a5d1f7e33..4cf60758698513 100644 --- a/src/plugins/data/common/index_patterns/fields/field_list.ts +++ b/src/plugins/data/common/index_patterns/fields/field_list.ts @@ -35,6 +35,7 @@ export interface IIndexPatternFieldList extends Array { removeAll(): void; replaceAll(specs: FieldSpec[]): void; update(field: FieldSpec): void; + toSpec(options?: { getFormatterForField?: IndexPattern['getFormatterForField'] }): FieldSpec[]; } export type CreateIndexPatternFieldList = ( @@ -44,87 +45,79 @@ export type CreateIndexPatternFieldList = ( onNotification?: OnNotification ) => IIndexPatternFieldList; -export class FieldList extends Array implements IIndexPatternFieldList { - private byName: FieldMap = new Map(); - private groups: Map = new Map(); - private indexPattern: IndexPattern; - private shortDotsEnable: boolean; - private onNotification: OnNotification; - private setByName = (field: IndexPatternField) => this.byName.set(field.name, field); - private setByGroup = (field: IndexPatternField) => { - if (typeof this.groups.get(field.type) === 'undefined') { - this.groups.set(field.type, new Map()); +// extending the array class and using a constructor doesn't work well +// when calling filter and similar so wrapping in a callback. +// to be removed in the future +export const fieldList = ( + specs: FieldSpec[] = [], + shortDotsEnable = false +): IIndexPatternFieldList => { + class FldList extends Array implements IIndexPatternFieldList { + private byName: FieldMap = new Map(); + private groups: Map = new Map(); + private setByName = (field: IndexPatternField) => this.byName.set(field.name, field); + private setByGroup = (field: IndexPatternField) => { + if (typeof this.groups.get(field.type) === 'undefined') { + this.groups.set(field.type, new Map()); + } + this.groups.get(field.type)!.set(field.name, field); + }; + private removeByGroup = (field: IFieldType) => this.groups.get(field.type)!.delete(field.name); + private calcDisplayName = (name: string) => + shortDotsEnable ? shortenDottedString(name) : name; + constructor() { + super(); + specs.map((field) => this.add(field)); } - this.groups.get(field.type)!.set(field.name, field); - }; - private removeByGroup = (field: IFieldType) => this.groups.get(field.type)!.delete(field.name); - private calcDisplayName = (name: string) => - this.shortDotsEnable ? shortenDottedString(name) : name; - constructor( - indexPattern: IndexPattern, - specs: FieldSpec[] = [], - shortDotsEnable = false, - onNotification: OnNotification = () => {} - ) { - super(); - this.indexPattern = indexPattern; - this.shortDotsEnable = shortDotsEnable; - this.onNotification = onNotification; - specs.map((field) => this.add(field)); - } + public readonly getAll = () => [...this.byName.values()]; + public readonly getByName = (name: IndexPatternField['name']) => this.byName.get(name); + public readonly getByType = (type: IndexPatternField['type']) => [ + ...(this.groups.get(type) || new Map()).values(), + ]; + public readonly add = (field: FieldSpec) => { + const newField = new IndexPatternField(field, this.calcDisplayName(field.name)); + this.push(newField); + this.setByName(newField); + this.setByGroup(newField); + }; - public readonly getAll = () => [...this.byName.values()]; - public readonly getByName = (name: IndexPatternField['name']) => this.byName.get(name); - public readonly getByType = (type: IndexPatternField['type']) => [ - ...(this.groups.get(type) || new Map()).values(), - ]; - public readonly add = (field: FieldSpec) => { - const newField = new IndexPatternField( - this.indexPattern, - field, - this.calcDisplayName(field.name), - this.onNotification - ); - this.push(newField); - this.setByName(newField); - this.setByGroup(newField); - }; + public readonly remove = (field: IFieldType) => { + this.removeByGroup(field); + this.byName.delete(field.name); - public readonly remove = (field: IFieldType) => { - this.removeByGroup(field); - this.byName.delete(field.name); + const fieldIndex = findIndex(this, { name: field.name }); + this.splice(fieldIndex, 1); + }; - const fieldIndex = findIndex(this, { name: field.name }); - this.splice(fieldIndex, 1); - }; + public readonly update = (field: FieldSpec) => { + const newField = new IndexPatternField(field, this.calcDisplayName(field.name)); + const index = this.findIndex((f) => f.name === newField.name); + this.splice(index, 1, newField); + this.setByName(newField); + this.removeByGroup(newField); + this.setByGroup(newField); + }; - public readonly update = (field: FieldSpec) => { - const newField = new IndexPatternField( - this.indexPattern, - field, - this.calcDisplayName(field.name), - this.onNotification - ); - const index = this.findIndex((f) => f.name === newField.name); - this.splice(index, 1, newField); - this.setByName(newField); - this.removeByGroup(newField); - this.setByGroup(newField); - }; + public readonly removeAll = () => { + this.length = 0; + this.byName.clear(); + this.groups.clear(); + }; - public readonly removeAll = () => { - this.length = 0; - this.byName.clear(); - this.groups.clear(); - }; + public readonly replaceAll = (spcs: FieldSpec[]) => { + this.removeAll(); + spcs.forEach(this.add); + }; - public readonly replaceAll = (specs: FieldSpec[]) => { - this.removeAll(); - specs.forEach(this.add); - }; + public toSpec({ + getFormatterForField, + }: { + getFormatterForField?: IndexPattern['getFormatterForField']; + } = {}) { + return [...this.map((field) => field.toSpec({ getFormatterForField }))]; + } + } - public readonly toSpec = () => { - return [...this.map((field) => field.toSpec())]; - }; -} + return new FldList(); +}; diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts index 0cd0fe8324809c..3c4fac81c2c7ce 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts @@ -19,7 +19,7 @@ import { IndexPatternField } from './index_pattern_field'; import { IndexPattern } from '../index_patterns'; -import { KBN_FIELD_TYPES } from '../../../common'; +import { KBN_FIELD_TYPES, FieldFormat } from '../../../common'; import { FieldSpec } from '../types'; describe('Field', function () { @@ -28,21 +28,16 @@ describe('Field', function () { } function getField(values = {}) { - return new IndexPatternField( - fieldValues.indexPattern as IndexPattern, - { ...fieldValues, ...values }, - 'displayName', - () => {} - ); + return new IndexPatternField({ ...fieldValues, ...values }, 'displayName'); } const fieldValues = { name: 'name', - type: 'type', + type: 'string', script: 'script', lang: 'lang', count: 1, - esTypes: ['type'], + esTypes: ['text'], aggregatable: true, filterable: true, searchable: true, @@ -125,7 +120,7 @@ describe('Field', function () { const fieldB = getField({ indexed: true, type: KBN_FIELD_TYPES.STRING }); expect(fieldB.sortable).toEqual(true); - const fieldC = getField({ indexed: false }); + const fieldC = getField({ indexed: false, aggregatable: false, scripted: false }); expect(fieldC.sortable).toEqual(false); }); @@ -139,31 +134,26 @@ describe('Field', function () { const fieldC = getField({ indexed: true, type: KBN_FIELD_TYPES.STRING }); expect(fieldC.filterable).toEqual(true); - const fieldD = getField({ scripted: false, indexed: false }); + const fieldD = getField({ scripted: false, indexed: false, searchable: false }); expect(fieldD.filterable).toEqual(false); }); it('exports the property to JSON', () => { - const field = new IndexPatternField( - { fieldFormatMap: { name: {} } } as IndexPattern, - fieldValues, - 'displayName', - () => {} - ); + const field = new IndexPatternField(fieldValues, 'displayName'); expect(flatten(field)).toMatchSnapshot(); }); it('spec snapshot', () => { - const field = new IndexPatternField( - { - fieldFormatMap: { - name: { toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }) }, - }, - } as IndexPattern, - fieldValues, - 'displayName', - () => {} - ); - expect(field.toSpec()).toMatchSnapshot(); + const field = new IndexPatternField(fieldValues, 'displayName'); + const getFormatterForField = () => + ({ + toJSON: () => ({ + id: 'number', + params: { + pattern: '$0,0.[00]', + }, + }), + } as FieldFormat); + expect(field.toSpec({ getFormatterForField })).toMatchSnapshot(); }); }); diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 965f1a7f630651..7f72bfe55c7cd5 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -20,40 +20,27 @@ import { i18n } from '@kbn/i18n'; import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; -import { FieldFormat } from '../../field_formats'; import { IFieldType } from './types'; -import { OnNotification, FieldSpec } from '../types'; - -import { IndexPattern } from '../index_patterns'; +import { FieldSpec, IndexPattern } from '../..'; +import { FieldTypeUnknownError } from '../errors'; export class IndexPatternField implements IFieldType { readonly spec: FieldSpec; // not writable or serialized - readonly indexPattern: IndexPattern; readonly displayName: string; private readonly kbnFieldType: KbnFieldType; - constructor( - indexPattern: IndexPattern, - spec: FieldSpec, - displayName: string, - onNotification: OnNotification - ) { - this.indexPattern = indexPattern; + constructor(spec: FieldSpec, displayName: string) { this.spec = { ...spec, type: spec.name === '_source' ? '_source' : spec.type }; this.displayName = displayName; this.kbnFieldType = getKbnFieldType(spec.type); if (spec.type && this.kbnFieldType?.name === KBN_FIELD_TYPES.UNKNOWN) { - const title = i18n.translate('data.indexPatterns.unknownFieldHeader', { - values: { type: spec.type }, - defaultMessage: 'Unknown field type {type}', - }); - const text = i18n.translate('data.indexPatterns.unknownFieldErrorMessage', { - values: { name: spec.name, title: indexPattern.title }, - defaultMessage: 'Field {name} in indexPattern {title} is using an unknown field type.', + const msg = i18n.translate('data.indexPatterns.unknownFieldTypeErrorMsg', { + values: { type: spec.type, name: spec.name }, + defaultMessage: `Field '{name}' Unknown field type '{type}'`, }); - onNotification({ title, text, color: 'danger', iconType: 'alert' }); + throw new FieldTypeUnknownError(msg, spec); } } @@ -143,10 +130,6 @@ export class IndexPatternField implements IFieldType { return this.aggregatable; } - public get format(): FieldFormat { - return this.indexPattern.getFormatterForField(this); - } - public toJSON() { return { count: this.count, @@ -165,7 +148,11 @@ export class IndexPatternField implements IFieldType { }; } - public toSpec() { + public toSpec({ + getFormatterForField, + }: { + getFormatterForField?: IndexPattern['getFormatterForField']; + } = {}) { return { count: this.count, script: this.script, @@ -179,7 +166,7 @@ export class IndexPatternField implements IFieldType { aggregatable: this.aggregatable, readFromDocValues: this.readFromDocValues, subType: this.subType, - format: this.indexPattern?.fieldFormatMap[this.name]?.toJSON() || undefined, + format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined, }; } } diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index 558b5b57dce40a..5814760601a675 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { FieldSpec, IFieldSubType } from '../types'; +import { FieldSpec, IFieldSubType, IndexPattern } from '../..'; export interface IFieldType { name: string; @@ -38,5 +38,5 @@ export interface IFieldType { subType?: IFieldSubType; displayName?: string; format?: any; - toSpec?: () => FieldSpec; + toSpec?: (options?: { getFormatterForField?: IndexPattern['getFormatterForField'] }) => FieldSpec; } diff --git a/src/plugins/data/common/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index.ts index 51a642b775c29c..08f478404be2cf 100644 --- a/src/plugins/data/common/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index.ts @@ -20,3 +20,5 @@ export * from './fields'; export * from './types'; export { IndexPatternsService } from './index_patterns'; +export type { IndexPattern } from './index_patterns'; +export * from './errors'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index 047ac836a87d1f..a0c380ec55bf67 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -32,7 +32,12 @@ Object { "esTypes": Array [ "boolean", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "ssl", "readFromDocValues": true, @@ -49,7 +54,12 @@ Object { "esTypes": Array [ "date", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "@timestamp", "readFromDocValues": true, @@ -66,7 +76,12 @@ Object { "esTypes": Array [ "date", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "time", "readFromDocValues": true, @@ -83,7 +98,12 @@ Object { "esTypes": Array [ "keyword", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "@tags", "readFromDocValues": true, @@ -100,7 +120,12 @@ Object { "esTypes": Array [ "date", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "utc_time", "readFromDocValues": true, @@ -117,7 +142,12 @@ Object { "esTypes": Array [ "integer", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "phpmemory", "readFromDocValues": true, @@ -134,7 +164,12 @@ Object { "esTypes": Array [ "ip", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "ip", "readFromDocValues": true, @@ -151,7 +186,12 @@ Object { "esTypes": Array [ "attachment", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "request_body", "readFromDocValues": true, @@ -168,7 +208,12 @@ Object { "esTypes": Array [ "geo_point", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "point", "readFromDocValues": true, @@ -185,7 +230,12 @@ Object { "esTypes": Array [ "geo_shape", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "area", "readFromDocValues": false, @@ -202,7 +252,12 @@ Object { "esTypes": Array [ "murmur3", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "hashed", "readFromDocValues": false, @@ -219,7 +274,12 @@ Object { "esTypes": Array [ "geo_point", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "geo.coordinates", "readFromDocValues": true, @@ -236,7 +296,12 @@ Object { "esTypes": Array [ "text", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "extension", "readFromDocValues": false, @@ -253,7 +318,12 @@ Object { "esTypes": Array [ "keyword", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "extension.keyword", "readFromDocValues": true, @@ -274,7 +344,12 @@ Object { "esTypes": Array [ "text", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "machine.os", "readFromDocValues": false, @@ -291,7 +366,12 @@ Object { "esTypes": Array [ "keyword", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "machine.os.raw", "readFromDocValues": true, @@ -312,7 +392,12 @@ Object { "esTypes": Array [ "keyword", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "geo.src", "readFromDocValues": true, @@ -329,7 +414,12 @@ Object { "esTypes": Array [ "_id", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "_id", "readFromDocValues": false, @@ -346,7 +436,12 @@ Object { "esTypes": Array [ "_type", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "_type", "readFromDocValues": false, @@ -363,7 +458,12 @@ Object { "esTypes": Array [ "_source", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "_source", "readFromDocValues": false, @@ -380,7 +480,12 @@ Object { "esTypes": Array [ "text", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "non-filterable", "readFromDocValues": false, @@ -397,7 +502,12 @@ Object { "esTypes": Array [ "text", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "non-sortable", "readFromDocValues": false, @@ -414,7 +524,12 @@ Object { "esTypes": Array [ "conflict", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "custom_user_field", "readFromDocValues": true, @@ -431,7 +546,12 @@ Object { "esTypes": Array [ "text", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": "expression", "name": "script string", "readFromDocValues": false, @@ -448,7 +568,12 @@ Object { "esTypes": Array [ "long", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": "expression", "name": "script number", "readFromDocValues": false, @@ -465,7 +590,12 @@ Object { "esTypes": Array [ "date", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": "painless", "name": "script date", "readFromDocValues": false, @@ -482,7 +612,12 @@ Object { "esTypes": Array [ "murmur3", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": "expression", "name": "script murmur3", "readFromDocValues": false, diff --git a/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts b/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts index a0597ed4b90269..b47fef107258ac 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts @@ -34,9 +34,9 @@ export function formatHitProvider(indexPattern: IndexPattern, defaultFormat: any type: FieldFormatsContentType = 'html' ) { const field = indexPattern.fields.getByName(fieldName); - const format = field ? field.format : defaultFormat; + const format = field ? indexPattern.getFormatterForField(field) : defaultFormat; - return format.convert(val, type, { field, hit }); + return format.convert(val, type, { field, hit, indexPattern }); } function formatHit(hit: Record, type: string = 'html') { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index f7e1156170f032..2350cbb67683ef 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -29,6 +29,7 @@ import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_ import { IndexPatternField } from '../fields'; import { fieldFormatsMock } from '../../field_formats/mocks'; +import { FieldFormat } from '../..'; class MockFieldFormatter {} @@ -170,7 +171,6 @@ describe('IndexPattern', () => { test('should have expected properties on fields', function () { expect(indexPattern.fields[0]).toHaveProperty('displayName'); expect(indexPattern.fields[0]).toHaveProperty('filterable'); - expect(indexPattern.fields[0]).toHaveProperty('format'); expect(indexPattern.fields[0]).toHaveProperty('sortable'); expect(indexPattern.fields[0]).toHaveProperty('scripted'); }); @@ -319,16 +319,18 @@ describe('IndexPattern', () => { describe('toSpec', () => { test('should match snapshot', () => { - indexPattern.fieldFormatMap.bytes = { + const formatter = { toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }), - }; + } as FieldFormat; + indexPattern.getFormatterForField = () => formatter; expect(indexPattern.toSpec()).toMatchSnapshot(); }); test('can restore from spec', async () => { - indexPattern.fieldFormatMap.bytes = { + const formatter = { toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }), - }; + } as FieldFormat; + indexPattern.getFormatterForField = () => formatter; const spec = indexPattern.toSpec(); const restoredPattern = await create(spec.id as string); restoredPattern.initFromSpec(spec); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index ea91a9bb14e1f5..c82bf7d759328a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -26,11 +26,12 @@ import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, + FieldTypeUnknownError, FieldFormatNotFoundError, } from '../../../common'; import { findByTitle } from '../utils'; import { IndexPatternMissingIndices } from '../lib'; -import { IndexPatternField, IIndexPatternFieldList, FieldList } from '../fields'; +import { IndexPatternField, IIndexPatternFieldList, fieldList } from '../fields'; import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; @@ -125,8 +126,7 @@ export class IndexPattern implements IIndexPattern { this.shortDotsEnable = shortDotsEnable; this.metaFields = metaFields; - - this.fields = new FieldList(this, [], this.shortDotsEnable, this.onNotification); + this.fields = fieldList([], this.shortDotsEnable); this.apiClient = apiClient; this.fieldsFetcher = createFieldsFetcher(this, apiClient, metaFields); @@ -138,6 +138,22 @@ export class IndexPattern implements IIndexPattern { this.formatField = this.formatHit.formatField; } + private unknownFieldErrorNotification( + fieldType: string, + fieldName: string, + indexPatternTitle: string + ) { + const title = i18n.translate('data.indexPatterns.unknownFieldHeader', { + values: { type: fieldType }, + defaultMessage: 'Unknown field type {type}', + }); + const text = i18n.translate('data.indexPatterns.unknownFieldErrorMessage', { + values: { name: fieldName, title: indexPatternTitle }, + defaultMessage: 'Field {name} in indexPattern {title} is using an unknown field type.', + }); + this.onNotification({ title, text, color: 'danger', iconType: 'alert' }); + } + private serializeFieldFormatMap(flat: any, format: string, field: string | undefined) { if (format && field) { flat[field] = format; @@ -181,7 +197,15 @@ export class IndexPattern implements IIndexPattern { await this.refreshFields(); } else { if (specs) { - this.fields.replaceAll(specs); + try { + this.fields.replaceAll(specs); + } catch (err) { + if (err instanceof FieldTypeUnknownError) { + this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); + } else { + throw err; + } + } } } } @@ -203,7 +227,15 @@ export class IndexPattern implements IIndexPattern { this.timeFieldName = spec.timeFieldName; this.sourceFilters = spec.sourceFilters; - this.fields.replaceAll(spec.fields || []); + try { + this.fields.replaceAll(spec.fields || []); + } catch (err) { + if (err instanceof FieldTypeUnknownError) { + this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); + } else { + throw err; + } + } this.typeMeta = spec.typeMeta; this.fieldFormatMap = _.mapValues(fieldFormatMap, (mapping) => { @@ -322,7 +354,7 @@ export class IndexPattern implements IIndexPattern { title: this.title, timeFieldName: this.timeFieldName, sourceFilters: this.sourceFilters, - fields: this.fields.toSpec(), + fields: this.fields.toSpec({ getFormatterForField: this.getFormatterForField.bind(this) }), typeMeta: this.typeMeta, }; } @@ -342,19 +374,27 @@ export class IndexPattern implements IIndexPattern { throw new DuplicateField(name); } - this.fields.add({ - name, - script, - type: fieldType, - scripted: true, - lang, - aggregatable: true, - searchable: true, - count: 0, - readFromDocValues: false, - }); + try { + this.fields.add({ + name, + script, + type: fieldType, + scripted: true, + lang, + aggregatable: true, + searchable: true, + count: 0, + readFromDocValues: false, + }); + } catch (err) { + if (err instanceof FieldTypeUnknownError) { + this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); + } else { + throw err; + } - await this.save(); + await this.save(); + } } removeScriptedField(fieldName: string) { @@ -572,7 +612,15 @@ export class IndexPattern implements IIndexPattern { async _fetchFields() { const fields = await this.fieldsFetcher.fetch(this); const scripted = this.getScriptedFields().map((field) => field.spec); - this.fields.replaceAll([...fields, ...scripted]); + try { + this.fields.replaceAll([...fields, ...scripted]); + } catch (err) { + if (err instanceof FieldTypeUnknownError) { + this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); + } else { + throw err; + } + } } refreshFields() { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 0ad9ae8f2014f8..fe0d14b2d9c19a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -25,7 +25,6 @@ import { createEnsureDefaultIndexPattern, EnsureDefaultIndexPattern, } from './ensure_default_index_pattern'; -import { IndexPatternField } from '../fields'; import { OnNotification, OnError, @@ -86,15 +85,6 @@ export class IndexPatternsService { ); } - public createField( - indexPattern: IndexPattern, - spec: IndexPatternField['spec'], - displayName: string, - onNotification: OnNotification - ) { - return new IndexPatternField(indexPattern, spec, displayName, onNotification); - } - private async refreshSavedObjectsCache() { this.savedObjectsCache = await this.savedObjectsClient.find({ type: 'index-pattern', diff --git a/src/plugins/data/common/search/aggs/agg_config.test.ts b/src/plugins/data/common/search/aggs/agg_config.test.ts index a443eacee731c4..f6fcc29805dc45 100644 --- a/src/plugins/data/common/search/aggs/agg_config.test.ts +++ b/src/plugins/data/common/search/aggs/agg_config.test.ts @@ -25,8 +25,7 @@ import { AggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { mockAggTypesRegistry } from './test_helpers'; import { MetricAggType } from './metrics/metric_agg_type'; -import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; -import { IIndexPatternFieldList } from '../../index_patterns/fields'; +import { IndexPattern, IndexPatternField, IIndexPatternFieldList } from '../../index_patterns'; describe('AggConfig', () => { let indexPattern: IndexPattern; @@ -67,6 +66,9 @@ describe('AggConfig', () => { getByName: (name: string) => fields.find((f) => f.name === name), filter: () => fields, } as unknown) as IndexPattern['fields'], + getFormatterForField: (field: IndexPatternField) => ({ + toJSON: () => ({}), + }), } as IndexPattern; typesRegistry = mockAggTypesRegistry(); }); diff --git a/src/plugins/data/common/search/aggs/agg_type.test.ts b/src/plugins/data/common/search/aggs/agg_type.test.ts index bf1136159dfe86..16a5586858ab9f 100644 --- a/src/plugins/data/common/search/aggs/agg_type.test.ts +++ b/src/plugins/data/common/search/aggs/agg_type.test.ts @@ -147,6 +147,9 @@ describe('AggType Class', () => { }, }, }, + aggConfigs: { + indexPattern: { getFormatterForField: () => ({ toJSON: () => ({ id: 'format' }) }) }, + }, } as unknown) as IAggConfig; const aggType = new AggType({ name: 'name', diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 2ee604c1bf25da..1e3839038b0f71 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -271,7 +271,9 @@ export class AggType< this.getSerializedFormat = config.getSerializedFormat || ((agg: TAggConfig) => { - return agg.params.field ? agg.params.field.format.toJSON() : {}; + return agg.params.field + ? agg.aggConfigs.indexPattern.getFormatterForField(agg.params.field).toJSON() + : {}; }); this.getValue = config.getValue || ((agg: TAggConfig, bucket: any) => {}); diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts index 1a7deafb548aed..7c09d2e64e8b73 100644 --- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts +++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts @@ -140,7 +140,7 @@ export const buildOtherBucketAgg = ( const bucketAggs = aggConfigs.aggs.filter((agg) => agg.type.type === AggGroupNames.Buckets); const index = bucketAggs.findIndex((agg) => agg.id === aggWithOtherBucket.id); const aggs = aggConfigs.toDsl(); - const indexPattern = aggWithOtherBucket.params.field.indexPattern; + const indexPattern = aggWithOtherBucket.aggConfigs.indexPattern; // create filters aggregation const filterAgg = aggConfigs.createAggConfig( @@ -211,7 +211,7 @@ export const buildOtherBucketAgg = ( filters.push( buildExistsFilter( aggWithOtherBucket.params.field, - aggWithOtherBucket.params.field.indexPattern + aggWithOtherBucket.aggConfigs.indexPattern ) ); } @@ -264,7 +264,7 @@ export const mergeOtherBucketAggResponse = ( const phraseFilter = buildPhrasesFilter( otherAgg.params.field, requestFilterTerms, - otherAgg.params.field.indexPattern + otherAgg.aggConfigs.indexPattern ); phraseFilter.meta.negate = true; bucket.filters = [phraseFilter]; @@ -276,7 +276,7 @@ export const mergeOtherBucketAggResponse = ( ) ) { bucket.filters.push( - buildExistsFilter(otherAgg.params.field, otherAgg.params.field.indexPattern) + buildExistsFilter(otherAgg.params.field, otherAgg.aggConfigs.indexPattern) ); } aggResultBuckets.push(bucket); diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts index b57d530ef40e8d..dc1d0ec0a152f6 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts @@ -40,6 +40,7 @@ describe('AggConfig Filters', () => { getByName: () => field, filter: () => [field], }, + getFormatterForField: () => new BytesFormat({}, getConfig), } as any; return new AggConfigs( diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts index 30af970f55aa9a..b53ae44c05075c 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts @@ -30,7 +30,6 @@ describe('AggConfig Filters', () => { const getAggConfigs = () => { const field = { name: 'bytes', - format: new BytesFormat({}, getConfig), }; const indexPattern = { @@ -40,6 +39,7 @@ describe('AggConfig Filters', () => { getByName: () => field, filter: () => [field], }, + getFormatterForField: () => new BytesFormat({}, getConfig), } as any; return new AggConfigs( diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts index 95de19b96abd4f..ccd1cf6e358b4e 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts @@ -27,7 +27,7 @@ import { export const createFilterTerms = (aggConfig: IBucketAggConfig, key: string, params: any) => { const field = aggConfig.params.field; - const indexPattern = field.indexPattern; + const indexPattern = aggConfig.aggConfigs.indexPattern; if (key === '__other__') { const terms = params.terms; diff --git a/src/plugins/data/common/search/aggs/buckets/date_range.ts b/src/plugins/data/common/search/aggs/buckets/date_range.ts index eda35a77afa5ff..f9a3acb990fbf8 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_range.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_range.ts @@ -58,7 +58,9 @@ export const getDateRangeBucketAgg = ({ getSerializedFormat(agg) { return { id: 'date_range', - params: agg.params.field ? agg.params.field.format.toJSON() : {}, + params: agg.params.field + ? agg.aggConfigs.indexPattern.getFormatterForField(agg.params.field).toJSON() + : {}, }; }, makeLabel(aggConfig) { diff --git a/src/plugins/data/common/search/aggs/buckets/ip_range.ts b/src/plugins/data/common/search/aggs/buckets/ip_range.ts index 46e0b62d0f8d7d..d0a6174b011fc4 100644 --- a/src/plugins/data/common/search/aggs/buckets/ip_range.ts +++ b/src/plugins/data/common/search/aggs/buckets/ip_range.ts @@ -59,7 +59,9 @@ export const getIpRangeBucketAgg = () => getSerializedFormat(agg) { return { id: 'ip_range', - params: agg.params.field ? agg.params.field.format.toJSON() : {}, + params: agg.params.field + ? agg.aggConfigs.indexPattern.getFormatterForField(agg.params.field).toJSON() + : {}, }; }, makeLabel(aggConfig) { diff --git a/src/plugins/data/common/search/aggs/buckets/range.test.ts b/src/plugins/data/common/search/aggs/buckets/range.test.ts index b23b03db6a9ec2..b8241e04ea1ee8 100644 --- a/src/plugins/data/common/search/aggs/buckets/range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/range.test.ts @@ -27,12 +27,6 @@ describe('Range Agg', () => { const getAggConfigs = () => { const field = { name: 'bytes', - format: new NumberFormat( - { - pattern: '0,0.[000] b', - }, - getConfig - ), }; const indexPattern = { @@ -42,6 +36,13 @@ describe('Range Agg', () => { getByName: () => field, filter: () => [field], }, + getFormatterForField: () => + new NumberFormat( + { + pattern: '0,0.[000] b', + }, + getConfig + ), } as any; return new AggConfigs( diff --git a/src/plugins/data/common/search/aggs/buckets/range.ts b/src/plugins/data/common/search/aggs/buckets/range.ts index 91a357b6359504..169b234845274c 100644 --- a/src/plugins/data/common/search/aggs/buckets/range.ts +++ b/src/plugins/data/common/search/aggs/buckets/range.ts @@ -78,7 +78,9 @@ export const getRangeBucketAgg = ({ getFieldFormatsStart }: RangeBucketAggDepend return key; }, getSerializedFormat(agg) { - const format = agg.params.field ? agg.params.field.format.toJSON() : {}; + const format = agg.params.field + ? agg.aggConfigs.indexPattern.getFormatterForField(agg.params.field).toJSON() + : { id: undefined, params: undefined }; return { id: 'range', params: { diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index 5c8483cf213695..1363d38748c8ba 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -82,7 +82,9 @@ export const getTermsBucketAgg = () => return agg.getFieldDisplayName() + ': ' + params.order.text; }, getSerializedFormat(agg) { - const format = agg.params.field ? agg.params.field.format.toJSON() : {}; + const format = agg.params.field + ? agg.aggConfigs.indexPattern.getFormatterForField(agg.params.field).toJSON() + : { id: undefined, params: undefined }; return { id: 'terms', params: { diff --git a/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts b/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts index c6bba56f73ec78..4815ab0ac56dca 100644 --- a/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts @@ -66,9 +66,6 @@ describe('parent pipeline aggs', function () { ) => { const field = { name: 'field', - format: { - toJSON: () => ({ id: 'bytes' }), - }, }; const indexPattern = { id: '1234', @@ -77,6 +74,9 @@ describe('parent pipeline aggs', function () { getByName: () => field, filter: () => [field], }, + getFormatterForField: () => ({ + toJSON: () => ({ id: 'bytes' }), + }), } as any; const aggConfigs = new AggConfigs( diff --git a/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts b/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts index a157d225c839cb..32737f7b7237d3 100644 --- a/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts @@ -72,6 +72,9 @@ describe('sibling pipeline aggs', () => { getByName: () => field, filter: () => [field], }, + getFormatterForField: () => ({ + toJSON: () => ({ id: 'bytes' }), + }), } as any; const aggConfigs = new AggConfigs( diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts index a3b9b0b3448235..2ad20c3807819c 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -30,11 +30,7 @@ import { ValueClickContext } from '../../../../embeddable/public'; const mockField = { name: 'bytes', - indexPattern: { - id: 'logstash-*', - }, filterable: true, - format: new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), }; describe('createFiltersFromValueClick', () => { @@ -81,6 +77,8 @@ describe('createFiltersFromValueClick', () => { getByName: () => mockField, filter: () => [mockField], }, + getFormatterForField: () => + new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), }), } as unknown) as IndexPatternsContract); }); diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 27b16c57ffecf2..a9714a95ff3385 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -262,7 +262,7 @@ export { UI_SETTINGS, TypeMeta as IndexPatternTypeMeta, AggregationRestrictions as IndexPatternAggRestrictions, - FieldList, + fieldList, } from '../common'; /* diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 0c4465ae7f4b91..b09e957944b657 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -604,46 +604,11 @@ export type FieldFormatsContentType = 'html' | 'text'; // @public (undocumented) export type FieldFormatsGetConfigFn = GetConfigFn; -// Warning: (ae-missing-release-tag) "FieldList" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "fieldList" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class FieldList extends Array implements IIndexPatternFieldList { - // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "OnNotification" needs to be exported by the entry point index.d.ts - constructor(indexPattern: IndexPattern, specs?: FieldSpec[], shortDotsEnable?: boolean, onNotification?: OnNotification); - // (undocumented) - readonly add: (field: FieldSpec) => void; - // (undocumented) - readonly getAll: () => IndexPatternField[]; - // (undocumented) - readonly getByName: (name: IndexPatternField['name']) => IndexPatternField | undefined; - // (undocumented) - readonly getByType: (type: IndexPatternField['type']) => any[]; - // (undocumented) - readonly remove: (field: IFieldType) => void; - // (undocumented) - readonly removeAll: () => void; - // (undocumented) - readonly replaceAll: (specs: FieldSpec[]) => void; - // (undocumented) - readonly toSpec: () => { - count: number; - script: string | undefined; - lang: string | undefined; - conflictDescriptions: Record | undefined; - name: string; - type: string; - esTypes: string[] | undefined; - scripted: boolean; - searchable: boolean; - aggregatable: boolean; - readFromDocValues: boolean; - subType: import("../types").IFieldSubType | undefined; - format: any; - }[]; - // (undocumented) - readonly update: (field: FieldSpec) => void; -} +export const fieldList: (specs?: FieldSpec[], shortDotsEnable?: boolean) => IIndexPatternFieldList; // @public (undocumented) export interface FieldMappingSpec { @@ -868,7 +833,9 @@ export interface IFieldType { // (undocumented) subType?: IFieldSubType; // (undocumented) - toSpec?: () => FieldSpec; + toSpec?: (options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }) => FieldSpec; // (undocumented) type: string; // (undocumented) @@ -919,6 +886,10 @@ export interface IIndexPatternFieldList extends Array { // (undocumented) replaceAll(specs: FieldSpec[]): void; // (undocumented) + toSpec(options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }): FieldSpec[]; + // (undocumented) update(field: FieldSpec): void; } @@ -1100,7 +1071,7 @@ export interface IndexPatternAttributes { // // @public (undocumented) export class IndexPatternField implements IFieldType { - constructor(indexPattern: IndexPattern, spec: FieldSpec, displayName: string, onNotification: OnNotification); + constructor(spec: FieldSpec, displayName: string); // (undocumented) get aggregatable(): boolean; // (undocumented) @@ -1116,10 +1087,6 @@ export class IndexPatternField implements IFieldType { // (undocumented) get filterable(): boolean; // (undocumented) - get format(): FieldFormat; - // (undocumented) - readonly indexPattern: IndexPattern; - // (undocumented) get lang(): string | undefined; set lang(lang: string | undefined); // (undocumented) @@ -1155,7 +1122,9 @@ export class IndexPatternField implements IFieldType { subType: import("../types").IFieldSubType | undefined; }; // (undocumented) - toSpec(): { + toSpec({ getFormatterForField, }?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }): { count: number; script: string | undefined; lang: string | undefined; @@ -1168,7 +1137,10 @@ export class IndexPatternField implements IFieldType { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; - format: any; + format: { + id: any; + params: any; + } | undefined; }; // (undocumented) get type(): string; diff --git a/src/plugins/data/public/search/expressions/create_filter.test.ts b/src/plugins/data/public/search/expressions/create_filter.test.ts index 7968c806285318..7cc336a1c20e96 100644 --- a/src/plugins/data/public/search/expressions/create_filter.test.ts +++ b/src/plugins/data/public/search/expressions/create_filter.test.ts @@ -52,6 +52,7 @@ describe('createFilter', () => { getByName: () => field, filter: () => [field], }, + getFormatterForField: () => new BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), } as any; return new AggConfigs( diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 9f114f21320098..9497a41b45ff9c 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -565,7 +565,9 @@ export interface IFieldType { // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts // // (undocumented) - toSpec?: () => FieldSpec; + toSpec?: (options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }) => FieldSpec; // (undocumented) type: string; // (undocumented) @@ -1063,6 +1065,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // Warnings were encountered during analysis: // +// src/plugins/data/common/index_patterns/fields/types.ts:41:25 - (ae-forgotten-export) The symbol "IndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 6d1238e02c7fb9..b03b37da40908d 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -62,7 +62,6 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals ); const field = new IndexPatternField( - indexPattern, { name: 'bytes', type: 'number', @@ -73,8 +72,7 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals aggregatable: true, readFromDocValues: true, }, - 'bytes', - () => {} + 'bytes' ); const props = { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 1f27766a1756d3..361c0707fef6b4 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -111,8 +111,8 @@ export function DiscoverSidebar({ ); const getDetailsByField = useCallback( - (ipField: IndexPatternField) => getDetails(ipField, hits, columns), - [hits, columns] + (ipField: IndexPatternField) => getDetails(ipField, hits, columns, selectedIndexPattern), + [hits, columns, selectedIndexPattern] ); const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING); diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.js b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.js index e055d644e1f915..0ee279ac5b7272 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.js +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.js @@ -20,9 +20,9 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -function getFieldValues(hits, field) { +function getFieldValues(hits, field, indexPattern) { const name = field.name; - const flattenHit = field.indexPattern.flattenHit; + const flattenHit = indexPattern.flattenHit; return _.map(hits, function (hit) { return flattenHit(hit)[name]; }); @@ -49,7 +49,7 @@ function getFieldValueCounts(params) { }; } - const allValues = getFieldValues(params.hits, params.field); + const allValues = getFieldValues(params.hits, params.field, params.indexPattern); let counts; const missing = _countMissing(allValues); diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts index 87401818c49073..8746883a5d9680 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts @@ -148,7 +148,8 @@ describe('fieldCalculator', function () { it('Should return an array of values for _source fields', function () { const extensions = fieldCalculator.getFieldValues( hits, - indexPattern.fields.getByName('extension') + indexPattern.fields.getByName('extension'), + indexPattern ); expect(extensions).toBeInstanceOf(Array); expect( @@ -160,7 +161,11 @@ describe('fieldCalculator', function () { }); it('Should return an array of values for core meta fields', function () { - const types = fieldCalculator.getFieldValues(hits, indexPattern.fields.getByName('_type')); + const types = fieldCalculator.getFieldValues( + hits, + indexPattern.fields.getByName('_type'), + indexPattern + ); expect(types).toBeInstanceOf(Array); expect( _.filter(types, function (v) { @@ -172,12 +177,13 @@ describe('fieldCalculator', function () { }); describe('getFieldValueCounts', function () { - let params: { hits: any; field: any; count: number }; + let params: { hits: any; field: any; count: number; indexPattern: IndexPattern }; beforeEach(function () { params = { hits: _.cloneDeep(realHits), field: indexPattern.fields.getByName('extension'), count: 3, + indexPattern, }; }); diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts index 41d3393672474b..13051f88c9591a 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts @@ -19,17 +19,19 @@ // @ts-ignore import { fieldCalculator } from './field_calculator'; -import { IndexPatternField } from '../../../../../../data/public'; +import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; export function getDetails( field: IndexPatternField, hits: Array>, - columns: string[] + columns: string[], + indexPattern: IndexPattern ) { const details = { ...fieldCalculator.getFieldValueCounts({ hits, field, + indexPattern, count: 5, grouped: false, }), @@ -37,7 +39,7 @@ export function getDetails( }; if (details.buckets) { for (const bucket of details.buckets) { - bucket.display = field.format.convert(bucket.value); + bucket.display = indexPattern.getFormatterForField(field).convert(bucket.value); } } return details; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts b/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts index 00e00aa8e29913..c96a8f5ce17b98 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts @@ -31,6 +31,7 @@ export function getIndexPatternFieldList( difference(fieldNamesInDocs, fieldNamesInIndexPattern).forEach((unknownFieldName) => { unknownTypes.push({ + displayName: String(unknownFieldName), name: String(unknownFieldName), type: 'unknown', } as IndexPatternField); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap index 47cabc4df662f0..45253f6ad27c03 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap @@ -15,13 +15,10 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = ` "displayName": "Elastic", "excluded": false, "format": undefined, - "indexPattern": Object { - "getNonScriptedFields": [Function], - }, "info": Array [], "name": "Elastic", "searchable": true, - "type": "name", + "type": "string", }, ] } @@ -44,9 +41,6 @@ exports[`IndexedFieldsTable should filter based on the type filter 1`] = ` "displayName": "timestamp", "excluded": false, "format": undefined, - "indexPattern": Object { - "getNonScriptedFields": [Function], - }, "info": Array [], "name": "timestamp", "type": "date", @@ -72,21 +66,15 @@ exports[`IndexedFieldsTable should render normally 1`] = ` "displayName": "Elastic", "excluded": false, "format": undefined, - "indexPattern": Object { - "getNonScriptedFields": [Function], - }, "info": Array [], "name": "Elastic", "searchable": true, - "type": "name", + "type": "string", }, Object { "displayName": "timestamp", "excluded": false, "format": undefined, - "indexPattern": Object { - "getNonScriptedFields": [Function], - }, "info": Array [], "name": "timestamp", "type": "date", @@ -95,9 +83,6 @@ exports[`IndexedFieldsTable should render normally 1`] = ` "displayName": "conflictingField", "excluded": false, "format": undefined, - "indexPattern": Object { - "getNonScriptedFields": [Function], - }, "info": Array [], "name": "conflictingField", "type": "conflict", diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 411bbe23e47618..319b9b2b3fce2a 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { IndexPatternField, IIndexPattern, IndexPattern } from 'src/plugins/data/public'; +import { IndexPatternField, IIndexPattern } from 'src/plugins/data/public'; import { IndexedFieldsTable } from './indexed_fields_table'; jest.mock('@elastic/eui', () => ({ @@ -47,10 +47,8 @@ const indexPattern = ({ const mockFieldToIndexPatternField = (spec: Record) => { return new IndexPatternField( - indexPattern as IndexPattern, (spec as unknown) as IndexPatternField['spec'], - spec.displayName as string, - () => {} + spec.displayName as string ); }; @@ -59,7 +57,7 @@ const fields = [ name: 'Elastic', displayName: 'Elastic', searchable: true, - type: 'name', + type: 'string', }, { name: 'timestamp', displayName: 'timestamp', type: 'date' }, { name: 'conflictingField', displayName: 'conflictingField', type: 'conflict' }, diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 90f81a88b3da05..23977aac7fa7a2 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -77,7 +77,6 @@ export class IndexedFieldsTable extends Component< return { ...field.spec, displayName: field.displayName, - indexPattern: field.indexPattern, format: getFieldFormat(indexPattern, field.name), excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field), diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx index 6c9d6db8de130e..3bc9cd34f29841 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -176,7 +176,7 @@ export function Tabs({ indexPattern, fields, history, location }: TabsProps) { indexedFieldTypeFilter={indexedFieldTypeFilter} helpers={{ redirectToRoute: (field: IndexPatternField) => { - history.push(getPath(field)); + history.push(getPath(field, indexPattern)); }, getFieldInfo: indexPatternManagementStart.list.getFieldInfo, }} @@ -195,7 +195,7 @@ export function Tabs({ indexPattern, fields, history, location }: TabsProps) { scriptedFieldLanguageFilter={scriptedFieldLanguageFilter} helpers={{ redirectToRoute: (field: IndexPatternField) => { - history.push(getPath(field)); + history.push(getPath(field, indexPattern)); }, }} onRemoveField={refreshFilters} diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts index b422de93de7a97..91c5cc1afdb49e 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts @@ -116,8 +116,8 @@ export function getTabs( return tabs; } -export function getPath(field: IndexPatternField) { - return `/patterns/${field.indexPattern?.id}/field/${field.name}`; +export function getPath(field: IndexPatternField, indexPattern: IndexPattern) { + return `/patterns/${indexPattern?.id}/field/${field.name}`; } const allTypesDropDown = i18n.translate( diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx index 96d3fc549ece01..b0385a61a72ace 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx @@ -138,12 +138,12 @@ describe('FieldEditor', () => { name: 'test', script: 'doc.test.value', }; - fieldList.push(testField as IndexPatternField); + fieldList.push((testField as unknown) as IndexPatternField); indexPattern.fields.getByName = (name) => { const flds = { [testField.name]: testField, }; - return flds[name] as IndexPatternField; + return (flds[name] as unknown) as IndexPatternField; }; const component = createComponentWithContext( @@ -173,7 +173,7 @@ describe('FieldEditor', () => { const flds = { [testField.name]: testField, }; - return flds[name] as IndexPatternField; + return (flds[name] as unknown) as IndexPatternField; }; const component = createComponentWithContext( diff --git a/src/plugins/input_control_vis/public/control/control.ts b/src/plugins/input_control_vis/public/control/control.ts index 91e8f1b26164ba..da2dc7bab7cf7a 100644 --- a/src/plugins/input_control_vis/public/control/control.ts +++ b/src/plugins/input_control_vis/public/control/control.ts @@ -81,9 +81,10 @@ export abstract class Control { abstract destroy(): void; format = (value: any) => { + const indexPattern = this.filterManager.getIndexPattern(); const field = this.filterManager.getField(); - if (field?.format?.convert) { - return field.format.convert(value); + if (field) { + return indexPattern.getFormatterForField(field).convert(value); } return value; diff --git a/src/test_utils/public/stub_index_pattern.js b/src/test_utils/public/stub_index_pattern.js index f7b65930b683d4..3289610a062b0e 100644 --- a/src/test_utils/public/stub_index_pattern.js +++ b/src/test_utils/public/stub_index_pattern.js @@ -22,7 +22,7 @@ import sinon from 'sinon'; // because it is one of the few places that we need to access the IndexPattern class itself, rather // than just the type. Doing this as a temporary measure; it will be left behind when migrating to NP. -import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, FieldList } from '../../plugins/data/public'; +import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, fieldList } from '../../plugins/data/public'; import { setFieldFormats } from '../../plugins/data/public/services'; @@ -64,7 +64,7 @@ export default function StubIndexPattern(pattern, getConfig, timeField, fields, }); this._reindexFields = function () { - this.fields = new FieldList(this, this.fields || fields, false); + this.fields = fieldList(this.fields || fields, false); }; this.stubSetFieldFormat = function (fieldName, id, params) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js index 56b830e9ff0983..d51ca46fd98fff 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js @@ -281,7 +281,7 @@ export class AbstractESSource extends AbstractVectorSource { return null; } - return fieldFromIndexPattern.format.getConverterFor('text'); + return indexPattern.getFormatterForField(fieldFromIndexPattern).getConverterFor('text'); } async loadStylePropsMeta( diff --git a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts index 516ad25f933f66..348cd5e5522582 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts @@ -10,8 +10,8 @@ import { IField } from '../fields/field'; import { esFilters, Filter, - IFieldType, IndexPattern, + IndexPatternField, } from '../../../../../../src/plugins/data/public'; export class ESTooltipProperty implements ITooltipProperty { @@ -37,7 +37,7 @@ export class ESTooltipProperty implements ITooltipProperty { return this._tooltipProperty.getRawValue(); } - _getIndexPatternField(): IFieldType | undefined { + _getIndexPatternField(): IndexPatternField | undefined { return this._indexPattern.fields.getByName(this._field.getRootName()); } @@ -56,10 +56,11 @@ export class ESTooltipProperty implements ITooltipProperty { } } - const htmlConverter = indexPatternField.format.getConverterFor('html'); + const formatter = this._indexPattern.getFormatterForField(indexPatternField); + const htmlConverter = formatter.getConverterFor('html'); return htmlConverter ? htmlConverter(this.getRawValue()) - : indexPatternField.format.convert(this.getRawValue()); + : formatter.convert(this.getRawValue()); } isFilterable(): boolean { diff --git a/x-pack/plugins/ml/public/application/services/field_format_service.ts b/x-pack/plugins/ml/public/application/services/field_format_service.ts index 1a5d22e7320a58..b66dac15b13647 100644 --- a/x-pack/plugins/ml/public/application/services/field_format_service.ts +++ b/x-pack/plugins/ml/public/application/services/field_format_service.ts @@ -77,7 +77,7 @@ class FieldFormatService { const fieldList = fullIndexPattern.fields; const field = fieldList.getByName(fieldName); if (field !== undefined) { - fieldFormat = field.format; + fieldFormat = fullIndexPattern.getFormatterForField(field); } } @@ -104,7 +104,9 @@ class FieldFormatService { if (dtr.field_name !== undefined && esAgg !== 'cardinality') { const field = fieldList.getByName(dtr.field_name); if (field !== undefined) { - formatsByDetector[dtr.detector_index!] = field.format; + formatsByDetector[dtr.detector_index!] = indexPatternData.getFormatterForField( + field + ); } } }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index 0d98a0f2f26ff5..073cb46d3949a3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -140,7 +140,11 @@ describe('helpers', () => { filterManager: mockFilterManager, query: mockQueryBarWithFilters.query, savedId: mockQueryBarWithFilters.saved_id, - indexPatterns: { fields: [{ name: 'test name', type: 'test type' }], title: 'test title' }, + indexPatterns: { + fields: [{ name: 'event.category', type: 'test type' }], + title: 'test title', + getFormatterForField: () => ({ convert: (val: unknown) => val }), + }, }); const wrapper = shallow(result[0].description as React.ReactElement); const filterLabelComponent = wrapper.find(esFilters.FilterLabel).at(0); From 9519fcd4a22d48c1ae8c01784d73acd4e67f9e19 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 2 Sep 2020 15:08:43 -0600 Subject: [PATCH 15/25] Fix issue where defaultAppId redirect could fire outside home app (#76415) --- src/plugins/home/public/application/application.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/plugins/home/public/application/application.tsx b/src/plugins/home/public/application/application.tsx index 5d71bf8651d88a..a4a158bc7dbde5 100644 --- a/src/plugins/home/public/application/application.tsx +++ b/src/plugins/home/public/application/application.tsx @@ -47,6 +47,13 @@ export const renderApp = async ( chrome.setBreadcrumbs([{ text: homeTitle }]); + // dispatch synthetic hash change event to update hash history objects + // this is necessary because hash updates triggered by using popState won't trigger this event naturally. + // This must be called before the app is mounted to avoid call this after the redirect to default app logic kicks in + const unlisten = history.listen((location) => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); + render( @@ -54,12 +61,6 @@ export const renderApp = async ( element ); - // dispatch synthetic hash change event to update hash history objects - // this is necessary because hash updates triggered by using popState won't trigger this event naturally. - const unlisten = history.listen(() => { - window.dispatchEvent(new HashChangeEvent('hashchange')); - }); - return () => { unmountComponentAtNode(element); unlisten(); From 1ffe24955cc72e2453b6f643a0f38876fa394ad3 Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 2 Sep 2020 14:53:58 -0700 Subject: [PATCH 16/25] Split up and clarify Enterprise Search codeowners (#76580) @elastic/enterprise-search-frontend is a shared team of both app-search-frontend and workplace-search-frontend, but combining it into one group means only 1 reviewer is required (as opposed to 1 per team). --- .github/CODEOWNERS | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1d0f6fc50ee9ba..b4563dd1f9a9c0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -209,8 +209,19 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/ @elastic/kibana-alerting-services # Enterprise Search -/x-pack/plugins/enterprise_search/ @elastic/app-search-frontend @elastic/workplace-search-frontend -/x-pack/test/functional_enterprise_search/ @elastic/app-search-frontend @elastic/workplace-search-frontend +# Shared +/x-pack/plugins/enterprise_search/ @elastic/enterprise-search-frontend +/x-pack/test/functional_enterprise_search/ @elastic/enterprise-search-frontend +# App Search +/x-pack/plugins/enterprise_search/public/applications/app_search @elastic/app-search-frontend +/x-pack/plugins/enterprise_search/server/routes/app_search @elastic/app-search-frontend +/x-pack/plugins/enterprise_search/server/collectors/app_search @elastic/app-search-frontend +/x-pack/plugins/enterprise_search/server/saved_objects/app_search @elastic/app-search-frontend +# Workplace Search +/x-pack/plugins/enterprise_search/public/applications/workplace_search @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/server/routes/workplace_search @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/server/collectors/workplace_search @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/server/saved_objects/workplace_search @elastic/workplace-search-frontend # Elasticsearch UI /src/plugins/dev_tools/ @elastic/es-ui From 46aef4b1e0594e41c51a6b4ed0ba069872c5bc33 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 2 Sep 2020 15:18:37 -0700 Subject: [PATCH 17/25] skip failing suite (#76581) --- .../reporting_api_integration/reporting_and_security/usage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts index feda5c1386e98d..49db8696c11349 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts @@ -21,7 +21,8 @@ export default function ({ getService }: FtrProviderContext) { const reportingAPI = getService('reportingAPI'); const usageAPI = getService('usageAPI'); - describe('Usage', () => { + // FAILING: https://github.com/elastic/kibana/issues/76581 + describe.skip('Usage', () => { before(async () => { await esArchiver.load(OSS_KIBANA_ARCHIVE_PATH); await esArchiver.load(OSS_DATA_ARCHIVE_PATH); From ee2ceef9bb904e18487441fe5f0853f2f9a3967a Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Wed, 2 Sep 2020 17:20:14 -0500 Subject: [PATCH 18/25] Index Pattern class - remove toJSON and toString (#76246) * remove toJSON * remove toJSON, toString * fix ml url * Update actions_panel.tsx * fix url Co-authored-by: Elastic Machine --- ...ana-plugin-plugins-data-public.indexpattern.md | 2 -- ...gin-plugins-data-public.indexpattern.tojson.md | 15 --------------- ...n-plugins-data-public.indexpattern.tostring.md | 15 --------------- .../index_patterns/index_pattern.test.ts | 2 -- .../index_patterns/index_pattern.ts | 8 -------- src/plugins/data/public/public.api.md | 4 ---- .../components/actions_panel/actions_panel.tsx | 6 ++++-- 7 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tojson.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tostring.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 37db063e284ec3..4c53af3f8970ed 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -61,7 +61,5 @@ export declare class IndexPattern implements IIndexPattern | [refreshFields()](./kibana-plugin-plugins-data-public.indexpattern.refreshfields.md) | | | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | | | [save(saveAttempts)](./kibana-plugin-plugins-data-public.indexpattern.save.md) | | | -| [toJSON()](./kibana-plugin-plugins-data-public.indexpattern.tojson.md) | | | | [toSpec()](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) | | | -| [toString()](./kibana-plugin-plugins-data-public.indexpattern.tostring.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tojson.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tojson.md deleted file mode 100644 index 0ae04bb424d44c..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tojson.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [toJSON](./kibana-plugin-plugins-data-public.indexpattern.tojson.md) - -## IndexPattern.toJSON() method - -Signature: - -```typescript -toJSON(): string | undefined; -``` -Returns: - -`string | undefined` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tostring.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tostring.md deleted file mode 100644 index a10b549a7b9ebb..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tostring.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [toString](./kibana-plugin-plugins-data-public.indexpattern.tostring.md) - -## IndexPattern.toString() method - -Signature: - -```typescript -toString(): string; -``` -Returns: - -`string` - diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index 2350cbb67683ef..f037a71b508a27 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -151,8 +151,6 @@ describe('IndexPattern', () => { expect(indexPattern).toHaveProperty('getNonScriptedFields'); expect(indexPattern).toHaveProperty('addScriptedField'); expect(indexPattern).toHaveProperty('removeScriptedField'); - expect(indexPattern).toHaveProperty('toString'); - expect(indexPattern).toHaveProperty('toJSON'); expect(indexPattern).toHaveProperty('save'); // properties diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index c82bf7d759328a..2feeb5441ab836 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -650,12 +650,4 @@ export class IndexPattern implements IIndexPattern { }); }); } - - toJSON() { - return this.id; - } - - toString() { - return '' + this.toJSON(); - } } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index b09e957944b657..e72fea160cc288 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1022,12 +1022,8 @@ export class IndexPattern implements IIndexPattern { // (undocumented) title: string; // (undocumented) - toJSON(): string | undefined; - // (undocumented) toSpec(): IndexPatternSpec; // (undocumented) - toString(): string; - // (undocumented) type: string | undefined; // (undocumented) typeMeta?: IndexPatternTypeMeta; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx index 16d9e0c5418fa4..1f2c97b128e3f8 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx @@ -13,6 +13,7 @@ import { EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; import { DataRecognizer } from '../../../../components/data_recognizer'; +import { getBasePath } from '../../../../util/dependency_cache'; interface Props { indexPattern: IndexPattern; @@ -20,6 +21,7 @@ interface Props { export const ActionsPanel: FC = ({ indexPattern }) => { const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); + const basePath = getBasePath(); const recognizerResults = { count: 0, @@ -31,7 +33,7 @@ export const ActionsPanel: FC = ({ indexPattern }) => { function openAdvancedJobWizard() { // TODO - pass the search string to the advanced job page as well as the index pattern // (add in with new advanced job wizard?) - window.open(`#/jobs/new_job/advanced?index=${indexPattern}`, '_self'); + window.open(`${basePath.get()}/app/ml/jobs/new_job/advanced?index=${indexPattern.id}`, '_self'); } // Note we use display:none for the DataRecognizer section as it needs to be @@ -86,7 +88,7 @@ export const ActionsPanel: FC = ({ indexPattern }) => { 'Use the full range of options to create a job for more advanced use cases', })} onClick={openAdvancedJobWizard} - href={`#/jobs/new_job/advanced?index=${indexPattern}`} + href={`${basePath.get()}/app/ml/jobs/new_job/advanced?index=${indexPattern.id}`} data-test-subj="mlDataVisualizerCreateAdvancedJobCard" />
From aab8d3c7ddf97306f00c219f6ab30d6e4cec2618 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 2 Sep 2020 20:07:40 -0400 Subject: [PATCH 19/25] [Security Solution][Exceptions] - Updates enum schema and tests (#76544) ## Summary Mistmatch caught between the io-ts type and the corresponding typescript enum. Currently, io-ts does not have support for enums - as a workaround I had made a matching typescript enum type. Tests were added to try to ensure the two stayed in sync, but didn't do a straight up comparison of the two. Updated the tests to now error if the keys do not match. --- .../common/schemas/common/schemas.test.ts | 66 +++---------------- .../lists/common/schemas/common/schemas.ts | 7 -- x-pack/plugins/lists/common/shared_exports.ts | 1 - .../common/shared_imports.ts | 1 - 4 files changed, 10 insertions(+), 65 deletions(-) diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts index fad8ecc86277bd..ec3871b6738883 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts @@ -16,6 +16,8 @@ import { EsDataTypeRangeTerm, EsDataTypeSingle, EsDataTypeUnion, + ExceptionListTypeEnum, + OperatorEnum, Type, esDataTypeGeoPoint, esDataTypeGeoPointRange, @@ -25,60 +27,10 @@ import { esDataTypeUnion, exceptionListType, operator, - operator_type as operatorType, type, } from './schemas'; describe('Common schemas', () => { - describe('operatorType', () => { - test('it should validate for "match"', () => { - const payload = 'match'; - const decoded = operatorType.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate for "match_any"', () => { - const payload = 'match_any'; - const decoded = operatorType.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate for "list"', () => { - const payload = 'list'; - const decoded = operatorType.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate for "exists"', () => { - const payload = 'exists'; - const decoded = operatorType.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should contain 4 keys', () => { - // Might seem like a weird test, but its meant to - // ensure that if operatorType is updated, you - // also update the OperatorTypeEnum, a workaround - // for io-ts not yet supporting enums - // https://github.com/gcanti/io-ts/issues/67 - const keys = Object.keys(operatorType.keys); - - expect(keys.length).toEqual(4); - }); - }); - describe('operator', () => { test('it should validate for "included"', () => { const payload = 'included'; @@ -98,15 +50,16 @@ describe('Common schemas', () => { expect(message.schema).toEqual(payload); }); - test('it should contain 2 keys', () => { + test('it should contain same amount of keys as enum', () => { // Might seem like a weird test, but its meant to // ensure that if operator is updated, you // also update the operatorEnum, a workaround // for io-ts not yet supporting enums // https://github.com/gcanti/io-ts/issues/67 - const keys = Object.keys(operator.keys); + const keys = Object.keys(operator.keys).sort().join(',').toLowerCase(); + const enumKeys = Object.keys(OperatorEnum).sort().join(',').toLowerCase(); - expect(keys.length).toEqual(2); + expect(keys).toEqual(enumKeys); }); }); @@ -129,15 +82,16 @@ describe('Common schemas', () => { expect(message.schema).toEqual(payload); }); - test('it should contain 2 keys', () => { + test('it should contain same amount of keys as enum', () => { // Might seem like a weird test, but its meant to // ensure that if exceptionListType is updated, you // also update the ExceptionListTypeEnum, a workaround // for io-ts not yet supporting enums // https://github.com/gcanti/io-ts/issues/67 - const keys = Object.keys(exceptionListType.keys); + const keys = Object.keys(exceptionListType.keys).sort().join(',').toLowerCase(); + const enumKeys = Object.keys(ExceptionListTypeEnum).sort().join(',').toLowerCase(); - expect(keys.length).toEqual(2); + expect(keys).toEqual(enumKeys); }); }); diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index 1556ef5a5dab9d..37da5fbcd1a1b0 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -282,13 +282,6 @@ export enum OperatorEnum { EXCLUDED = 'excluded', } -export const operator_type = t.keyof({ - exists: null, - list: null, - match: null, - match_any: null, -}); -export type OperatorType = t.TypeOf; export enum OperatorTypeEnum { NESTED = 'nested', MATCH = 'match', diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 1f6c65919b063a..361837bdef2291 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -25,7 +25,6 @@ export { NamespaceType, Operator, OperatorEnum, - OperatorType, OperatorTypeEnum, ExceptionListTypeEnum, comment, diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index e28d1969b39763..564254b6a75963 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -25,7 +25,6 @@ export { NamespaceType, Operator, OperatorEnum, - OperatorType, OperatorTypeEnum, ExceptionListTypeEnum, exceptionListItemSchema, From 2f017b0e85c5e2e82d344a5fe8d8408a2faec502 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Wed, 2 Sep 2020 21:48:41 -0600 Subject: [PATCH 20/25] [Security Solution][Detection Engine] Remove RuleTypeSchema in favor of Type for TypeScript (#76586) ## Summary Removes RuleTypeSchema in favor of Type for TypeScript. Does break out one function called `parseScheduleDates` into its own file to remove a circular ref issue. --- .../detection_engine/parse_schedule_dates.ts | 20 +++++++++ .../schemas/common/schemas.ts | 2 +- .../common/detection_engine/types.ts | 10 ----- .../common/detection_engine/utils.ts | 19 +-------- .../common/machine_learning/helpers.ts | 4 +- .../timeline_actions/alert_context_menu.tsx | 5 +-- .../rules/description_step/helpers.tsx | 5 +-- .../rules/description_step/index.tsx | 4 +- .../rules/select_rule_type/index.tsx | 6 +-- .../detection_engine/rules/types.ts | 6 +-- .../detection_engine/rules/create/helpers.ts | 6 +-- .../pages/detection_engine/rules/helpers.tsx | 41 ++++++++++--------- .../pages/detection_engine/rules/types.ts | 7 ++-- .../rules_notification_alert_type.ts | 2 +- .../signals/signal_rule_alert_type.test.ts | 3 +- .../signals/signal_rule_alert_type.ts | 2 +- .../detection_engine/signals/utils.test.ts | 2 +- .../lib/detection_engine/signals/utils.ts | 3 +- .../server/lib/detection_engine/types.ts | 4 +- .../server/lib/machine_learning/authz.ts | 6 +-- 20 files changed, 77 insertions(+), 80 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/parse_schedule_dates.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/parse_schedule_dates.ts b/x-pack/plugins/security_solution/common/detection_engine/parse_schedule_dates.ts new file mode 100644 index 00000000000000..b21cd84684fbc2 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/parse_schedule_dates.ts @@ -0,0 +1,20 @@ +/* + * 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 moment from 'moment'; +import dateMath from '@elastic/datemath'; + +export const parseScheduleDates = (time: string): moment.Moment | null => { + const isValidDateString = !isNaN(Date.parse(time)); + const isValidInput = isValidDateString || time.trim().startsWith('now'); + const formattedDate = isValidDateString + ? moment(time) + : isValidInput + ? dateMath.parse(time) + : null; + + return formattedDate ?? null; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 64f2f223a3073c..a9f39d2db6080f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -14,7 +14,7 @@ import { UUID } from '../types/uuid'; import { IsoDateString } from '../types/iso_date_string'; import { PositiveIntegerGreaterThanZero } from '../types/positive_integer_greater_than_zero'; import { PositiveInteger } from '../types/positive_integer'; -import { parseScheduleDates } from '../../utils'; +import { parseScheduleDates } from '../../parse_schedule_dates'; export const author = t.array(t.string); export type Author = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/types.ts b/x-pack/plugins/security_solution/common/detection_engine/types.ts index 7c752bca49dbdf..e7aab2fa5d219e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/types.ts @@ -3,18 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import * as t from 'io-ts'; - import { AlertAction } from '../../../alerts/common'; export type RuleAlertAction = Omit & { action_type_id: string; }; - -export const RuleTypeSchema = t.keyof({ - query: null, - saved_query: null, - machine_learning: null, - threshold: null, -}); -export type RuleType = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index a70258c2684b6c..a72d641fbaf783 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; -import dateMath from '@elastic/datemath'; - import { EntriesArray } from '../shared_imports'; -import { RuleType } from './types'; +import { Type } from './schemas/common/schemas'; export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); @@ -20,16 +17,4 @@ export const hasNestedEntry = (entries: EntriesArray): boolean => { return found.length > 0; }; -export const isThresholdRule = (ruleType: RuleType) => ruleType === 'threshold'; - -export const parseScheduleDates = (time: string): moment.Moment | null => { - const isValidDateString = !isNaN(Date.parse(time)); - const isValidInput = isValidDateString || time.trim().startsWith('now'); - const formattedDate = isValidDateString - ? moment(time) - : isValidInput - ? dateMath.parse(time) - : null; - - return formattedDate ?? null; -}; +export const isThresholdRule = (ruleType: Type) => ruleType === 'threshold'; diff --git a/x-pack/plugins/security_solution/common/machine_learning/helpers.ts b/x-pack/plugins/security_solution/common/machine_learning/helpers.ts index fe3eb79a6f6109..73dc30f238c7e1 100644 --- a/x-pack/plugins/security_solution/common/machine_learning/helpers.ts +++ b/x-pack/plugins/security_solution/common/machine_learning/helpers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RuleType } from '../detection_engine/types'; +import { Type } from '../detection_engine/schemas/common/schemas'; // Based on ML Job/Datafeed States from x-pack/legacy/plugins/ml/common/constants/states.js const enabledStates = ['started', 'opened']; @@ -23,4 +23,4 @@ export const isJobFailed = (jobState: string, datafeedState: string): boolean => return failureStates.includes(jobState) || failureStates.includes(datafeedState); }; -export const isMlRule = (ruleType: RuleType) => ruleType === 'machine_learning'; +export const isMlRule = (ruleType: Type) => ruleType === 'machine_learning'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 589116c901c303..216ed0cbe264d3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -17,9 +17,8 @@ import styled from 'styled-components'; import { TimelineId } from '../../../../../common/types/timeline'; import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; -import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Status, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { isThresholdRule } from '../../../../../common/detection_engine/utils'; -import { RuleType } from '../../../../../common/detection_engine/types'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { timelineActions } from '../../../../timelines/store/timeline'; import { EventsTd, EventsTdContent } from '../../../../timelines/components/timeline/styles'; @@ -409,7 +408,7 @@ const AlertContextMenuComponent: React.FC = ({ data: nonEcsRowData, fieldName: 'signal.rule.type', }); - const [ruleType] = ruleTypes as RuleType[]; + const [ruleType] = ruleTypes as Type[]; return !isMlRule(ruleType) && !isThresholdRule(ruleType); }, [nonEcsRowData]); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 3a0a5b04c58740..680ca78fc222e8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -24,8 +24,7 @@ import styled from 'styled-components'; import { assertUnreachable } from '../../../../../common/utility_types'; import * as i18nSeverity from '../severity_mapping/translations'; import * as i18nRiskScore from '../risk_score_mapping/translations'; -import { Threshold } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { RuleType } from '../../../../../common/detection_engine/types'; +import { Threshold, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; @@ -357,7 +356,7 @@ export const buildNoteDescription = (label: string, note: string): ListItems[] = return []; }; -export const buildRuleTypeDescription = (label: string, ruleType: RuleType): ListItems[] => { +export const buildRuleTypeDescription = (label: string, ruleType: Type): ListItems[] => { switch (ruleType) { case 'machine_learning': { return [ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 00141c9a453d82..cf27fa97b14794 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -9,7 +9,6 @@ import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; import React, { memo, useState } from 'react'; import styled from 'styled-components'; -import { RuleType } from '../../../../../common/detection_engine/types'; import { IIndexPattern, Filter, @@ -42,6 +41,7 @@ import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/ import { buildMlJobDescription } from './ml_job_description'; import { buildActionsDescription } from './actions_description'; import { buildThrottleDescription } from './throttle_description'; +import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; const DescriptionListContainer = styled(EuiDescriptionList)` &.euiDescriptionList--column .euiDescriptionList__title { @@ -211,7 +211,7 @@ export const getDescriptionItem = ( const val: string = get(field, data); return buildNoteDescription(label, val); } else if (field === 'ruleType') { - const ruleType: RuleType = get(field, data); + const ruleType: Type = get(field, data); return buildRuleTypeDescription(label, ruleType); } else if (field === 'kibanaSiemAppUrl') { return []; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx index c6ea269e1a355a..6f5f3361d2b3e1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx @@ -9,11 +9,11 @@ import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiFormRow, EuiIcon } from '@elastic import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../common/detection_engine/utils'; -import { RuleType } from '../../../../../common/detection_engine/types'; import { FieldHook } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; import { MlCardDescription } from './ml_card_description'; +import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; interface SelectRuleTypeProps { describedByIds?: string[]; @@ -30,9 +30,9 @@ export const SelectRuleType: React.FC = ({ hasValidLicense = false, isMlAdmin = false, }) => { - const ruleType = field.value as RuleType; + const ruleType = field.value as Type; const setType = useCallback( - (type: RuleType) => { + (type: Type) => { field.setValue(type); }, [field] diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index a5bbc0465ccc1c..166bb90113aef7 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -6,7 +6,6 @@ import * as t from 'io-ts'; -import { RuleTypeSchema } from '../../../../../common/detection_engine/types'; import { author, building_block_type, @@ -16,6 +15,7 @@ import { severity_mapping, timestamp_override, threshold, + type, } from '../../../../../common/detection_engine/schemas/common/schemas'; import { listArray, @@ -44,7 +44,7 @@ export const NewRuleSchema = t.intersection([ name: t.string, risk_score: t.number, severity: t.string, - type: RuleTypeSchema, + type, }), t.partial({ actions: t.array(action), @@ -117,7 +117,7 @@ export const RuleSchema = t.intersection([ severity: t.string, severity_mapping, tags: t.array(t.string), - type: RuleTypeSchema, + type, to: t.string, threat: t.array(t.unknown), updated_at: t.string, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 434a33ac377228..f4a40b771c9fa9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -10,12 +10,12 @@ import deepmerge from 'deepmerge'; import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/constants'; import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; -import { RuleType } from '../../../../../../common/detection_engine/types'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; import { List } from '../../../../../../common/detection_engine/schemas/types'; import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; import { NewRule, Rule } from '../../../../containers/detection_engine/rules'; +import { Type } from '../../../../../../common/detection_engine/schemas/common/schemas'; import { AboutStepRule, @@ -68,7 +68,7 @@ const isThresholdFields = ( fields: QueryRuleFields | MlRuleFields | ThresholdRuleFields ): fields is ThresholdRuleFields => has('threshold', fields); -export const filterRuleFieldsForType = (fields: T, type: RuleType) => { +export const filterRuleFieldsForType = (fields: T, type: Type) => { if (isMlRule(type)) { const { index, queryBar, threshold, ...mlRuleFields } = fields; return mlRuleFields; @@ -119,7 +119,7 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep query: ruleFields.queryBar?.query?.query as string, saved_id: ruleFields.queryBar?.saved_id, ...(ruleType === 'query' && - ruleFields.queryBar?.saved_id && { type: 'saved_query' as RuleType }), + ruleFields.queryBar?.saved_id && { type: 'saved_query' as Type }), }; return { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index f9279ce6395344..8178f5ae5ba1d4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -10,7 +10,7 @@ import memoizeOne from 'memoize-one'; import { useLocation } from 'react-router-dom'; import { ActionVariable } from '../../../../../../triggers_actions_ui/public'; -import { RuleAlertAction, RuleType } from '../../../../../common/detection_engine/types'; +import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; import { Filter } from '../../../../../../../../src/plugins/data/public'; @@ -24,7 +24,10 @@ import { ScheduleStepRule, ActionsStepRule, } from './types'; -import { SeverityMapping } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { + SeverityMapping, + Type, +} from '../../../../../common/detection_engine/schemas/common/schemas'; import { severityOptions } from '../../../components/rules/step_about_rule/data'; export interface GetStepsData { @@ -307,7 +310,7 @@ export const redirectToDetections = ( hasEncryptionKey === false || needsListsConfiguration; -const getRuleSpecificRuleParamKeys = (ruleType: RuleType) => { +const getRuleSpecificRuleParamKeys = (ruleType: Type) => { const queryRuleParams = ['index', 'filters', 'language', 'query', 'saved_id']; if (isMlRule(ruleType)) { @@ -321,7 +324,7 @@ const getRuleSpecificRuleParamKeys = (ruleType: RuleType) => { return queryRuleParams; }; -export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { +export const getActionMessageRuleParams = (ruleType: Type): string[] => { const commonRuleParamsKeys = [ 'id', 'name', @@ -349,23 +352,21 @@ export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { return ruleParamsKeys; }; -export const getActionMessageParams = memoizeOne( - (ruleType: RuleType | undefined): ActionVariable[] => { - if (!ruleType) { - return []; - } - const actionMessageRuleParams = getActionMessageRuleParams(ruleType); - - return [ - { name: 'state.signals_count', description: 'state.signals_count' }, - { name: '{context.results_link}', description: 'context.results_link' }, - ...actionMessageRuleParams.map((param) => { - const extendedParam = `context.rule.${param}`; - return { name: extendedParam, description: extendedParam }; - }), - ]; +export const getActionMessageParams = memoizeOne((ruleType: Type | undefined): ActionVariable[] => { + if (!ruleType) { + return []; } -); + const actionMessageRuleParams = getActionMessageRuleParams(ruleType); + + return [ + { name: 'state.signals_count', description: 'state.signals_count' }, + { name: '{context.results_link}', description: 'context.results_link' }, + ...actionMessageRuleParams.map((param) => { + const extendedParam = `context.rule.${param}`; + return { name: extendedParam, description: extendedParam }; + }), + ]; +}); // typed as null not undefined as the initial state for this value is null. export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean => diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index e36f08703dae51..891af4b8ca80ef 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RuleAlertAction, RuleType } from '../../../../../common/detection_engine/types'; +import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { AlertAction } from '../../../../../../alerts/common'; import { Filter } from '../../../../../../../../src/plugins/data/common'; import { FormData, FormHook } from '../../../../shared_imports'; @@ -19,6 +19,7 @@ import { RuleNameOverride, SeverityMapping, TimestampOverride, + Type, } from '../../../../../common/detection_engine/schemas/common/schemas'; import { List } from '../../../../../common/detection_engine/schemas/types'; @@ -102,7 +103,7 @@ export interface DefineStepRule extends StepRuleData { index: string[]; machineLearningJobId: string; queryBar: FieldValueQueryBar; - ruleType: RuleType; + ruleType: Type; timeline: FieldValueTimeline; threshold: FieldValueThreshold; } @@ -134,7 +135,7 @@ export interface DefineStepRuleJson { }; timeline_id?: string; timeline_title?: string; - type: RuleType; + type: Type; } export interface AboutStepRuleJson { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index 0a899562d61c2d..802b9472a44870 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -14,7 +14,7 @@ import { RuleAlertAttributes } from '../signals/types'; import { siemRuleActionGroups } from '../signals/siem_rule_action_groups'; import { scheduleNotificationActions } from './schedule_notification_actions'; import { getNotificationResultsLink } from './utils'; -import { parseScheduleDates } from '../../../../common/detection_engine/utils'; +import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; export const rulesNotificationAlertType = ({ logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index a7213c30eb3fb2..b8311182f3ca8c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -17,7 +17,7 @@ import { getExceptions, sortExceptionItems, } from './utils'; -import { parseScheduleDates } from '../../../../common/detection_engine/utils'; +import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { RuleExecutorOptions } from './types'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; @@ -38,6 +38,7 @@ jest.mock('../notifications/schedule_notification_actions'); jest.mock('./find_ml_signals'); jest.mock('./bulk_create_ml_signals'); jest.mock('./../../../../common/detection_engine/utils'); +jest.mock('../../../../common/detection_engine/parse_schedule_dates'); const getPayload = (ruleAlert: RuleAlertType, services: AlertServicesMock) => ({ alertId: ruleAlert.id, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 9d688736a98460..da17d4a1f123a7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -14,7 +14,7 @@ import { SERVER_APP_ID, } from '../../../../common/constants'; import { isJobStarted, isMlRule } from '../../../../common/machine_learning/helpers'; -import { parseScheduleDates } from '../../../../common/detection_engine/utils'; +import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { SetupPlugins } from '../../../plugin'; import { getInputIndex } from './get_input_output_index'; import { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index a2e2fec3309c31..d053a8e1089ad3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -13,7 +13,7 @@ import { buildRuleMessageFactory } from './rule_messages'; import { ExceptionListClient } from '../../../../../lists/server'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { parseScheduleDates } from '../../../../common/detection_engine/utils'; +import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { generateId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 92cc9be69839f3..dc09c6d5386fce 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -14,7 +14,8 @@ import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; import { BulkResponse, BulkResponseErrorAggregation, isValidUnit } from './types'; import { BuildRuleMessage } from './rule_messages'; -import { hasLargeValueList, parseScheduleDates } from '../../../../common/detection_engine/utils'; +import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; +import { hasLargeValueList } from '../../../../common/detection_engine/utils'; import { MAX_EXCEPTION_LIST_SIZE } from '../../../../../lists/common/constants'; interface SortExceptionsReturn { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts index 4b4f5147c9a42c..cbe756064b72b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts @@ -36,10 +36,10 @@ import { RuleNameOverrideOrUndefined, SeverityMappingOrUndefined, TimestampOverrideOrUndefined, + Type, } from '../../../common/detection_engine/schemas/common/schemas'; import { LegacyCallAPIOptions } from '../../../../../../src/core/server'; import { Filter } from '../../../../../../src/plugins/data/server'; -import { RuleType } from '../../../common/detection_engine/types'; import { ListArrayOrUndefined } from '../../../common/detection_engine/schemas/types'; export type PartialFilter = Partial; @@ -75,7 +75,7 @@ export interface RuleTypeParams { threshold: ThresholdOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: To; - type: RuleType; + type: Type; references: References; version: Version; exceptionsList: ListArrayOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts index 386eda5281f0c2..ff565c05848f97 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts @@ -13,12 +13,12 @@ import { SetupPlugins } from '../../plugin'; import { MINIMUM_ML_LICENSE } from '../../../common/constants'; import { hasMlAdminPermissions } from '../../../common/machine_learning/has_ml_admin_permissions'; import { isMlRule } from '../../../common/machine_learning/helpers'; -import { RuleType } from '../../../common/detection_engine/types'; import { Validation } from './validation'; import { cache } from './cache'; +import { Type } from '../../../common/detection_engine/schemas/common/schemas'; export interface MlAuthz { - validateRuleType: (type: RuleType) => Promise; + validateRuleType: (type: Type) => Promise; } /** @@ -40,7 +40,7 @@ export const buildMlAuthz = ({ request: KibanaRequest; }): MlAuthz => { const cachedValidate = cache(() => validateMlAuthz({ license, ml, request })); - const validateRuleType = async (type: RuleType): Promise => { + const validateRuleType = async (type: Type): Promise => { if (!isMlRule(type)) { return { valid: true, message: undefined }; } else { From d3416c018907f13da89d99356322d0c7b9936c0d Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 2 Sep 2020 21:39:03 -0700 Subject: [PATCH 21/25] [APM] Service maps layout enhancements (#76481) * Fixes storybook anomaly score generation and better utilizes available screen space * Closes #71770 for APM service maps by replacing breadthfirst layout with one from the cytoscape-dagre extension. Also replaces the taxi edges with cubic bezier edges. Finally, this adds the ability to drag individual nodes around the service map. * Removes unused code * removes commented line of code * - Adds ability for scripts/notice.js to check files with the .tsx file extension - Adds attribution for `applyCubicBezierStyles` * Refine comment text and MIT license url --- NOTICE.txt | 7 ++ src/dev/notice/generate_notice_from_source.ts | 2 +- x-pack/package.json | 1 + .../components/app/ServiceMap/Cytoscape.tsx | 98 ++++++++----------- .../app/ServiceMap/Popover/index.tsx | 2 + .../__stories__/Cytoscape.stories.tsx | 4 +- .../CytoscapeExampleData.stories.tsx | 25 +++-- .../generate_service_map_elements.ts | 25 +++-- .../app/ServiceMap/cytoscapeOptions.ts | 5 +- .../components/app/ServiceMap/index.tsx | 3 +- .../plugins/apm/typings/cytoscape_dagre.d.ts | 7 ++ yarn.lock | 22 +++++ 12 files changed, 113 insertions(+), 88 deletions(-) create mode 100644 x-pack/plugins/apm/typings/cytoscape_dagre.d.ts diff --git a/NOTICE.txt b/NOTICE.txt index e1552852d03493..d689abf4c4e05c 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -281,6 +281,13 @@ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +--- +This product includes code in the function applyCubicBezierStyles that was +inspired by a public Codepen, which was available under a "MIT" license. + +Copyright (c) 2020 by Guillaume (https://codepen.io/guillaumethomas/pen/xxbbBKO) +MIT License http://www.opensource.org/licenses/mit-license + --- This product includes code that is adapted from mapbox-gl-js, which is available under a "BSD-3-Clause" license. diff --git a/src/dev/notice/generate_notice_from_source.ts b/src/dev/notice/generate_notice_from_source.ts index 0bef5bc5f32d42..9f7eb9d9e1aa4b 100644 --- a/src/dev/notice/generate_notice_from_source.ts +++ b/src/dev/notice/generate_notice_from_source.ts @@ -41,7 +41,7 @@ interface Options { * into the repository. */ export async function generateNoticeFromSource({ productName, directory, log }: Options) { - const globs = ['**/*.{js,less,css,ts}']; + const globs = ['**/*.{js,less,css,ts,tsx}']; const options = { cwd: directory, diff --git a/x-pack/package.json b/x-pack/package.json index 0e9aef37aa2532..bf093fed307475 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -305,6 +305,7 @@ "concat-stream": "1.6.2", "content-disposition": "0.5.3", "cytoscape": "^3.10.0", + "cytoscape-dagre": "^2.2.2", "d3-array": "1.2.4", "dedent": "^0.7.0", "del": "^5.1.0", diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index 1cde473aae6fa6..0b00c8a8bf0934 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -13,6 +13,7 @@ import React, { useState, } from 'react'; import cytoscape from 'cytoscape'; +import dagre from 'cytoscape-dagre'; import { debounce } from 'lodash'; import { useTheme } from '../../../hooks/useTheme'; import { @@ -22,6 +23,8 @@ import { } from './cytoscapeOptions'; import { useUiTracker } from '../../../../../observability/public'; +cytoscape.use(dagre); + export const CytoscapeContext = createContext( undefined ); @@ -30,7 +33,6 @@ interface CytoscapeProps { children?: ReactNode; elements: cytoscape.ElementDefinition[]; height: number; - width: number; serviceName?: string; style?: CSSProperties; } @@ -57,59 +59,52 @@ function useCytoscape(options: cytoscape.CytoscapeOptions) { return [ref, cy] as [React.MutableRefObject, cytoscape.Core | undefined]; } -function rotatePoint( - { x, y }: { x: number; y: number }, - degreesRotated: number -) { - const radiansPerDegree = Math.PI / 180; - const θ = radiansPerDegree * degreesRotated; - const cosθ = Math.cos(θ); - const sinθ = Math.sin(θ); - return { - x: x * cosθ - y * sinθ, - y: x * sinθ + y * cosθ, - }; -} - -function getLayoutOptions( - selectedRoots: string[], - height: number, - width: number, - nodeHeight: number -): cytoscape.LayoutOptions { +function getLayoutOptions(nodeHeight: number): cytoscape.LayoutOptions { return { - name: 'breadthfirst', - // @ts-ignore DefinitelyTyped is incorrect here. Roots can be an Array - roots: selectedRoots.length ? selectedRoots : undefined, + name: 'dagre', fit: true, padding: nodeHeight, spacingFactor: 1.2, // @ts-ignore - // Rotate nodes counter-clockwise to transform layout from top→bottom to left→right. - // The extra 5° achieves the effect of separating overlapping taxi-styled edges. - transform: (node: any, pos: cytoscape.Position) => rotatePoint(pos, -95), - // swap width/height of boundingBox to compensate for the rotation - boundingBox: { x1: 0, y1: 0, w: height, h: width }, + nodeSep: nodeHeight, + edgeSep: 32, + rankSep: 128, + rankDir: 'LR', + ranker: 'network-simplex', }; } -function selectRoots(cy: cytoscape.Core): string[] { - const bfs = cy.elements().bfs({ - roots: cy.elements().leaves(), +/* + * @notice + * This product includes code in the function applyCubicBezierStyles that was + * inspired by a public Codepen, which was available under a "MIT" license. + * + * Copyright (c) 2020 by Guillaume (https://codepen.io/guillaumethomas/pen/xxbbBKO) + * MIT License http://www.opensource.org/licenses/mit-license + */ +function applyCubicBezierStyles(edges: cytoscape.EdgeCollection) { + edges.forEach((edge) => { + const { x: x0, y: y0 } = edge.source().position(); + const { x: x1, y: y1 } = edge.target().position(); + const x = x1 - x0; + const y = y1 - y0; + const z = Math.sqrt(x * x + y * y); + const costheta = z === 0 ? 0 : x / z; + const alpha = 0.25; + // Two values for control-point-distances represent a pair symmetric quadratic + // bezier curves joined in the middle as a seamless cubic bezier curve: + edge.style('control-point-distances', [ + -alpha * y * costheta, + alpha * y * costheta, + ]); + edge.style('control-point-weights', [alpha, 1 - alpha]); }); - const furthestNodeFromLeaves = bfs.path.last(); - return cy - .elements() - .roots() - .union(furthestNodeFromLeaves) - .map((el) => el.id()); } export function Cytoscape({ children, elements, height, - width, serviceName, style, }: CytoscapeProps) { @@ -151,13 +146,7 @@ export function Cytoscape({ } else { resetConnectedEdgeStyle(); } - - const selectedRoots = selectRoots(event.cy); - const layout = cy.layout( - getLayoutOptions(selectedRoots, height, width, nodeHeight) - ); - - layout.run(); + cy.layout(getLayoutOptions(nodeHeight)).run(); } }; let layoutstopDelayTimeout: NodeJS.Timeout; @@ -180,6 +169,7 @@ export function Cytoscape({ event.cy.fit(undefined, nodeHeight); } }, 0); + applyCubicBezierStyles(event.cy.edges()); }; // debounce hover tracking so it doesn't spam telemetry with redundant events const trackNodeEdgeHover = debounce( @@ -211,6 +201,9 @@ export function Cytoscape({ console.debug('cytoscape:', event); } }; + const dragHandler: cytoscape.EventHandler = (event) => { + applyCubicBezierStyles(event.target.connectedEdges()); + }; if (cy) { cy.on('data layoutstop select unselect', debugHandler); @@ -220,6 +213,7 @@ export function Cytoscape({ cy.on('mouseout', 'edge, node', mouseoutHandler); cy.on('select', 'node', selectHandler); cy.on('unselect', 'node', unselectHandler); + cy.on('drag', 'node', dragHandler); cy.remove(cy.elements()); cy.add(elements); @@ -239,19 +233,11 @@ export function Cytoscape({ cy.removeListener('mouseout', 'edge, node', mouseoutHandler); cy.removeListener('select', 'node', selectHandler); cy.removeListener('unselect', 'node', unselectHandler); + cy.removeListener('drag', 'node', dragHandler); } clearTimeout(layoutstopDelayTimeout); }; - }, [ - cy, - elements, - height, - serviceName, - trackApmEvent, - width, - nodeHeight, - theme, - ]); + }, [cy, elements, height, serviceName, trackApmEvent, nodeHeight, theme]); return ( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx index 1658c36e8a92fb..8291d94d91c48f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx @@ -71,6 +71,7 @@ export function Popover({ focusedServiceName }: PopoverProps) { cy.on('select', 'node', selectHandler); cy.on('unselect', 'node', deselect); cy.on('data viewport', deselect); + cy.on('drag', 'node', deselect); } return () => { @@ -78,6 +79,7 @@ export function Popover({ focusedServiceName }: PopoverProps) { cy.removeListener('select', 'node', selectHandler); cy.removeListener('unselect', 'node', deselect); cy.removeListener('data viewport', undefined, deselect); + cy.removeListener('drag', 'node', deselect); } }; }, [cy, deselect]); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx index 2a7d11bb57ca5b..5b50eb953d896e 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx @@ -49,13 +49,11 @@ storiesOf('app/ServiceMap/Cytoscape', module) }, ]; const height = 300; - const width = 1340; const serviceName = 'opbeans-python'; return ( ); @@ -330,7 +328,7 @@ storiesOf('app/ServiceMap/Cytoscape', module) }, }, ]; - return ; + return ; }, { info: { propTables: false, source: false }, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/CytoscapeExampleData.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/CytoscapeExampleData.stories.tsx index 830e3719b11f9d..d8dcc71f5051d9 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/CytoscapeExampleData.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/CytoscapeExampleData.stories.tsx @@ -35,6 +35,8 @@ function setSessionJson(json: string) { window.sessionStorage.setItem(SESSION_STORAGE_KEY, json); } +const getCytoscapeHeight = () => window.innerHeight - 300; + storiesOf(STORYBOOK_PATH, module) .addDecorator((storyFn) => {storyFn()}) .add( @@ -43,16 +45,17 @@ storiesOf(STORYBOOK_PATH, module) const [size, setSize] = useState(10); const [json, setJson] = useState(''); const [elements, setElements] = useState( - generateServiceMapElements(size) + generateServiceMapElements({ size, hasAnomalies: true }) ); - return (
{ - setElements(generateServiceMapElements(size)); + setElements( + generateServiceMapElements({ size, hasAnomalies: true }) + ); setJson(''); }} > @@ -79,7 +82,7 @@ storiesOf(STORYBOOK_PATH, module) - + {json && ( - + @@ -204,8 +207,7 @@ storiesOf(STORYBOOK_PATH, module)
); @@ -224,8 +226,7 @@ storiesOf(STORYBOOK_PATH, module)
); @@ -244,8 +245,7 @@ storiesOf(STORYBOOK_PATH, module)
); @@ -264,8 +264,7 @@ storiesOf(STORYBOOK_PATH, module)
); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts index 012256db3ab987..57ef2d49291c40 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSeverity } from '../Popover/getSeverity'; - -export function generateServiceMapElements(size: number): any[] { +export function generateServiceMapElements({ + size, + hasAnomalies, +}: { + size: number; + hasAnomalies: boolean; +}): any[] { const services = range(size).map((i) => { const name = getName(); const anomalyScore = randn(101); @@ -15,11 +19,14 @@ export function generateServiceMapElements(size: number): any[] { 'service.environment': 'production', 'service.name': name, 'agent.name': getAgentName(), - anomaly_score: anomalyScore, - anomaly_severity: getSeverity(anomalyScore), - actual_value: Math.random() * 2000000, - typical_value: Math.random() * 1000000, - ml_job_id: `${name}-request-high_mean_response_time`, + serviceAnomalyStats: hasAnomalies + ? { + transactionType: 'request', + anomalyScore, + actualValue: Math.random() * 2000000, + jobId: `${name}-request-high_mean_response_time`, + } + : undefined, }; }); @@ -146,7 +153,7 @@ const NAMES = [ 'leech', 'loki', 'longshot', - 'lumpkin,', + 'lumpkin', 'madame-web', 'magician', 'magneto', diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index 4a271019e06db9..9d58ed142dab79 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -168,9 +168,7 @@ const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => { { selector: 'edge', style: { - 'curve-style': 'taxi', - // @ts-ignore - 'taxi-direction': 'auto', + 'curve-style': 'unbundled-bezier', 'line-color': lineColor, 'overlay-opacity': 0, 'target-arrow-color': lineColor, @@ -264,7 +262,6 @@ ${theme.eui.euiColorLightShade}`, export const getCytoscapeOptions = ( theme: EuiTheme ): cytoscape.CytoscapeOptions => ({ - autoungrabify: true, boxSelectionEnabled: false, maxZoom: 3, minZoom: 0.2, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index d4be4da2ae1c54..83fab95bc91c9b 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -57,7 +57,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { } }, [license, serviceName, urlParams]); - const { ref, height, width } = useRefDimensions(); + const { ref, height } = useRefDimensions(); useTrackPageview({ app: 'apm', path: 'service_map' }); useTrackPageview({ app: 'apm', path: 'service_map', delay: 15000 }); @@ -78,7 +78,6 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { height={height} serviceName={serviceName} style={getCytoscapeDivStyle(theme)} - width={width} > diff --git a/x-pack/plugins/apm/typings/cytoscape_dagre.d.ts b/x-pack/plugins/apm/typings/cytoscape_dagre.d.ts new file mode 100644 index 00000000000000..b5bbdfc14d9d35 --- /dev/null +++ b/x-pack/plugins/apm/typings/cytoscape_dagre.d.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. + */ + +declare module 'cytoscape-dagre'; diff --git a/yarn.lock b/yarn.lock index 56530a0ab4fdc0..98092d71caebbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9989,6 +9989,13 @@ cypress@4.11.0: url "0.11.0" yauzl "2.10.0" +cytoscape-dagre@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/cytoscape-dagre/-/cytoscape-dagre-2.2.2.tgz#5f32a85c0ba835f167efee531df9e89ac58ff411" + integrity sha512-zsg36qNwua/L2stJSWkcbSDcvW3E6VZf6KRe6aLnQJxuXuz89tMqI5EVYVKEcNBgzTEzFMFv0PE3T0nD4m6VDw== + dependencies: + dagre "^0.8.2" + cytoscape@^3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.10.0.tgz#3b462e0d35121ecd2d2702f470915fd6dae01777" @@ -10201,6 +10208,14 @@ d@1: dependencies: es5-ext "^0.10.9" +dagre@^0.8.2: + version "0.8.5" + resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee" + integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw== + dependencies: + graphlib "^2.1.8" + lodash "^4.17.15" + damerau-levenshtein@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514" @@ -14359,6 +14374,13 @@ graceful-fs@~1.1: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.1.14.tgz#07078db5f6377f6321fceaaedf497de124dc9465" integrity sha1-BweNtfY3f2Mh/Oqu30l94STclGU= +graphlib@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da" + integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A== + dependencies: + lodash "^4.17.15" + graphql-anywhere@^4.1.0-alpha.0: version "4.1.16" resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.16.tgz#82bb59643e30183cfb7b485ed4262a7b39d8a6c1" From 210d6f2df18cce2b78eb075b3d8247559a359944 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 3 Sep 2020 08:50:03 +0200 Subject: [PATCH 22/25] Legacy SO import: Fix bug causing multiple overrides to only show the last confirm modal (#76482) * Legacy SO import: Fix bug causing multiple overrides to only show the last confirm modal * eslint * fix for loops --- .../public/lib/resolve_saved_objects.ts | 35 ++- .../apps/management/_import_objects.js | 54 +++- ...import_index_patterns_multiple_exists.json | 26 ++ .../_import_objects_multiple_exists.json | 36 +++ .../saved_objects_imports/data.json.gz | Bin 0 -> 1754 bytes .../saved_objects_imports/mappings.json | 244 ++++++++++++++++++ test/functional/page_objects/common_page.ts | 6 +- 7 files changed, 379 insertions(+), 22 deletions(-) create mode 100644 test/functional/apps/management/exports/_import_index_patterns_multiple_exists.json create mode 100644 test/functional/apps/management/exports/_import_objects_multiple_exists.json create mode 100644 test/functional/fixtures/es_archiver/saved_objects_imports/data.json.gz create mode 100644 test/functional/fixtures/es_archiver/saved_objects_imports/mappings.json diff --git a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts index 79b8c33b84cfee..679ea5ffc23eea 100644 --- a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts @@ -160,10 +160,6 @@ function groupByType(docs: SavedObjectsRawDoc[]): Record(list: T[], op: (item: T) => R) { - return await Promise.all(list.map((item) => op(item))); -} - export async function resolveIndexPatternConflicts( resolutions: Array<{ oldId: string; newId: string }>, conflictedIndexPatterns: any[], @@ -175,7 +171,7 @@ export async function resolveIndexPatternConflicts( ) { let importCount = 0; - await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj, doc }) => { + for (const { obj, doc } of conflictedIndexPatterns) { const serializedSearchSource = JSON.parse( doc._source.kibanaSavedObjectMeta?.searchSourceJSON || '{}' ); @@ -220,7 +216,7 @@ export async function resolveIndexPatternConflicts( if (!allResolved) { // The user decided to skip this conflict so do nothing - return; + continue; } obj.searchSource = await dependencies.search.searchSource.create( serializedSearchSourceWithInjectedReferences @@ -228,17 +224,17 @@ export async function resolveIndexPatternConflicts( if (await saveObject(obj, overwriteAll)) { importCount++; } - }); + } return importCount; } export async function saveObjects(objs: SavedObject[], overwriteAll: boolean) { let importCount = 0; - await awaitEachItemInParallel(objs, async (obj) => { + for (const obj of objs) { if (await saveObject(obj, overwriteAll)) { importCount++; } - }); + } return importCount; } @@ -253,16 +249,16 @@ export async function resolveSavedSearches( overwriteAll: boolean ) { let importCount = 0; - await awaitEachItemInParallel(savedSearches, async (searchDoc) => { + for (const searchDoc of savedSearches) { const obj = await getSavedObject(searchDoc, services); if (!obj) { // Just ignore? - return; + continue; } if (await importDocument(obj, searchDoc, overwriteAll)) { importCount++; } - }); + } return importCount; } @@ -280,7 +276,10 @@ export async function resolveSavedObjects( let importedObjectCount = 0; const failedImports: FailedImport[] = []; // Start with the index patterns since everything is dependent on them - await awaitEachItemInParallel(docTypes.indexPatterns, async (indexPatternDoc) => { + // As the confirmation opens a modal, and as we only allow one modal at a time + // (opening a new one close the previous with a rejection) + // we can't do that in parallel + for (const indexPatternDoc of docTypes.indexPatterns) { try { const importedIndexPatternId = await importIndexPattern( indexPatternDoc, @@ -294,7 +293,7 @@ export async function resolveSavedObjects( } catch (error) { failedImports.push({ obj: indexPatternDoc as any, error }); } - }); + } // We want to do the same for saved searches, but we want to keep them separate because they need // to be applied _first_ because other saved objects can be dependent on those saved searches existing @@ -311,7 +310,7 @@ export async function resolveSavedObjects( // likely that these saved objects will work once resaved so keep them around to resave them. const conflictedSavedObjectsLinkedToSavedSearches: any[] = []; - await awaitEachItemInParallel(docTypes.searches, async (searchDoc) => { + for (const searchDoc of docTypes.searches) { const obj = await getSavedObject(searchDoc, services); try { @@ -329,9 +328,9 @@ export async function resolveSavedObjects( failedImports.push({ obj, error }); } } - }); + } - await awaitEachItemInParallel(docTypes.other, async (otherDoc) => { + for (const otherDoc of docTypes.other) { const obj = await getSavedObject(otherDoc, services); try { @@ -350,7 +349,7 @@ export async function resolveSavedObjects( failedImports.push({ obj, error }); } } - }); + } return { conflictedIndexPatterns, diff --git a/test/functional/apps/management/_import_objects.js b/test/functional/apps/management/_import_objects.js index 5fbeb978f9a1c2..3941b117e6efe8 100644 --- a/test/functional/apps/management/_import_objects.js +++ b/test/functional/apps/management/_import_objects.js @@ -21,6 +21,8 @@ import expect from '@kbn/expect'; import path from 'path'; import { keyBy } from 'lodash'; +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); @@ -203,12 +205,12 @@ export default function ({ getService, getPageObjects }) { // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await esArchiver.load('management'); + await esArchiver.load('saved_objects_imports'); await PageObjects.settings.clickKibanaSavedObjects(); }); afterEach(async function () { - await esArchiver.unload('management'); + await esArchiver.unload('saved_objects_imports'); }); it('should import saved objects', async function () { @@ -280,6 +282,54 @@ export default function ({ getService, getPageObjects }) { expect(isSuccessful).to.be(true); }); + it('should allow the user to confirm overriding multiple duplicate saved objects', async function () { + // This data has already been loaded by the "visualize" esArchive. We'll load it again + // so that we can override the existing visualization. + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', '_import_objects_multiple_exists.json'), + false + ); + + await PageObjects.savedObjects.checkImportLegacyWarning(); + await PageObjects.savedObjects.checkImportConflictsWarning(); + + await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*'); + await PageObjects.savedObjects.clickConfirmChanges(); + + // Override the visualizations. + await PageObjects.common.clickConfirmOnModal(false); + // as the second confirm can pop instantly, we can't wait for it to be hidden + // with is why we call clickConfirmOnModal with ensureHidden: false in previous statement + // but as the initial popin can take a few ms before fading, we need to wait a little + // to avoid clicking twice on the same modal. + await delay(1000); + await PageObjects.common.clickConfirmOnModal(false); + + const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess'); + expect(isSuccessful).to.be(true); + }); + + it('should allow the user to confirm overriding multiple duplicate index patterns', async function () { + // This data has already been loaded by the "visualize" esArchive. We'll load it again + // so that we can override the existing visualization. + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', '_import_index_patterns_multiple_exists.json'), + false + ); + + // Override the index patterns. + await PageObjects.common.clickConfirmOnModal(false); + // as the second confirm can pop instantly, we can't wait for it to be hidden + // with is why we call clickConfirmOnModal with ensureHidden: false in previous statement + // but as the initial popin can take a few ms before fading, we need to wait a little + // to avoid clicking twice on the same modal. + await delay(1000); + await PageObjects.common.clickConfirmOnModal(false); + + const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess'); + expect(isSuccessful).to.be(true); + }); + it('should import saved objects linked to saved searches', async function () { await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_saved_search.json') diff --git a/test/functional/apps/management/exports/_import_index_patterns_multiple_exists.json b/test/functional/apps/management/exports/_import_index_patterns_multiple_exists.json new file mode 100644 index 00000000000000..2eb64b1c7ca9f5 --- /dev/null +++ b/test/functional/apps/management/exports/_import_index_patterns_multiple_exists.json @@ -0,0 +1,26 @@ +[ + { + "_id": "f1e4c910-a2e6-11e7-bb30-233be9be6a20", + "_type": "index-pattern", + "_source": { + "title": "index-pattern-test-1", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"expression script\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['bytes'].value\",\"lang\":\"expression\",\"indexed\":true,\"analyzed\":false,\"doc_values\":false}]" + }, + "_meta": { + "savedObjectVersion": 1 + } + }, + { + "_id": "f1e4c910-a2e6-11e7-bb30-233be9be6a87", + "_type": "index-pattern", + "_source": { + "title": "index-pattern-test-2", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"expression script\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['bytes'].value\",\"lang\":\"expression\",\"indexed\":true,\"analyzed\":false,\"doc_values\":false}]" + }, + "_meta": { + "savedObjectVersion": 1 + } + } +] diff --git a/test/functional/apps/management/exports/_import_objects_multiple_exists.json b/test/functional/apps/management/exports/_import_objects_multiple_exists.json new file mode 100644 index 00000000000000..9e554aecd9f7a3 --- /dev/null +++ b/test/functional/apps/management/exports/_import_objects_multiple_exists.json @@ -0,0 +1,36 @@ +[ + { + "_id": "test-1", + "_type": "visualization", + "_source": { + "title": "Visualization test 1", + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}", + "uiStateJSON": "{}", + "description": "AreaChart", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-2", + "_type": "visualization", + "_source": { + "title": "Visualization test 2", + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}", + "uiStateJSON": "{}", + "description": "AreaChart", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + } +] diff --git a/test/functional/fixtures/es_archiver/saved_objects_imports/data.json.gz b/test/functional/fixtures/es_archiver/saved_objects_imports/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..006476f867ae7e151f9dae0599cbcc405b3dbfa1 GIT binary patch literal 1754 zcmXYuX*e5*7KTZJXlR7cHn>!-sjb>xTTxpEtwE{%5=#&=mMB79GGuB?Mr~DFNeG>& zQc~;KQpPg2*r}zp7HL(-)<$c)jp?8BJm-79_dP$3TnZQ*?P}x62lT}dFzNx}A$Yz` zH_MQ@>td{R=kqMMk~cE4VRNbs-mN}jCYKC{z(c)8#DhbT6cr1_+oC62S-bRGY4JT9 zf88C`1(7W%GHj8T7yb7y+vriNnueq9O}tC`4~I6J_2dcyj~O~tTG6MZ^>zE>$bQe-4pkkOGUE8z(U&m$oSJ9u184#0 zE@5g7M=*Y6wRL?W@=?X=9|biD=`E$9&f6E9(WxzEK3v(VJJO0{O@RpkT<*OS1NCURwmXp9*5b(+^DTL)VpwLqnNpljdnl|OslEnV8&Y_fY?8m; zEC^A^40CA|_Ld==MVG?r@>w(U#T zx>8wqaNMJ;ifK7a`@OoWMDK&qbn^xJK(Xca#0+p0r#}2^<$3B0l8^byCBUx{o;G0+ za(_NtxUB)i?(t8ttv|F99kxlPSL2dZ^A2w5gKDJq0YN1%vaEVz`ub=MfCD$AHGCK9 zcGPQam?Zi7@*_4t&`zbDm=P%Yi5~vY;!ciY8q1TPEe}i`P|3FYvZ9%NQySxaNZNa< zx02|fmsQ>)wW6@(x)OqT2FlyR{ne7=z`e-?WM*D&7|COVmH)IS*$Q*cK>qH;AJ!+b zptHel)Onhex})8ciuTwJslPAtFZdsrj*_zc)>`XYHL^SVvvOSP(i0AK^F63BE}--5 z$RvKwh`pbo+seeM2D{WaarC`61ENoqI`6)H9~i3A#bPX1G1v*S1RZvfYSFw&Q6m#g z#nuuL5Lrwv`O=27L0X|C?ZbNPM|E+Iu?kCn?1MbO)0a=@bKZn9W7_HQ*x}>=QfGA% zl1#r+b%4inV+Sk!-0Yh_KQ}1f|7~N(sjWpV~w&s{4DbWqy)!PB3>Z<0=SIO~b7M_F32f2f22;y=T6;1w$hQLAHK(PqsiZ{O-z!_ zZ3k^M?|WvOk2K#~09sgwEXvXsxjLpw#_8%FeP#&&1-KZU1Qxhn5B&faft{hDcTJV# zQ5}M>z6~;1;B>jYR0I}~ezf9fa+*vt#x~N|#d+A=;+ZwWhhTw~<#vmIT#KXw$-{xH zQ%?P7=y$!T(zMSPp*K1dTVq}=DAR9eNc*(IKanI`{5}2K)(7^u7K2opxv^?hRB*hFJe(*Vmn02R!sJ`{> zuy!Oe1Bc67;6kx+-tT*6iao}NJv-CHgHFcJR+fUS@=vV8{@*o}3x?l_3@raC559`4 zs4!LKTEJ{v>MQOjxQs9N^Ly*|Z-r_Wnxdq2tMORvq;Vq60iDNil!@>k_nT4{XVS0Y z@V@+&x(|woxX+>x1xtl7N+Rn_v``t=RW)JSAhCyect3kmvx~q1l&Q`J+?bQ_%C1xW t^NEj-opf^L1wA0(B7p8Gm5>BN8IndajLF8F6A0=AAm-&ITT0{O`#*DZMwS2o literal 0 HcmV?d00001 diff --git a/test/functional/fixtures/es_archiver/saved_objects_imports/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_imports/mappings.json new file mode 100644 index 00000000000000..a89fe1dfacfc83 --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_imports/mappings.json @@ -0,0 +1,244 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "mappings": { + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "dynamic": "strict", + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "dynamic": "strict", + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "dynamic": "strict", + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "timelion-sheet": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "url": { + "dynamic": "strict", + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index d6a96eb651d022..ce7a3f9e132f78 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -332,12 +332,14 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo }); } - async clickConfirmOnModal() { + async clickConfirmOnModal(ensureHidden = true) { log.debug('Clicking modal confirm'); // make sure this data-test-subj 'confirmModalTitleText' exists because we're going to wait for it to be gone later await testSubjects.exists('confirmModalTitleText'); await testSubjects.click('confirmModalConfirmButton'); - await this.ensureModalOverlayHidden(); + if (ensureHidden) { + await this.ensureModalOverlayHidden(); + } } async pressEnterKey() { From 182e0de18f7149f3ed5ddf3768d2903a321c81ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 3 Sep 2020 08:58:05 +0200 Subject: [PATCH 23/25] [Form lib] Fix regression on field not being validated after reset to its default value. (#76379) --- .../components/use_field.test.tsx | 87 +++++++++++++++++++ .../forms/hook_form_lib/hooks/use_field.ts | 19 ++-- .../hook_form_lib/hooks/use_form.test.tsx | 60 ++++++++++++- 3 files changed, 157 insertions(+), 9 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx index a55b2f0a8fa291..c14471991ccd33 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx @@ -64,6 +64,93 @@ describe('', () => { }); }); + describe('validation', () => { + let formHook: FormHook | null = null; + + beforeEach(() => { + formHook = null; + }); + + const onFormHook = (form: FormHook) => { + formHook = form; + }; + + const getTestComp = (fieldConfig: FieldConfig) => { + const TestComp = ({ onForm }: { onForm: (form: FormHook) => void }) => { + const { form } = useForm(); + + useEffect(() => { + onForm(form); + }, [onForm, form]); + + return ( +
+ + + ); + }; + return TestComp; + }; + + const setup = (fieldConfig: FieldConfig) => { + return registerTestBed(getTestComp(fieldConfig), { + memoryRouter: { wrapComponent: false }, + defaultProps: { onForm: onFormHook }, + })() as TestBed; + }; + + test('should update the form validity whenever the field value changes', async () => { + const fieldConfig: FieldConfig = { + defaultValue: '', // empty string, which is not valid + validations: [ + { + validator: ({ value }) => { + // Validate that string is not empty + if ((value as string).trim() === '') { + return { message: 'Error: field is empty.' }; + } + }, + }, + ], + }; + + // Mount our TestComponent + const { + form: { setInputValue }, + } = setup(fieldConfig); + + if (formHook === null) { + throw new Error('FormHook object has not been set.'); + } + + let { isValid } = formHook; + expect(isValid).toBeUndefined(); // Initially the form validity is undefined... + + await act(async () => { + await formHook!.validate(); // ...until we validate the form + }); + + ({ isValid } = formHook); + expect(isValid).toBe(false); + + // Change to a non empty string to pass validation + await act(async () => { + setInputValue('myField', 'changedValue'); + }); + + ({ isValid } = formHook); + expect(isValid).toBe(true); + + // Change back to an empty string to fail validation + await act(async () => { + setInputValue('myField', ''); + }); + + ({ isValid } = formHook); + expect(isValid).toBe(false); + }); + }); + describe('serializer(), deserializer(), formatter()', () => { interface MyForm { name: string; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index fa29f900af2ef5..f01c7226ea4cea 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -69,11 +69,12 @@ export const useField = ( const [isChangingValue, setIsChangingValue] = useState(false); const [isValidated, setIsValidated] = useState(false); + const isMounted = useRef(false); const validateCounter = useRef(0); const changeCounter = useRef(0); + const hasBeenReset = useRef(false); const inflightValidation = useRef | null>(null); const debounceTimeout = useRef(null); - const isMounted = useRef(false); // -- HELPERS // ---------------------------------- @@ -142,11 +143,7 @@ export const useField = ( __updateFormDataAt(path, value); // Validate field(s) (that will update form.isValid state) - // We only validate if the value is different than the initial or default value - // to avoid validating after a form.reset() call. - if (value !== initialValue && value !== defaultValue) { - await __validateFields(fieldsToValidateOnChange ?? [path]); - } + await __validateFields(fieldsToValidateOnChange ?? [path]); if (isMounted.current === false) { return; @@ -172,8 +169,6 @@ export const useField = ( }, [ path, value, - defaultValue, - initialValue, valueChangeListener, errorDisplayDelay, fieldsToValidateOnChange, @@ -468,6 +463,7 @@ export const useField = ( setErrors([]); if (resetValue) { + hasBeenReset.current = true; const newValue = deserializeValue(updatedDefaultValue ?? defaultValue); setValue(newValue); return newValue; @@ -539,6 +535,13 @@ export const useField = ( }, [path, __removeField]); useEffect(() => { + // If the field value has been reset, we don't want to call the "onValueChange()" + // as it will set the "isPristine" state to true or validate the field, which initially we don't want. + if (hasBeenReset.current) { + hasBeenReset.current = false; + return; + } + if (!isMounted.current) { return; } diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index 4a880415b6d228..edcd84daf5d2fe 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -22,7 +22,13 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, getRandomString, TestBed } from '../shared_imports'; import { Form, UseField } from '../components'; -import { FormSubmitHandler, OnUpdateHandler, FormHook, ValidationFunc } from '../types'; +import { + FormSubmitHandler, + OnUpdateHandler, + FormHook, + ValidationFunc, + FieldConfig, +} from '../types'; import { useForm } from './use_form'; interface MyForm { @@ -441,5 +447,57 @@ describe('useForm() hook', () => { deeply: { nested: { value: '' } }, // Fallback to empty string as no config was provided }); }); + + test('should not validate the fields after resetting its value (form validity should be undefined)', async () => { + const fieldConfig: FieldConfig = { + defaultValue: '', + validations: [ + { + validator: ({ value }) => { + if ((value as string).trim() === '') { + return { message: 'Error: empty string' }; + } + }, + }, + ], + }; + + const TestResetComp = () => { + const { form } = useForm(); + + useEffect(() => { + formHook = form; + }, [form]); + + return ( +
+ + + ); + }; + + const { + form: { setInputValue }, + } = registerTestBed(TestResetComp, { + memoryRouter: { wrapComponent: false }, + })() as TestBed; + + let { isValid } = formHook!; + expect(isValid).toBeUndefined(); + + await act(async () => { + setInputValue('myField', 'changedValue'); + }); + ({ isValid } = formHook!); + expect(isValid).toBe(true); + + await act(async () => { + // When we reset the form, value is back to "", which is invalid for the field + formHook!.reset(); + }); + + ({ isValid } = formHook!); + expect(isValid).toBeUndefined(); // Make sure it is "undefined" and not "false". + }); }); }); From 593298ee3147250b91d7c7daf6a768dde4b9a67a Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 3 Sep 2020 10:20:20 +0200 Subject: [PATCH 24/25] remove legacy kibana plugin (#76064) * remove legacy kibana plugin * as ES as a dependency to security instead of kibana * remove 'kibana' plugin presence check in so mixin * moved the `kibana` config section to the server --- src/legacy/core_plugins/kibana/index.js | 39 ------------------- src/legacy/core_plugins/kibana/package.json | 9 ----- .../core_plugins/kibana/public/index.scss | 7 ---- .../kibana/server/ui_setting_defaults.js | 23 ----------- src/legacy/server/config/schema.js | 9 +++++ .../saved_objects/saved_objects_mixin.js | 8 ---- .../saved_objects/saved_objects_mixin.test.js | 15 ------- x-pack/legacy/plugins/security/index.ts | 2 +- x-pack/legacy/plugins/spaces/index.ts | 2 +- 9 files changed, 11 insertions(+), 103 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/index.js delete mode 100644 src/legacy/core_plugins/kibana/package.json delete mode 100644 src/legacy/core_plugins/kibana/public/index.scss delete mode 100644 src/legacy/core_plugins/kibana/server/ui_setting_defaults.js diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js deleted file mode 100644 index 722d75d00f78fc..00000000000000 --- a/src/legacy/core_plugins/kibana/index.js +++ /dev/null @@ -1,39 +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 { getUiSettingDefaults } from './server/ui_setting_defaults'; - -export default function (kibana) { - return new kibana.Plugin({ - id: 'kibana', - config: function (Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - index: Joi.string().default('.kibana'), - autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000), - // TODO Also allow units here like in elasticsearch config once this is moved to the new platform - autocompleteTimeout: Joi.number().integer().min(1).default(1000), - }).default(); - }, - - uiExports: { - uiSettingDefaults: getUiSettingDefaults(), - }, - }); -} diff --git a/src/legacy/core_plugins/kibana/package.json b/src/legacy/core_plugins/kibana/package.json deleted file mode 100644 index 94db646611df0b..00000000000000 --- a/src/legacy/core_plugins/kibana/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "kibana", - "version": "kibana", - "config": { - "@elastic/eslint-import-resolver-kibana": { - "projectRoot": false - } - } -} diff --git a/src/legacy/core_plugins/kibana/public/index.scss b/src/legacy/core_plugins/kibana/public/index.scss deleted file mode 100644 index 7de0c8fc15f943..00000000000000 --- a/src/legacy/core_plugins/kibana/public/index.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Elastic charts -@import '@elastic/charts/dist/theme'; -@import '@elastic/eui/src/themes/charts/theme'; - -// Public UI styles -@import 'src/legacy/ui/public/index'; - diff --git a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js deleted file mode 100644 index 7de5fb581643ac..00000000000000 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ /dev/null @@ -1,23 +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. - */ - -export function getUiSettingDefaults() { - // wrapped in provider so that a new instance is given to each app/test - return {}; -} diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 6cd3f8dc448b09..dd65e45659ffce 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -231,6 +231,15 @@ export default () => locale: Joi.string().default('en'), }).default(), + // temporarily moved here from the (now deleted) kibana legacy plugin + kibana: Joi.object({ + enabled: Joi.boolean().default(true), + index: Joi.string().default('.kibana'), + autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000), + // TODO Also allow units here like in elasticsearch config once this is moved to the new platform + autocompleteTimeout: Joi.number().integer().min(1).default(1000), + }).default(), + savedObjects: Joi.object({ maxImportPayloadBytes: Joi.number().default(10485760), maxImportExportSize: Joi.number().default(10000), diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index 185c8807ae8b5f..96cf2058839cfb 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -39,14 +39,6 @@ export function savedObjectsMixin(kbnServer, server) { server.decorate('server', 'kibanaMigrator', migrator); - const warn = (message) => server.log(['warning', 'saved-objects'], message); - // we use kibana.index which is technically defined in the kibana plugin, so if - // we don't have the plugin (mainly tests) we can't initialize the saved objects - if (!kbnServer.pluginSpecs.some((p) => p.getId() === 'kibana')) { - warn('Saved Objects uninitialized because the Kibana plugin is disabled.'); - return; - } - const serializer = kbnServer.newPlatform.start.core.savedObjects.createSerializer(); const createRepository = (callCluster, includedHiddenTypes = []) => { diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js index 63e4a632ab5e01..d1d6c052ad5891 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js @@ -161,21 +161,6 @@ describe('Saved Objects Mixin', () => { }; }); - describe('no kibana plugin', () => { - it('should not try to create anything', () => { - mockKbnServer.pluginSpecs.some = () => false; - savedObjectsMixin(mockKbnServer, mockServer); - expect(mockServer.log).toHaveBeenCalledWith(expect.any(Array), expect.any(String)); - expect(mockServer.decorate).toHaveBeenCalledWith( - 'server', - 'kibanaMigrator', - expect.any(Object) - ); - expect(mockServer.decorate).toHaveBeenCalledTimes(1); - expect(mockServer.route).not.toHaveBeenCalled(); - }); - }); - describe('Saved object service', () => { let service; diff --git a/x-pack/legacy/plugins/security/index.ts b/x-pack/legacy/plugins/security/index.ts index 3ab5126e52c5f0..c3596d3745e57e 100644 --- a/x-pack/legacy/plugins/security/index.ts +++ b/x-pack/legacy/plugins/security/index.ts @@ -11,7 +11,7 @@ export const security = (kibana: Record) => new kibana.Plugin({ id: 'security', publicDir: resolve(__dirname, 'public'), - require: ['kibana'], + require: ['elasticsearch'], configPrefix: 'xpack.security', config: (Joi: Root) => Joi.object({ enabled: Joi.boolean().default(true) }) diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 40339f198df6d1..725d022879e0d7 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -16,7 +16,7 @@ export const spaces = (kibana: Record) => id: 'spaces', configPrefix: 'xpack.spaces', publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main'], + require: ['elasticsearch', 'xpack_main'], config(Joi: any) { return Joi.object({ enabled: Joi.boolean().default(true), From ebc50e6c691a2483485f94a23314a8bb1a6ec5a0 Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Thu, 3 Sep 2020 10:45:28 +0200 Subject: [PATCH 25/25] [Ingest Manager] Wording & linking improvements (#76284) * Improve wording on add agent flyout. * Add agent explanation to agent flyout. * Add link to data stream page. --- .../components/agent_enrollment_flyout/index.tsx | 10 ++++++++++ .../managed_instructions.tsx | 6 +++--- .../standalone_instructions.tsx | 16 ++++++++++++++-- .../plugins/translations/translations/ja-JP.json | 2 -- .../plugins/translations/translations/zh-CN.json | 2 -- 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx index 414c4e0d7ad2ce..05817f28f92925 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx @@ -8,6 +8,8 @@ import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, + EuiSpacer, + EuiText, EuiTitle, EuiFlexGroup, EuiFlexItem, @@ -44,6 +46,14 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ /> + + + + + setMode('managed')}> = ({ agentPolic <> ), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx index 049ceca82b309b..9262cc2cb42ac0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -15,12 +15,13 @@ import { EuiFlexGroup, EuiCodeBlock, EuiCopy, + EuiLink, } from '@elastic/eui'; import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentPolicy } from '../../../../types'; -import { useCore, sendGetOneAgentPolicyFull } from '../../../../hooks'; +import { useCore, useLink, sendGetOneAgentPolicyFull } from '../../../../hooks'; import { DownloadStep, AgentPolicySelectionStep } from './steps'; import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../../../services'; @@ -31,6 +32,7 @@ interface Props { const RUN_INSTRUCTIONS = './elastic-agent run'; export const StandaloneInstructions: React.FunctionComponent = ({ agentPolicies }) => { + const { getHref } = useLink(); const core = useCore(); const { notifications } = core; @@ -157,7 +159,17 @@ export const StandaloneInstructions: React.FunctionComponent = ({ agentPo + + + ), + }} /> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 59d952d08c1811..936f5aaf6522d1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9119,10 +9119,8 @@ "xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "スタンドアロンモード", "xpack.ingestManager.agentEnrollment.fleetNotInitializedText": "エージェントを登録する前に、フリートを設定する必要があります。{link}", "xpack.ingestManager.agentEnrollment.flyoutTitle": "エージェントの追加", - "xpack.ingestManager.agentEnrollment.goToFleetButton": "フリートに移動します。", "xpack.ingestManager.agentEnrollment.managedDescription": "必要なエージェントの数に関係なく、Fleetでは、簡単に一元的に更新を管理し、エージェントにデプロイすることができます。次の手順に従い、Elasticエージェントをダウンロードし、Fleetに登録してください。", "xpack.ingestManager.agentEnrollment.standaloneDescription": "スタンドアロンモードで実行中のエージェントは、構成を変更したい場合には、手動で更新する必要があります。次の手順に従い、スタンドアロンモードでElasticエージェントをダウンロードし、セットアップしてください。", - "xpack.ingestManager.agentEnrollment.stepCheckForDataDescription": "エージェントを起動した後、エージェントはデータの送信を開始します。Ingest Managerのデータセットページでは、このデータを確認できます。", "xpack.ingestManager.agentEnrollment.stepCheckForDataTitle": "データを確認", "xpack.ingestManager.agentEnrollment.stepChooseAgentPolicyTitle": "エージェント構成を選択", "xpack.ingestManager.agentEnrollment.stepConfigureAgentDescription": "この構成をコピーし、Elasticエージェントがインストールされているシステムのファイル{fileName}に置きます。必ず、構成ファイルの{outputSection}セクションで{ESUsernameVariable}と{ESPasswordVariable}を修正し、実際のElasticsearch認証資格情報が使用されるようにしてください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6641eede91e66b..800439f7b7b3f7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9122,10 +9122,8 @@ "xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "独立模式", "xpack.ingestManager.agentEnrollment.fleetNotInitializedText": "注册代理前需要设置 Fleet。{link}", "xpack.ingestManager.agentEnrollment.flyoutTitle": "添加代理", - "xpack.ingestManager.agentEnrollment.goToFleetButton": "前往 Fleet。", "xpack.ingestManager.agentEnrollment.managedDescription": "无论是需要一个代理还是需要数以千计的代理,Fleet 允许您轻松地集中管理并部署代理的更新。按照下面的说明下载 Elastic 代理并将代理注册到 Fleet。", "xpack.ingestManager.agentEnrollment.standaloneDescription": "如果希望对以独立模式运行的代理进行配置更改,则需要手动更新。按照下面的说明下载并设置独立模式的 Elastic 代理。", - "xpack.ingestManager.agentEnrollment.stepCheckForDataDescription": "启动代理后,代理应开始发送数据。您可以在采集管理器的数据集页面查看此数据。", "xpack.ingestManager.agentEnrollment.stepCheckForDataTitle": "检查数据", "xpack.ingestManager.agentEnrollment.stepChooseAgentPolicyTitle": "选择代理配置", "xpack.ingestManager.agentEnrollment.stepConfigureAgentDescription": "在安装 Elastic 代理的系统上复制此配置并将其放入名为 {fileName} 的文件中。切勿忘记修改配置文件中 {outputSection} 部分的{ESUsernameVariable} 和 {ESPasswordVariable},以便其使用您的实际 Elasticsearch 凭据。",