diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index 55e1475fcb03a..86bce5997cdd2 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -25,7 +25,6 @@ */ export { npSetup, npStart } from 'ui/new_platform'; -export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { configureAppAngularModule, migrateLegacyQuery, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts deleted file mode 100644 index 60ca1b39d29d6..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts +++ /dev/null @@ -1,118 +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. - */ - -jest.mock('../', () => ({ - DashboardConstants: { - ADD_EMBEDDABLE_ID: 'addEmbeddableId', - ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', - }, -})); - -jest.mock('../legacy_imports', () => { - return { - absoluteToParsedUrl: jest.fn(() => { - return { - basePath: '/pep', - appId: 'kibana', - appPath: '/dashboard?addEmbeddableType=lens&addEmbeddableId=123eb456cd&x=1&y=2&z=3', - hostname: 'localhost', - port: 5601, - protocol: 'http:', - addQueryParameter: () => {}, - getAbsoluteUrl: () => { - return 'http://localhost:5601/pep/app/kibana#/dashboard?addEmbeddableType=lens&addEmbeddableId=123eb456cd&x=1&y=2&z=3'; - }, - }; - }), - }; -}); - -import { - addEmbeddableToDashboardUrl, - getLensUrlFromDashboardAbsoluteUrl, - getUrlVars, -} from './url_helper'; - -describe('Dashboard URL Helper', () => { - beforeEach(() => { - jest.resetModules(); - }); - - it('addEmbeddableToDashboardUrl', () => { - const id = '123eb456cd'; - const type = 'lens'; - const urlVars = { - x: '1', - y: '2', - z: '3', - }; - const basePath = '/pep'; - const url = - "http://localhost:5601/pep/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; - expect(addEmbeddableToDashboardUrl(url, basePath, id, urlVars, type)).toEqual( - `http://localhost:5601/pep/app/kibana#/dashboard?addEmbeddableType=${type}&addEmbeddableId=${id}&x=1&y=2&z=3` - ); - }); - - it('getUrlVars', () => { - let url = - "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; - expect(getUrlVars(url)).toEqual({ - _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))', - _a: "(description:'',filters:!()", - }); - url = 'http://mybusiness.mydomain.com/app/kibana#/dashboard?x=y&y=z'; - expect(getUrlVars(url)).toEqual({ - x: 'y', - y: 'z', - }); - url = 'http://localhost:5601/app/kibana#/dashboard/777182'; - expect(getUrlVars(url)).toEqual({}); - url = - 'http://localhost:5601/app/kibana#/dashboard/777182?title=Some%20Dashboard%20With%20Spaces'; - expect(getUrlVars(url)).toEqual({ title: 'Some Dashboard With Spaces' }); - }); - - it('getLensUrlFromDashboardAbsoluteUrl', () => { - const id = '1244'; - const basePath = '/wev'; - let url = - "http://localhost:5601/wev/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; - expect(getLensUrlFromDashboardAbsoluteUrl(url, basePath, id)).toEqual( - 'http://localhost:5601/wev/app/kibana#/lens/edit/1244' - ); - - url = - "http://localhost:5601/wev/app/kibana#/dashboard/625357282?_a=(description:'',filters:!()&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))"; - expect(getLensUrlFromDashboardAbsoluteUrl(url, basePath, id)).toEqual( - 'http://localhost:5601/wev/app/kibana#/lens/edit/1244' - ); - - url = 'http://myserver.mydomain.com:5601/wev/app/kibana#/dashboard/777182'; - expect(getLensUrlFromDashboardAbsoluteUrl(url, basePath, id)).toEqual( - 'http://myserver.mydomain.com:5601/wev/app/kibana#/lens/edit/1244' - ); - - url = - "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; - expect(getLensUrlFromDashboardAbsoluteUrl(url, '', id)).toEqual( - 'http://localhost:5601/app/kibana#/lens/edit/1244' - ); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts deleted file mode 100644 index 73383f2ff3f68..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts +++ /dev/null @@ -1,100 +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 { parse } from 'url'; -import { absoluteToParsedUrl } from '../legacy_imports'; -import { DashboardConstants } from './dashboard_constants'; -/** - * Return query params from URL - * @param url given url - */ -export function getUrlVars(url: string): Record { - const vars: Record = {}; - for (const [, key, value] of url.matchAll(/[?&]+([^=&]+)=([^&]*)/gi)) { - vars[key] = decodeURIComponent(value); - } - return vars; -} - -/** * - * Returns dashboard URL with added embeddableType and embeddableId query params - * eg. - * input: url: http://localhost:5601/lib/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)), embeddableId: 12345, embeddableType: 'lens' - * output: http://localhost:5601/lib/app/kibana#dashboard?addEmbeddableType=lens&addEmbeddableId=12345&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)) - * @param url dasbhoard absolute url - * @param embeddableId id of the saved visualization - * @param basePath current base path - * @param urlVars url query params (optional) - * @param embeddableType 'lens' or 'visualization' (optional, default is 'lens') - */ -export function addEmbeddableToDashboardUrl( - url: string | undefined, - basePath: string, - embeddableId: string, - urlVars?: Record, - embeddableType?: string -): string | null { - if (!url) { - return null; - } - const dashboardUrl = getUrlWithoutQueryParams(url); - const dashboardParsedUrl = absoluteToParsedUrl(dashboardUrl, basePath); - if (urlVars) { - const keys = Object.keys(urlVars).sort(); - keys.forEach(key => { - dashboardParsedUrl.addQueryParameter(key, urlVars[key]); - }); - } - dashboardParsedUrl.addQueryParameter( - DashboardConstants.ADD_EMBEDDABLE_TYPE, - embeddableType || 'lens' - ); - dashboardParsedUrl.addQueryParameter(DashboardConstants.ADD_EMBEDDABLE_ID, embeddableId); - return dashboardParsedUrl.getAbsoluteUrl(); -} - -/** - * Return Lens URL from dashboard absolute URL - * @param dashboardAbsoluteUrl - * @param basePath current base path - * @param id Lens id - */ -export function getLensUrlFromDashboardAbsoluteUrl( - dashboardAbsoluteUrl: string | undefined | null, - basePath: string | null | undefined, - id: string -): string | null { - if (!dashboardAbsoluteUrl || basePath === null || basePath === undefined) { - return null; - } - const { host, protocol } = parse(dashboardAbsoluteUrl); - return `${protocol}//${host}${basePath}/app/kibana#/lens/edit/${id}`; -} - -/** - * Returns the portion of the URL without query params - * eg. - * input: http://localhost:5601/lib/app/kibana#/dashboard?param1=x¶m2=y¶m3=z - * output:http://localhost:5601/lib/app/kibana#/dashboard - * input: http://localhost:5601/lib/app/kibana#/dashboard/39292992?param1=x¶m2=y¶m3=z - * output: http://localhost:5601/lib/app/kibana#/dashboard/39292992 - * @param url url to parse - */ -function getUrlWithoutQueryParams(url: string): string { - return url.split('?')[0]; -} diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index f29f07ba4b20b..2ed7e3d43168c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -45,7 +45,6 @@ export interface VisualizeKibanaServices { core: CoreStart; data: DataPublicPluginStart; embeddable: EmbeddableStart; - getBasePath: () => string; indexPatterns: IndexPatternsContract; localStorage: Storage; navigation: NavigationStart; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index a6774e2dd47e8..6a2034d9a62e4 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -24,9 +24,6 @@ * directly where they are needed. */ -export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; -export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; -export { wrapInI18nContext } from 'ui/i18n'; export { DashboardConstants } from '../dashboard/np_ready/dashboard_constants'; export { VisSavedObject, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index c5325ca3108b4..9ccd45dfc1b45 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -26,14 +26,14 @@ import { EventEmitter } from 'events'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { makeStateful, useVisualizeAppState } from './lib'; +import { makeStateful, useVisualizeAppState, addEmbeddableToDashboardUrl } from './lib'; import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from '../breadcrumbs'; import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; import { unhashUrl, removeQueryParam } from '../../../../../../../plugins/kibana_utils/public'; import { MarkdownSimple, toMountPoint } from '../../../../../../../plugins/kibana_react/public'; -import { addFatalError, kbnBaseUrl } from '../../../../../../../plugins/kibana_legacy/public'; +import { addFatalError } from '../../../../../../../plugins/kibana_legacy/public'; import { SavedObjectSaveModal, showSaveModal, @@ -46,14 +46,7 @@ import { import { initVisEditorDirective } from './visualization_editor'; import { initVisualizationDirective } from './visualization'; -import { - VISUALIZE_EMBEDDABLE_TYPE, - subscribeWithScope, - absoluteToParsedUrl, - KibanaParsedUrl, - migrateLegacyQuery, - DashboardConstants, -} from '../../legacy_imports'; +import { subscribeWithScope, migrateLegacyQuery, DashboardConstants } from '../../legacy_imports'; import { getServices } from '../../kibana_services'; @@ -79,7 +72,6 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState data: { query: queryService }, toastNotifications, chrome, - getBasePath, core: { docLinks, fatalErrors }, savedQueryService, uiSettings, @@ -653,29 +645,14 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState }); if ($scope.isAddToDashMode()) { - const savedVisualizationParsedUrl = new KibanaParsedUrl({ - basePath: getBasePath(), - appId: kbnBaseUrl.slice('/app/'.length), - appPath: `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(savedVis.id)}`, - }); + const appPath = `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(savedVis.id)}`; // Manually insert a new url so the back button will open the saved visualization. - history.replace(savedVisualizationParsedUrl.appPath); - setActiveUrl(savedVisualizationParsedUrl.appPath); + history.replace(appPath); + setActiveUrl(appPath); - const lastDashboardAbsoluteUrl = chrome.navLinks.get('kibana:dashboard').url; - const dashboardParsedUrl = absoluteToParsedUrl( - lastDashboardAbsoluteUrl, - getBasePath() - ); - dashboardParsedUrl.addQueryParameter( - DashboardConstants.ADD_EMBEDDABLE_TYPE, - VISUALIZE_EMBEDDABLE_TYPE - ); - dashboardParsedUrl.addQueryParameter( - DashboardConstants.ADD_EMBEDDABLE_ID, - savedVis.id - ); - history.push(dashboardParsedUrl.appPath); + const lastDashboardUrl = chrome.navLinks.get('kibana:dashboard').url; + const dashboardUrl = addEmbeddableToDashboardUrl(lastDashboardUrl, savedVis.id); + history.push(dashboardUrl); } else if (savedVis.id === $route.current.params.id) { chrome.docTitle.change(savedVis.lastSavedTitle); chrome.setBreadcrumbs($injector.invoke(getEditBreadcrumbs)); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts index fa5b91b00edaf..6e2f759c73f2f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts @@ -19,3 +19,4 @@ export { useVisualizeAppState } from './visualize_app_state'; export { makeStateful } from './make_stateful'; +export { addEmbeddableToDashboardUrl } from './url_helper'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.test.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.test.ts new file mode 100644 index 0000000000000..e6974af9af832 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { addEmbeddableToDashboardUrl } from './url_helper'; + +jest.mock('../../../legacy_imports', () => ({ + DashboardConstants: { + ADD_EMBEDDABLE_ID: 'addEmbeddableId', + ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', + CREATE_NEW_DASHBOARD_URL: '/dashboard', + }, + VISUALIZE_EMBEDDABLE_TYPE: 'visualization', +})); + +describe('', () => { + it('addEmbeddableToDashboardUrl when dashboard is not saved', () => { + const id = '123eb456cd'; + const url = + "/pep/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!())"; + expect(addEmbeddableToDashboardUrl(url, id)).toEqual( + `/dashboard?_a=%28description%3A%27%27%2Cfilters%3A%21%28%29%29&_g=%28refreshInterval%3A%28pause%3A%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3Anow-15m%2Cto%3Anow%29%29&addEmbeddableId=${id}&addEmbeddableType=visualization` + ); + }); + it('addEmbeddableToDashboardUrl when dashboard is saved', () => { + const id = '123eb456cd'; + const url = + "/pep/app/kibana#/dashboard/9b780cd0-3dd3-11e8-b2b9-5d5dc1715159?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!())"; + expect(addEmbeddableToDashboardUrl(url, id)).toEqual( + `/dashboard/9b780cd0-3dd3-11e8-b2b9-5d5dc1715159?_a=%28description%3A%27%27%2Cfilters%3A%21%28%29%29&_g=%28refreshInterval%3A%28pause%3A%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3Anow-15m%2Cto%3Anow%29%29&addEmbeddableId=${id}&addEmbeddableType=visualization` + ); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.ts new file mode 100644 index 0000000000000..c7937c856184a --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.ts @@ -0,0 +1,39 @@ +/* + * 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 { parseUrl, stringify } from 'query-string'; +import { DashboardConstants, VISUALIZE_EMBEDDABLE_TYPE } from '../../../legacy_imports'; + +/** * + * Returns relative dashboard URL with added embeddableType and embeddableId query params + * eg. + * input: url: lol/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)), embeddableId: 12345 + * output: /dashboard?addEmbeddableType=visualization&addEmbeddableId=12345&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)) + * @param url dasbhoard absolute url + * @param embeddableId id of the saved visualization + */ +export function addEmbeddableToDashboardUrl(dashboardUrl: string, embeddableId: string) { + const { url, query } = parseUrl(dashboardUrl); + const [, dashboardId] = url.split(DashboardConstants.CREATE_NEW_DASHBOARD_URL); + + query[DashboardConstants.ADD_EMBEDDABLE_TYPE] = VISUALIZE_EMBEDDABLE_TYPE; + query[DashboardConstants.ADD_EMBEDDABLE_ID] = embeddableId; + + return `${DashboardConstants.CREATE_NEW_DASHBOARD_URL}${dashboardId}?${stringify(query)}`; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js index 6c02afb672e4c..098633d046062 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js @@ -18,19 +18,17 @@ */ import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; -import { VisualizeListingTable } from './visualize_listing_table'; +import { withI18nContext } from './visualize_listing_table'; import { VisualizeConstants } from '../visualize_constants'; import { i18n } from '@kbn/i18n'; import { getServices } from '../../kibana_services'; -import { wrapInI18nContext } from '../../legacy_imports'; - import { syncQueryStateWithUrl } from '../../../../../../../plugins/data/public'; -export function initListingDirective(app) { +export function initListingDirective(app, I18nContext) { app.directive('visualizeListingTable', reactDirective => - reactDirective(wrapInI18nContext(VisualizeListingTable)) + reactDirective(withI18nContext(I18nContext)) ); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js index b770625cd3d70..932ac8996e97e 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js @@ -230,4 +230,10 @@ VisualizeListingTable.propTypes = { listingLimit: PropTypes.number.isRequired, }; -export { VisualizeListingTable }; +const withI18nContext = I18nContext => props => ( + + + +); + +export { withI18nContext }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_app.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_app.ts index 1e7ac668697de..a4afac23f4842 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_app.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_app.ts @@ -27,5 +27,5 @@ import { initListingDirective } from './listing/visualize_listing'; export function initVisualizeAppDirective(app: IModule, deps: VisualizeKibanaServices) { initEditorDirective(app, deps); - initListingDirective(app); + initListingDirective(app, deps.core.i18n.Context); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 59b814c98dd08..6d32579f5c541 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -140,7 +140,6 @@ export class VisualizePlugin implements Plugin { chrome: coreStart.chrome, data: dataStart, embeddable, - getBasePath: core.http.basePath.get, indexPatterns: dataStart.indexPatterns, localStorage: new Storage(localStorage), navigation, diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx b/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx index 0b6d4e5982a00..58e67b5064da5 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; import { EventEmitter } from 'events'; import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types'; @@ -83,7 +82,7 @@ class DefaultEditorController { render({ data, core, ...props }: EditorRenderProps) { render( - + - , + , this.el ); } diff --git a/src/plugins/data/common/query/filter_manager/compare_filters.test.ts b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts index b0bb2f754d6cf..0c3947ade8221 100644 --- a/src/plugins/data/common/query/filter_manager/compare_filters.test.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts @@ -197,6 +197,22 @@ describe('filter manager utilities', () => { expect(compareFilters([f1], [f2], COMPARE_ALL_OPTIONS)).toBeTruthy(); }); + test('should compare alias with alias true', () => { + const f1 = { + $state: { store: FilterStateStore.GLOBAL_STATE }, + ...buildQueryFilter({ _type: { match: { query: 'apache', type: 'phrase' } } }, 'index', ''), + }; + const f2 = { + $state: { store: FilterStateStore.GLOBAL_STATE }, + ...buildQueryFilter({ _type: { match: { query: 'apache', type: 'phrase' } } }, 'index', ''), + }; + + f2.meta.alias = 'wassup'; + f2.meta.alias = 'dog'; + + expect(compareFilters([f1], [f2], { alias: true })).toBeFalsy(); + }); + test('should compare alias with COMPARE_ALL_OPTIONS', () => { const f1 = { $state: { store: FilterStateStore.GLOBAL_STATE }, diff --git a/src/plugins/data/common/query/filter_manager/compare_filters.ts b/src/plugins/data/common/query/filter_manager/compare_filters.ts index e047d5e0665d5..3be52a9a60977 100644 --- a/src/plugins/data/common/query/filter_manager/compare_filters.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.ts @@ -46,7 +46,7 @@ const mapFilter = ( if (comparators.negate) cleaned.negate = filter.meta && Boolean(filter.meta.negate); if (comparators.disabled) cleaned.disabled = filter.meta && Boolean(filter.meta.disabled); - if (comparators.disabled) cleaned.alias = filter.meta?.alias; + if (comparators.alias) cleaned.alias = filter.meta?.alias; return cleaned; }; diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index 7017c01cc5634..c7fa0f40e1d0c 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -32,7 +32,6 @@ export default async function({ readConfigFile }) { return { testFiles: [ - require.resolve('./test_suites/app_plugins'), require.resolve('./test_suites/custom_visualizations'), require.resolve('./test_suites/panel_actions'), require.resolve('./test_suites/embeddable_explorer'), diff --git a/test/plugin_functional/plugins/kbn_top_nav/kibana.json b/test/plugin_functional/plugins/kbn_top_nav/kibana.json new file mode 100644 index 0000000000000..b274e80b9ef65 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_top_nav/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "kbn_top_nav", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["kbn_top_nav"], + "server": false, + "ui": true, + "requiredPlugins": ["navigation"] +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/kbn_top_nav/package.json b/test/plugin_functional/plugins/kbn_top_nav/package.json new file mode 100644 index 0000000000000..510d681a4a75c --- /dev/null +++ b/test/plugin_functional/plugins/kbn_top_nav/package.json @@ -0,0 +1,18 @@ +{ + "name": "kbn_top_nav", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/kbn_top_nav", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" + } +} + diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/public/top_nav.tsx b/test/plugin_functional/plugins/kbn_top_nav/public/application.tsx similarity index 71% rename from test/plugin_functional/plugins/kbn_tp_top_nav/public/top_nav.tsx rename to test/plugin_functional/plugins/kbn_top_nav/public/application.tsx index f77db4fe1654e..0f65e6159796b 100644 --- a/test/plugin_functional/plugins/kbn_tp_top_nav/public/top_nav.tsx +++ b/test/plugin_functional/plugins/kbn_top_nav/public/application.tsx @@ -18,11 +18,15 @@ */ import React from 'react'; -import './initialize'; -import { npStart } from 'ui/new_platform'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { AppMountParameters } from 'kibana/public'; +import { AppPluginDependencies } from './types'; -export const AppWithTopNav = () => { - const { TopNavMenu } = npStart.plugins.navigation.ui; +export const renderApp = ( + depsStart: AppPluginDependencies, + { appBasePath, element }: AppMountParameters +) => { + const { TopNavMenu } = depsStart.navigation.ui; const config = [ { id: 'new', @@ -32,10 +36,12 @@ export const AppWithTopNav = () => { testId: 'demoNewButton', }, ]; - - return ( + render( Hey - + , + element ); + + return () => unmountComponentAtNode(element); }; diff --git a/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/public/app.js b/test/plugin_functional/plugins/kbn_top_nav/public/index.ts similarity index 75% rename from test/plugin_functional/plugins/kbn_tp_sample_app_plugin/public/app.js rename to test/plugin_functional/plugins/kbn_top_nav/public/index.ts index a7a516bb0cdbd..bd478f1dd3bdb 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/public/app.js +++ b/test/plugin_functional/plugins/kbn_top_nav/public/index.ts @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import 'ui/autoload/all'; -import chrome from 'ui/chrome'; +import { PluginInitializer } from 'kibana/public'; +import { TopNavTestPlugin, TopNavTestPluginSetup, TopNavTestPluginStart } from './plugin'; -chrome.setRootTemplate('
Super simple app plugin
'); +export const plugin: PluginInitializer = () => + new TopNavTestPlugin(); diff --git a/test/plugin_functional/plugins/kbn_top_nav/public/plugin.tsx b/test/plugin_functional/plugins/kbn_top_nav/public/plugin.tsx new file mode 100644 index 0000000000000..a433de98357fb --- /dev/null +++ b/test/plugin_functional/plugins/kbn_top_nav/public/plugin.tsx @@ -0,0 +1,65 @@ +/* + * 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 { CoreSetup, Plugin, AppMountParameters } from 'kibana/public'; +import { NavigationPublicPluginSetup } from '../../../../../src/plugins/navigation/public'; +import { AppPluginDependencies } from './types'; + +export class TopNavTestPlugin implements Plugin { + public setup(core: CoreSetup, { navigation }: { navigation: NavigationPublicPluginSetup }) { + const customExtension = { + id: 'registered-prop', + label: 'Registered Button', + description: 'Registered Demo', + run() {}, + testId: 'demoRegisteredNewButton', + }; + + navigation.registerMenuItem(customExtension); + + const customDiscoverExtension = { + id: 'registered-discover-prop', + label: 'Registered Discover Button', + description: 'Registered Discover Demo', + run() {}, + testId: 'demoDiscoverRegisteredNewButton', + appName: 'discover', + }; + + navigation.registerMenuItem(customDiscoverExtension); + + core.application.register({ + id: 'topNavMenu', + title: 'Top nav menu example', + async mount(params: AppMountParameters) { + const { renderApp } = await import('./application'); + const services = await core.getStartServices(); + return renderApp(services[1] as AppPluginDependencies, params); + }, + }); + + return {}; + } + + public start() {} + public stop() {} +} + +export type TopNavTestPluginSetup = ReturnType; +export type TopNavTestPluginStart = ReturnType; diff --git a/test/plugin_functional/test_suites/app_plugins/index.js b/test/plugin_functional/plugins/kbn_top_nav/public/types.ts similarity index 81% rename from test/plugin_functional/test_suites/app_plugins/index.js rename to test/plugin_functional/plugins/kbn_top_nav/public/types.ts index 83faa7377c7ac..c70a78bedb54f 100644 --- a/test/plugin_functional/test_suites/app_plugins/index.js +++ b/test/plugin_functional/plugins/kbn_top_nav/public/types.ts @@ -17,8 +17,8 @@ * under the License. */ -export default function({ loadTestFile }) { - describe('app plugins', () => { - loadTestFile(require.resolve('./app_navigation')); - }); +import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; + +export interface AppPluginDependencies { + navigation: NavigationPublicPluginStart; } diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/tsconfig.json b/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json similarity index 100% rename from test/plugin_functional/plugins/kbn_tp_top_nav/tsconfig.json rename to test/plugin_functional/plugins/kbn_top_nav/tsconfig.json diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json new file mode 100644 index 0000000000000..622cbd80090ba --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "kbn_tp_custom_visualizations", + "version": "0.0.1", + "kibanaVersion": "kibana", + "requiredPlugins": [ + "visualizations" + ], + "server": false, + "ui": true +} diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 344aae30b5bbc..9ee7845816faa 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -1,6 +1,7 @@ { "name": "kbn_tp_custom_visualizations", "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/kbn_tp_custom_visualizations", "kibana": { "version": "kibana", "templateVersion": "1.0.0" @@ -9,5 +10,13 @@ "dependencies": { "@elastic/eui": "21.0.1", "react": "^16.12.0" + }, + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "@kbn/plugin-helpers": "9.0.2", + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/index.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/index.ts similarity index 68% rename from test/plugin_functional/plugins/kbn_tp_top_nav/index.js rename to test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/index.ts index b4c3e05c28b66..cb821a2698479 100644 --- a/test/plugin_functional/plugins/kbn_tp_top_nav/index.js +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/index.ts @@ -17,15 +17,14 @@ * under the License. */ -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - app: { - title: 'Top Nav Menu test', - description: 'This is a sample plugin for the functional tests.', - main: 'plugins/kbn_tp_top_nav/app', - }, - hacks: ['plugins/kbn_tp_top_nav/initialize'], - }, - }); -} +import { PluginInitializer } from 'kibana/public'; +import { + CustomVisualizationsPublicPlugin, + CustomVisualizationsSetup, + CustomVisualizationsStart, +} from './plugin'; + +export { CustomVisualizationsPublicPlugin as Plugin }; + +export const plugin: PluginInitializer = () => + new CustomVisualizationsPublicPlugin(); diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/plugin.ts b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/plugin.ts new file mode 100644 index 0000000000000..1be4aa9ee42ae --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/plugin.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 { CoreSetup, Plugin } from 'kibana/public'; +import { VisualizationsSetup } from '../../../../../src/plugins/visualizations/public'; +import { SelfChangingEditor } from './self_changing_vis/self_changing_editor'; +import { SelfChangingComponent } from './self_changing_vis/self_changing_components'; + +export interface SetupDependencies { + visualizations: VisualizationsSetup; +} + +export class CustomVisualizationsPublicPlugin + implements Plugin { + public setup(core: CoreSetup, setupDeps: SetupDependencies) { + setupDeps.visualizations.createReactVisualization({ + name: 'self_changing_vis', + title: 'Self Changing Vis', + icon: 'controlsHorizontal', + description: + 'This visualization is able to change its own settings, that you could also set in the editor.', + visConfig: { + component: SelfChangingComponent, + defaults: { + counter: 0, + }, + }, + editorConfig: { + optionTabs: [ + { + name: 'options', + title: 'Options', + editor: SelfChangingEditor, + }, + ], + }, + requestHandler: 'none', + }); + } + + public start() {} + public stop() {} +} + +export type CustomVisualizationsSetup = ReturnType; +export type CustomVisualizationsStart = ReturnType; diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.js deleted file mode 100644 index c5b074db43a1b..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.js +++ /dev/null @@ -1,52 +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 React from 'react'; - -import { EuiBadge } from '@elastic/eui'; - -export class SelfChangingComponent extends React.Component { - onClick = () => { - this.props.vis.params.counter++; - this.props.vis.updateState(); - }; - - render() { - return ( -
- - {this.props.vis.params.counter} - -
- ); - } - - componentDidMount() { - this.props.renderComplete(); - } - - componentDidUpdate() { - this.props.renderComplete(); - } -} diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.tsx similarity index 59% rename from test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js rename to test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.tsx index b2497a824ba2b..2f01908122457 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.tsx @@ -17,10 +17,32 @@ * under the License. */ -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - hacks: ['plugins/kbn_tp_custom_visualizations/self_changing_vis/self_changing_vis'], - }, +import React, { useEffect } from 'react'; + +import { EuiBadge } from '@elastic/eui'; + +interface SelfChangingComponentProps { + renderComplete: () => {}; + visParams: { + counter: number; + }; +} + +export function SelfChangingComponent(props: SelfChangingComponentProps) { + useEffect(() => { + props.renderComplete(); }); + + return ( +
+ {}} + data-test-subj="counter" + onClickAriaLabel="Increase counter" + color="primary" + > + {props.visParams.counter} + +
+ ); } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx similarity index 76% rename from test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js rename to test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx index fa3a0c8b9f6fe..d3f66d708603c 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx @@ -20,10 +20,15 @@ import React from 'react'; import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; +import { VisOptionsProps } from '../../../../../../src/legacy/core_plugins/vis_default_editor/public/vis_options_props'; -export class SelfChangingEditor extends React.Component { - onCounterChange = ev => { - this.props.setValue('counter', parseInt(ev.target.value)); +interface CounterParams { + counter: number; +} + +export class SelfChangingEditor extends React.Component> { + onCounterChange = (ev: any) => { + this.props.setValue('counter', parseInt(ev.target.value, 10)); }; render() { diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json new file mode 100644 index 0000000000000..d8096d9aab27a --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true, + "types": [ + "node", + "jest", + "react" + ] + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [] +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/index.js b/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/index.js deleted file mode 100644 index ff4be4113eeb3..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/index.js +++ /dev/null @@ -1,30 +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 default function(kibana) { - return new kibana.Plugin({ - uiExports: { - app: { - title: 'Test Plugin App', - description: 'This is a sample plugin for the functional tests.', - main: 'plugins/kbn_tp_sample_app_plugin/app', - }, - }, - }); -} diff --git a/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/package.json b/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/package.json deleted file mode 100644 index 2537bb9a7ed5c..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "kbn_tp_sample_app_plugin", - "version": "1.0.0", - "kibana": { - "version": "kibana", - "templateVersion": "1.0.0" - }, - "license": "Apache-2.0" -} diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/package.json b/test/plugin_functional/plugins/kbn_tp_top_nav/package.json deleted file mode 100644 index 7102d24d3292d..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_top_nav/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "kbn_tp_top_nav", - "version": "1.0.0", - "kibana": { - "version": "kibana", - "templateVersion": "1.0.0" - }, - "license": "Apache-2.0" -} diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/public/app.js b/test/plugin_functional/plugins/kbn_tp_top_nav/public/app.js deleted file mode 100644 index e7f97e68c086d..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_top_nav/public/app.js +++ /dev/null @@ -1,54 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; - -// This is required so some default styles and required scripts/Angular modules are loaded, -// or the timezone setting is correctly applied. -import 'ui/autoload/all'; - -import { AppWithTopNav } from './top_nav'; - -const app = uiModules.get('apps/topnavDemoPlugin', ['kibana']); - -app.config($locationProvider => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); -}); - -function RootController($scope, $element) { - const domNode = $element[0]; - - // render react to DOM - render(, domNode); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); -} - -chrome.setRootController('topnavDemoPlugin', RootController); diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/public/initialize.js b/test/plugin_functional/plugins/kbn_tp_top_nav/public/initialize.js deleted file mode 100644 index d46e47f6d248a..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_top_nav/public/initialize.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { npSetup } from 'ui/new_platform'; - -const customExtension = { - id: 'registered-prop', - label: 'Registered Button', - description: 'Registered Demo', - run() {}, - testId: 'demoRegisteredNewButton', -}; - -npSetup.plugins.navigation.registerMenuItem(customExtension); - -const customDiscoverExtension = { - id: 'registered-discover-prop', - label: 'Registered Discover Button', - description: 'Registered Discover Demo', - run() {}, - testId: 'demoDiscoverRegisteredNewButton', - appName: 'discover', -}; - -npSetup.plugins.navigation.registerMenuItem(customDiscoverExtension); diff --git a/test/plugin_functional/test_suites/app_plugins/app_navigation.js b/test/plugin_functional/test_suites/app_plugins/app_navigation.js deleted file mode 100644 index bb39e52287556..0000000000000 --- a/test/plugin_functional/test_suites/app_plugins/app_navigation.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; - -export default function({ getService, getPageObjects }) { - const appsMenu = getService('appsMenu'); - const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'header', 'home']); - - describe('app navigation', function describeIndexTests() { - before(async () => { - await PageObjects.common.navigateToApp('settings'); - }); - - it('should show nav link that navigates to the app', async () => { - await appsMenu.clickLink('Test Plugin App'); - const pluginContent = await testSubjects.find('pluginContent'); - expect(await pluginContent.getVisibleText()).to.be('Super simple app plugin'); - }); - }); -} diff --git a/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js b/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js index ef6f0a626bd15..83258a1ca3bdc 100644 --- a/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js +++ b/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js @@ -28,11 +28,7 @@ export default function({ getService, getPageObjects }) { return await testSubjects.getVisibleText('counter'); } - async function getEditorValue() { - return await testSubjects.getAttribute('counterEditor', 'value'); - } - - describe.skip('self changing vis', function describeIndexTests() { + describe('self changing vis', function describeIndexTests() { before(async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('self_changing_vis'); @@ -45,16 +41,17 @@ export default function({ getService, getPageObjects }) { const isApplyEnabled = await PageObjects.visEditor.isApplyEnabled(); expect(isApplyEnabled).to.be(true); await PageObjects.visEditor.clickGo(); + await renderable.waitForRender(); const counter = await getCounterValue(); expect(counter).to.be('10'); }); - it('should allow changing params from within the vis', async () => { + it.skip('should allow changing params from within the vis', async () => { await testSubjects.click('counter'); await renderable.waitForRender(); const visValue = await getCounterValue(); expect(visValue).to.be('11'); - const editorValue = await getEditorValue(); + const editorValue = await testSubjects.getAttribute('counterEditor', 'value'); expect(editorValue).to.be('11'); // If changing a param from within the vis it should immediately apply and not bring editor in an unchanged state const isApplyEnabled = await PageObjects.visEditor.isApplyEnabled(); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx new file mode 100644 index 0000000000000..938962cc9dd18 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TraceAPIResponse } from '../../../../../../../../../plugins/apm/server/lib/traces/get_trace'; +import { WaterfallContainer } from './index'; +import { + location, + urlParams, + simpleTrace, + traceWithErrors, + traceChildStartBeforeParent +} from './waterfallContainer.stories.data'; +import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; + +storiesOf('app/TransactionDetails/Waterfall', module).add( + 'simple', + () => { + const waterfall = getWaterfall( + simpleTrace as TraceAPIResponse, + '975c8d5bfd1dd20b' + ); + return ( + + ); + }, + { info: { source: false } } +); + +storiesOf('app/TransactionDetails/Waterfall', module).add( + 'with errors', + () => { + const waterfall = getWaterfall( + (traceWithErrors as unknown) as TraceAPIResponse, + '975c8d5bfd1dd20b' + ); + return ( + + ); + }, + { info: { source: false } } +); + +storiesOf('app/TransactionDetails/Waterfall', module).add( + 'child starts before parent', + () => { + const waterfall = getWaterfall( + traceChildStartBeforeParent as TraceAPIResponse, + '975c8d5bfd1dd20b' + ); + return ( + + ); + }, + { info: { source: false } } +); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts new file mode 100644 index 0000000000000..835183e73b298 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts @@ -0,0 +1,1647 @@ +/* + * 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 { Location } from 'history'; +import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; + +export const location = { + pathname: '/services/opbeans-go/transactions/view', + search: + '?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=service.name%253A%2520%2522opbeans-java%2522%2520or%2520service.name%2520%253A%2520%2522opbeans-go%2522&traceId=513d33fafe99bbe6134749310c9b5322&transactionId=975c8d5bfd1dd20b&transactionName=GET%20%2Fapi%2Forders&transactionType=request', + hash: '' +} as Location; + +export const urlParams = { + start: '2020-03-22T15:16:38.742Z', + end: '2020-03-23T15:16:38.742Z', + rangeFrom: 'now-24h', + rangeTo: 'now', + refreshPaused: true, + refreshInterval: 0, + page: 0, + transactionId: '975c8d5bfd1dd20b', + traceId: '513d33fafe99bbe6134749310c9b5322', + kuery: 'service.name: "opbeans-java" or service.name : "opbeans-go"', + transactionName: 'GET /api/orders', + transactionType: 'request', + processorEvent: 'transaction', + serviceName: 'opbeans-go' +} as IUrlParams; + +export const simpleTrace = { + trace: { + items: [ + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT' + }, + internal: { + sampler: { + value: 46 + } + }, + source: { + ip: '172.19.0.13' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: '172.19.0.9', + full: 'http://172.19.0.9:3000/api/orders' + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + type: 'apm-server', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0' + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2' + }, + language: { + name: 'Java', + version: '10.0.2' + }, + version: 'None' + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux' + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64' + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.19.0.9:3000'], + 'Accept-Encoding': ['gzip, deflate'] + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.19.0.13' + }, + body: { + original: '[REDACTED]' + } + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'] + }, + status_code: 200, + finished: true, + headers_sent: true + }, + version: '1.1' + }, + client: { + ip: '172.19.0.13' + }, + transaction: { + duration: { + us: 18842 + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: '49809ad3c26adf74', + span_count: { + dropped: 0, + started: 1 + }, + type: 'request', + sampled: true + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other' + } + }, + timestamp: { + us: 1584975868785000 + } + }, + { + parent: { + id: 'fc107f7b556eb49b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + framework: { + name: 'gin', + version: 'v1.4.0' + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + duration: { + us: 16597 + }, + result: 'HTTP 2xx', + name: 'GET /api/orders', + id: '975c8d5bfd1dd20b', + span_count: { + dropped: 0, + started: 1 + }, + type: 'request', + sampled: true + }, + timestamp: { + us: 1584975868787052 + } + }, + { + parent: { + id: 'daae24d83c269918' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + timestamp: { + us: 1584975868788603 + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders' + }, + '@timestamp': '2020-03-23T15:04:28.788Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13' + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10' + }, + language: { + name: 'python', + version: '3.6.10' + }, + version: 'None' + }, + transaction: { + result: 'HTTP 2xx', + duration: { + us: 14648 + }, + name: 'GET opbeans.views.orders', + span_count: { + dropped: 0, + started: 1 + }, + id: '6fb0ff7365b87298', + type: 'request', + sampled: true + } + }, + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + parent: { + id: '49809ad3c26adf74' + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT' + }, + internal: { + sampler: { + value: 44 + } + }, + destination: { + address: 'opbeans-go', + port: 3000 + }, + processor: { + name: 'transaction', + event: 'span' + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + type: 'apm-server', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0' + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2' + }, + language: { + name: 'Java', + version: '10.0.2' + }, + version: 'None' + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux' + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64' + }, + connection: { + hash: + "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}" + }, + transaction: { + id: '49809ad3c26adf74' + }, + timestamp: { + us: 1584975868785273 + }, + span: { + duration: { + us: 17530 + }, + subtype: 'http', + name: 'GET opbeans-go', + destination: { + service: { + resource: 'opbeans-go:3000', + name: 'http://opbeans-go:3000', + type: 'external' + } + }, + http: { + response: { + status_code: 200 + }, + url: { + original: 'http://opbeans-go:3000/api/orders' + } + }, + id: 'fc107f7b556eb49b', + type: 'external' + } + }, + { + parent: { + id: '975c8d5bfd1dd20b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + processor: { + name: 'transaction', + event: 'span' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + id: '975c8d5bfd1dd20b' + }, + timestamp: { + us: 1584975868787174 + }, + span: { + duration: { + us: 16250 + }, + subtype: 'http', + destination: { + service: { + resource: 'opbeans-python:3000', + name: 'http://opbeans-python:3000', + type: 'external' + } + }, + name: 'GET opbeans-python:3000', + http: { + response: { + status_code: 200 + }, + url: { + original: 'http://opbeans-python:3000/api/orders' + } + }, + id: 'daae24d83c269918', + type: 'external' + } + }, + { + container: { + id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + parent: { + id: '6fb0ff7365b87298' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + processor: { + name: 'transaction', + event: 'span' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.790Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13' + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10' + }, + language: { + name: 'python', + version: '3.6.10' + }, + version: 'None' + }, + transaction: { + id: '6fb0ff7365b87298' + }, + timestamp: { + us: 1584975868790080 + }, + span: { + duration: { + us: 2519 + }, + subtype: 'postgresql', + name: 'SELECT FROM opbeans_order', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db' + } + }, + action: 'query', + id: 'c9407abb4d08ead1', + type: 'db', + sync: true, + db: { + statement: + 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', + type: 'sql' + } + } + } + ], + exceedsMax: false, + errorDocs: [] + }, + errorsPerTransaction: {} +}; + +export const traceWithErrors = { + trace: { + items: [ + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT' + }, + internal: { + sampler: { + value: 46 + } + }, + source: { + ip: '172.19.0.13' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: '172.19.0.9', + full: 'http://172.19.0.9:3000/api/orders' + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + type: 'apm-server', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0' + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2' + }, + language: { + name: 'Java', + version: '10.0.2' + }, + version: 'None' + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux' + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64' + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.19.0.9:3000'], + 'Accept-Encoding': ['gzip, deflate'] + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.19.0.13' + }, + body: { + original: '[REDACTED]' + } + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'] + }, + status_code: 200, + finished: true, + headers_sent: true + }, + version: '1.1' + }, + client: { + ip: '172.19.0.13' + }, + transaction: { + duration: { + us: 18842 + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: '49809ad3c26adf74', + span_count: { + dropped: 0, + started: 1 + }, + type: 'request', + sampled: true + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other' + } + }, + timestamp: { + us: 1584975868785000 + } + }, + { + parent: { + id: 'fc107f7b556eb49b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + framework: { + name: 'gin', + version: 'v1.4.0' + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + duration: { + us: 16597 + }, + result: 'HTTP 2xx', + name: 'GET /api/orders', + id: '975c8d5bfd1dd20b', + span_count: { + dropped: 0, + started: 1 + }, + type: 'request', + sampled: true + }, + timestamp: { + us: 1584975868787052 + } + }, + { + parent: { + id: 'daae24d83c269918' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + timestamp: { + us: 1584975868788603 + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders' + }, + '@timestamp': '2020-03-23T15:04:28.788Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13' + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10' + }, + language: { + name: 'python', + version: '3.6.10' + }, + version: 'None' + }, + transaction: { + result: 'HTTP 2xx', + duration: { + us: 14648 + }, + name: 'GET opbeans.views.orders', + span_count: { + dropped: 0, + started: 1 + }, + id: '6fb0ff7365b87298', + type: 'request', + sampled: true + } + }, + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + parent: { + id: '49809ad3c26adf74' + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT' + }, + internal: { + sampler: { + value: 44 + } + }, + destination: { + address: 'opbeans-go', + port: 3000 + }, + processor: { + name: 'transaction', + event: 'span' + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + type: 'apm-server', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0' + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2' + }, + language: { + name: 'Java', + version: '10.0.2' + }, + version: 'None' + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux' + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64' + }, + connection: { + hash: + "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}" + }, + transaction: { + id: '49809ad3c26adf74' + }, + timestamp: { + us: 1584975868785273 + }, + span: { + duration: { + us: 17530 + }, + subtype: 'http', + name: 'GET opbeans-go', + destination: { + service: { + resource: 'opbeans-go:3000', + name: 'http://opbeans-go:3000', + type: 'external' + } + }, + http: { + response: { + status_code: 200 + }, + url: { + original: 'http://opbeans-go:3000/api/orders' + } + }, + id: 'fc107f7b556eb49b', + type: 'external' + } + }, + { + parent: { + id: '975c8d5bfd1dd20b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + processor: { + name: 'transaction', + event: 'span' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + id: '975c8d5bfd1dd20b' + }, + timestamp: { + us: 1584975868787174 + }, + span: { + duration: { + us: 16250 + }, + subtype: 'http', + destination: { + service: { + resource: 'opbeans-python:3000', + name: 'http://opbeans-python:3000', + type: 'external' + } + }, + name: 'GET opbeans-python:3000', + http: { + response: { + status_code: 200 + }, + url: { + original: 'http://opbeans-python:3000/api/orders' + } + }, + id: 'daae24d83c269918', + type: 'external' + } + }, + { + container: { + id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + parent: { + id: '6fb0ff7365b87298' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + processor: { + name: 'transaction', + event: 'span' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.790Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13' + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10' + }, + language: { + name: 'python', + version: '3.6.10' + }, + version: 'None' + }, + transaction: { + id: '6fb0ff7365b87298' + }, + timestamp: { + us: 1584975868790080 + }, + span: { + duration: { + us: 2519 + }, + subtype: 'postgresql', + name: 'SELECT FROM opbeans_order', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db' + } + }, + action: 'query', + id: 'c9407abb4d08ead1', + type: 'db', + sync: true, + db: { + statement: + 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', + type: 'sql' + } + } + } + ], + exceedsMax: false, + errorDocs: [ + { + parent: { + id: '975c8d5bfd1dd20b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + error: { + culprit: 'logrusMiddleware', + log: { + level: 'error', + message: 'GET //api/products (502)' + }, + id: '1f3cb98206b5c54225cb7c8908a658da', + grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a' + }, + processor: { + name: 'error', + event: 'error' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T16:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + id: '975c8d5bfd1dd20b', + sampled: false + }, + timestamp: { + us: 1584975868787052 + } + }, + { + parent: { + id: '6fb0ff7365b87298' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + error: { + culprit: 'logrusMiddleware', + log: { + level: 'error', + message: 'GET //api/products (502)' + }, + id: '1f3cb98206b5c54225cb7c8908a658d2', + grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a' + }, + processor: { + name: 'error', + event: 'error' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T16:04:28.790Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + name: 'opbeans-python', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + id: '6fb0ff7365b87298', + sampled: false + }, + timestamp: { + us: 1584975868790000 + } + } + ] + }, + errorsPerTransaction: { + '975c8d5bfd1dd20b': 1, + '6fb0ff7365b87298': 1 + } +}; + +export const traceChildStartBeforeParent = { + trace: { + items: [ + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT' + }, + internal: { + sampler: { + value: 46 + } + }, + source: { + ip: '172.19.0.13' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: '172.19.0.9', + full: 'http://172.19.0.9:3000/api/orders' + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + type: 'apm-server', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0' + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2' + }, + language: { + name: 'Java', + version: '10.0.2' + }, + version: 'None' + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux' + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64' + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.19.0.9:3000'], + 'Accept-Encoding': ['gzip, deflate'] + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.19.0.13' + }, + body: { + original: '[REDACTED]' + } + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'] + }, + status_code: 200, + finished: true, + headers_sent: true + }, + version: '1.1' + }, + client: { + ip: '172.19.0.13' + }, + transaction: { + duration: { + us: 18842 + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: '49809ad3c26adf74', + span_count: { + dropped: 0, + started: 1 + }, + type: 'request', + sampled: true + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other' + } + }, + timestamp: { + us: 1584975868785000 + } + }, + { + parent: { + id: 'fc107f7b556eb49b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + framework: { + name: 'gin', + version: 'v1.4.0' + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + duration: { + us: 16597 + }, + result: 'HTTP 2xx', + name: 'GET /api/orders', + id: '975c8d5bfd1dd20b', + span_count: { + dropped: 0, + started: 1 + }, + type: 'request', + sampled: true + }, + timestamp: { + us: 1584975868787052 + } + }, + { + parent: { + id: 'daae24d83c269918' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + timestamp: { + us: 1584975868780000 + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders' + }, + '@timestamp': '2020-03-23T15:04:28.788Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13' + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10' + }, + language: { + name: 'python', + version: '3.6.10' + }, + version: 'None' + }, + transaction: { + result: 'HTTP 2xx', + duration: { + us: 1464 + }, + name: 'I started before my parent 😰', + span_count: { + dropped: 0, + started: 1 + }, + id: '6fb0ff7365b87298', + type: 'request', + sampled: true + } + }, + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + parent: { + id: '49809ad3c26adf74' + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT' + }, + internal: { + sampler: { + value: 44 + } + }, + destination: { + address: 'opbeans-go', + port: 3000 + }, + processor: { + name: 'transaction', + event: 'span' + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + type: 'apm-server', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0' + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2' + }, + language: { + name: 'Java', + version: '10.0.2' + }, + version: 'None' + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux' + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64' + }, + connection: { + hash: + "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}" + }, + transaction: { + id: '49809ad3c26adf74' + }, + timestamp: { + us: 1584975868785273 + }, + span: { + duration: { + us: 17530 + }, + subtype: 'http', + name: 'GET opbeans-go', + destination: { + service: { + resource: 'opbeans-go:3000', + name: 'http://opbeans-go:3000', + type: 'external' + } + }, + http: { + response: { + status_code: 200 + }, + url: { + original: 'http://opbeans-go:3000/api/orders' + } + }, + id: 'fc107f7b556eb49b', + type: 'external' + } + }, + { + parent: { + id: '975c8d5bfd1dd20b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + processor: { + name: 'transaction', + event: 'span' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + id: '975c8d5bfd1dd20b' + }, + timestamp: { + us: 1584975868787174 + }, + span: { + duration: { + us: 16250 + }, + subtype: 'http', + destination: { + service: { + resource: 'opbeans-python:3000', + name: 'http://opbeans-python:3000', + type: 'external' + } + }, + name: 'I am his 👇🏻 parent 😡', + http: { + response: { + status_code: 200 + }, + url: { + original: 'http://opbeans-python:3000/api/orders' + } + }, + id: 'daae24d83c269918', + type: 'external' + } + }, + { + container: { + id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + parent: { + id: '6fb0ff7365b87298' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + processor: { + name: 'transaction', + event: 'span' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.790Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13' + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10' + }, + language: { + name: 'python', + version: '3.6.10' + }, + version: 'None' + }, + transaction: { + id: '6fb0ff7365b87298' + }, + timestamp: { + us: 1584975868781000 + }, + span: { + duration: { + us: 2519 + }, + subtype: 'postgresql', + name: 'I am using my parents skew 😇', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db' + } + }, + action: 'query', + id: 'c9407abb4d08ead1', + type: 'db', + sync: true, + db: { + statement: + 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', + type: 'sql' + } + } + } + ], + exceedsMax: false, + errorDocs: [] + }, + errorsPerTransaction: {} +}; diff --git a/x-pack/legacy/plugins/graph/index.ts b/x-pack/legacy/plugins/graph/index.ts index 53d32a836cfa1..5c7f8fa46c18b 100644 --- a/x-pack/legacy/plugins/graph/index.ts +++ b/x-pack/legacy/plugins/graph/index.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; - // @ts-ignore import migrations from './migrations'; import mappings from './mappings.json'; @@ -30,40 +28,5 @@ export const graph: LegacyPluginInitializer = kibana => { .default('configAndData'), }).default(); }, - - init(server) { - server.plugins.xpack_main.registerFeature({ - id: 'graph', - name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', { - defaultMessage: 'Graph', - }), - order: 1200, - icon: 'graphApp', - navLinkId: 'graph', - app: ['graph', 'kibana'], - catalogue: ['graph'], - validLicenses: ['platinum', 'enterprise', 'trial'], - privileges: { - all: { - app: ['graph', 'kibana'], - catalogue: ['graph'], - savedObject: { - all: ['graph-workspace'], - read: ['index-pattern'], - }, - ui: ['save', 'delete'], - }, - read: { - app: ['graph', 'kibana'], - catalogue: ['graph'], - savedObject: { - all: [], - read: ['index-pattern', 'graph-workspace'], - }, - ui: [], - }, - }, - }); - }, }); }; diff --git a/x-pack/legacy/plugins/lens/public/helpers/index.ts b/x-pack/legacy/plugins/lens/public/helpers/index.ts new file mode 100644 index 0000000000000..f464b5dcc97a3 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/helpers/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 { addEmbeddableToDashboardUrl, getUrlVars } from './url_helper'; diff --git a/x-pack/legacy/plugins/lens/public/helpers/url_helper.test.ts b/x-pack/legacy/plugins/lens/public/helpers/url_helper.test.ts new file mode 100644 index 0000000000000..9c59c9a96d00f --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/helpers/url_helper.test.ts @@ -0,0 +1,49 @@ +/* + * 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. + */ + +jest.mock('../legacy_imports', () => ({ + DashboardConstants: { + ADD_EMBEDDABLE_ID: 'addEmbeddableId', + ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', + }, +})); + +import { addEmbeddableToDashboardUrl, getUrlVars } from './url_helper'; + +describe('Dashboard URL Helper', () => { + it('addEmbeddableToDashboardUrl', () => { + const id = '123eb456cd'; + const urlVars = { + x: '1', + y: '2', + z: '3', + }; + const url = + "/pep/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!())"; + expect(addEmbeddableToDashboardUrl(url, id, urlVars)).toEqual( + `/pep/app/kibana#/dashboard?_a=%28description%3A%27%27%2Cfilters%3A%21%28%29%29&_g=%28refreshInterval%3A%28pause%3A%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3Anow-15m%2Cto%3Anow%29%29&addEmbeddableId=${id}&addEmbeddableType=lens&x=1&y=2&z=3` + ); + }); + + it('getUrlVars', () => { + let url = + "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; + expect(getUrlVars(url)).toEqual({ + _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))', + _a: "(description:'',filters:!()", + }); + url = 'http://mybusiness.mydomain.com/app/kibana#/dashboard?x=y&y=z'; + expect(getUrlVars(url)).toEqual({ + x: 'y', + y: 'z', + }); + url = 'http://localhost:5601/app/kibana#/dashboard/777182'; + expect(getUrlVars(url)).toEqual({}); + url = + 'http://localhost:5601/app/kibana#/dashboard/777182?title=Some%20Dashboard%20With%20Spaces'; + expect(getUrlVars(url)).toEqual({ title: 'Some Dashboard With Spaces' }); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/helpers/url_helper.ts b/x-pack/legacy/plugins/lens/public/helpers/url_helper.ts new file mode 100644 index 0000000000000..fca44195b98c4 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/helpers/url_helper.ts @@ -0,0 +1,45 @@ +/* + * 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 { parseUrl, stringify } from 'query-string'; +import { DashboardConstants } from '../legacy_imports'; + +type UrlVars = Record; + +/** + * Return query params from URL + * @param url given url + */ +export function getUrlVars(url: string): Record { + const vars: UrlVars = {}; + for (const [, key, value] of url.matchAll(/[?&]+([^=&]+)=([^&]*)/gi)) { + vars[key] = decodeURIComponent(value); + } + return vars; +} + +/** * + * Returns dashboard URL with added embeddableType and embeddableId query params + * eg. + * input: url: /lol/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)), embeddableId: 12345 + * output: /lol/app/kibana#/dashboard?addEmbeddableType=lens&addEmbeddableId=12345&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)) + * @param url dasbhoard absolute url + * @param embeddableId id of the saved visualization + * @param urlVars url query params + */ +export function addEmbeddableToDashboardUrl(url: string, embeddableId: string, urlVars: UrlVars) { + const dashboardParsedUrl = parseUrl(url); + const keys = Object.keys(urlVars).sort(); + + keys.forEach(key => { + dashboardParsedUrl.query[key] = urlVars[key]; + }); + dashboardParsedUrl.query[DashboardConstants.ADD_EMBEDDABLE_TYPE] = 'lens'; + dashboardParsedUrl.query[DashboardConstants.ADD_EMBEDDABLE_ID] = embeddableId; + const query = stringify(dashboardParsedUrl.query); + + return `${dashboardParsedUrl.url}?${query}`; +} diff --git a/x-pack/legacy/plugins/lens/public/legacy_imports.ts b/x-pack/legacy/plugins/lens/public/legacy_imports.ts index 5c5afc1a87df0..857443ae0fa1c 100644 --- a/x-pack/legacy/plugins/lens/public/legacy_imports.ts +++ b/x-pack/legacy/plugins/lens/public/legacy_imports.ts @@ -7,3 +7,4 @@ import { npSetup } from 'ui/new_platform'; export const { visualizations } = npSetup.plugins; export { VisualizationsSetup } from '../../../../../src/plugins/visualizations/public'; +export { DashboardConstants } from '../../../../../src/legacy/core_plugins/kibana/public/dashboard'; diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx index fad1371199e6a..45817fdc3c05f 100644 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -8,10 +8,14 @@ import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { render, unmountComponentAtNode } from 'react-dom'; -import { AppMountParameters, CoreSetup, CoreStart } from 'src/core/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; import rison, { RisonObject, RisonValue } from 'rison-node'; import { isObject } from 'lodash'; + +import { AppMountParameters, CoreSetup, CoreStart } from 'src/core/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; +import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; +import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; +import { KibanaLegacySetup } from 'src/plugins/kibana_legacy/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { EditorFrameService } from './editor_frame_service'; import { IndexPatternDatasource } from './indexpattern_datasource'; @@ -19,7 +23,6 @@ import { addHelpMenuToAppChrome } from './help_menu_util'; import { SavedObjectIndexStore } from './persistence'; import { XyVisualization } from './xy_visualization'; import { MetricVisualization } from './metric_visualization'; -import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; import { DatatableVisualization } from './datatable_visualization'; import { App } from './app_plugin'; import { @@ -30,17 +33,12 @@ import { } from './lens_ui_telemetry'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; -import { KibanaLegacySetup } from '../../../../../src/plugins/kibana_legacy/public'; import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../../../plugins/lens/common'; -import { - addEmbeddableToDashboardUrl, - getUrlVars, - getLensUrlFromDashboardAbsoluteUrl, -} from '../../../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper'; -import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { addEmbeddableToDashboardUrl, getUrlVars } from './helpers'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; -import { VisualizationsSetup } from './legacy_imports'; +import { VisualizationsSetup, DashboardConstants } from './legacy_imports'; + export interface LensPluginSetupDependencies { kibanaLegacy: KibanaLegacySetup; expressions: ExpressionsSetup; @@ -144,40 +142,24 @@ export class LensPlugin { routeProps.history.push(`/lens/edit/${id}`); } else if (addToDashboardMode && id) { routeProps.history.push(`/lens/edit/${id}`); - const url = coreStart.chrome.navLinks.get('kibana:dashboard'); - if (!url) { + const lastDashboardLink = coreStart.chrome.navLinks.get('kibana:dashboard'); + if (!lastDashboardLink || !lastDashboardLink.url) { throw new Error('Cannot get last dashboard url'); } - const lastDashboardAbsoluteUrl = url.url; - const basePath = coreStart.http.basePath.get(); - const lensUrl = getLensUrlFromDashboardAbsoluteUrl( - lastDashboardAbsoluteUrl, - basePath, - id - ); - if (!lastDashboardAbsoluteUrl || !lensUrl) { - throw new Error('Cannot get last dashboard url'); - } - window.history.pushState({}, '', lensUrl); - const urlVars = getUrlVars(lastDashboardAbsoluteUrl); + const urlVars = getUrlVars(lastDashboardLink.url); updateUrlTime(urlVars); // we need to pass in timerange in query params directly - const dashboardParsedUrl = addEmbeddableToDashboardUrl( - lastDashboardAbsoluteUrl, - basePath, - id, - urlVars - ); - if (!dashboardParsedUrl) { - throw new Error('Problem parsing dashboard url'); - } - window.history.pushState({}, '', dashboardParsedUrl); + const dashboardUrl = addEmbeddableToDashboardUrl(lastDashboardLink.url, id, urlVars); + window.history.pushState({}, '', dashboardUrl); } }; const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { trackUiEvent('loaded'); const addToDashboardMode = - !!routeProps.location.search && routeProps.location.search.includes('addToDashboard'); + !!routeProps.location.search && + routeProps.location.search.includes( + DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM + ); return ( { .invoke('text') .should('eql', expectedTags); + cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE) + .eq(INVESTIGATION_NOTES_TOGGLE) + .click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES) + .invoke('text') + .should('eql', INVESTIGATION_NOTES_MARKDOWN); + cy.get(DEFINITION_INDEX_PATTERNS).then(patterns => { cy.wrap(patterns).each((pattern, index) => { cy.wrap(pattern) diff --git a/x-pack/legacy/plugins/siem/cypress/objects/rule.ts b/x-pack/legacy/plugins/siem/cypress/objects/rule.ts index a3c648c9cc934..37c325c3b8030 100644 --- a/x-pack/legacy/plugins/siem/cypress/objects/rule.ts +++ b/x-pack/legacy/plugins/siem/cypress/objects/rule.ts @@ -22,6 +22,7 @@ export interface CustomRule { referenceUrls: string[]; falsePositivesExamples: string[]; mitre: Mitre[]; + note: string; } export interface MachineLearningRule { @@ -36,6 +37,7 @@ export interface MachineLearningRule { referenceUrls: string[]; falsePositivesExamples: string[]; mitre: Mitre[]; + note: string; } const mitre1: Mitre = { @@ -58,6 +60,7 @@ export const newRule: CustomRule = { referenceUrls: ['https://www.google.com/', 'https://elastic.co/'], falsePositivesExamples: ['False1', 'False2'], mitre: [mitre1, mitre2], + note: '# test markdown', }; export const machineLearningRule: MachineLearningRule = { @@ -71,4 +74,5 @@ export const machineLearningRule: MachineLearningRule = { referenceUrls: ['https://elastic.co/'], falsePositivesExamples: ['False1'], mitre: [mitre1], + note: '# test markdown', }; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/create_new_rule.ts b/x-pack/legacy/plugins/siem/cypress/screens/create_new_rule.ts index e603e2ee5158e..db9866cdf7f63 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/create_new_rule.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/create_new_rule.ts @@ -24,7 +24,8 @@ export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]'; export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; -export const SCHEDULE_CONTINUE_BUTTON = '[data-test-subj="schedule-continue"]'; +export const INVESTIGATION_NOTES_TEXTAREA = + '[data-test-subj="detectionEngineStepAboutRuleNote"] textarea'; export const FALSE_POSITIVES_INPUT = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] input'; @@ -53,6 +54,8 @@ export const RULE_DESCRIPTION_INPUT = export const RULE_NAME_INPUT = '[data-test-subj="detectionEngineStepAboutRuleName"] [data-test-subj="input"]'; +export const SCHEDULE_CONTINUE_BUTTON = '[data-test-subj="schedule-continue"]'; + export const SEVERITY_DROPDOWN = '[data-test-subj="detectionEngineStepAboutRuleSeverity"] [data-test-subj="select"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts index fc9e4c56dd824..ec57e142125da 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts @@ -6,6 +6,8 @@ export const ABOUT_FALSE_POSITIVES = 3; +export const ABOUT_INVESTIGATION_NOTES = '[data-test-subj="stepAboutDetailsNoteContent"]'; + export const ABOUT_MITRE = 4; export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]'; @@ -32,10 +34,16 @@ export const DEFINITION_INDEX_PATTERNS = export const DEFINITION_STEP = '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description'; +export const INVESTIGATION_NOTES_MARKDOWN = 'test markdown'; + +export const INVESTIGATION_NOTES_TOGGLE = 1; + export const MACHINE_LEARNING_JOB_ID = '[data-test-subj="machineLearningJobId"]'; export const MACHINE_LEARNING_JOB_STATUS = '[data-test-subj="machineLearningJobStatus" ]'; +export const RULE_ABOUT_DETAILS_HEADER_TOGGLE = '[data-test-subj="stepAboutDetailsToggle"]'; + export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]'; export const RULE_TYPE = 0; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/create_new_rule.ts b/x-pack/legacy/plugins/siem/cypress/tasks/create_new_rule.ts index 59ed156bf56b1..a20ad372a689c 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/create_new_rule.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/create_new_rule.ts @@ -14,6 +14,7 @@ import { CUSTOM_QUERY_INPUT, DEFINE_CONTINUE_BUTTON, FALSE_POSITIVES_INPUT, + INVESTIGATION_NOTES_TEXTAREA, MACHINE_LEARNING_DROPDOWN, MACHINE_LEARNING_LIST, MACHINE_LEARNING_TYPE, @@ -82,6 +83,8 @@ export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule) cy.get(MITRE_BTN).click({ force: true }); }); + cy.get(INVESTIGATION_NOTES_TEXTAREA).type(rule.note, { force: true }); + cy.get(ABOUT_CONTINUE_BTN) .should('exist') .click({ force: true }); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts index 60ebd2578b7c0..a779d579bf4d1 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts @@ -4,16 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ import { cloneDeep, omit } from 'lodash/fp'; +import { Dispatch } from 'redux'; -import { mockTimelineResults } from '../../mock/timeline_results'; +import { + mockTimelineResults, + mockTimelineResult, + mockTimelineModel, +} from '../../mock/timeline_results'; import { timelineDefaults } from '../../store/timeline/defaults'; +import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../store/inputs/actions'; +import { + setKqlFilterQueryDraft as dispatchSetKqlFilterQueryDraft, + applyKqlFilterQuery as dispatchApplyKqlFilterQuery, + addTimeline as dispatchAddTimeline, + addNote as dispatchAddGlobalTimelineNote, +} from '../../store/timeline/actions'; +import { + addNotes as dispatchAddNotes, + updateNote as dispatchUpdateNote, +} from '../../store/app/actions'; import { defaultTimelineToTimelineModel, getNotesCount, getPinnedEventCount, isUntitled, + omitTypenameInTimeline, + dispatchUpdateTimeline, } from './helpers'; -import { OpenTimelineResult } from './types'; +import { OpenTimelineResult, DispatchUpdateTimeline } from './types'; +import { KueryFilterQueryKind } from '../../store/model'; +import { Note } from '../../lib/note'; +import moment from 'moment'; +import sinon from 'sinon'; + +jest.mock('../../store/inputs/actions'); +jest.mock('../../store/timeline/actions'); +jest.mock('../../store/app/actions'); +jest.mock('uuid', () => { + return { + v1: jest.fn(() => 'uuid.v1()'), + v4: jest.fn(() => 'uuid.v4()'), + }; +}); describe('helpers', () => { let mockResults: OpenTimelineResult[]; @@ -620,4 +652,229 @@ describe('helpers', () => { }); }); }); + + describe('omitTypenameInTimeline', () => { + test('it does not modify the passed in timeline if no __typename exists', () => { + const result = omitTypenameInTimeline(mockTimelineResult); + + expect(result).toEqual(mockTimelineResult); + }); + + test('it returns timeline with __typename removed when it exists', () => { + const mockTimeline = { + ...mockTimelineResult, + __typename: 'something, something', + }; + const result = omitTypenameInTimeline(mockTimeline); + const expectedTimeline = { + ...mockTimeline, + __typename: undefined, + }; + + expect(result).toEqual(expectedTimeline); + }); + }); + + describe('dispatchUpdateTimeline', () => { + const dispatch = jest.fn() as Dispatch; + const anchor = '2020-03-27T20:34:51.337Z'; + const unix = moment(anchor).valueOf(); + let clock: sinon.SinonFakeTimers; + let timelineDispatch: DispatchUpdateTimeline; + + beforeEach(() => { + jest.clearAllMocks(); + + clock = sinon.useFakeTimers(unix); + timelineDispatch = dispatchUpdateTimeline(dispatch); + }); + + afterEach(function() { + clock.restore(); + }); + + test('it invokes date range picker dispatch', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + })(); + + expect(dispatchSetTimelineRangeDatePicker).toHaveBeenCalledWith({ + from: 1585233356356, + to: 1585233716356, + }); + }); + + test('it invokes add timeline dispatch', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + })(); + + expect(dispatchAddTimeline).toHaveBeenCalledWith({ + id: 'timeline-1', + timeline: mockTimelineModel, + }); + }); + + test('it does not invoke kql filter query dispatches if timeline.kqlQuery.filterQuery is null', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + })(); + + expect(dispatchSetKqlFilterQueryDraft).not.toHaveBeenCalled(); + expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); + }); + + test('it does not invoke notes dispatch if duplicate is true', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + })(); + + expect(dispatchAddNotes).not.toHaveBeenCalled(); + }); + + test('it does not invoke kql filter query dispatches if timeline.kqlQuery.kuery is null', () => { + const mockTimeline = { + ...mockTimelineModel, + kqlQuery: { + filterQuery: { + kuery: null, + serializedQuery: 'some-serialized-query', + }, + filterQueryDraft: null, + }, + }; + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimeline, + })(); + + expect(dispatchSetKqlFilterQueryDraft).not.toHaveBeenCalled(); + expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); + }); + + test('it invokes kql filter query dispatches if timeline.kqlQuery.filterQuery.kuery is not null', () => { + const mockTimeline = { + ...mockTimelineModel, + kqlQuery: { + filterQuery: { + kuery: { expression: 'expression', kind: 'kuery' as KueryFilterQueryKind }, + serializedQuery: 'some-serialized-query', + }, + filterQueryDraft: null, + }, + }; + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimeline, + })(); + + expect(dispatchSetKqlFilterQueryDraft).toHaveBeenCalledWith({ + id: 'timeline-1', + filterQueryDraft: { + kind: 'kuery', + expression: 'expression', + }, + }); + expect(dispatchApplyKqlFilterQuery).toHaveBeenCalledWith({ + id: 'timeline-1', + filterQuery: { + kuery: { + kind: 'kuery', + expression: 'expression', + }, + serializedQuery: 'some-serialized-query', + }, + }); + }); + + test('it invokes dispatchAddNotes if duplicate is false', () => { + timelineDispatch({ + duplicate: false, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [ + { + created: 1585233356356, + updated: 1585233356356, + noteId: 'note-id', + note: 'I am a note', + }, + ], + timeline: mockTimelineModel, + })(); + + expect(dispatchAddGlobalTimelineNote).not.toHaveBeenCalled(); + expect(dispatchUpdateNote).not.toHaveBeenCalled(); + expect(dispatchAddNotes).toHaveBeenCalledWith({ + notes: [ + { + created: new Date('2020-03-26T14:35:56.356Z'), + id: 'note-id', + lastEdit: new Date('2020-03-26T14:35:56.356Z'), + note: 'I am a note', + user: 'unknown', + saveObjectId: 'note-id', + version: undefined, + }, + ], + }); + }); + + test('it invokes dispatch to create a timeline note if duplicate is true and ruleNote exists', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + ruleNote: '# this would be some markdown', + })(); + const expectedNote: Note = { + created: new Date(anchor), + id: 'uuid.v4()', + lastEdit: null, + note: '# this would be some markdown', + saveObjectId: null, + user: 'elastic', + version: null, + }; + + expect(dispatchAddNotes).not.toHaveBeenCalled(); + expect(dispatchUpdateNote).toHaveBeenCalledWith({ note: expectedNote }); + expect(dispatchAddGlobalTimelineNote).toHaveBeenLastCalledWith({ + id: 'timeline-1', + noteId: 'uuid.v4()', + }); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts index 4f7d6cd64f1d9..16ba2de872bd1 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts @@ -5,18 +5,23 @@ */ import ApolloClient from 'apollo-client'; -import { getOr, set } from 'lodash/fp'; +import { getOr, set, isEmpty } from 'lodash/fp'; import { Action } from 'typescript-fsa'; +import uuid from 'uuid'; import { Dispatch } from 'redux'; import { oneTimelineQuery } from '../../containers/timeline/one/index.gql_query'; import { TimelineResult, GetOneTimeline, NoteResult } from '../../graphql/types'; -import { addNotes as dispatchAddNotes } from '../../store/app/actions'; +import { + addNotes as dispatchAddNotes, + updateNote as dispatchUpdateNote, +} from '../../store/app/actions'; import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../store/inputs/actions'; import { setKqlFilterQueryDraft as dispatchSetKqlFilterQueryDraft, applyKqlFilterQuery as dispatchApplyKqlFilterQuery, addTimeline as dispatchAddTimeline, + addNote as dispatchAddGlobalTimelineNote, } from '../../store/timeline/actions'; import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model'; @@ -32,6 +37,7 @@ import { import { OpenTimelineResult, UpdateTimeline, DispatchUpdateTimeline } from './types'; import { getTimeRangeSettings } from '../../utils/default_date_settings'; +import { createNote } from '../notes/helpers'; export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline'; @@ -250,6 +256,7 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli notes, timeline, to, + ruleNote, }: UpdateTimeline): (() => void) => () => { dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); dispatch(dispatchAddTimeline({ id, timeline })); @@ -281,6 +288,14 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli }) ); } + + if (duplicate && ruleNote != null && !isEmpty(ruleNote)) { + const getNewNoteId = (): string => uuid.v4(); + const newNote = createNote({ newNote: ruleNote, getNewNoteId }); + dispatch(dispatchUpdateNote({ note: newNote })); + dispatch(dispatchAddGlobalTimelineNote({ noteId: newNote.id, id })); + } + if (!duplicate) { dispatch( dispatchAddNotes({ diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx index 8805037ecc4ca..b0f8963dd501e 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx @@ -70,6 +70,25 @@ describe('#getActionsColumns', () => { expect(wrapper.find('[data-test-subj="open-duplicate"]').exists()).toBe(true); }); + test('it renders only duplicate icon (without heading)', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + actionTimelineToShow: ['duplicate'], + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('[data-test-subj="open-duplicate"]') + .first() + .text() + ).toEqual(''); + }); + test('it does NOT render the duplicate timeline when actionTimelineToShow is NOT including the action duplicate)', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(mockResults), diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx index 8588beed64b79..746503308c833 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx @@ -42,6 +42,7 @@ export const getActionsColumns = ({ timelineId: savedObjectId ?? '', }); }, + type: 'icon', enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null, description: i18n.OPEN_AS_DUPLICATE, 'data-test-subj': 'open-duplicate', diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts index 51c72681c0863..b7cc92ebd183f 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts @@ -173,6 +173,7 @@ export interface UpdateTimeline { notes: NoteResult[] | null | undefined; timeline: TimelineModel; to: number; + ruleNote?: string; } export type DispatchUpdateTimeline = ({ @@ -182,4 +183,5 @@ export type DispatchUpdateTimeline = ({ notes, timeline, to, + ruleNote, }: UpdateTimeline) => () => void; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx index fa474c4d601ad..cf1a4ebec9bb6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiPopover, EuiSelectableOption } from '@elastic/eui'; +import { EuiButtonIcon, EuiPopover, EuiSelectableOption, EuiToolTip } from '@elastic/eui'; import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { useDispatch } from 'react-redux'; @@ -62,13 +62,15 @@ export const InsertTimelinePopoverComponent: React.FC = ({ const insertTimelineButton = useMemo( () => ( - + {i18n.INSERT_TIMELINE}

}> + +
), [handleOpenPopover, isDisabled] ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts index de3e3c8e792fe..101837168350f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts @@ -25,5 +25,5 @@ export const SEARCH_BOX_TIMELINE_PLACEHOLDER = i18n.translate( ); export const INSERT_TIMELINE = i18n.translate('xpack.siem.insert.timeline.insertTimelineButton', { - defaultMessage: 'Insert Timeline…', + defaultMessage: 'Insert timeline link', }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts new file mode 100644 index 0000000000000..dbd618f40155d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts @@ -0,0 +1,13 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export * from '../translations'; + +export const SUCCESS_CONFIGURE = i18n.translate('xpack.siem.case.configure.successSaveToast', { + defaultMessage: 'Saved external connection settings', +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx index b25667f070fdf..6524c40a8e6e4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx @@ -7,8 +7,8 @@ import { useState, useEffect, useCallback } from 'react'; import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api'; -import { useStateToaster, errorToToaster } from '../../../components/toasters'; -import * as i18n from '../translations'; +import { useStateToaster, errorToToaster, displaySuccessToast } from '../../../components/toasters'; +import * as i18n from './translations'; import { ClosureType } from './types'; import { CurrentConfiguration } from '../../../pages/case/components/configure_cases/reducer'; @@ -124,6 +124,8 @@ export const useCaseConfigure = ({ closureType: res.closureType, }); } + + displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); } } catch (error) { if (!didCancel) { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts index 601db373f041e..a453be32480e2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts @@ -10,6 +10,46 @@ export const ERROR_TITLE = i18n.translate('xpack.siem.containers.case.errorTitle defaultMessage: 'Error fetching data', }); +export const ERROR_DELETING = i18n.translate('xpack.siem.containers.case.errorDeletingTitle', { + defaultMessage: 'Error deleting data', +}); + +export const UPDATED_CASE = (caseTitle: string) => + i18n.translate('xpack.siem.containers.case.updatedCase', { + values: { caseTitle }, + defaultMessage: 'Updated "{caseTitle}"', + }); + +export const DELETED_CASES = (totalCases: number, caseTitle?: string) => + i18n.translate('xpack.siem.containers.case.deletedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Deleted {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const CLOSED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.siem.containers.case.closedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Closed {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const REOPENED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.siem.containers.case.reopenedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Reopened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + export const TAG_FETCH_FAILURE = i18n.translate( 'xpack.siem.containers.case.tagFetchFailDescription', { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index bb215d6ac271c..cb3df78257dc1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -114,3 +114,8 @@ export interface ActionLicense { enabledInConfig: boolean; enabledInLicense: boolean; } + +export interface DeleteCase { + id: string; + title?: string; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx index f1129bae9f537..7d040c49f1971 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx @@ -5,7 +5,7 @@ */ import { useCallback, useReducer } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { patchCasesStatus } from './api'; import { BulkUpdateStatus, Case } from './types'; @@ -71,9 +71,22 @@ export const useUpdateCases = (): UseUpdateCase => { const patchData = async () => { try { dispatch({ type: 'FETCH_INIT' }); - await patchCasesStatus(cases, abortCtrl.signal); + const patchResponse = await patchCasesStatus(cases, abortCtrl.signal); if (!cancel) { + const resultCount = Object.keys(patchResponse).length; + const firstTitle = patchResponse[0].title; + dispatch({ type: 'FETCH_SUCCESS', payload: true }); + const messageArgs = { + totalCases: resultCount, + caseTitle: resultCount === 1 ? firstTitle : '', + }; + const message = + resultCount && patchResponse[0].status === 'open' + ? i18n.REOPENED_CASES(messageArgs) + : i18n.CLOSED_CASES(messageArgs); + + displaySuccessToast(message, dispatchToaster); } } catch (error) { if (!cancel) { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx index b44e01d06acaf..07e3786758aeb 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx @@ -5,9 +5,10 @@ */ import { useCallback, useReducer } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { deleteCases } from './api'; +import { DeleteCase } from './types'; interface DeleteState { isDisplayConfirmDeleteModal: boolean; @@ -57,9 +58,10 @@ const dataFetchReducer = (state: DeleteState, action: Action): DeleteState => { return state; } }; + interface UseDeleteCase extends DeleteState { dispatchResetIsDeleted: () => void; - handleOnDeleteConfirm: (caseIds: string[]) => void; + handleOnDeleteConfirm: (caseIds: DeleteCase[]) => void; handleToggleModal: () => void; } @@ -72,21 +74,26 @@ export const useDeleteCases = (): UseDeleteCase => { }); const [, dispatchToaster] = useStateToaster(); - const dispatchDeleteCases = useCallback((caseIds: string[]) => { + const dispatchDeleteCases = useCallback((cases: DeleteCase[]) => { let cancel = false; const abortCtrl = new AbortController(); const deleteData = async () => { try { dispatch({ type: 'FETCH_INIT' }); + const caseIds = cases.map(theCase => theCase.id); await deleteCases(caseIds, abortCtrl.signal); if (!cancel) { dispatch({ type: 'FETCH_SUCCESS', payload: true }); + displaySuccessToast( + i18n.DELETED_CASES(cases.length, cases.length === 1 ? cases[0].title : ''), + dispatchToaster + ); } } catch (error) { if (!cancel) { errorToToaster({ - title: i18n.ERROR_TITLE, + title: i18n.ERROR_DELETING, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 85ad4fd3fc47a..4973deef4d91a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -5,8 +5,8 @@ */ import { useReducer, useCallback } from 'react'; +import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; import { CasePatchRequest } from '../../../../../../plugins/case/common/api'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; import { patchCase } from './api'; import * as i18n from './translations'; @@ -94,6 +94,7 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => updateCase(response[0]); } dispatch({ type: 'FETCH_SUCCESS' }); + displaySuccessToast(i18n.UPDATED_CASE(response[0].title), dispatchToaster); } } catch (error) { if (!cancel) { diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts index c54238c5d8687..53d0b98570bcb 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts @@ -206,6 +206,7 @@ export const timelineQuery = gql` query to filters + note } } suricata { diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 5d43024625d0d..2a9dd8f2aacfe 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -4696,6 +4696,14 @@ "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "note", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index a5d1e3fbcba27..e15c099a007ad 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -1012,6 +1012,8 @@ export interface RuleField { updated_by?: Maybe; version?: Maybe; + + note?: Maybe; } export interface SuricataEcsFields { @@ -4660,6 +4662,8 @@ export namespace GetTimelineQuery { to: Maybe; filters: Maybe; + + note: Maybe; }; export type Suricata = { diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx index d67007399abea..536798ffad41b 100644 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx @@ -181,6 +181,7 @@ const ServiceNowConnectorFields: React.FunctionComponent + + + - column.id !== 'event.action'), + dateRange: { start: 1584539198929, end: 1584539558929 }, + description: 'This is a sample rule description', + eventType: 'all', + filters: [ + { + meta: { + key: 'host.name', + negate: false, + params: '"{"query":"placeholder"}"', + type: 'phrase', + }, + query: '"{"match_phrase":{"host.name":"placeholder"}}"', + }, + ], + kqlMode: 'filter', + title: 'Test rule', + savedQueryId: null, + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + version: '1', +}; + +export const mockTimelineApolloResult = { + data: { + getOneTimeline: mockTimelineResult, + }, + loading: false, + networkStatus: 7, + stale: false, +}; + +export const defaultTimelineProps: CreateTimelineProps = { + from: 1541444305937, + timeline: { + columns: [ + { columnHeaderType: 'not-filtered', id: '@timestamp', width: 190 }, + { columnHeaderType: 'not-filtered', id: 'message', width: 180 }, + { columnHeaderType: 'not-filtered', id: 'event.category', width: 180 }, + { columnHeaderType: 'not-filtered', id: 'event.action', width: 180 }, + { columnHeaderType: 'not-filtered', id: 'host.name', width: 180 }, + { columnHeaderType: 'not-filtered', id: 'source.ip', width: 180 }, + { columnHeaderType: 'not-filtered', id: 'destination.ip', width: 180 }, + { columnHeaderType: 'not-filtered', id: 'user.name', width: 180 }, + ], + dataProviders: [ + { + and: [], + enabled: true, + excluded: false, + id: + 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-1', + kqlQuery: '', + name: '1', + queryMatch: { field: '_id', operator: ':', value: '1' }, + }, + ], + dateRange: { end: 1541444605937, start: 1541444305937 }, + deletedEventIds: [], + description: '', + eventIdToNoteIds: {}, + eventType: 'all', + filters: [], + highlightedDropAndProviderId: '', + historyIds: [], + id: 'timeline-1', + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + isSelectAllChecked: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { kuery: { expression: '', kind: 'kuery' }, serializedQuery: '' }, + filterQueryDraft: { expression: '', kind: 'kuery' }, + }, + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: null, + selectedEventIds: {}, + show: false, + showCheckboxes: false, + showRowRenderers: true, + sort: { columnId: '@timestamp', sortDirection: Direction.desc }, + title: '', + version: null, + width: 1100, + }, + to: 1541444605937, + ruleNote: '# this is some markdown documentation', +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index bdcb87b483851..5f61ccf68fc86 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -202,7 +202,7 @@ describe('AllCases', () => { .last() .simulate('click'); expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual( - useGetCasesMockState.data.cases.map(theCase => theCase.id) + useGetCasesMockState.data.cases.map(({ id }) => ({ id })) ); }); it('Bulk close status update', () => { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 27316ab8427cb..dcfa1712c6ef9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -21,7 +21,7 @@ import styled, { css } from 'styled-components'; import * as i18n from './translations'; import { getCasesColumns } from './columns'; -import { Case, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; +import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; import { useGetCases, UpdateCase } from '../../../../containers/case/use_get_cases'; import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; @@ -107,11 +107,24 @@ export const AllCases = React.memo(() => { isDisplayConfirmDeleteModal, } = useDeleteCases(); - const { dispatchResetIsUpdated, isUpdated, updateBulkStatus } = useUpdateCases(); + // Update case + const { + dispatchResetIsUpdated, + isLoading: isUpdating, + isUpdated, + updateBulkStatus, + } = useUpdateCases(); + const [deleteThisCase, setDeleteThisCase] = useState({ + title: '', + id: '', + }); + const [deleteBulk, setDeleteBulk] = useState([]); const refreshCases = useCallback(() => { refetchCases(filterOptions, queryParams); fetchCasesStatus(); + setSelectedCases([]); + setDeleteBulk([]); }, [filterOptions, queryParams]); useEffect(() => { @@ -124,11 +137,6 @@ export const AllCases = React.memo(() => { dispatchResetIsUpdated(); } }, [isDeleted, isUpdated]); - const [deleteThisCase, setDeleteThisCase] = useState({ - title: '', - id: '', - }); - const [deleteBulk, setDeleteBulk] = useState([]); const confirmDeleteModal = useMemo( () => ( { onCancel={handleToggleModal} onConfirm={handleOnDeleteConfirm.bind( null, - deleteBulk.length > 0 ? deleteBulk : [deleteThisCase.id] + deleteBulk.length > 0 ? deleteBulk : [deleteThisCase] )} /> ), @@ -150,10 +158,20 @@ export const AllCases = React.memo(() => { setDeleteThisCase(deleteCase); }, []); - const toggleBulkDeleteModal = useCallback((deleteCases: string[]) => { - handleToggleModal(); - setDeleteBulk(deleteCases); - }, []); + const toggleBulkDeleteModal = useCallback( + (caseIds: string[]) => { + handleToggleModal(); + if (caseIds.length === 1) { + const singleCase = selectedCases.find(theCase => theCase.id === caseIds[0]); + if (singleCase) { + return setDeleteThisCase({ id: singleCase.id, title: singleCase.title }); + } + } + const convertToDeleteCases: DeleteCase[] = caseIds.map(id => ({ id })); + setDeleteBulk(convertToDeleteCases); + }, + [selectedCases] + ); const handleUpdateCaseStatus = useCallback( (status: string) => { @@ -289,7 +307,7 @@ export const AllCases = React.memo(() => { - {(isCasesLoading || isDeleting) && !isDataEmpty && ( + {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && ( )} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx index 1be0d6a3b5fcc..49f5f44cba271 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx @@ -60,6 +60,6 @@ describe('CaseView actions', () => { expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([data.id]); + expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([{ id: data.id, title: data.title }]); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx index 1d90470eab0e1..04b79967aa36e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx @@ -34,7 +34,7 @@ const CaseViewActionsComponent: React.FC = ({ caseData }) => { isModalVisible={isDisplayConfirmDeleteModal} isPlural={false} onCancel={handleToggleModal} - onConfirm={handleOnDeleteConfirm.bind(null, [caseData.id])} + onConfirm={handleOnDeleteConfirm.bind(null, [{ id: caseData.id, title: caseData.title }])} /> ), [isDisplayConfirmDeleteModal, caseData] diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx index a1f24275df6cd..18d5191fe6d33 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -57,8 +57,12 @@ const FormWrapper = styled.div` margin-top 40px; } - padding-top: ${theme.eui.paddingSizes.l}; - padding-bottom: ${theme.eui.paddingSizes.l}; + & > :first-child { + margin-top: 0; + } + + padding-top: ${theme.eui.paddingSizes.xl}; + padding-bottom: ${theme.eui.paddingSizes.xl}; `} `; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts index d1f04a34b7bad..49caeae1c3a34 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts @@ -155,7 +155,7 @@ export const WARNING_NO_CONNECTOR_MESSAGE = i18n.translate( 'xpack.siem.case.configureCases.warningMessage', { defaultMessage: - 'Configuration seems to be invalid. The selected connector is missing. Did you delete the connector?', + 'The selected connector has been deleted. Either select a different connector or create a new one.', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts index 0ca6bcff513fc..066145f7762c9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts @@ -22,13 +22,13 @@ export const REQUIRED_UPDATE_TO_SERVICE = i18n.translate( } ); -export const COPY_LINK_COMMENT = i18n.translate('xpack.siem.case.caseView.copyCommentLinkAria', { - defaultMessage: 'click to copy comment link', +export const COPY_REFERENCE_LINK = i18n.translate('xpack.siem.case.caseView.copyCommentLinkAria', { + defaultMessage: 'Copy reference link', }); export const MOVE_TO_ORIGINAL_COMMENT = i18n.translate( 'xpack.siem.case.caseView.moveToCommentAria', { - defaultMessage: 'click to highlight the reference comment', + defaultMessage: 'Highlight the referenced comment', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx index cc36e791e35b4..340e24e8fa55b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx @@ -154,6 +154,7 @@ export const UserActionItem = ({ labelQuoteAction={labelQuoteAction} labelTitle={labelTitle ?? <>} linkId={linkId} + fullName={fullName} username={username} updatedAt={updatedAt} onEdit={onEdit} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx index 94185cb4d130c..af1a1fdff26ce 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiText, EuiButtonIcon } from '@elastic/eui'; +import { + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButtonIcon, + EuiToolTip, +} from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n/react'; import copy from 'copy-to-clipboard'; import { isEmpty } from 'lodash/fp'; @@ -33,6 +40,7 @@ interface UserActionTitleProps { labelQuoteAction?: string; labelTitle: JSX.Element; linkId?: string | null; + fullName?: string | null; updatedAt?: string | null; username: string; onEdit?: (id: string) => void; @@ -48,6 +56,7 @@ export const UserActionTitle = ({ labelQuoteAction, labelTitle, linkId, + fullName, username, updatedAt, onEdit, @@ -105,7 +114,9 @@ export const UserActionTitle = ({ - {username} + {fullName ?? username}

}> + {username} +
{labelTitle} @@ -137,20 +148,24 @@ export const UserActionTitle = ({ {!isEmpty(linkId) && ( - + {i18n.MOVE_TO_ORIGINAL_COMMENT}

}> + +
)} - + {i18n.COPY_REFERENCE_LINK}

}> + +
{propertyActions.length > 0 && ( diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx index 3109f2382c362..87a446c45d891 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx @@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, + EuiToolTip, } from '@elastic/eui'; import styled, { css } from 'styled-components'; import { ElasticUser } from '../../../../containers/case/types'; @@ -40,8 +41,8 @@ const MyFlexGroup = styled(EuiFlexGroup)` const renderUsers = ( users: ElasticUser[], handleSendEmail: (emailAddress: string | undefined | null) => void -) => { - return users.map(({ fullName, username, email }, key) => ( +) => + users.map(({ fullName, username, email }, key) => ( @@ -49,11 +50,13 @@ const renderUsers = ( -

- - {username} - -

+ {fullName ?? username}

}> +

+ + {username} + +

+
@@ -63,11 +66,11 @@ const renderUsers = ( onClick={handleSendEmail.bind(null, email)} iconType="email" aria-label="email" + isDisabled={email == null} />
)); -}; export const UserList = React.memo(({ email, headline, loading, users }: UserListProps) => { const handleSendEmail = useCallback( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx new file mode 100644 index 0000000000000..8aaed08a0a0a1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx @@ -0,0 +1,380 @@ +/* + * 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 sinon from 'sinon'; +import moment from 'moment'; + +import { sendSignalToTimelineAction, determineToAndFrom } from './actions'; +import { + mockEcsDataWithSignal, + defaultTimelineProps, + apolloClient, + mockTimelineApolloResult, +} from '../../../../mock/'; +import { CreateTimeline, UpdateTimelineLoading } from './types'; +import { Ecs } from '../../../../graphql/types'; + +jest.mock('apollo-client'); + +describe('signals actions', () => { + const anchor = '2020-03-01T17:59:46.349Z'; + const unix = moment(anchor).valueOf(); + let createTimeline: CreateTimeline; + let updateTimelineIsLoading: UpdateTimelineLoading; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + createTimeline = jest.fn() as jest.Mocked; + updateTimelineIsLoading = jest.fn() as jest.Mocked; + + jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResult); + + clock = sinon.useFakeTimers(unix); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('sendSignalToTimelineAction', () => { + describe('timeline id is NOT empty string and apollo client exists', () => { + test('it invokes updateTimelineIsLoading to set to true', async () => { + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + + expect(updateTimelineIsLoading).toHaveBeenCalledTimes(1); + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); + }); + + test('it invokes createTimeline with designated timeline template if "timelineTemplate" exists', async () => { + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + const expected = { + from: 1541444305937, + timeline: { + columns: [ + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: '@timestamp', + placeholder: undefined, + type: undefined, + width: 190, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'message', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'event.category', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'host.name', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'source.ip', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'destination.ip', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'user.name', + placeholder: undefined, + type: undefined, + width: 180, + }, + ], + dataProviders: [], + dateRange: { + end: 1541444605937, + start: 1541444305937, + }, + deletedEventIds: [], + description: 'This is a sample rule description', + eventIdToNoteIds: {}, + eventType: 'all', + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + key: 'host.name', + negate: false, + params: { + query: 'apache', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'host.name': 'apache', + }, + }, + }, + ], + highlightedDropAndProviderId: '', + historyIds: [], + id: '', + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + isSelectAllChecked: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + expression: '', + kind: 'kuery', + }, + serializedQuery: '', + }, + filterQueryDraft: { + expression: '', + kind: 'kuery', + }, + }, + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: null, + selectedEventIds: {}, + show: true, + showCheckboxes: false, + showRowRenderers: true, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + title: '', + version: null, + width: 1100, + }, + to: 1541444605937, + ruleNote: '# this is some markdown documentation', + }; + + expect(createTimeline).toHaveBeenCalledWith(expected); + }); + + test('it invokes createTimeline with kqlQuery.filterQuery.kuery.kind as "kuery" if not specified in returned timeline template', async () => { + const mockTimelineApolloResultModified = { + ...mockTimelineApolloResult, + kqlQuery: { + filterQuery: { + kuery: { + expression: [''], + }, + }, + filterQueryDraft: { + expression: [''], + }, + }, + }; + jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResultModified); + + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + // @ts-ignore + const createTimelineArg = createTimeline.mock.calls[0][0]; + + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimelineArg.timeline.kqlQuery.filterQuery.kuery.kind).toEqual('kuery'); + }); + + test('it invokes createTimeline with kqlQuery.filterQueryDraft.kuery.kind as "kuery" if not specified in returned timeline template', async () => { + const mockTimelineApolloResultModified = { + ...mockTimelineApolloResult, + kqlQuery: { + filterQuery: { + kuery: { + expression: [''], + }, + }, + filterQueryDraft: { + expression: [''], + }, + }, + }; + jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResultModified); + + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + // @ts-ignore + const createTimelineArg = createTimeline.mock.calls[0][0]; + + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimelineArg.timeline.kqlQuery.filterQueryDraft.kind).toEqual('kuery'); + }); + + test('it invokes createTimeline with default timeline if apolloClient throws', async () => { + jest.spyOn(apolloClient, 'query').mockImplementation(() => { + throw new Error('Test error'); + }); + + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ + id: 'timeline-1', + isLoading: false, + }); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); + }); + }); + + describe('timelineId is empty string', () => { + test('it invokes createTimeline with timelineDefaults', async () => { + const ecsDataMock: Ecs = { + ...mockEcsDataWithSignal, + signal: { + rule: { + ...mockEcsDataWithSignal.signal?.rule!, + timeline_id: null, + }, + }, + }; + + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: ecsDataMock, + updateTimelineIsLoading, + }); + + expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); + }); + }); + + describe('apolloClient is not defined', () => { + test('it invokes createTimeline with timelineDefaults', async () => { + const ecsDataMock: Ecs = { + ...mockEcsDataWithSignal, + signal: { + rule: { + ...mockEcsDataWithSignal.signal?.rule!, + timeline_id: [''], + }, + }, + }; + + await sendSignalToTimelineAction({ + createTimeline, + ecsData: ecsDataMock, + updateTimelineIsLoading, + }); + + expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); + }); + }); + }); + + describe('determineToAndFrom', () => { + test('it uses ecs.Data.timestamp if one is provided', () => { + const ecsDataMock: Ecs = { + ...mockEcsDataWithSignal, + timestamp: '2020-03-20T17:59:46.349Z', + }; + const result = determineToAndFrom({ ecsData: ecsDataMock }); + + expect(result.from).toEqual(1584726886349); + expect(result.to).toEqual(1584727186349); + }); + + test('it uses current time timestamp if ecsData.timestamp is not provided', () => { + const { timestamp, ...ecsDataMock } = { + ...mockEcsDataWithSignal, + }; + const result = determineToAndFrom({ ecsData: ecsDataMock }); + + expect(result.from).toEqual(1583085286349); + expect(result.to).toEqual(1583085586349); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx index b23b051e8b2e8..c71ede32d8403 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx @@ -10,7 +10,7 @@ import moment from 'moment'; import { updateSignalStatus } from '../../../../containers/detection_engine/signals/api'; import { SendSignalToTimelineActionProps, UpdateSignalStatusActionProps } from './types'; -import { TimelineNonEcsData, GetOneTimeline, TimelineResult } from '../../../../graphql/types'; +import { TimelineNonEcsData, GetOneTimeline, TimelineResult, Ecs } from '../../../../graphql/types'; import { oneTimelineQuery } from '../../../../containers/timeline/one/index.gql_query'; import { omitTypenameInTimeline, @@ -72,16 +72,7 @@ export const updateSignalStatusAction = async ({ } }; -export const sendSignalToTimelineAction = async ({ - apolloClient, - createTimeline, - ecsData, - updateTimelineIsLoading, -}: SendSignalToTimelineActionProps) => { - let openSignalInBasicTimeline = true; - const timelineId = - ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : ''; - +export const determineToAndFrom = ({ ecsData }: { ecsData: Ecs }) => { const ellapsedTimeRule = moment.duration( moment().diff( dateMath.parse(ecsData.signal?.rule?.from != null ? ecsData.signal?.rule?.from[0] : 'now-0s') @@ -93,6 +84,21 @@ export const sendSignalToTimelineAction = async ({ .valueOf(); const to = moment(ecsData.timestamp ?? new Date()).valueOf(); + return { to, from }; +}; + +export const sendSignalToTimelineAction = async ({ + apolloClient, + createTimeline, + ecsData, + updateTimelineIsLoading, +}: SendSignalToTimelineActionProps) => { + let openSignalInBasicTimeline = true; + const noteContent = ecsData.signal?.rule?.note != null ? ecsData.signal?.rule?.note[0] : ''; + const timelineId = + ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : ''; + const { to, from } = determineToAndFrom({ ecsData }); + if (timelineId !== '' && apolloClient != null) { try { updateTimelineIsLoading({ id: 'timeline-1', isLoading: true }); @@ -106,10 +112,10 @@ export const sendSignalToTimelineAction = async ({ id: timelineId, }, }); - const timelineTemplate: TimelineResult = omitTypenameInTimeline( - getOr({}, 'data.getOneTimeline', responseTimeline) - ); - if (!isEmpty(timelineTemplate)) { + const resultingTimeline: TimelineResult = getOr({}, 'data.getOneTimeline', responseTimeline); + + if (!isEmpty(resultingTimeline)) { + const timelineTemplate: TimelineResult = omitTypenameInTimeline(resultingTimeline); openSignalInBasicTimeline = false; const { timeline } = formatTimelineResultToModel(timelineTemplate, true); const query = replaceTemplateFieldFromQuery( @@ -148,6 +154,7 @@ export const sendSignalToTimelineAction = async ({ show: true, }, to, + ruleNote: noteContent, }); } } catch { @@ -197,6 +204,7 @@ export const sendSignalToTimelineAction = async ({ }, }, to, + ruleNote: noteContent, }); } }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx new file mode 100644 index 0000000000000..6212cad7e1845 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx @@ -0,0 +1,193 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; + +import { Filter } from '../../../../../../../../../src/plugins/data/common/es_query'; +import { TimelineAction } from '../../../../components/timeline/body/actions'; +import { buildSignalsRuleIdFilter, getSignalsActions } from './default_config'; +import { + CreateTimeline, + SetEventsDeletedProps, + SetEventsLoadingProps, + UpdateTimelineLoading, +} from './types'; +import { mockEcsDataWithSignal } from '../../../../mock/mock_ecs'; +import { sendSignalToTimelineAction, updateSignalStatusAction } from './actions'; +import * as i18n from './translations'; + +jest.mock('./actions'); + +describe('signals default_config', () => { + describe('buildSignalsRuleIdFilter', () => { + test('given a rule id this will return an array with a single filter', () => { + const filters: Filter[] = buildSignalsRuleIdFilter('rule-id-1'); + const expectedFilter: Filter = { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.rule.id', + params: { + query: 'rule-id-1', + }, + }, + query: { + match_phrase: { + 'signal.rule.id': 'rule-id-1', + }, + }, + }; + expect(filters).toHaveLength(1); + expect(filters[0]).toEqual(expectedFilter); + }); + }); + + describe('getSignalsActions', () => { + let setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; + let setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; + let createTimeline: CreateTimeline; + let updateTimelineIsLoading: UpdateTimelineLoading; + + beforeEach(() => { + setEventsLoading = jest.fn(); + setEventsDeleted = jest.fn(); + createTimeline = jest.fn(); + updateTimelineIsLoading = jest.fn(); + }); + + describe('timeline tooltip', () => { + test('it invokes sendSignalToTimelineAction when button clicked', () => { + const signalsActions = getSignalsActions({ + canUserCRUD: true, + hasIndexWrite: true, + setEventsLoading, + setEventsDeleted, + createTimeline, + status: 'open', + updateTimelineIsLoading, + }); + const timelineAction = signalsActions[0].getAction({ + eventId: 'even-id', + ecsData: mockEcsDataWithSignal, + }); + const wrapper = mount(timelineAction as React.ReactElement); + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(sendSignalToTimelineAction).toHaveBeenCalled(); + }); + }); + + describe('signal open action', () => { + let signalsActions: TimelineAction[]; + let signalOpenAction: JSX.Element; + let wrapper: ReactWrapper; + + beforeEach(() => { + signalsActions = getSignalsActions({ + canUserCRUD: true, + hasIndexWrite: true, + setEventsLoading, + setEventsDeleted, + createTimeline, + status: 'open', + updateTimelineIsLoading, + }); + + signalOpenAction = signalsActions[1].getAction({ + eventId: 'event-id', + ecsData: mockEcsDataWithSignal, + }); + + wrapper = mount(signalOpenAction as React.ReactElement); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + test('it invokes updateSignalStatusAction when button clicked', () => { + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(updateSignalStatusAction).toHaveBeenCalledWith({ + signalIds: ['event-id'], + status: 'open', + setEventsLoading, + setEventsDeleted, + }); + }); + + test('it displays expected text on hover', () => { + const openSignal = wrapper.find(EuiToolTip); + openSignal.simulate('mouseOver'); + const tooltip = wrapper.find('.euiToolTipPopover').text(); + + expect(tooltip).toEqual(i18n.ACTION_OPEN_SIGNAL); + }); + + test('it displays expected icon', () => { + const icon = wrapper.find(EuiButtonIcon).props().iconType; + + expect(icon).toEqual('securitySignalDetected'); + }); + }); + + describe('signal close action', () => { + let signalsActions: TimelineAction[]; + let signalCloseAction: JSX.Element; + let wrapper: ReactWrapper; + + beforeEach(() => { + signalsActions = getSignalsActions({ + canUserCRUD: true, + hasIndexWrite: true, + setEventsLoading, + setEventsDeleted, + createTimeline, + status: 'closed', + updateTimelineIsLoading, + }); + + signalCloseAction = signalsActions[1].getAction({ + eventId: 'event-id', + ecsData: mockEcsDataWithSignal, + }); + + wrapper = mount(signalCloseAction as React.ReactElement); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + test('it invokes updateSignalStatusAction when status button clicked', () => { + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(updateSignalStatusAction).toHaveBeenCalledWith({ + signalIds: ['event-id'], + status: 'closed', + setEventsLoading, + setEventsDeleted, + }); + }); + + test('it displays expected text on hover', () => { + const closeSignal = wrapper.find(EuiToolTip); + closeSignal.simulate('mouseOver'); + const tooltip = wrapper.find('.euiToolTipPopover').text(); + expect(tooltip).toEqual(i18n.ACTION_CLOSE_SIGNAL); + }); + + test('it displays expected icon', () => { + const icon = wrapper.find(EuiButtonIcon).props().iconType; + + expect(icon).toEqual('securitySignalResolved'); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx index 44c48b1879e89..fd3b9a6f68e82 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx @@ -23,7 +23,12 @@ import { timelineDefaults } from '../../../../store/timeline/defaults'; import { FILTER_OPEN } from './signals_filter_group'; import { sendSignalToTimelineAction, updateSignalStatusAction } from './actions'; import * as i18n from './translations'; -import { CreateTimeline, SetEventsDeletedProps, SetEventsLoadingProps } from './types'; +import { + CreateTimeline, + SetEventsDeletedProps, + SetEventsLoadingProps, + UpdateTimelineLoading, +} from './types'; export const signalsOpenFilters: Filter[] = [ { @@ -198,13 +203,13 @@ export const getSignalsActions = ({ setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; createTimeline: CreateTimeline; status: 'open' | 'closed'; - updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => void; + updateTimelineIsLoading: UpdateTimelineLoading; }): TimelineAction[] => [ { getAction: ({ ecsData }: TimelineActionProps): JSX.Element => ( { let localValueToChange = valueToChange; - if (keuryNode.function === 'is' && templateFields.includes(keuryNode.arguments[0].value)) { + if (kueryNode.function === 'is' && templateFields.includes(kueryNode.arguments[0].value)) { localValueToChange = [ ...localValueToChange, { - field: keuryNode.arguments[0].value, - valueToChange: keuryNode.arguments[1].value, + field: kueryNode.arguments[0].value, + valueToChange: kueryNode.arguments[1].value, }, ]; } - return keuryNode.arguments.reduce( + return kueryNode.arguments.reduce( (addValueToChange: FindValueToChangeInQuery[], ast: KueryNode) => { if (ast.function === 'is' && templateFields.includes(ast.arguments[0].value)) { return [ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx index afd325f539966..6cdb2f326901e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx @@ -114,7 +114,7 @@ const SignalsTableComponent: React.FC = ({ // Callback for creating a new timeline -- utilized by row/batch actions const createTimelineCallback = useCallback( - ({ from: fromTimeline, timeline, to: toTimeline }: CreateTimelineProps) => { + ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); updateTimeline({ duplicate: true, @@ -126,6 +126,7 @@ const SignalsTableComponent: React.FC = ({ show: true, }, to: toTimeline, + ruleNote, })(); }, [updateTimeline, updateTimelineIsLoading] diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts index c2807db179780..f68dcd932bc32 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts @@ -95,9 +95,9 @@ export const ACTION_CLOSE_SIGNAL = i18n.translate( } ); -export const ACTION_VIEW_IN_TIMELINE = i18n.translate( - 'xpack.siem.detectionEngine.signals.actions.viewInTimelineTitle', +export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( + 'xpack.siem.detectionEngine.signals.actions.investigateInTimelineTitle', { - defaultMessage: 'View in timeline', + defaultMessage: 'Investigate in timeline', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts index b3e7ed75cfb99..909b217646746 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts @@ -45,13 +45,16 @@ export interface SendSignalToTimelineActionProps { apolloClient?: ApolloClient<{}>; createTimeline: CreateTimeline; ecsData: Ecs; - updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => void; + updateTimelineIsLoading: UpdateTimelineLoading; } +export type UpdateTimelineLoading = ({ id, isLoading }: { id: string; isLoading: boolean }) => void; + export interface CreateTimelineProps { from: number; timeline: TimelineModel; to: number; + ruleNote?: string; } export type CreateTimeline = ({ from, timeline, to }: CreateTimelineProps) => void; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap index 9a534297e5e29..31abea53462fa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap @@ -145,7 +145,7 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against # this is some markdown documentation , - "title": "Investigation notes", + "title": "Investigation guide", }, ] } @@ -287,7 +287,7 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against # this is some markdown documentation , - "title": "Investigation notes", + "title": "Investigation guide", }, ] } @@ -430,7 +430,7 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against # this is some markdown documentation , - "title": "Investigation notes", + "title": "Investigation guide", }, ] } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx index a01aec0ccf2cf..8e8927cb7bbd1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx @@ -461,12 +461,12 @@ describe('description_step', () => { test('returns default "note" description', () => { const result: ListItems[] = getDescriptionItem( 'note', - 'Investigation notes', + 'Investigation guide', mockAboutStep, mockFilterManager ); - expect(result[0].title).toEqual('Investigation notes'); + expect(result[0].title).toEqual('Investigation guide'); expect(React.isValidElement(result[0].description)).toBeTruthy(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index 8cb38b9dc7393..7c088c068c9b2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -178,12 +178,12 @@ export const schema: FormSchema = { }, note: { type: FIELD_TYPES.TEXTAREA, - label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.noteLabel', { - defaultMessage: 'Investigation notes', + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.guideLabel', { + defaultMessage: 'Investigation guide', }), - helpText: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.noteHelpText', { + helpText: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.guideHelpText', { defaultMessage: - 'Provide helpful information for analysts that are performing a signal investigation. These notes will appear on both the rule details page and in timelines created from signals generated by this rule.', + 'Provide helpful information for analysts that are performing a signal investigation. This guide will appear on both the rule details page and in timelines created from signals generated by this rule.', }), labelAppend: OptionalFieldLabel, }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts index dfa60268e903a..0b1e712c663f3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts @@ -72,6 +72,6 @@ export const URL_FORMAT_INVALID = i18n.translate( export const ADD_RULE_NOTE_HELP_TEXT = i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutrule.noteHelpText', { - defaultMessage: 'Add rule investigation notes...', + defaultMessage: 'Add rule investigation guide...', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx index bbd037af10c3f..76a3c590a62a6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx @@ -136,7 +136,7 @@ describe('StepAboutRuleToggleDetails', () => { expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeFalsy(); wrapper - .find('input[title="Investigation notes"]') + .find('input[title="Investigation guide"]') .at(0) .simulate('change', { target: { value: 'notes' } }); @@ -159,7 +159,7 @@ describe('StepAboutRuleToggleDetails', () => { ); wrapper - .find('input[title="Investigation notes"]') + .find('input[title="Investigation guide"]') .at(0) .simulate('change', { target: { value: 'notes' } }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts index fa725366210de..79c5eb12d4663 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts @@ -20,8 +20,8 @@ export const ABOUT_TEXT = i18n.translate( ); export const ABOUT_PANEL_NOTES_TAB = i18n.translate( - 'xpack.siem.detectionEngine.details.stepAboutRule.investigationNotesLabel', + 'xpack.siem.detectionEngine.details.stepAboutRule.investigationGuideLabel', { - defaultMessage: 'Investigation notes', + defaultMessage: 'Investigation guide', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx new file mode 100644 index 0000000000000..62399891c9606 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 { TimelinesPageComponent } from './timelines_page'; +import { useKibana } from '../../lib/kibana'; +import { shallow, ShallowWrapper } from 'enzyme'; +import React from 'react'; +import ApolloClient from 'apollo-client'; + +jest.mock('../../lib/kibana', () => { + return { + useKibana: jest.fn(), + }; +}); +describe('TimelinesPageComponent', () => { + const mockAppollloClient = {} as ApolloClient; + let wrapper: ShallowWrapper; + + describe('If the user is authorised', () => { + beforeAll(() => { + ((useKibana as unknown) as jest.Mock).mockReturnValue({ + services: { + application: { + capabilities: { + siem: { + crud: true, + }, + }, + }, + }, + }); + wrapper = shallow(); + }); + + afterAll(() => { + ((useKibana as unknown) as jest.Mock).mockReset(); + }); + + test('should not show the import timeline modal by default', () => { + expect( + wrapper.find('[data-test-subj="stateful-open-timeline"]').prop('importDataModalToggle') + ).toEqual(false); + }); + + test('should show the import timeline button', () => { + expect(wrapper.find('[data-test-subj="open-import-data-modal-btn"]').exists()).toEqual(true); + }); + + test('should show the import timeline modal after user clicking on the button', () => { + wrapper.find('[data-test-subj="open-import-data-modal-btn"]').simulate('click'); + expect( + wrapper.find('[data-test-subj="stateful-open-timeline"]').prop('importDataModalToggle') + ).toEqual(true); + }); + }); + + describe('If the user is not authorised', () => { + beforeAll(() => { + ((useKibana as unknown) as jest.Mock).mockReturnValue({ + services: { + application: { + capabilities: { + siem: { + crud: false, + }, + }, + }, + }, + }); + wrapper = shallow(); + }); + + afterAll(() => { + ((useKibana as unknown) as jest.Mock).mockReset(); + }); + test('should not show the import timeline modal by default', () => { + expect( + wrapper.find('[data-test-subj="stateful-open-timeline"]').prop('importDataModalToggle') + ).toEqual(false); + }); + + test('should not show the import timeline button', () => { + expect(wrapper.find('[data-test-subj="open-import-data-modal-btn"]').exists()).toEqual(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx index 75bef7a04a4c9..73070d2b94aac 100644 --- a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx @@ -28,7 +28,7 @@ type OwnProps = TimelinesProps; export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; -const TimelinesPageComponent: React.FC = ({ apolloClient }) => { +export const TimelinesPageComponent: React.FC = ({ apolloClient }) => { const [importDataModalToggle, setImportDataModalToggle] = useState(false); const onImportTimelineBtnClick = useCallback(() => { setImportDataModalToggle(true); @@ -43,7 +43,11 @@ const TimelinesPageComponent: React.FC = ({ apolloClient }) => { {capabilitiesCanUserCRUD && ( - + {i18n.ALL_TIMELINES_IMPORT_TIMELINE_TITLE} )} @@ -57,6 +61,7 @@ const TimelinesPageComponent: React.FC = ({ apolloClient }) => { importDataModalToggle={importDataModalToggle && capabilitiesCanUserCRUD} setImportDataModalToggle={setImportDataModalToggle} title={i18n.ALL_TIMELINES_PANEL_TITLE} + data-test-subj="stateful-open-timeline" /> diff --git a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json b/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json index bec6988bdebd9..c4705c8b8c16a 100644 --- a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json +++ b/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json @@ -4,7 +4,8 @@ "plugins/siem/**/*", "legacy/plugins/siem/**/*", "plugins/apm/typings/numeral.d.ts", - "legacy/plugins/canvas/types/webpack.d.ts" + "legacy/plugins/canvas/types/webpack.d.ts", + "plugins/triggers_actions_ui/**/*" ], "exclude": [ "test/**/*", diff --git a/x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts index f897236b3470e..9bf55cfe1ed2a 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts @@ -410,6 +410,7 @@ export const ecsSchema = gql` created_by: ToStringArray updated_by: ToStringArray version: ToStringArray + note: ToStringArray } type SignalField { diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index e2b365f8bfa5b..d272b7ff59b79 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -1014,6 +1014,8 @@ export interface RuleField { updated_by?: Maybe; version?: Maybe; + + note?: Maybe; } export interface SuricataEcsFields { @@ -4822,6 +4824,8 @@ export namespace RuleFieldResolvers { updated_by?: UpdatedByResolver, TypeParent, TContext>; version?: VersionResolver, TypeParent, TContext>; + + note?: NoteResolver, TypeParent, TContext>; } export type IdResolver< @@ -4974,6 +4978,11 @@ export namespace RuleFieldResolvers { Parent = RuleField, TContext = SiemContext > = Resolver; + export type NoteResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; } export namespace SuricataEcsFieldsResolvers { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts index 36764439462c3..3195483013c19 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -30,9 +30,13 @@ export const createIndexRoute = (router: IRouter) => { try { const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); const callCluster = clusterClient.callAsCurrentUser; + if (!siemClient) { + return siemResponse.error({ statusCode: 404 }); + } + const index = siemClient.signalsIndex; const indexExists = await getIndexExists(callCluster, index); if (indexExists) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts index aa418c11d9d16..c667e7ae9c463 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -38,7 +38,11 @@ export const deleteIndexRoute = (router: IRouter) => { try { const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); + + if (!siemClient) { + return siemResponse.error({ statusCode: 404 }); + } const callCluster = clusterClient.callAsCurrentUser; const index = siemClient.signalsIndex; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index 4fc5a4e1f347f..047176f155611 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -23,7 +23,11 @@ export const readIndexRoute = (router: IRouter) => { try { const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); + + if (!siemClient) { + return siemResponse.error({ statusCode: 404 }); + } const index = siemClient.signalsIndex; const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, index); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index aa4f6150889f9..3209f5ce9f519 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -62,6 +62,13 @@ describe('read_privileges route', () => { expect(response.status).toEqual(500); expect(response.body).toEqual({ message: 'Test error', status_code: 500 }); }); + + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(getPrivilegeRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); }); describe('when security plugin is disabled', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 2f5ea4d1ec767..d86880de65386 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -27,9 +27,14 @@ export const readPrivilegesRoute = ( }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); + try { const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); + + if (!siemClient) { + return siemResponse.error({ statusCode: 404 }); + } const index = siemClient.signalsIndex; const clusterPrivileges = await readPrivileges(clusterClient.callAsCurrentUser, index); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index f53efc8a3234d..f0b975379388f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -63,7 +63,7 @@ describe('add_prepackaged_rules_route', () => { addPrepackedRulesRoute(server.router); }); - describe('status codes with actionClient and alertClient', () => { + describe('status codes', () => { test('returns 200 when creating with a valid actionClient and alertClient', async () => { const request = addPrepackagedRulesRequest(); const response = await server.inject(request, context); @@ -96,6 +96,13 @@ describe('add_prepackaged_rules_route', () => { ), }); }); + + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(addPrepackagedRulesRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); }); describe('responses', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 4e08188af0d12..3eba04debb21f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -33,16 +33,13 @@ export const addPrepackedRulesRoute = (router: IRouter) => { const siemResponse = buildSiemResponse(response); try { - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const clusterClient = context.core.elasticsearch.dataClient; const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); - if (!actionsClient || !alertsClient) { + if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 32b8eca298229..e6facf6f3b7a8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -42,7 +42,7 @@ describe('create_rules_bulk', () => { createRulesBulkRoute(server.router); }); - describe('status codes with actionClient and alertClient', () => { + describe('status codes', () => { test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { const response = await server.inject(getReadBulkRequest(), context); expect(response.status).toEqual(200); @@ -54,6 +54,13 @@ describe('create_rules_bulk', () => { expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); + + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(getReadBulkRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); }); describe('unhappy paths', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 1ca9f7ef9075e..daeb11e88508b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -37,15 +37,12 @@ export const createRulesBulkRoute = (router: IRouter) => { }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); - if (!actionsClient || !alertsClient) { + if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 4da879d12f809..a77911bbb35e8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -60,6 +60,13 @@ describe('create_rules', () => { expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(getCreateRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); + it('returns 200 if license is not platinum', async () => { (context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index edf37bcb8dbe7..f68f204c12730 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -72,16 +72,13 @@ export const createRulesRoute = (router: IRouter): void => { try { validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const clusterClient = context.core.elasticsearch.dataClient; const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); - if (!actionsClient || !alertsClient) { + if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 85cfeefdceead..33ffc245e7668 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -35,11 +35,8 @@ export const deleteRulesBulkRoute = (router: IRouter) => { const handler: Handler = async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; if (!actionsClient || !alertsClient) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 6fd50abd9364a..a4e659da76bb2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -34,12 +34,9 @@ export const deleteRulesRoute = (router: IRouter) => { try { const { id, rule_id: ruleId } = request.query; - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; if (!actionsClient || !alertsClient) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index c434f42780e47..50eafe163c265 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -28,10 +28,7 @@ export const exportRulesRoute = (router: IRouter, config: LegacyServices['config }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); + const alertsClient = context.alerting?.getAlertsClient(); if (!alertsClient) { return siemResponse.error({ statusCode: 404 }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index 961859417ef1b..77351d2e0751b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -32,10 +32,7 @@ export const findRulesRoute = (router: IRouter) => { try { const { query } = request; - if (!context.alerting) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); + const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; if (!alertsClient) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index 4f4ae7c2c1fa6..6fee4d71a904e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -35,10 +35,7 @@ export const findRulesStatusesRoute = (router: IRouter) => { async (context, request, response) => { const { query } = request; const siemResponse = buildSiemResponse(response); - if (!context.alerting) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); + const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; if (!alertsClient) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index 7e16b4495593e..7f0bf4bf81179 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -29,10 +29,7 @@ export const getPrepackagedRulesStatusRoute = (router: IRouter) => { }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); + const alertsClient = context.alerting?.getAlertsClient(); if (!alertsClient) { return siemResponse.error({ statusCode: 404 }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index aacf83b9ec58a..61f5e6faf1bdb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -101,6 +101,13 @@ describe('import_rules_route', () => { expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); + + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(request, contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); }); describe('unhappy paths', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 2e6c72a87ec7f..d9fc89740c9ef 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -57,30 +57,27 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); - const clusterClient = context.core.elasticsearch.dataClient; - const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.siem.getSiemClient(); + try { + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); + const clusterClient = context.core.elasticsearch.dataClient; + const savedObjectsClient = context.core.savedObjects.client; + const siemClient = context.siem?.getSiemClient(); - if (!actionsClient || !alertsClient) { - return siemResponse.error({ statusCode: 404 }); - } + if (!siemClient || !actionsClient || !alertsClient) { + return siemResponse.error({ statusCode: 404 }); + } - const { filename } = request.body.file.hapi; - const fileExtension = extname(filename).toLowerCase(); - if (fileExtension !== '.ndjson') { - return siemResponse.error({ - statusCode: 400, - body: `Invalid file extension ${fileExtension}`, - }); - } + const { filename } = request.body.file.hapi; + const fileExtension = extname(filename).toLowerCase(); + if (fileExtension !== '.ndjson') { + return siemResponse.error({ + statusCode: 400, + body: `Invalid file extension ${fileExtension}`, + }); + } - const objectLimit = config().get('savedObjects.maxImportExportSize'); - try { + const objectLimit = config().get('savedObjects.maxImportExportSize'); const readStream = createRulesStreamFromNdJson(objectLimit); const parsedObjects = await createPromiseFromStreams([ request.body.file, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 645dbdadf8cab..b19039321a6d8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -37,11 +37,8 @@ export const patchRulesBulkRoute = (router: IRouter) => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; if (!actionsClient || !alertsClient) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index 620bcd8fc17b0..fab53079361ad 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -74,12 +74,8 @@ export const patchRulesRoute = (router: IRouter) => { validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); } - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; if (!actionsClient || !alertsClient) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index e4117166ed4fa..bc52445feee76 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -32,10 +32,7 @@ export const readRulesRoute = (router: IRouter) => { const { id, rule_id: ruleId } = request.query; const siemResponse = buildSiemResponse(response); - if (!context.alerting) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); + const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; try { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 611b38ccbae8b..332a47d0c0fc2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -69,6 +69,13 @@ describe('update_rules_bulk', () => { expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(getUpdateBulkRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); + test('returns an error if update throws', async () => { clients.alertsClient.update.mockImplementation(() => { throw new Error('Test error'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 4abeb840c8c0a..789f7d1ca0744 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -37,15 +37,12 @@ export const updateRulesBulkRoute = (router: IRouter) => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); - if (!actionsClient || !alertsClient) { + if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 717f2cc4a52fe..454fe1f0706cb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -67,6 +67,13 @@ describe('update_rules', () => { expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(getUpdateRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); + test('returns error when updating non-rule', async () => { clients.alertsClient.find.mockResolvedValue(nonRuleFindResult()); const response = await server.inject(getUpdateRequest(), context); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index f0d5f08c5f636..5856575eb9799 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -74,15 +74,12 @@ export const updateRulesRoute = (router: IRouter) => { try { validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); - if (!actionsClient || !alertsClient) { + if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index 612d08c09785a..72f3c89f660c7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -49,6 +49,13 @@ describe('set signal status', () => { expect(response.status).toEqual(200); }); + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(getSetSignalStatusByQueryRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); + test('catches error if callAsCurrentUser throws error', async () => { clients.clusterClient.callAsCurrentUser.mockImplementation(async () => { throw new Error('Test error'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index c1cba641de3ef..2daf63c468593 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -24,9 +24,13 @@ export const setSignalsStatusRoute = (router: IRouter) => { async (context, request, response) => { const { signal_ids: signalIds, query, status } = request.body; const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); const siemResponse = buildSiemResponse(response); + if (!siemClient) { + return siemResponse.error({ statusCode: 404 }); + } + let queryObject; if (signalIds) { queryObject = { ids: { values: signalIds } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts index 77b62b058fa54..f05f494619b9c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -24,7 +24,7 @@ export const querySignalsRoute = (router: IRouter) => { async (context, request, response) => { const { query, aggs, _source, track_total_hits, size } = request.body; const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem!.getSiemClient(); const siemResponse = buildSiemResponse(response); try { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts index e12bf50169c17..adabc62a9456f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts @@ -20,11 +20,7 @@ export const readTagsRoute = (router: IRouter) => { }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - - if (!context.alerting) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); + const alertsClient = context.alerting?.getAlertsClient(); if (!alertsClient) { return siemResponse.error({ statusCode: 404 }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index ada11174c5340..68716bb4e3795 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -16,7 +16,6 @@ import { import { AlertsClient, PartialAlert } from '../../../../../../../plugins/alerting/server'; import { Alert } from '../../../../../../../plugins/alerting/common'; import { SIGNALS_ID } from '../../../../common/constants'; -import { LegacyRequest } from '../../../types'; import { ActionsClient } from '../../../../../../../plugins/actions/server'; import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; @@ -39,14 +38,6 @@ export interface FindParamsRest { filter: string; } -export interface PatchRulesRequest extends LegacyRequest { - payload: PatchRuleAlertParamsRest; -} - -export interface UpdateRulesRequest extends LegacyRequest { - payload: UpdateRuleAlertParamsRest; -} - export interface RuleAlertType extends Alert { params: RuleTypeParams; } diff --git a/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts b/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts index eb483de000915..f2662c79d3393 100644 --- a/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts @@ -316,6 +316,7 @@ export const signalFieldsMap: Readonly> = { 'signal.rule.created_by': 'signal.rule.created_by', 'signal.rule.updated_by': 'signal.rule.updated_by', 'signal.rule.version': 'signal.rule.version', + 'signal.rule.note': 'signal.rule.note', }; export const ruleFieldsMap: Readonly> = { diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts index 63aee97729141..6552f973a66fa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts @@ -6,14 +6,14 @@ import Joi from 'joi'; const allowEmptyString = Joi.string().allow([null, '']); -const columnHeaderType = Joi.string(); +const columnHeaderType = allowEmptyString; export const created = Joi.number().allow(null); -export const createdBy = Joi.string(); +export const createdBy = allowEmptyString; export const description = allowEmptyString; export const end = Joi.number(); export const eventId = allowEmptyString; -export const eventType = Joi.string(); +export const eventType = allowEmptyString; export const filters = Joi.array() .items( @@ -24,19 +24,11 @@ export const filters = Joi.array() disabled: Joi.boolean().allow(null), field: allowEmptyString, formattedValue: allowEmptyString, - index: { - type: 'keyword', - }, - key: { - type: 'keyword', - }, - negate: { - type: 'boolean', - }, + index: allowEmptyString, + key: allowEmptyString, + negate: Joi.boolean().allow(null), params: allowEmptyString, - type: { - type: 'keyword', - }, + type: allowEmptyString, value: allowEmptyString, }), exists: allowEmptyString, @@ -68,22 +60,22 @@ export const version = allowEmptyString; export const columns = Joi.array().items( Joi.object({ aggregatable: Joi.boolean().allow(null), - category: Joi.string(), + category: allowEmptyString, columnHeaderType, description, example: allowEmptyString, indexes: allowEmptyString, - id: Joi.string(), + id: allowEmptyString, name, placeholder: allowEmptyString, searchable: Joi.boolean().allow(null), - type: Joi.string(), + type: allowEmptyString, }).required() ); export const dataProviders = Joi.array() .items( Joi.object({ - id: Joi.string(), + id: allowEmptyString, name: allowEmptyString, enabled: Joi.boolean().allow(null), excluded: Joi.boolean().allow(null), @@ -98,7 +90,7 @@ export const dataProviders = Joi.array() and: Joi.array() .items( Joi.object({ - id: Joi.string(), + id: allowEmptyString, name, enabled: Joi.boolean().allow(null), excluded: Joi.boolean().allow(null), @@ -122,9 +114,9 @@ export const dateRange = Joi.object({ }); export const favorite = Joi.array().items( Joi.object({ - keySearch: Joi.string(), - fullName: Joi.string(), - userName: Joi.string(), + keySearch: allowEmptyString, + fullName: allowEmptyString, + userName: allowEmptyString, favoriteDate: Joi.number(), }).allow(null) ); @@ -141,26 +133,26 @@ const noteItem = Joi.object({ }); export const eventNotes = Joi.array().items(noteItem); export const globalNotes = Joi.array().items(noteItem); -export const kqlMode = Joi.string(); +export const kqlMode = allowEmptyString; export const kqlQuery = Joi.object({ filterQuery: Joi.object({ kuery: Joi.object({ - kind: Joi.string(), + kind: allowEmptyString, expression: allowEmptyString, }), serializedQuery: allowEmptyString, }), }); export const pinnedEventIds = Joi.array() - .items(Joi.string()) + .items(allowEmptyString) .allow(null); export const sort = Joi.object({ - columnId: Joi.string(), - sortDirection: Joi.string(), + columnId: allowEmptyString, + sortDirection: allowEmptyString, }); /* eslint-disable @typescript-eslint/camelcase */ -export const ids = Joi.array().items(Joi.string()); +export const ids = Joi.array().items(allowEmptyString); export const exclude_export_details = Joi.boolean(); export const file_name = allowEmptyString; diff --git a/x-pack/legacy/plugins/siem/server/types.ts b/x-pack/legacy/plugins/siem/server/types.ts index 4119645a5af47..a52322f5f830c 100644 --- a/x-pack/legacy/plugins/siem/server/types.ts +++ b/x-pack/legacy/plugins/siem/server/types.ts @@ -7,12 +7,8 @@ import { Legacy } from 'kibana'; import { SiemClient } from './client'; -export { LegacyRequest } from '../../../../../src/core/server'; - export interface LegacyServices { - alerting?: Legacy.Server['plugins']['alerting']; config: Legacy.Server['config']; - route: Legacy.Server['route']; } export { SiemClient }; @@ -23,6 +19,6 @@ export interface SiemRequestContext { declare module 'src/core/server' { interface RequestHandlerContext { - siem: SiemRequestContext; + siem?: SiemRequestContext; } } diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap index 354521e7c55b9..ead27425c26f3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap @@ -53,9 +53,18 @@ exports[`ML Flyout component renders without errors 1`] = ` + + + Cancel + + @@ -206,8 +215,26 @@ exports[`ML Flyout component shows license info if no ml available 1`] = ` class="euiFlyoutFooter" >
+
+ +
diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx index 917367f3e8dad..fdecfbf20810c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx @@ -7,6 +7,7 @@ import React, { useContext } from 'react'; import { EuiButton, + EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFlyout, @@ -64,11 +65,15 @@ export function MLFlyoutView({ isCreatingJob, onClickCreate, onClose, canCreateM {labels.TAKE_SOME_TIME_TEXT}

- - + + + onClose()} disabled={isCreatingJob || isLoadingMLJob}> + {labels.CANCEL_LABEL} + + onClickCreate()} diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx index 570dd9d1bfa26..32374674771e8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx @@ -124,6 +124,13 @@ export const CREATE_NEW_JOB = i18n.translate( } ); +export const CANCEL_LABEL = i18n.translate( + 'xpack.uptime.ml.enableAnomalyDetectionPanel.cancelLabel', + { + defaultMessage: 'Cancel', + } +); + export const CREAT_ML_JOB_DESC = i18n.translate( 'xpack.uptime.ml.enableAnomalyDetectionPanel.createMLJobDescription', { diff --git a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts b/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts index 01d6a2e2e81bc..572d73e368c7a 100644 --- a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts @@ -140,4 +140,21 @@ describe('dedupeConnections', () => { // @ts-ignore expect(nodejsNode?.data[SPAN_SUBTYPE]).toBe('aa'); }); + + it('processes connections without a matching "service" aggregation', () => { + const response: ServiceMapResponse = { + services: [javaService], + discoveredServices: [], + connections: [ + { + source: javaService, + destination: nodejsService + } + ] + }; + + const { elements } = dedupeConnections(response); + + expect(elements.length).toBe(3); + }); }); diff --git a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts b/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts index 6a433367d8217..e5d7c0b2de10c 100644 --- a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts +++ b/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts @@ -88,7 +88,7 @@ export function dedupeConnections(response: ServiceMapResponse) { serviceName = node[SERVICE_NAME]; } - const matchedServiceNodes = services.filter( + const matchedServiceNodes = serviceNodes.filter( serviceNode => serviceNode[SERVICE_NAME] === serviceName ); diff --git a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts index c493e8ce86781..70bdcdfd3cf1f 100644 --- a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts @@ -33,7 +33,7 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index a88b1d9049c76..528b9a69327fa 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -49,6 +49,8 @@ export interface LogEntriesAroundParams { export const LOG_ENTRIES_PAGE_SIZE = 200; +const FIELDS_FROM_CONTEXT = ['log.file.path', 'host.name', 'container.id'] as const; + export class InfraLogEntriesDomain { constructor( private readonly adapter: LogEntriesAdapter, @@ -154,6 +156,14 @@ export class InfraLogEntriesDomain { } } ), + context: FIELDS_FROM_CONTEXT.reduce((ctx, field) => { + // Users might have different types here in their mappings. + const value = doc.fields[field]; + if (typeof value === 'string') { + ctx[field] = value; + } + return ctx; + }, {}), }; }); @@ -329,7 +339,9 @@ const getRequiredFields = ( ); const fieldsFromFormattingRules = messageFormattingRules.requiredFields; - return Array.from(new Set([...fieldsFromCustomColumns, ...fieldsFromFormattingRules])); + return Array.from( + new Set([...fieldsFromCustomColumns, ...fieldsFromFormattingRules, ...FIELDS_FROM_CONTEXT]) + ); }; const createHighlightQueryDsl = (phrase: string, fields: string[]) => ({ diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index 8623d02e72862..727d26b5868de 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -76,7 +76,8 @@ export const getFileHandler: RequestHandler { try { const { pkgkey, filePath } = request.params; - const registryResponse = await getFile(`/package/${pkgkey}/${filePath}`); + const [pkgName, pkgVersion] = pkgkey.split('-'); + const registryResponse = await getFile(`/package/${pkgName}/${pkgVersion}/${filePath}`); const contentType = registryResponse.headers.get('Content-Type'); const customResponseObj: CustomHttpResponseOptions = { body: registryResponse.body, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts index 5153f9205dde7..6d5ca036aeb13 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts @@ -11,19 +11,18 @@ const tests = [ { package: { assets: [ - '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json', - '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json', + '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json', + '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json', ], - name: 'coredns', - version: '1.0.1', + path: '/package/coredns/1.0.1', }, dataset: 'log', filter: (path: string) => { return true; }, expected: [ - '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json', - '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json', + '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json', + '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json', ], }, { @@ -32,8 +31,7 @@ const tests = [ '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json', '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json', ], - name: 'coredns', - version: '1.0.1', + path: '/package/coredns/1.0.1', }, // Non existant dataset dataset: 'foo', diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts index e36c2de1b4e80..d7a5c5569986e 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts @@ -9,14 +9,16 @@ import * as Registry from '../registry'; import { cacheHas } from '../registry/cache'; // paths from RegistryPackage are routes to the assets on EPR -// e.g. `/package/nginx-1.2.0/dataset/access/fields/fields.yml` +// e.g. `/package/nginx/1.2.0/dataset/access/fields/fields.yml` // paths for ArchiveEntry are routes to the assets in the archive // e.g. `nginx-1.2.0/dataset/access/fields/fields.yml` // RegistryPackage paths have a `/package/` prefix compared to ArchiveEntry paths +// and different package and version structure const EPR_PATH_PREFIX = '/package'; function registryPathToArchivePath(registryPath: RegistryPackage['path']): string { - const archivePath = registryPath.replace(`${EPR_PATH_PREFIX}/`, ''); - return archivePath; + const path = registryPath.replace(`${EPR_PATH_PREFIX}/`, ''); + const [pkgName, pkgVersion] = path.split('/'); + return path.replace(`${pkgName}/${pkgVersion}`, `${pkgName}-${pkgVersion}`); } export function getAssets( @@ -35,7 +37,7 @@ export function getAssets( // if dataset, filter for them if (datasetName) { - const comparePath = `${EPR_PATH_PREFIX}/${packageInfo.name}-${packageInfo.version}/dataset/${datasetName}/`; + const comparePath = `${packageInfo.path}/dataset/${datasetName}/`; if (!path.includes(comparePath)) { continue; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 7c315f7616e1f..36a04b88bba29 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -6,7 +6,6 @@ import { Response } from 'node-fetch'; import { URL } from 'url'; -import { sortBy } from 'lodash'; import { AssetParts, AssetsGroupedByServiceByType, @@ -51,11 +50,7 @@ export async function fetchFindLatestPackage( const res = await fetchUrl(url.toString()); const searchResults = JSON.parse(res); if (searchResults.length) { - // sort by version, then get the last (most recent) - const latestPackage = sortBy(searchResults, ['version'])[ - searchResults.length - 1 - ]; - return latestPackage; + return searchResults[0]; } else { throw new Error('package not found'); } @@ -63,7 +58,8 @@ export async function fetchFindLatestPackage( export async function fetchInfo(pkgkey: string): Promise { const registryUrl = appContextService.getConfig()?.epm.registryUrl; - return fetchUrl(`${registryUrl}/package/${pkgkey}`).then(JSON.parse); + // change pkg-version to pkg/version + return fetchUrl(`${registryUrl}/package/${pkgkey.replace('-', '/')}`).then(JSON.parse); } export async function fetchFile(filePath: string): Promise { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 95a8dfbb308f8..9791cd9210fe2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -62,6 +62,9 @@ export interface LoadExploreDataArg { export const SEARCH_SIZE = 1000; +export const TRAINING_PERCENT_MIN = 1; +export const TRAINING_PERCENT_MAX = 100; + export const defaultSearchQuery = { match_all: {}, }; @@ -172,6 +175,19 @@ export const getDependentVar = (analysis: AnalysisConfig) => { return depVar; }; +export const getTrainingPercent = (analysis: AnalysisConfig) => { + let trainingPercent; + + if (isRegressionAnalysis(analysis)) { + trainingPercent = analysis.regression.training_percent; + } + + if (isClassificationAnalysis(analysis)) { + trainingPercent = analysis.classification.training_percent; + } + return trainingPercent; +}; + export const getPredictionFieldName = (analysis: AnalysisConfig) => { // If undefined will be defaulted to dependent_variable when config is created let predictionFieldName; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index 263d43ceb2630..41430b163c029 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -18,6 +18,7 @@ import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useMlContext } from '../../../../../contexts/ml'; +import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( @@ -47,11 +48,11 @@ const jobCapsErrorTitle = i18n.translate( interface Props { jobId: string; - jobStatus: DATA_FRAME_TASK_STATE; } -export const ClassificationExploration: FC = ({ jobId, jobStatus }) => { +export const ClassificationExploration: FC = ({ jobId }) => { const [jobConfig, setJobConfig] = useState(undefined); + const [jobStatus, setJobStatus] = useState(undefined); const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); @@ -65,6 +66,15 @@ export const ClassificationExploration: FC = ({ jobId, jobStatus }) => { setIsLoadingJobConfig(true); try { const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + if ( Array.isArray(analyticsConfigs.data_frame_analytics) && analyticsConfigs.data_frame_analytics.length > 0 diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 1c5563bdb4f83..91dae49ba5c49 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -50,10 +50,47 @@ const defaultPanelWidth = 500; interface Props { jobConfig: DataFrameAnalyticsConfig; - jobStatus: DATA_FRAME_TASK_STATE; + jobStatus?: DATA_FRAME_TASK_STATE; searchQuery: ResultsSearchQuery; } +enum SUBSET_TITLE { + TRAINING = 'training', + TESTING = 'testing', + ENTIRE = 'entire', +} + +const entireDatasetHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixEntireHelpText', + { + defaultMessage: 'Normalized confusion matrix for entire dataset', + } +); + +const testingDatasetHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTestingHelpText', + { + defaultMessage: 'Normalized confusion matrix for testing dataset', + } +); + +const trainingDatasetHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTrainingHelpText', + { + defaultMessage: 'Normalized confusion matrix for training dataset', + } +); + +function getHelpText(dataSubsetTitle: string) { + let helpText = entireDatasetHelpText; + if (dataSubsetTitle === SUBSET_TITLE.TESTING) { + helpText = testingDatasetHelpText; + } else if (dataSubsetTitle === SUBSET_TITLE.TRAINING) { + helpText = trainingDatasetHelpText; + } + return helpText; +} + export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) => { const { services: { docLinks }, @@ -66,6 +103,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) const [popoverContents, setPopoverContents] = useState([]); const [docsCount, setDocsCount] = useState(null); const [error, setError] = useState(null); + const [dataSubsetTitle, setDataSubsetTitle] = useState(SUBSET_TITLE.ENTIRE); const [panelWidth, setPanelWidth] = useState(defaultPanelWidth); // Column visibility const [visibleColumns, setVisibleColumns] = useState(() => @@ -197,6 +235,18 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) hasIsTrainingClause[0] && hasIsTrainingClause[0].match[`${resultsField}.is_training`]; + const noTrainingQuery = isTrainingClause === false || isTrainingClause === undefined; + + if (noTrainingQuery) { + setDataSubsetTitle(SUBSET_TITLE.ENTIRE); + } else { + setDataSubsetTitle( + isTrainingClause && isTrainingClause.query === 'true' + ? SUBSET_TITLE.TRAINING + : SUBSET_TITLE.TESTING + ); + } + loadData({ isTrainingClause }); }, [JSON.stringify(searchQuery)]); @@ -268,9 +318,11 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} @@ -302,14 +354,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) - - {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixHelpText', - { - defaultMessage: 'Normalized confusion matrix', - } - )} - + {getHelpText(dataSubsetTitle)} >; } @@ -381,9 +381,11 @@ export const ResultsTable: FC = React.memo( - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} = React.memo( - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx index 030447873f6a5..7cdd15e49bd14 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx @@ -6,7 +6,6 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { MlContext } from '../../../../../contexts/ml'; import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; @@ -22,7 +21,7 @@ describe('Data Frame Analytics: ', () => { test('Minimal initialization', () => { const wrapper = shallow( - + ); // Without the jobConfig being loaded, the component will just return empty. diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 214bc01c6a2ef..d686c605f1912 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -27,7 +27,6 @@ import { import { sortColumns, INDEX_STATUS, defaultSearchQuery } from '../../../../common'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { useExploreData, TableItem } from '../../hooks/use_explore_data'; @@ -50,7 +49,6 @@ const ExplorationTitle: FC<{ jobId: string }> = ({ jobId }) => ( interface ExplorationProps { jobId: string; - jobStatus: DATA_FRAME_TASK_STATE; } const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => { @@ -63,11 +61,12 @@ const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => ).length; }; -export const OutlierExploration: FC = React.memo(({ jobId, jobStatus }) => { +export const OutlierExploration: FC = React.memo(({ jobId }) => { const { errorMessage, indexPattern, jobConfig, + jobStatus, pagination, searchQuery, selectedFields, @@ -173,9 +172,11 @@ export const OutlierExploration: FC = React.memo(({ jobId, job - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 74937bf761285..9f235ae6c45c0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -39,7 +39,7 @@ import { interface Props { jobConfig: DataFrameAnalyticsConfig; - jobStatus: DATA_FRAME_TASK_STATE; + jobStatus?: DATA_FRAME_TASK_STATE; searchQuery: ResultsSearchQuery; } @@ -248,9 +248,11 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 3dfd95a27f8a7..4f3c4048d40d5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -18,6 +18,7 @@ import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useMlContext } from '../../../../../contexts/ml'; +import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( @@ -47,11 +48,11 @@ const jobCapsErrorTitle = i18n.translate( interface Props { jobId: string; - jobStatus: DATA_FRAME_TASK_STATE; } -export const RegressionExploration: FC = ({ jobId, jobStatus }) => { +export const RegressionExploration: FC = ({ jobId }) => { const [jobConfig, setJobConfig] = useState(undefined); + const [jobStatus, setJobStatus] = useState(undefined); const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); @@ -65,6 +66,15 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { setIsLoadingJobConfig(true); try { const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + if ( Array.isArray(analyticsConfigs.data_frame_analytics) && analyticsConfigs.data_frame_analytics.length > 0 diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index 7a6b2b23ba7a3..b896c34a582f7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -86,7 +86,7 @@ const showingFirstDocs = i18n.translate( interface Props { jobConfig: DataFrameAnalyticsConfig; - jobStatus: DATA_FRAME_TASK_STATE; + jobStatus?: DATA_FRAME_TASK_STATE; setEvaluateSearchQuery: React.Dispatch>; } @@ -381,9 +381,11 @@ export const ResultsTable: FC = React.memo( - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} = React.memo( - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts index 6ad0a1822e490..d637057a4430d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts @@ -19,6 +19,7 @@ import { newJobCapsService } from '../../../../../services/new_job_capabilities_ import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { getNestedProperty } from '../../../../../util/object_utils'; import { useMlContext } from '../../../../../contexts/ml'; +import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; import { getDefaultSelectableFields, @@ -31,6 +32,7 @@ import { import { isKeywordAndTextType } from '../../../../common/fields'; import { getOutlierScoreFieldName } from './common'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; export type TableItem = Record; @@ -40,6 +42,7 @@ interface UseExploreDataReturnType { errorMessage: string; indexPattern: IndexPattern | undefined; jobConfig: DataFrameAnalyticsConfig | undefined; + jobStatus: DATA_FRAME_TASK_STATE | undefined; pagination: Pagination; searchQuery: SavedSearchQuery; selectedFields: EsFieldName[]; @@ -74,6 +77,7 @@ export const useExploreData = (jobId: string): UseExploreDataReturnType => { const [indexPattern, setIndexPattern] = useState(undefined); const [jobConfig, setJobConfig] = useState(undefined); + const [jobStatus, setJobStatus] = useState(undefined); const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(INDEX_STATUS.UNUSED); @@ -90,6 +94,15 @@ export const useExploreData = (jobId: string): UseExploreDataReturnType => { useEffect(() => { (async function() { const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + if ( Array.isArray(analyticsConfigs.data_frame_analytics) && analyticsConfigs.data_frame_analytics.length > 0 @@ -215,6 +228,7 @@ export const useExploreData = (jobId: string): UseExploreDataReturnType => { errorMessage, indexPattern, jobConfig, + jobStatus, pagination, rowCount, searchQuery, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx index efbebc1564bf9..c8349084dbda8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx @@ -27,13 +27,11 @@ import { RegressionExploration } from './components/regression_exploration'; import { ClassificationExploration } from './components/classification_exploration'; import { ANALYSIS_CONFIG_TYPE } from '../../common/analytics'; -import { DATA_FRAME_TASK_STATE } from '../analytics_management/components/analytics_list/common'; export const Page: FC<{ jobId: string; analysisType: ANALYSIS_CONFIG_TYPE; - jobStatus: DATA_FRAME_TASK_STATE; -}> = ({ jobId, analysisType, jobStatus }) => ( +}> = ({ jobId, analysisType }) => ( @@ -68,13 +66,13 @@ export const Page: FC<{ {analysisType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && ( - + )} {analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && ( - + )} {analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && ( - + )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 425e3bc903d04..4e19df9ae22a8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -33,13 +33,12 @@ export const AnalyticsViewAction = { isPrimary: true, render: (item: DataFrameAnalyticsListRow) => { const analysisType = getAnalysisType(item.config.analysis); - const jobStatus = item.stats.state; const isDisabled = !isRegressionAnalysis(item.config.analysis) && !isOutlierAnalysis(item.config.analysis) && !isClassificationAnalysis(item.config.analysis); - const url = getResultsUrl(item.id, analysisType, jobStatus); + const url = getResultsUrl(item.id, analysisType); return ( = ({ actions, state }) => { - const { - resetAdvancedEditorMessages, - setAdvancedEditorRawString, - setFormState, - setJobConfig, - } = actions; + const { setAdvancedEditorRawString, setFormState } = actions; const { advancedEditorMessages, advancedEditorRawString, isJobCreated, requestMessages } = state; @@ -45,12 +39,6 @@ export const CreateAnalyticsAdvancedEditor: FC = ({ ac const onChange = (str: string) => { setAdvancedEditorRawString(str); - try { - const resultJobConfig = JSON.parse(collapseLiteralStrings(str)); - setJobConfig(resultJobConfig); - } catch (e) { - resetAdvancedEditorMessages(); - } }; // Temp effect to close the context menu popover on Clone button click diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx index 32384e1949d0a..b0f13e398cc50 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx @@ -26,7 +26,14 @@ export const CreateAnalyticsFlyout: FC = ({ state, }) => { const { closeModal, createAnalyticsJob, startAnalyticsJob } = actions; - const { isJobCreated, isJobStarted, isModalButtonDisabled, isValid, cloneJob } = state; + const { + isJobCreated, + isJobStarted, + isModalButtonDisabled, + isValid, + isAdvancedEditorValidJson, + cloneJob, + } = state; const headerText = !!cloneJob ? i18n.translate('xpack.ml.dataframe.analytics.clone.flyoutHeaderTitle', { @@ -61,7 +68,7 @@ export const CreateAnalyticsFlyout: FC = ({ {!isJobCreated && !isJobStarted && ( = ({ actions, sta })} > setFormState({ trainingPercent: e.target.value })} + onChange={e => setFormState({ trainingPercent: +e.target.value })} data-test-subj="mlAnalyticsCreateJobFlyoutTrainingPercentSlider" /> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts index 8112a0fdb9e29..c40ab31f6615f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts @@ -16,9 +16,11 @@ type SourceIndex = DataFrameAnalyticsConfig['source']['index']; const getMockState = ({ index, + trainingPercent = 75, modelMemoryLimit = '100mb', }: { index: SourceIndex; + trainingPercent?: number; modelMemoryLimit?: string; }) => merge(getInitialState(), { @@ -31,7 +33,9 @@ const getMockState = ({ jobConfig: { source: { index }, dest: { index: 'the-destination-index' }, - analysis: {}, + analysis: { + classification: { dependent_variable: 'the-variable', training_percent: trainingPercent }, + }, model_memory_limit: modelMemoryLimit, }, }); @@ -151,6 +155,24 @@ describe('useCreateAnalyticsForm', () => { .isValid ).toBe(false); }); + + test('validateAdvancedEditor(): check training percent validation', () => { + // valid training_percent value + expect( + validateAdvancedEditor(getMockState({ index: 'the-source-index', trainingPercent: 75 })) + .isValid + ).toBe(true); + // invalid training_percent numeric value + expect( + validateAdvancedEditor(getMockState({ index: 'the-source-index', trainingPercent: 102 })) + .isValid + ).toBe(false); + // invalid training_percent numeric value if 0 + expect( + validateAdvancedEditor(getMockState({ index: 'the-source-index', trainingPercent: 0 })) + .isValid + ).toBe(false); + }); }); describe('validateMinMML', () => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index d045749a1a0dd..28d8afbcd88cc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -11,6 +11,8 @@ import numeral from '@elastic/numeral'; import { isEmpty } from 'lodash'; import { isValidIndexName } from '../../../../../../../common/util/es_utils'; +import { collapseLiteralStrings } from '../../../../../../../../../../src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools'; + import { Action, ACTION } from './actions'; import { getInitialState, getJobConfigFromFormState, State } from './state'; import { @@ -29,9 +31,12 @@ import { } from '../../../../../../../common/constants/validation'; import { getDependentVar, + getTrainingPercent, isRegressionAnalysis, isClassificationAnalysis, ANALYSIS_CONFIG_TYPE, + TRAINING_PERCENT_MIN, + TRAINING_PERCENT_MAX, } from '../../../../common/analytics'; import { indexPatterns } from '../../../../../../../../../../src/plugins/data/public'; @@ -141,6 +146,7 @@ export const validateAdvancedEditor = (state: State): State => { let dependentVariableEmpty = false; let excludesValid = true; + let trainingPercentValid = true; if ( jobConfig.analysis === undefined && @@ -169,6 +175,30 @@ export const validateAdvancedEditor = (state: State): State => { message: '', }); } + + const trainingPercent = getTrainingPercent(jobConfig.analysis); + if ( + trainingPercent !== undefined && + (isNaN(trainingPercent) || + trainingPercent < TRAINING_PERCENT_MIN || + trainingPercent > TRAINING_PERCENT_MAX) + ) { + trainingPercentValid = false; + + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.trainingPercentInvalid', + { + defaultMessage: 'The training percent must be a value between {min} and {max}.', + values: { + min: TRAINING_PERCENT_MIN, + max: TRAINING_PERCENT_MAX, + }, + } + ), + message: '', + }); + } } if (sourceIndexNameEmpty) { @@ -249,6 +279,7 @@ export const validateAdvancedEditor = (state: State): State => { state.isValid = maxDistinctValuesError === undefined && excludesValid && + trainingPercentValid && state.form.modelMemoryLimitUnitValid && !jobIdEmpty && jobIdValid && @@ -365,7 +396,23 @@ export function reducer(state: State, action: Action): State { return getInitialState(); case ACTION.SET_ADVANCED_EDITOR_RAW_STRING: - return { ...state, advancedEditorRawString: action.advancedEditorRawString }; + let resultJobConfig; + try { + resultJobConfig = JSON.parse(collapseLiteralStrings(action.advancedEditorRawString)); + } catch (e) { + return { + ...state, + advancedEditorRawString: action.advancedEditorRawString, + isAdvancedEditorValidJson: false, + advancedEditorMessages: [], + }; + } + + return { + ...validateAdvancedEditor({ ...state, jobConfig: resultJobConfig }), + advancedEditorRawString: action.advancedEditorRawString, + isAdvancedEditorValidJson: true, + }; case ACTION.SET_FORM_STATE: const newFormState = { ...state.form, ...action.payload }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 719bb6c5b07c7..fe741fe9a92d4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -82,6 +82,7 @@ export interface State { indexNames: EsIndexName[]; indexPatternsMap: SourceIndexMap; isAdvancedEditorEnabled: boolean; + isAdvancedEditorValidJson: boolean; isJobCreated: boolean; isJobStarted: boolean; isModalButtonDisabled: boolean; @@ -140,6 +141,7 @@ export const getInitialState = (): State => ({ indexNames: [], indexPatternsMap: {}, isAdvancedEditorEnabled: false, + isAdvancedEditorValidJson: true, isJobCreated: false, isJobStarted: false, isModalVisible: false, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 37b9fe5e1f2d0..1f2a57f999775 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -388,17 +388,23 @@ function getUrlVars(url) { } export function getSelectedJobIdFromUrl(url) { - if (typeof url === 'string' && url.includes('mlManagement') && url.includes('jobId')) { - const urlParams = getUrlVars(url); - const decodedJson = rison.decode(urlParams.mlManagement); - return decodedJson.jobId; + if (typeof url === 'string') { + url = decodeURIComponent(url); + if (url.includes('mlManagement') && url.includes('jobId')) { + const urlParams = getUrlVars(url); + const decodedJson = rison.decode(urlParams.mlManagement); + return decodedJson.jobId; + } } } export function clearSelectedJobIdFromUrl(url) { - if (typeof url === 'string' && url.includes('mlManagement') && url.includes('jobId')) { - const urlParams = getUrlVars(url); - const clearedParams = `ml#/jobs?_g=${urlParams._g}`; - window.history.replaceState({}, document.title, clearedParams); + if (typeof url === 'string') { + url = decodeURIComponent(url); + if (url.includes('mlManagement') && url.includes('jobId')) { + const urlParams = getUrlVars(url); + const clearedParams = `ml#/jobs?_g=${urlParams._g}`; + window.history.replaceState({}, document.title, clearedParams); + } } } diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index e00ff0333bb73..2dde5426ec9a0 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -13,7 +13,6 @@ import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_exploration'; import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics'; -import { DATA_FRAME_TASK_STATE } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { ML_BREADCRUMB } from '../../breadcrumbs'; const breadcrumbs = [ @@ -46,11 +45,10 @@ const PageWrapper: FC = ({ location, deps }) => { } const jobId: string = globalState.ml.jobId; const analysisType: ANALYSIS_CONFIG_TYPE = globalState.ml.analysisType; - const jobStatus: DATA_FRAME_TASK_STATE = globalState.ml.jobStatus; return ( - + ); }; diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index a84400f236134..0454d40e78923 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -586,6 +586,7 @@ class JobService { const data = { index: job.datafeed_config.indices, body, + ...(job.datafeed_config.indices_options || {}), }; ml.esSearch(data) diff --git a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts index 7fd8b4a894989..a204bd44901b7 100644 --- a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts +++ b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts @@ -112,6 +112,40 @@ describe('cluster_serialization', () => { }, 'localhost:9300' ) + ).toEqual({ + name: 'test_cluster', + proxyAddress: 'localhost:9300', + mode: 'proxy', + hasDeprecatedProxySetting: true, + isConnected: true, + connectedNodesCount: 1, + maxConnectionsPerCluster: 3, + initialConnectTimeout: '30s', + skipUnavailable: false, + transportPingSchedule: '-1', + transportCompress: false, + }); + }); + + it('should deserialize a cluster that contains a deprecated proxy address and is in cloud', () => { + expect( + deserializeCluster( + 'test_cluster', + { + seeds: ['localhost:9300'], + connected: true, + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + transport: { + ping_schedule: '-1', + compress: false, + }, + }, + 'localhost:9300', + true + ) ).toEqual({ name: 'test_cluster', proxyAddress: 'localhost:9300', diff --git a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts index 3d8ffa13b8218..07dbe8da28d8a 100644 --- a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts +++ b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts @@ -68,7 +68,8 @@ export interface ClusterSettingsPayloadEs { export function deserializeCluster( name: string, esClusterObject: ClusterInfoEs, - deprecatedProxyAddress?: string | undefined + deprecatedProxyAddress?: string | undefined, + isCloudEnabled?: boolean | undefined ): Cluster { if (!name || !esClusterObject || typeof esClusterObject !== 'object') { throw new Error('Unable to deserialize cluster'); @@ -117,7 +118,7 @@ export function deserializeCluster( // If a user has a remote cluster with the deprecated proxy setting, // we transform the data to support the new implementation and also flag the deprecation if (deprecatedProxyAddress) { - // Create server name (address, without port), since field doesn't exist in deprecated implementation + // Cloud-specific logic: Create default server name, since field doesn't exist in deprecated implementation const defaultServerName = deprecatedProxyAddress.split(':')[0]; deserializedClusterObject = { @@ -126,7 +127,7 @@ export function deserializeCluster( seeds: undefined, hasDeprecatedProxySetting: true, mode: PROXY_MODE, - serverName: defaultServerName, + serverName: isCloudEnabled ? defaultServerName : undefined, }; } diff --git a/x-pack/plugins/remote_clusters/kibana.json b/x-pack/plugins/remote_clusters/kibana.json index 8922bf621aa03..f1b9d20f762d3 100644 --- a/x-pack/plugins/remote_clusters/kibana.json +++ b/x-pack/plugins/remote_clusters/kibana.json @@ -11,7 +11,8 @@ "indexManagement" ], "optionalPlugins": [ - "usageCollection" + "usageCollection", + "cloud" ], "server": true, "ui": true diff --git a/x-pack/plugins/remote_clusters/public/application/app_context.tsx b/x-pack/plugins/remote_clusters/public/application/app_context.tsx new file mode 100644 index 0000000000000..86c0b401d416d --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/app_context.tsx @@ -0,0 +1,22 @@ +/* + * 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, { createContext } from 'react'; + +export interface Context { + isCloudEnabled: boolean; +} + +export const AppContext = createContext({} as any); + +export const AppContextProvider = ({ + children, + context, +}: { + children: React.ReactNode; + context: Context; +}) => { + return {children}; +}; diff --git a/x-pack/plugins/remote_clusters/public/application/index.d.ts b/x-pack/plugins/remote_clusters/public/application/index.d.ts index b5c5ad5522134..b021dca51bacd 100644 --- a/x-pack/plugins/remote_clusters/public/application/index.d.ts +++ b/x-pack/plugins/remote_clusters/public/application/index.d.ts @@ -8,5 +8,8 @@ import { RegisterManagementAppArgs, I18nStart } from '../types'; export declare const renderApp: ( elem: HTMLElement | null, - I18nContext: I18nStart['Context'] + I18nContext: I18nStart['Context'], + appDependencies: { + isCloudEnabled?: boolean; + } ) => ReturnType; diff --git a/x-pack/plugins/remote_clusters/public/application/index.js b/x-pack/plugins/remote_clusters/public/application/index.js index 0b8b26ace5daa..f2d788c741342 100644 --- a/x-pack/plugins/remote_clusters/public/application/index.js +++ b/x-pack/plugins/remote_clusters/public/application/index.js @@ -11,14 +11,17 @@ import { Provider } from 'react-redux'; import { App } from './app'; import { remoteClustersStore } from './store'; +import { AppContextProvider } from './app_context'; -export const renderApp = (elem, I18nContext) => { +export const renderApp = (elem, I18nContext, appDependencies) => { render( - - - + + + + + , elem diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap index 6ff8c538ca89c..4c109c557fdb0 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap @@ -5,7 +5,6 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u disabledFields={Object {}} fields={ Object { - "mode": "sniff", "name": "", "nodeConnections": 3, "proxyAddress": "", @@ -805,6 +804,7 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u data-test-subj="remoteClusterFormServerNameFormRow" describedByIds={Array []} display="row" + error={null} fullWidth={true} hasChildLabel={true} hasEmptyLabelSpace={false} @@ -827,10 +827,11 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u } /> } + isInvalid={false} label={ } @@ -845,18 +846,21 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u className="euiFormRow__labelWrapper" >