diff --git a/docs/infrastructure/index.asciidoc b/docs/infrastructure/index.asciidoc index 60695c0e3f1cf..416e95a8941ce 100644 --- a/docs/infrastructure/index.asciidoc +++ b/docs/infrastructure/index.asciidoc @@ -21,6 +21,8 @@ You can optionally save these views and add them to {kibana-ref}/dashboard.html[ * Seamlessly switch to view the corresponding logs, application traces or uptime information for a component. +* Create alerts based on metric thresholds for one or more components. + To get started, you need to <>. Then you can <>. [role="screenshot"] diff --git a/docs/infrastructure/metrics-explorer.asciidoc b/docs/infrastructure/metrics-explorer.asciidoc index d47581ffe720a..793f09ea83b4f 100644 --- a/docs/infrastructure/metrics-explorer.asciidoc +++ b/docs/infrastructure/metrics-explorer.asciidoc @@ -20,6 +20,7 @@ By default that is set to `@timestamp`. * The interval for the X Axis is set to `auto`. The bucket size is determined by the time range. * To use *Open in Visualize* you need access to the Visualize app. +* To use *Create alert* you need to {kibana-ref}/alerting-getting-started.html#alerting-setup-prerequisites[set up alerting]. [float] [[metrics-explorer-tutorial]] @@ -67,4 +68,8 @@ Choose a graph, click the *Actions* dropdown and select *Open In Visualize*. This opens the graph in {kibana-ref}/TSVB.html[TSVB]. From here you can save the graph and add it to a dashboard as usual. +9. You can also create an alert based on the metrics in a graph. +Choose a graph, click the *Actions* dropdown and select *Create alert*. +This opens the {kibana-ref}/defining-alerts.html[alert flyout] prefilled with mertrics from the chart. + Who's the Metrics Explorer now? You are! diff --git a/package.json b/package.json index 49b5baecda474..46e0b9adfea25 100644 --- a/package.json +++ b/package.json @@ -238,6 +238,7 @@ "react-monaco-editor": "~0.27.0", "react-redux": "^7.1.3", "react-resize-detector": "^4.2.0", + "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "react-sizeme": "^2.3.6", "react-use": "^13.27.0", diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 9337daf419bfa..a3a11783cd82a 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -27,7 +27,7 @@ import TerserPlugin from 'terser-webpack-plugin'; import webpackMerge from 'webpack-merge'; // @ts-ignore import { CleanWebpackPlugin } from 'clean-webpack-plugin'; -import * as SharedDeps from '@kbn/ui-shared-deps'; +import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { Bundle, WorkerConfig, parseDirPath, DisallowedSyntaxPlugin } from '../common'; @@ -73,7 +73,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { }, externals: { - ...SharedDeps.externals, + ...UiSharedDeps.externals, }, plugins: [new CleanWebpackPlugin(), new DisallowedSyntaxPlugin()], diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index bcebdf591d6f0..5ea031595d1d4 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -38,7 +38,7 @@ "brace": "0.11.1", "chalk": "^2.4.2", "chokidar": "3.2.1", - "core-js": "^3.2.1", + "core-js": "^3.6.4", "css-loader": "^3.4.2", "expose-loader": "^0.7.5", "file-loader": "^4.2.0", diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index 5028c6efdb40e..f19271de8ad27 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -17,31 +17,40 @@ * under the License. */ -// import global polyfills before everything else require('./polyfills'); // must load before angular export const Jquery = require('jquery'); window.$ = window.jQuery = Jquery; -export const Angular = require('angular'); -export const ElasticCharts = require('@elastic/charts'); -export const ElasticEui = require('@elastic/eui'); -export const ElasticEuiLibServices = require('@elastic/eui/lib/services'); -export const ElasticEuiLightTheme = require('@elastic/eui/dist/eui_theme_light.json'); -export const ElasticEuiDarkTheme = require('@elastic/eui/dist/eui_theme_dark.json'); +// stateful deps export const KbnI18n = require('@kbn/i18n'); export const KbnI18nAngular = require('@kbn/i18n/angular'); export const KbnI18nReact = require('@kbn/i18n/react'); +export const Angular = require('angular'); export const Moment = require('moment'); export const MomentTimezone = require('moment-timezone/moment-timezone'); +export const Monaco = require('./monaco.ts'); +export const MonacoBare = require('monaco-editor/esm/vs/editor/editor.api'); export const React = require('react'); export const ReactDom = require('react-dom'); +export const ReactDomServer = require('react-dom/server'); export const ReactIntl = require('react-intl'); export const ReactRouter = require('react-router'); // eslint-disable-line export const ReactRouterDom = require('react-router-dom'); -export const Monaco = require('./monaco.ts'); -export const MonacoBare = require('monaco-editor/esm/vs/editor/editor.api'); -// load timezone data into moment-timezone Moment.tz.load(require('moment-timezone/data/packed/latest.json')); + +// big deps which are locked to a single version +export const Rxjs = require('rxjs'); +export const RxjsOperators = require('rxjs/operators'); +export const ElasticCharts = require('@elastic/charts'); +export const ElasticEui = require('@elastic/eui'); +export const ElasticEuiLibServices = require('@elastic/eui/lib/services'); +export const ElasticEuiLibServicesFormat = require('@elastic/eui/lib/services/format'); +export const ElasticEuiLightTheme = require('@elastic/eui/dist/eui_theme_light.json'); +export const ElasticEuiDarkTheme = require('@elastic/eui/dist/eui_theme_dark.json'); +export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme'); + +// massive deps that we should really get rid of or reduce in size substantially +export const ElasticsearchBrowser = require('elasticsearch-browser/elasticsearch.js'); diff --git a/packages/kbn-ui-shared-deps/index.d.ts b/packages/kbn-ui-shared-deps/index.d.ts index 7ee96050a1248..dec519da69641 100644 --- a/packages/kbn-ui-shared-deps/index.d.ts +++ b/packages/kbn-ui-shared-deps/index.d.ts @@ -25,7 +25,12 @@ export const distDir: string; /** * Filename of the main bundle file in the distributable directory */ -export const distFilename: string; +export const jsFilename: string; + +/** + * Filename of files that must be loaded before the jsFilename + */ +export const jsDepFilenames: string[]; /** * Filename of the unthemed css file in the distributable directory diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index d1bb93ddecd0a..666ec7a46ff06 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -20,17 +20,14 @@ const Path = require('path'); exports.distDir = Path.resolve(__dirname, 'target'); -exports.distFilename = 'kbn-ui-shared-deps.js'; +exports.jsDepFilenames = ['kbn-ui-shared-deps.@elastic.js']; +exports.jsFilename = 'kbn-ui-shared-deps.js'; exports.baseCssDistFilename = 'kbn-ui-shared-deps.css'; exports.lightCssDistFilename = 'kbn-ui-shared-deps.light.css'; exports.darkCssDistFilename = 'kbn-ui-shared-deps.dark.css'; exports.externals = { + // stateful deps angular: '__kbnSharedDeps__.Angular', - '@elastic/charts': '__kbnSharedDeps__.ElasticCharts', - '@elastic/eui': '__kbnSharedDeps__.ElasticEui', - '@elastic/eui/lib/services': '__kbnSharedDeps__.ElasticEuiLibServices', - '@elastic/eui/dist/eui_theme_light.json': '__kbnSharedDeps__.ElasticEuiLightTheme', - '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.ElasticEuiDarkTheme', '@kbn/i18n': '__kbnSharedDeps__.KbnI18n', '@kbn/i18n/angular': '__kbnSharedDeps__.KbnI18nAngular', '@kbn/i18n/react': '__kbnSharedDeps__.KbnI18nReact', @@ -39,10 +36,31 @@ exports.externals = { 'moment-timezone': '__kbnSharedDeps__.MomentTimezone', react: '__kbnSharedDeps__.React', 'react-dom': '__kbnSharedDeps__.ReactDom', + 'react-dom/server': '__kbnSharedDeps__.ReactDomServer', 'react-intl': '__kbnSharedDeps__.ReactIntl', 'react-router': '__kbnSharedDeps__.ReactRouter', 'react-router-dom': '__kbnSharedDeps__.ReactRouterDom', '@kbn/ui-shared-deps/monaco': '__kbnSharedDeps__.Monaco', // this is how plugins/consumers from npm load monaco 'monaco-editor/esm/vs/editor/editor.api': '__kbnSharedDeps__.MonacoBare', + + /** + * big deps which are locked to a single version + */ + rxjs: '__kbnSharedDeps__.Rxjs', + 'rxjs/operators': '__kbnSharedDeps__.RxjsOperators', + '@elastic/charts': '__kbnSharedDeps__.ElasticCharts', + '@elastic/eui': '__kbnSharedDeps__.ElasticEui', + '@elastic/eui/lib/services': '__kbnSharedDeps__.ElasticEuiLibServices', + '@elastic/eui/lib/services/format': '__kbnSharedDeps__.ElasticEuiLibServicesFormat', + '@elastic/eui/dist/eui_charts_theme': '__kbnSharedDeps__.ElasticEuiChartsTheme', + '@elastic/eui/dist/eui_theme_light.json': '__kbnSharedDeps__.ElasticEuiLightTheme', + '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.ElasticEuiDarkTheme', + + /** + * massive deps that we should really get rid of or reduce in size substantially + */ + elasticsearch: '__kbnSharedDeps__.ElasticsearchBrowser', + 'elasticsearch-browser': '__kbnSharedDeps__.ElasticsearchBrowser', + 'elasticsearch-browser/elasticsearch': '__kbnSharedDeps__.ElasticsearchBrowser', }; diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index c76e909d2adbc..e2823f23d0431 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -1,37 +1,41 @@ { "name": "@kbn/ui-shared-deps", "version": "1.0.0", - "license": "Apache-2.0", "private": true, + "license": "Apache-2.0", "scripts": { "build": "node scripts/build", "kbn:bootstrap": "node scripts/build --dev", "kbn:watch": "node scripts/build --watch" }, - "devDependencies": { + "dependencies": { "@elastic/charts": "^18.1.1", - "abortcontroller-polyfill": "^1.4.0", "@elastic/eui": "21.0.1", - "@kbn/babel-preset": "1.0.0", - "@kbn/dev-utils": "1.0.0", "@kbn/i18n": "1.0.0", - "@yarnpkg/lockfile": "^1.1.0", + "abortcontroller-polyfill": "^1.4.0", "angular": "^1.7.9", - "core-js": "^3.2.1", - "css-loader": "^3.4.2", + "core-js": "^3.6.4", "custom-event-polyfill": "^0.3.0", - "del": "^5.1.0", + "elasticsearch-browser": "^16.7.0", "jquery": "^3.4.1", - "mini-css-extract-plugin": "0.8.0", "moment": "^2.24.0", "moment-timezone": "^0.5.27", + "monaco-editor": "~0.17.0", "react": "^16.12.0", "react-dom": "^16.12.0", "react-intl": "^2.8.0", - "read-pkg": "^5.2.0", + "react-router": "^5.1.2", + "react-router-dom": "^5.1.2", "regenerator-runtime": "^0.13.3", + "rxjs": "^6.5.3", "symbol-observable": "^1.2.0", - "webpack": "^4.41.5", "whatwg-fetch": "^3.0.0" + }, + "devDependencies": { + "@kbn/babel-preset": "1.0.0", + "@kbn/dev-utils": "1.0.0", + "css-loader": "^3.4.2", + "del": "^5.1.0", + "webpack": "^4.41.5" } } diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index dc6e7ae33dbec..a875274544905 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -23,19 +23,19 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const { REPO_ROOT } = require('@kbn/dev-utils'); const webpack = require('webpack'); -const SharedDeps = require('./index'); +const UiSharedDeps = require('./index'); const MOMENT_SRC = require.resolve('moment/min/moment-with-locales.js'); exports.getWebpackConfig = ({ dev = false } = {}) => ({ mode: dev ? 'development' : 'production', entry: { - [SharedDeps.distFilename.replace(/\.js$/, '')]: './entry.js', - [SharedDeps.darkCssDistFilename.replace(/\.css$/, '')]: [ + 'kbn-ui-shared-deps': './entry.js', + 'kbn-ui-shared-deps.dark': [ '@elastic/eui/dist/eui_theme_dark.css', '@elastic/charts/dist/theme_only_dark.css', ], - [SharedDeps.lightCssDistFilename.replace(/\.css$/, '')]: [ + 'kbn-ui-shared-deps.light': [ '@elastic/eui/dist/eui_theme_light.css', '@elastic/charts/dist/theme_only_light.css', ], @@ -43,7 +43,7 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ context: __dirname, devtool: dev ? '#cheap-source-map' : false, output: { - path: SharedDeps.distDir, + path: UiSharedDeps.distDir, filename: '[name].js', sourceMapFilename: '[file].map', publicPath: '__REPLACE_WITH_PUBLIC_PATH__', @@ -81,6 +81,16 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ optimization: { noEmitOnErrors: true, + splitChunks: { + cacheGroups: { + 'kbn-ui-shared-deps.@elastic': { + name: 'kbn-ui-shared-deps.@elastic', + test: m => m.resource && m.resource.includes('@elastic'), + chunks: 'all', + enforce: true, + }, + }, + }, }, performance: { diff --git a/packages/kbn-ui-shared-deps/yarn.lock b/packages/kbn-ui-shared-deps/yarn.lock new file mode 120000 index 0000000000000..3f82ebc9cdbae --- /dev/null +++ b/packages/kbn-ui-shared-deps/yarn.lock @@ -0,0 +1 @@ +../../yarn.lock \ No newline at end of file diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 106dbcd9f8ab2..ad4aa97d8ea7a 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -76,24 +76,33 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { load({ deps: [ + {{#each sharedJsDepFilenames}} + '{{../regularBundlePath}}/kbn-ui-shared-deps/{{this}}', + {{/each}} + ], + urls: [ { deps: [ - '{{dllBundlePath}}/vendors_runtime.bundle.dll.js' + '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedJsFilename}}', + { + deps: [ + '{{dllBundlePath}}/vendors_runtime.bundle.dll.js' + ], + urls: [ + {{#each dllJsChunks}} + '{{this}}', + {{/each}} + ] + }, + '{{regularBundlePath}}/commons.bundle.js', ], urls: [ - {{#each dllJsChunks}} + '{{regularBundlePath}}/{{appId}}.bundle.js', + {{#each styleSheetPaths}} '{{this}}', {{/each}} ] - }, - '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedDepsFilename}}', - '{{regularBundlePath}}/commons.bundle.js', - ], - urls: [ - '{{regularBundlePath}}/{{appId}}.bundle.js', - {{#each styleSheetPaths}} - '{{this}}', - {{/each}}, + } ] }); }; diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 99560b0bf653f..0912d8683fc48 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -135,7 +135,8 @@ export function uiRenderMixin(kbnServer, server, config) { dllBundlePath, dllJsChunks, styleSheetPaths, - sharedDepsFilename: UiSharedDeps.distFilename, + sharedJsFilename: UiSharedDeps.jsFilename, + sharedJsDepFilenames: UiSharedDeps.jsDepFilenames, darkMode, }, }); diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts index 5626fc80bb749..dc8321aa07004 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts @@ -17,25 +17,6 @@ * under the License. */ -/* - * 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 { ValidationFunc } from '../../hook_form_lib'; import { isJSON } from '../../../validators/string'; import { ERROR_CODE } from './types'; diff --git a/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap index 57cbe0f17498f..c1dc560b4353f 100644 --- a/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap @@ -104,6 +104,7 @@ exports[`apmUiEnabled 1`] = ` { footer={ diff --git a/src/plugins/home/public/application/components/feature_directory.js b/src/plugins/home/public/application/components/feature_directory.js index 2e979bf589975..7d827b1ca9229 100644 --- a/src/plugins/home/public/application/components/feature_directory.js +++ b/src/plugins/home/public/application/components/feature_directory.js @@ -89,6 +89,7 @@ export class FeatureDirectory extends React.Component { renderTabs = () => { return this.tabs.map((tab, index) => ( this.onSelectedTabChanged(tab.id)} isSelected={tab.id === this.state.selectedTabId} key={index} diff --git a/src/plugins/home/public/application/components/home.js b/src/plugins/home/public/application/components/home.js index 77cde6a574aec..5263dc06e96fc 100644 --- a/src/plugins/home/public/application/components/home.js +++ b/src/plugins/home/public/application/components/home.js @@ -203,7 +203,7 @@ export class Home extends Component {

- + `http://localhost:5610/bundles/kbn-ui-shared-deps/${chunkFilename}` + ), + `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, + 'http://localhost:5610/built_assets/dlls/vendors_runtime.bundle.dll.js', ...DllCompiler.getRawDllConfig().chunks.map( chunk => `http://localhost:5610/built_assets/dlls/vendors${chunk}.bundle.dll.js` diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index f0a7844b29987..08e0cb0b8d23a 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -31,7 +31,8 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); const esArchiver = getService('esArchiver'); - describe('doc link in discover', function contextSize() { + // FLAKY: https://github.com/elastic/kibana/issues/62281 + describe.skip('doc link in discover', function contextSize() { this.tags('smoke'); before(async function() { await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index 6225b4e3aca62..6fdc306e39192 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -79,6 +79,39 @@ export function HomePageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click(`launchSampleDataSet${id}`); } + async clickAllKibanaPlugins() { + await testSubjects.click('allPlugins'); + } + + async clickVisualizeExplorePlugins() { + await testSubjects.click('tab-data'); + } + + async clickAdminPlugin() { + await testSubjects.click('tab-admin'); + } + + async clickOnConsole() { + await testSubjects.click('homeSynopsisLinkconsole'); + } + async clickOnLogo() { + await testSubjects.click('logo'); + } + + async ClickOnLogsData() { + await testSubjects.click('logsData'); + } + + // clicks on Active MQ logs + async clickOnLogsTutorial() { + await testSubjects.click('homeSynopsisLinkactivemq logs'); + } + + // clicks on cloud tutorial link + async clickOnCloudTutorial() { + await testSubjects.click('onCloudTutorial'); + } + async loadSavedObjects() { await retry.try(async () => { await testSubjects.click('loadSavedObjects'); diff --git a/webpackShims/elasticsearch-browser.js b/webpackShims/elasticsearch-browser.js deleted file mode 100644 index a4373dcdfe1d1..0000000000000 --- a/webpackShims/elasticsearch-browser.js +++ /dev/null @@ -1,21 +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. - */ - -require('angular'); -module.exports = require('elasticsearch-browser/elasticsearch.angular.js'); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts index 646132c3f88eb..f38cb2285b480 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts @@ -42,16 +42,15 @@ describe('Detections', () => { cy.get(NUMBER_OF_SIGNALS) .invoke('text') .then(numberOfSignals => { - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should('eql', `Showing ${numberOfSignals} signals`); + cy.get(SHOWING_SIGNALS).should('have.text', `Showing ${numberOfSignals} signals`); const numberOfSignalsToBeClosed = 3; selectNumberOfSignals(numberOfSignalsToBeClosed); - cy.get(SELECTED_SIGNALS) - .invoke('text') - .should('eql', `Selected ${numberOfSignalsToBeClosed} signals`); + cy.get(SELECTED_SIGNALS).should( + 'have.text', + `Selected ${numberOfSignalsToBeClosed} signals` + ); closeSignals(); waitForSignals(); @@ -59,30 +58,33 @@ describe('Detections', () => { waitForSignals(); const expectedNumberOfSignalsAfterClosing = +numberOfSignals - numberOfSignalsToBeClosed; - cy.get(NUMBER_OF_SIGNALS) - .invoke('text') - .should('eq', expectedNumberOfSignalsAfterClosing.toString()); - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfSignalsAfterClosing.toString()} signals`); + cy.get(NUMBER_OF_SIGNALS).should( + 'have.text', + expectedNumberOfSignalsAfterClosing.toString() + ); + + cy.get(SHOWING_SIGNALS).should( + 'have.text', + `Showing ${expectedNumberOfSignalsAfterClosing.toString()} signals` + ); goToClosedSignals(); waitForSignals(); - cy.get(NUMBER_OF_SIGNALS) - .invoke('text') - .should('eql', numberOfSignalsToBeClosed.toString()); - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should('eql', `Showing ${numberOfSignalsToBeClosed.toString()} signals`); + cy.get(NUMBER_OF_SIGNALS).should('have.text', numberOfSignalsToBeClosed.toString()); + cy.get(SHOWING_SIGNALS).should( + 'have.text', + `Showing ${numberOfSignalsToBeClosed.toString()} signals` + ); cy.get(SIGNALS).should('have.length', numberOfSignalsToBeClosed); const numberOfSignalsToBeOpened = 1; selectNumberOfSignals(numberOfSignalsToBeOpened); - cy.get(SELECTED_SIGNALS) - .invoke('text') - .should('eql', `Selected ${numberOfSignalsToBeOpened} signal`); + cy.get(SELECTED_SIGNALS).should( + 'have.text', + `Selected ${numberOfSignalsToBeOpened} signal` + ); openSignals(); waitForSignals(); @@ -93,15 +95,14 @@ describe('Detections', () => { waitForSignals(); const expectedNumberOfClosedSignalsAfterOpened = 2; - cy.get(NUMBER_OF_SIGNALS) - .invoke('text') - .should('eql', expectedNumberOfClosedSignalsAfterOpened.toString()); - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should( - 'eql', - `Showing ${expectedNumberOfClosedSignalsAfterOpened.toString()} signals` - ); + cy.get(NUMBER_OF_SIGNALS).should( + 'have.text', + expectedNumberOfClosedSignalsAfterOpened.toString() + ); + cy.get(SHOWING_SIGNALS).should( + 'have.text', + `Showing ${expectedNumberOfClosedSignalsAfterOpened.toString()} signals` + ); cy.get(SIGNALS).should('have.length', expectedNumberOfClosedSignalsAfterOpened); goToOpenedSignals(); @@ -109,13 +110,15 @@ describe('Detections', () => { const expectedNumberOfOpenedSignals = +numberOfSignals - expectedNumberOfClosedSignalsAfterOpened; - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfOpenedSignals.toString()} signals`); - - cy.get('[data-test-subj="server-side-event-count"]') - .invoke('text') - .should('eql', expectedNumberOfOpenedSignals.toString()); + cy.get(SHOWING_SIGNALS).should( + 'have.text', + `Showing ${expectedNumberOfOpenedSignals.toString()} signals` + ); + + cy.get('[data-test-subj="server-side-event-count"]').should( + 'have.text', + expectedNumberOfOpenedSignals.toString() + ); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts index f388ac1215d01..cb776be8d7b6b 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts @@ -10,7 +10,7 @@ export const LOADING_SIGNALS_PANEL = '[data-test-subj="loading-signals-panel"]'; export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal-detection-rules"]'; -export const NUMBER_OF_SIGNALS = '[data-test-subj="server-side-event-count"]'; +export const NUMBER_OF_SIGNALS = '[data-test-subj="server-side-event-count"] .euiBadge__text'; export const OPEN_CLOSE_SIGNAL_BTN = '[data-test-subj="update-signal-status-button"]'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap index 65a606604d4a7..1bee36ed9e185 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RuleActionsOverflow renders correctly against snapshot 1`] = ` +exports[`RuleActionsOverflow snapshots renders correctly against snapshot 1`] = ` } closePopover={[Function]} + data-test-subj="rules-details-popover" display="inlineBlock" hasArrow={true} id="ruleActionsOverflow" @@ -27,24 +29,28 @@ exports[`RuleActionsOverflow renders correctly against snapshot 1`] = ` panelPaddingSize="none" > Duplicate ruleā€¦ , Export rule , ({ }), })); +jest.mock('../../all/actions', () => ({ + deleteRulesAction: jest.fn(), + duplicateRulesAction: jest.fn(), +})); + describe('RuleActionsOverflow', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); + describe('snapshots', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + }); + + describe('rules details menu panel', () => { + test('there is at least one item when there is a rule within the rules-details-menu-panel', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + const items: unknown[] = wrapper + .find('[data-test-subj="rules-details-menu-panel"]') + .first() + .prop('items'); + + expect(items.length).toBeGreaterThan(0); + }); + + test('items are empty when there is a null rule within the rules-details-menu-panel', () => { + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-menu-panel"]') + .first() + .prop('items') + ).toEqual([]); + }); + + test('items are empty when there is an undefined rule within the rules-details-menu-panel', () => { + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-menu-panel"]') + .first() + .prop('items') + ).toEqual([]); + }); + + test('it opens the popover when rules-details-popover-button-icon is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(true); + }); + }); + + describe('rules details pop over button icon', () => { + test('it does not open the popover when rules-details-popover-button-icon is clicked when the user does not have permission', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(false); + }); + }); + + describe('rules details duplicate rule', () => { + test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="rules-details-delete-rule"] button').exists()).toEqual( + false + ); + }); + + test('it opens the popover when rules-details-popover-button-icon is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(true); + }); + + test('it closes the popover when rules-details-duplicate-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(false); + }); + + test('it calls duplicateRulesAction when rules-details-duplicate-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); + wrapper.update(); + expect(duplicateRulesAction).toHaveBeenCalled(); + }); + + test('it calls duplicateRulesAction with the rule and rule.id when rules-details-duplicate-rule is clicked', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); + wrapper.update(); + expect(duplicateRulesAction).toHaveBeenCalledWith( + [rule], + [rule.id], + expect.anything(), + expect.anything() + ); + }); + }); + + describe('rules details export rule', () => { + test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="rules-details-export-rule"] button').exists()).toEqual( + false + ); + }); + + test('it closes the popover when rules-details-export-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(false); + }); + + test('it sets the rule.rule_id on the generic downloader when rules-details-export-rule is clicked', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper.find('[data-test-subj="rules-details-generic-downloader"]').prop('ids') + ).toEqual([rule.rule_id]); + }); + + test('it does not close the pop over on rules-details-export-rule when the rule is an immutable rule and the user does a click', () => { + const rule = mockRule('id'); + rule.immutable = true; + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(true); + }); + + test('it does not set the rule.rule_id on rules-details-export-rule when the rule is an immutable rule', () => { + const rule = mockRule('id'); + rule.immutable = true; + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper.find('[data-test-subj="rules-details-generic-downloader"]').prop('ids') + ).toEqual([]); + }); + }); + + describe('rules details delete rule', () => { + test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="rules-details-delete-rule"] button').exists()).toEqual( + false + ); + }); + + test('it closes the popover when rules-details-delete-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(false); + }); + + test('it calls deleteRulesAction when rules-details-delete-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); + wrapper.update(); + expect(deleteRulesAction).toHaveBeenCalled(); + }); + + test('it calls deleteRulesAction with the rule.id when rules-details-delete-rule is clicked', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); + wrapper.update(); + expect(deleteRulesAction).toHaveBeenCalledWith( + [rule.id], + expect.anything(), + expect.anything(), + expect.anything() + ); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx index e1ca84ed8cc64..a7ce0c85ffdcf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx @@ -62,8 +62,9 @@ const RuleActionsOverflowComponent = ({ ? [ { setIsPopoverOpen(false); await duplicateRulesAction([rule], [rule.id], noop, dispatchToaster); @@ -73,11 +74,12 @@ const RuleActionsOverflowComponent = ({ , { setIsPopoverOpen(false); - setRulesToExport([rule.id]); + setRulesToExport([rule.rule_id]); }} > {i18nActions.EXPORT_RULE} @@ -86,6 +88,7 @@ const RuleActionsOverflowComponent = ({ key={i18nActions.DELETE_RULE} icon="trash" disabled={userHasNoPermissions} + data-test-subj="rules-details-delete-rule" onClick={async () => { setIsPopoverOpen(false); await deleteRulesAction([rule.id], noop, dispatchToaster, onRuleDeletedCallback); @@ -109,6 +112,7 @@ const RuleActionsOverflowComponent = ({ iconType="boxesHorizontal" aria-label={i18n.ALL_ACTIONS} isDisabled={userHasNoPermissions} + data-test-subj="rules-details-popover-button-icon" onClick={handlePopoverOpen} /> @@ -124,15 +128,17 @@ const RuleActionsOverflowComponent = ({ closePopover={() => setIsPopoverOpen(false)} id="ruleActionsOverflow" isOpen={isPopoverOpen} + data-test-subj="rules-details-popover" ownFocus={true} panelPaddingSize="none" > - + { displaySuccessToast( i18nActions.SUCCESSFULLY_EXPORTED_RULES(exportCount), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts index 8e4b5ce3c9924..bdbb6ff7d1052 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts @@ -28,25 +28,23 @@ describe('buildRuleMessageFactory', () => { expect(message).toEqual(expect.stringContaining('signals index: "index"')); }); - it('joins message parts with newlines', () => { + it('joins message parts with spaces', () => { const buildMessage = buildRuleMessageFactory(factoryParams); const message = buildMessage('my message'); - const messageParts = message.split('\n'); - expect(messageParts).toContain('my message'); - expect(messageParts).toContain('name: "name"'); - expect(messageParts).toContain('id: "id"'); - expect(messageParts).toContain('rule id: "ruleId"'); - expect(messageParts).toContain('signals index: "index"'); + expect(message).toEqual(expect.stringContaining('my message ')); + expect(message).toEqual(expect.stringContaining(' name: "name" ')); + expect(message).toEqual(expect.stringContaining(' id: "id" ')); + expect(message).toEqual(expect.stringContaining(' rule id: "ruleId" ')); + expect(message).toEqual(expect.stringContaining(' signals index: "index"')); }); - it('joins multiple arguments with newlines', () => { + it('joins multiple arguments with spaces', () => { const buildMessage = buildRuleMessageFactory(factoryParams); const message = buildMessage('my message', 'here is more'); - const messageParts = message.split('\n'); - expect(messageParts).toContain('my message'); - expect(messageParts).toContain('here is more'); + expect(message).toEqual(expect.stringContaining('my message ')); + expect(message).toEqual(expect.stringContaining(' here is more')); }); it('defaults the rule ID if not provided ', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts index d5f9d332bbcdd..cc97a1f8a9f0b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts @@ -24,4 +24,4 @@ export const buildRuleMessageFactory = ({ `id: "${id}"`, `rule id: "${ruleId ?? '(unknown rule id)'}"`, `signals index: "${index}"`, - ].join('\n'); + ].join(' '); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 91905722fbca3..246701e94c99a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -55,6 +55,7 @@ export const signalRulesAlertType = ({ index, filters, language, + maxSignals, meta, machineLearningJobId, outputIndex, @@ -63,6 +64,14 @@ export const signalRulesAlertType = ({ to, type, } = params; + const searchAfterSize = Math.min(maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); + let hasError: boolean = false; + let result: SearchAfterAndBulkCreateReturnType = { + success: false, + bulkCreateTimes: [], + searchAfterTimes: [], + lastLookBackDate: null, + }; const ruleStatusClient = ruleStatusSavedObjectsClientFactory(services.savedObjectsClient); const ruleStatusService = await ruleStatusServiceFactory({ alertId, @@ -104,17 +113,10 @@ export const signalRulesAlertType = ({ ); logger.warn(gapMessage); + hasError = true; await ruleStatusService.error(gapMessage, { gap: gapString }); } - const searchAfterSize = Math.min(params.maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); - let result: SearchAfterAndBulkCreateReturnType = { - success: false, - bulkCreateTimes: [], - searchAfterTimes: [], - lastLookBackDate: null, - }; - try { if (isMlRule(type)) { if (ml == null) { @@ -126,7 +128,7 @@ export const signalRulesAlertType = ({ 'Machine learning rule is missing job id and/or anomaly threshold:', `job id: "${machineLearningJobId}"`, `anomaly threshold: "${anomalyThreshold}"`, - ].join('\n') + ].join(' ') ); } @@ -143,6 +145,7 @@ export const signalRulesAlertType = ({ `datafeed status: "${jobSummary?.datafeedState}"` ); logger.warn(errorMessage); + hasError = true; await ruleStatusService.error(errorMessage); } @@ -270,11 +273,13 @@ export const signalRulesAlertType = ({ } logger.debug(buildRuleMessage('[+] Signal Rule execution completed.')); - await ruleStatusService.success('succeeded', { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, - lastLookBackDate: result.lastLookBackDate?.toISOString(), - }); + if (!hasError) { + await ruleStatusService.success('succeeded', { + bulkCreateTimeDurations: result.bulkCreateTimes, + searchAfterTimeDurations: result.searchAfterTimes, + lastLookBackDate: result.lastLookBackDate?.toISOString(), + }); + } } else { const errorMessage = buildRuleMessage( 'Bulk Indexing of signals failed. Check logs for further details.' diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts index 5596d0c70f5ea..f69a715f9b2c9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts @@ -127,7 +127,7 @@ export const saveNotes = ( existingNoteIds?: string[], newNotes?: NoteResult[] ) => { - return ( + return Promise.all( newNotes?.map(note => { const newNote: SavedNote = { eventId: note.eventId, diff --git a/x-pack/package.json b/x-pack/package.json index bbab1a96f52f4..24b23256bf18e 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -315,6 +315,7 @@ "react-portal": "^3.2.0", "react-redux": "^7.1.3", "react-reverse-portal": "^1.0.4", + "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "react-shortcuts": "^2.0.0", "react-sticky": "^6.0.3", diff --git a/x-pack/plugins/endpoint/common/generate_data.test.ts b/x-pack/plugins/endpoint/common/generate_data.test.ts index dfb906c7af606..88e1c66ea3e82 100644 --- a/x-pack/plugins/endpoint/common/generate_data.test.ts +++ b/x-pack/plugins/endpoint/common/generate_data.test.ts @@ -86,7 +86,7 @@ describe('data generator', () => { let events: Event[]; beforeEach(() => { - events = generator.generateAlertEventAncestry(3); + events = generator.createAlertEventAncestry(3); }); it('with n-1 process events', () => { @@ -153,7 +153,7 @@ describe('data generator', () => { const timestamp = new Date().getTime(); const root = generator.generateEvent({ timestamp }); const generations = 2; - const events = [root, ...generator.generateDescendantsTree(root, generations)]; + const events = [root, ...generator.descendantsTreeGenerator(root, generations)]; const rootNode = buildResolverTree(events); const visitedEvents = countResolverEvents(rootNode, generations); expect(visitedEvents).toEqual(events.length); @@ -162,7 +162,7 @@ describe('data generator', () => { it('creates full resolver tree', () => { const alertAncestors = 3; const generations = 2; - const events = generator.generateFullResolverTree(alertAncestors, generations); + const events = [...generator.fullResolverTreeGenerator(alertAncestors, generations)]; const rootNode = buildResolverTree(events); const visitedEvents = countResolverEvents(rootNode, alertAncestors + generations); expect(visitedEvents).toEqual(events.length); diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 430ba1d422b96..0ec105129b7ac 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -100,19 +100,30 @@ interface HostInfo { }; } +interface NodeState { + event: Event; + childrenCreated: number; + maxChildren: number; +} + export class EndpointDocGenerator { commonInfo: HostInfo; random: seedrandom.prng; - constructor(seed = Math.random().toString()) { - this.random = seedrandom(seed); + constructor(seed: string | seedrandom.prng = Math.random().toString()) { + if (typeof seed === 'string') { + this.random = seedrandom(seed); + } else { + this.random = seed; + } this.commonInfo = this.createHostData(); } - // This function will create new values for all the host fields, so documents from a different host can be created - // This provides a convenient way to make documents from multiple hosts that are all tied to a single seed value - public randomizeHostData() { - this.commonInfo = this.createHostData(); + /** + * Creates new random IP addresses for the host to simulate new DHCP assignment + */ + public updateHostData() { + this.commonInfo.host.ip = this.randomArray(3, () => this.randomIP()); } private createHostData(): HostInfo { @@ -139,6 +150,10 @@ export class EndpointDocGenerator { }; } + /** + * Creates a host metadata document + * @param ts - Timestamp to put in the event + */ public generateHostMetadata(ts = new Date().getTime()): HostMetadata { return { '@timestamp': ts, @@ -149,6 +164,12 @@ export class EndpointDocGenerator { }; } + /** + * Creates an alert from the simulated host represented by this EndpointDocGenerator + * @param ts - Timestamp to put in the event + * @param entityID - entityID of the originating process + * @param parentEntityID - optional entityID of the parent process, if it exists + */ public generateAlert( ts = new Date().getTime(), entityID = this.randomString(10), @@ -183,7 +204,7 @@ export class EndpointDocGenerator { trusted: false, subject_name: 'bad signer', }, - malware_classifier: { + malware_classification: { identifier: 'endpointpe', score: 1, threshold: 0.66, @@ -241,7 +262,7 @@ export class EndpointDocGenerator { sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', }, - malware_classifier: { + malware_classification: { identifier: 'Whitelisted', score: 0, threshold: 0, @@ -255,6 +276,10 @@ export class EndpointDocGenerator { }; } + /** + * Creates an event, customized by the options parameter + * @param options - Allows event field values to be specified + */ public generateEvent(options: EventOptions = {}): EndpointEvent { return { '@timestamp': options.timestamp ? options.timestamp : new Date().getTime(), @@ -277,17 +302,31 @@ export class EndpointDocGenerator { }; } - public generateFullResolverTree( + /** + * Generator function that creates the full set of events needed to render resolver. + * The number of nodes grows exponentially with the number of generations and children per node. + * Each node is logically a process, and will have 1 or more process events associated with it. + * @param alertAncestors - number of ancestor generations to create relative to the alert + * @param childGenerations - number of child generations to create relative to the alert + * @param maxChildrenPerNode - maximum number of children for any given node in the tree + * @param relatedEventsPerNode - number of related events (file, registry, etc) to create for each process event in the tree + * @param percentNodesWithRelated - percent of nodes which should have related events + * @param percentChildrenTerminated - percent of nodes which will have process termination events + */ + public *fullResolverTreeGenerator( alertAncestors?: number, childGenerations?: number, maxChildrenPerNode?: number, relatedEventsPerNode?: number, percentNodesWithRelated?: number, percentChildrenTerminated?: number - ): Event[] { - const ancestry = this.generateAlertEventAncestry(alertAncestors); + ) { + const ancestry = this.createAlertEventAncestry(alertAncestors); + for (let i = 0; i < ancestry.length; i++) { + yield ancestry[i]; + } // ancestry will always have at least 2 elements, and the second to last element will be the process associated with the alert - const descendants = this.generateDescendantsTree( + yield* this.descendantsTreeGenerator( ancestry[ancestry.length - 2], childGenerations, maxChildrenPerNode, @@ -295,10 +334,13 @@ export class EndpointDocGenerator { percentNodesWithRelated, percentChildrenTerminated ); - return ancestry.concat(descendants); } - public generateAlertEventAncestry(alertAncestors = 3): Event[] { + /** + * Creates an alert event and associated process ancestry. The alert event will always be the last event in the return array. + * @param alertAncestors - number of ancestor generations to create + */ + public createAlertEventAncestry(alertAncestors = 3): Event[] { const events = []; const startDate = new Date().getTime(); const root = this.generateEvent({ timestamp: startDate + 1000 }); @@ -321,75 +363,93 @@ export class EndpointDocGenerator { return events; } - public generateDescendantsTree( + /** + * Creates the child generations of a process. The number of returned events grows exponentially with generations and maxChildrenPerNode. + * @param root - The process event to use as the root node of the tree + * @param generations - number of child generations to create. The root node is not counted as a generation. + * @param maxChildrenPerNode - maximum number of children for any given node in the tree + * @param relatedEventsPerNode - number of related events (file, registry, etc) to create for each process event in the tree + * @param percentNodesWithRelated - percent of nodes which should have related events + * @param percentChildrenTerminated - percent of nodes which will have process termination events + */ + public *descendantsTreeGenerator( root: Event, generations = 2, maxChildrenPerNode = 2, relatedEventsPerNode = 3, percentNodesWithRelated = 100, percentChildrenTerminated = 100 - ): Event[] { - let events: Event[] = []; - let parents = [root]; + ) { + const rootState: NodeState = { + event: root, + childrenCreated: 0, + maxChildren: this.randomN(maxChildrenPerNode + 1), + }; + const lineage: NodeState[] = [rootState]; let timestamp = root['@timestamp']; - for (let i = 0; i < generations; i++) { - const newParents: EndpointEvent[] = []; - parents.forEach(element => { - const numChildren = this.randomN(maxChildrenPerNode + 1); - for (let j = 0; j < numChildren; j++) { - timestamp = timestamp + 1000; - const child = this.generateEvent({ - timestamp, - parentEntityID: element.process.entity_id, - }); - newParents.push(child); - } + while (lineage.length > 0) { + const currentState = lineage[lineage.length - 1]; + // If we get to a state node and it has made all the children, move back up a level + if ( + currentState.childrenCreated === currentState.maxChildren || + lineage.length === generations + 1 + ) { + lineage.pop(); + continue; + } + // Otherwise, add a child and any nodes associated with it + currentState.childrenCreated++; + timestamp = timestamp + 1000; + const child = this.generateEvent({ + timestamp, + parentEntityID: currentState.event.process.entity_id, }); - events = events.concat(newParents); - parents = newParents; - } - const terminationEvents: EndpointEvent[] = []; - let relatedEvents: EndpointEvent[] = []; - events.forEach(element => { + lineage.push({ + event: child, + childrenCreated: 0, + maxChildren: this.randomN(maxChildrenPerNode + 1), + }); + yield child; + let processDuration: number = 6 * 3600; if (this.randomN(100) < percentChildrenTerminated) { - timestamp = timestamp + 1000; - terminationEvents.push( - this.generateEvent({ - timestamp, - entityID: element.process.entity_id, - parentEntityID: element.process.parent?.entity_id, - eventCategory: 'process', - eventType: 'end', - }) - ); + processDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days) + yield this.generateEvent({ + timestamp: timestamp + processDuration * 1000, + entityID: child.process.entity_id, + parentEntityID: child.process.parent?.entity_id, + eventCategory: 'process', + eventType: 'end', + }); } if (this.randomN(100) < percentNodesWithRelated) { - relatedEvents = relatedEvents.concat( - this.generateRelatedEvents(element, relatedEventsPerNode) - ); + yield* this.relatedEventsGenerator(child, relatedEventsPerNode, processDuration); } - }); - events = events.concat(terminationEvents); - events = events.concat(relatedEvents); - return events; + } } - public generateRelatedEvents(node: Event, numRelatedEvents = 10): EndpointEvent[] { - const ts = node['@timestamp'] + 1000; - const relatedEvents: EndpointEvent[] = []; + /** + * Creates related events for a process event + * @param node - process event to relate events to by entityID + * @param numRelatedEvents - number of related events to generate + * @param processDuration - maximum number of seconds after process event that related event timestamp can be + */ + public *relatedEventsGenerator( + node: Event, + numRelatedEvents = 10, + processDuration: number = 6 * 3600 + ) { for (let i = 0; i < numRelatedEvents; i++) { const eventInfo = this.randomChoice(OTHER_EVENT_CATEGORIES); - relatedEvents.push( - this.generateEvent({ - timestamp: ts, - entityID: node.process.entity_id, - parentEntityID: node.process.parent?.entity_id, - eventCategory: eventInfo.category, - eventType: eventInfo.creationType, - }) - ); + + const ts = node['@timestamp'] + this.randomN(processDuration) * 1000; + yield this.generateEvent({ + timestamp: ts, + entityID: node.process.entity_id, + parentEntityID: node.process.parent?.entity_id, + eventCategory: eventInfo.category, + eventType: eventInfo.creationType, + }); } - return relatedEvents; } private randomN(n: number): number { diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 565f47e7a0d6f..e8e1281a88925 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -113,7 +113,7 @@ export interface HashFields { sha1: string; sha256: string; } -export interface MalwareClassifierFields { +export interface MalwareClassificationFields { identifier: string; score: number; threshold: number; @@ -142,7 +142,7 @@ export interface DllFields { }; compile_time: number; hash: HashFields; - malware_classifier: MalwareClassifierFields; + malware_classification: MalwareClassificationFields; mapped_address: number; mapped_size: number; path: string; @@ -194,7 +194,7 @@ export type AlertEvent = Immutable<{ executable: string; sid?: string; start: number; - malware_classifier?: MalwareClassifierFields; + malware_classification?: MalwareClassificationFields; token: { domain: string; type: string; @@ -224,7 +224,7 @@ export type AlertEvent = Immutable<{ trusted: boolean; subject_name: string; }; - malware_classifier: MalwareClassifierFields; + malware_classification: MalwareClassificationFields; temp_file_path: string; }; host: HostFields; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx index 0183e9663bb44..79cb61693056c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx @@ -40,7 +40,7 @@ export const GeneralAccordion = memo(({ alertData }: { alertData: Immutable { } else if (columnId === 'archived') { return null; } else if (columnId === 'malware_score') { - return row.file.malware_classifier.score; + return row.file.malware_classification.score; } return null; }; diff --git a/x-pack/plugins/endpoint/scripts/mapping.json b/x-pack/plugins/endpoint/scripts/mapping.json index 34c039d643517..5878e01b52a47 100644 --- a/x-pack/plugins/endpoint/scripts/mapping.json +++ b/x-pack/plugins/endpoint/scripts/mapping.json @@ -90,7 +90,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { @@ -452,7 +452,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { @@ -849,7 +849,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { @@ -1494,7 +1494,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { @@ -1687,7 +1687,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { diff --git a/x-pack/plugins/endpoint/scripts/resolver_generator.ts b/x-pack/plugins/endpoint/scripts/resolver_generator.ts index 3d11ccaad005d..aebf92eff6cb8 100644 --- a/x-pack/plugins/endpoint/scripts/resolver_generator.ts +++ b/x-pack/plugins/endpoint/scripts/resolver_generator.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import * as yargs from 'yargs'; +import seedrandom from 'seedrandom'; import { Client, ClientOptions } from '@elastic/elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; -import { EndpointDocGenerator } from '../common/generate_data'; +import { EndpointDocGenerator, Event } from '../common/generate_data'; import { default as mapping } from './mapping.json'; main(); @@ -137,14 +138,24 @@ async function main() { // eslint-disable-next-line no-console console.log('No seed supplied, using random seed: ' + seed); } - const generator = new EndpointDocGenerator(seed); + const random = seedrandom(seed); for (let i = 0; i < argv.numHosts; i++) { - await client.index({ - index: argv.metadataIndex, - body: generator.generateHostMetadata(), - }); + const generator = new EndpointDocGenerator(random); + const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents + const numMetadataDocs = 5; + const timestamp = new Date().getTime(); + for (let j = 0; j < numMetadataDocs; j++) { + generator.updateHostData(); + await client.index({ + index: argv.metadataIndex, + body: generator.generateHostMetadata( + timestamp - timeBetweenDocs * (numMetadataDocs - j - 1) + ), + }); + } + for (let j = 0; j < argv.alertsPerHost; j++) { - const resolverDocs = generator.generateFullResolverTree( + const resolverDocGenerator = generator.fullResolverTreeGenerator( argv.ancestors, argv.generations, argv.children, @@ -152,15 +163,23 @@ async function main() { argv.percentWithRelated, argv.percentTerminated ); - const body = resolverDocs.reduce( - (array: Array>, doc) => ( - array.push({ index: { _index: argv.eventIndex } }, doc), array - ), - [] - ); - - await client.bulk({ body }); + let result = resolverDocGenerator.next(); + while (!result.done) { + let k = 0; + const resolverDocs: Event[] = []; + while (k < 1000 && !result.done) { + resolverDocs.push(result.value); + result = resolverDocGenerator.next(); + k++; + } + const body = resolverDocs.reduce( + (array: Array>, doc) => ( + array.push({ index: { _index: argv.eventIndex } }, doc), array + ), + [] + ); + await client.bulk({ body }); + } } - generator.randomizeHostData(); } } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts index 663017e2e47af..cc4c17c5c63a3 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts @@ -63,6 +63,10 @@ export * from './max_shingle_size_parameter'; export * from './relations_parameter'; +export * from './other_type_name_parameter'; + +export * from './other_type_json_parameter'; + export const PARAMETER_SERIALIZERS = [relationsSerializer, dynamicSerializer]; export const PARAMETER_DESERIALIZERS = [relationsDeserializer, dynamicDeserializer]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_json_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_json_parameter.tsx new file mode 100644 index 0000000000000..64e50f711a249 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_json_parameter.tsx @@ -0,0 +1,92 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { + UseField, + JsonEditorField, + ValidationFuncArg, + fieldValidators, + FieldConfig, +} from '../../../shared_imports'; + +const { isJsonField } = fieldValidators; + +/** + * This is a special component that does not have an explicit entry in {@link PARAMETERS_DEFINITION}. + * + * We use it to store custom defined parameters in a field called "otherTypeJson". + */ + +const fieldConfig: FieldConfig = { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.otherTypeJsonFieldLabel', { + defaultMessage: 'Type Parameters JSON', + }), + defaultValue: {}, + validations: [ + { + validator: isJsonField( + i18n.translate( + 'xpack.idxMgmt.mappingsEditor.parameters.validations.otherTypeJsonInvalidJSONErrorMessage', + { + defaultMessage: 'Invalid JSON.', + } + ) + ), + }, + { + validator: ({ value }: ValidationFuncArg) => { + const json = JSON.parse(value); + if (Array.isArray(json)) { + return { + message: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.parameters.validations.otherTypeJsonArrayNotAllowedErrorMessage', + { + defaultMessage: 'Arrays are not allowed.', + } + ), + }; + } + }, + }, + { + validator: ({ value }: ValidationFuncArg) => { + const json = JSON.parse(value); + if (json.type) { + return { + code: 'ERR_CUSTOM_TYPE_OVERRIDDEN', + message: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.parameters.validations.otherTypeJsonTypeFieldErrorMessage', + { + defaultMessage: 'Cannot override the "type" field.', + } + ), + }; + } + }, + }, + ], + deserializer: (value: any) => { + if (value === '') { + return value; + } + return JSON.stringify(value, null, 2); + }, + serializer: (value: string) => { + try { + return JSON.parse(value); + } catch (error) { + // swallow error and return non-parsed value; + return value; + } + }, +}; + +export const OtherTypeJsonParameter = () => ( + +); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_name_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_name_parameter.tsx new file mode 100644 index 0000000000000..6004e484323a1 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_name_parameter.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { UseField, TextField, FieldConfig } from '../../../shared_imports'; +import { fieldValidators } from '../../../shared_imports'; + +const { emptyField } = fieldValidators; + +/** + * This is a special component that does not have an explicit entry in {@link PARAMETERS_DEFINITION}. + * + * We use it to store the name of types unknown to the mappings editor in the "subType" path. + */ + +const fieldConfig: FieldConfig = { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.otherTypeNameFieldLabel', { + defaultMessage: 'Type Name', + }), + defaultValue: '', + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.idxMgmt.mappingsEditor.parameters.validations.otherTypeNameIsRequiredErrorMessage', + { + defaultMessage: 'The type name is required.', + } + ) + ), + }, + ], +}; + +export const OtherTypeNameParameter = () => ( + +); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx index 60b025ce644ef..b41f35b983885 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx @@ -5,6 +5,7 @@ */ import React, { useEffect, useCallback } from 'react'; import classNames from 'classnames'; +import * as _ from 'lodash'; import { i18n } from '@kbn/i18n'; @@ -31,7 +32,7 @@ import { filterTypesForNonRootFields, } from '../../../../lib'; import { Field, MainType, SubType, NormalizedFields, ComboBoxOption } from '../../../../types'; -import { NameParameter, TypeParameter } from '../../field_parameters'; +import { NameParameter, TypeParameter, OtherTypeNameParameter } from '../../field_parameters'; import { getParametersFormForType } from './required_parameters_forms'; const formWrapper = (props: any) =>
; @@ -155,9 +156,9 @@ export const CreateField = React.memo(function CreateFieldComponent({ }, [form, getSubTypeMeta] ); - const renderFormFields = useCallback( ({ type }) => { + const isOtherType = type === 'other'; const { subTypeOptions, subTypeLabel } = getSubTypeMeta(type); const docLink = documentationService.getTypeDocLink(type) as string; @@ -178,7 +179,13 @@ export const CreateField = React.memo(function CreateFieldComponent({ docLink={docLink} /> - {/* Field sub type (if any) */} + {/* Other type */} + {isOtherType && ( + + + + )} + {/* Field sub type (if any) - will never be the case if we have an "other" type */} {subTypeOptions && ( {/* Documentation link */} - - - {i18n.translate( - 'xpack.idxMgmt.mappingsEditor.editField.typeDocumentation', - { - defaultMessage: '{type} documentation', - values: { - type: subTypeDefinition - ? subTypeDefinition.label - : typeDefinition.label, - }, - } - )} - - + {linkDocumentation && ( + + + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.editField.typeDocumentation', + { + defaultMessage: '{type} documentation', + values: { + type: subTypeDefinition + ? subTypeDefinition.label + : typeDefinition.label, + }, + } + )} + + + )} {/* Field path */} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx index ddb808094428d..75a083d64b6db 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx @@ -17,7 +17,7 @@ import { } from '../../../../lib'; import { TYPE_DEFINITION } from '../../../../constants'; -import { NameParameter, TypeParameter } from '../../field_parameters'; +import { NameParameter, TypeParameter, OtherTypeNameParameter } from '../../field_parameters'; import { FieldDescriptionSection } from './field_description_section'; interface Props { @@ -80,9 +80,17 @@ export const EditFieldHeaderForm = React.memo( /> - {/* Field sub type (if any) */} + {/* Other type */} + {type === 'other' && ( + + + + )} + + {/* Field sub type (if any) - will never be the case if we have an "other" type */} {hasSubType && ( + {' '} } = { shape: ShapeType, dense_vector: DenseVectorType, object: ObjectType, + other: OtherType, nested: NestedType, join: JoinType, }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/other_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/other_type.tsx new file mode 100644 index 0000000000000..c403bbfb79056 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/other_type.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +import { OtherTypeJsonParameter } from '../../field_parameters'; +import { BasicParametersSection } from '../edit_field'; + +export const OtherType = () => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 4c1c8bc1da114..f274159bd6c30 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -16,11 +16,13 @@ import { import { i18n } from '@kbn/i18n'; import { NormalizedField, NormalizedFields } from '../../../types'; +import { getTypeLabelFromType } from '../../../lib'; import { TYPE_DEFINITION, CHILD_FIELD_INDENT_SIZE, LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER, } from '../../../constants'; + import { FieldsList } from './fields_list'; import { CreateField } from './create_field'; import { DeleteFieldProvider } from './delete_field_provider'; @@ -265,7 +267,7 @@ function FieldListItemComponent( dataType: TYPE_DEFINITION[source.type].label, }, }) - : TYPE_DEFINITION[source.type].label} + : getTypeLabelFromType(source.type)} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx index dbb8a788514bc..614b7cb56bef6 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { SearchResult } from '../../../types'; import { TYPE_DEFINITION } from '../../../constants'; import { useDispatch } from '../../../mappings_state'; +import { getTypeLabelFromType } from '../../../lib'; import { DeleteFieldProvider } from '../fields/delete_field_provider'; interface Props { @@ -115,7 +116,7 @@ export const SearchResultItem = React.memo(function FieldListItemFlatComponent({ dataType: TYPE_DEFINITION[source.type].label, }, }) - : TYPE_DEFINITION[source.type].label} + : getTypeLabelFromType(source.type)} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx index f904281181c48..4206fe8b696da 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx @@ -784,6 +784,20 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = {

), }, + other: { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.otherDescription', { + defaultMessage: 'Other', + }), + value: 'other', + description: () => ( +

+ +

+ ), + }, }; export const MAIN_TYPES: MainType[] = [ @@ -811,6 +825,7 @@ export const MAIN_TYPES: MainType[] = [ 'shape', 'text', 'token_count', + 'other', ]; export const MAIN_DATA_TYPE_DEFINITION: { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx index 5a277073c5f1a..618d106b0e7a1 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx @@ -185,8 +185,6 @@ const getSearchMetadata = (searchData: SearchData, fieldData: FieldData): Search const score = calculateScore(metadata); const display = getJSXdisplayFromMeta(searchData, fieldData, metadata); - // console.log(fieldData.path, score, metadata); - return { ...metadata, display, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts index 131d886ff05d9..6b817c829251f 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts @@ -45,16 +45,19 @@ const runParametersDeserializers = (field: Field): Field => ); export const fieldSerializer: SerializerFunc = (field: Field) => { + const { otherTypeJson, ...rest } = field; + const updatedField: Field = Boolean(otherTypeJson) ? { ...otherTypeJson, ...rest } : { ...rest }; + // If a subType is present, use it as type for ES - if ({}.hasOwnProperty.call(field, 'subType')) { - field.type = field.subType as DataType; - delete field.subType; + if ({}.hasOwnProperty.call(updatedField, 'subType')) { + updatedField.type = updatedField.subType as DataType; + delete updatedField.subType; } // Delete temp fields - delete (field as any).useSameAnalyzerForSearch; + delete (updatedField as any).useSameAnalyzerForSearch; - return sanitizeField(runParametersSerializers(field)); + return sanitizeField(runParametersSerializers(updatedField)); }; export const fieldDeserializer: SerializerFunc = (field: Field): Field => { @@ -70,8 +73,18 @@ export const fieldDeserializer: SerializerFunc = (field: Field): Field => field.type = type; } - (field as any).useSameAnalyzerForSearch = - {}.hasOwnProperty.call(field, 'search_analyzer') === false; + if (field.type === 'other') { + const { type, subType, name, ...otherTypeJson } = field; + /** + * For "other" type (type we don't support through a form) + * we grab all the parameters and put them in the "otherTypeJson" object + * that we will render in a JSON editor. + */ + field.otherTypeJson = otherTypeJson; + } else { + (field as any).useSameAnalyzerForSearch = + {}.hasOwnProperty.call(field, 'search_analyzer') === false; + } return runParametersDeserializers(field); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts index 337554ab5fa5a..cece26618ced8 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts @@ -25,6 +25,7 @@ import { PARAMETERS_DEFINITION, TYPE_NOT_ALLOWED_MULTIFIELD, TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL, + TYPE_DEFINITION, } from '../constants'; import { State } from '../reducer'; @@ -71,6 +72,9 @@ export const getFieldMeta = (field: Field, isMultiField?: boolean): FieldMeta => }; }; +export const getTypeLabelFromType = (type: DataType) => + TYPE_DEFINITION[type] ? TYPE_DEFINITION[type].label : `${TYPE_DEFINITION.other.label}: ${type}`; + export const getFieldConfig = (param: ParameterName, prop?: string): FieldConfig => { if (prop !== undefined) { if ( @@ -122,7 +126,7 @@ const replaceAliasPathByAliasId = ( }; export const getMainTypeFromSubType = (subType: SubType): MainType => - SUB_TYPE_MAP_TO_MAIN[subType] as MainType; + (SUB_TYPE_MAP_TO_MAIN[subType] ?? 'other') as MainType; /** * In order to better work with the recursive pattern of the mappings `properties`, this method flatten the fields @@ -287,7 +291,9 @@ export const deNormalize = ({ rootLevelFields, byId, aliases }: NormalizedFields const { source, childFields, childFieldsName } = serializedFieldsById[id]; const { name, ...normalizedField } = source; const field: Omit = normalizedField; + to[name] = field; + if (childFields) { field[childFieldsName!] = {}; return deNormalizePaths(childFields, field[childFieldsName!]); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts index dbbffe5a0bd31..5b18af68ed55b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts @@ -56,7 +56,12 @@ export type MainType = | 'date_nanos' | 'geo_point' | 'geo_shape' - | 'token_count'; + | 'token_count' + /** + * 'other' is a special type that only exists inside of MappingsEditor as a placeholder + * for undocumented field types. + */ + | 'other'; export type SubType = NumericType | RangeType; @@ -156,6 +161,10 @@ interface FieldBasic { subType?: SubType; properties?: { [key: string]: Omit }; fields?: { [key: string]: Omit }; + + // other* exist together as a holder of types that the mappings editor does not yet know about but + // enables the user to create mappings with them. + otherTypeJson?: GenericObject; } type FieldParams = { diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index 0909a3c2ed569..cd3ba43c3607c 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -89,7 +89,7 @@ export const Expressions: React.FC = props => { const defaultExpression = useMemo( () => ({ - aggType: AGGREGATION_TYPES.MAX, + aggType: AGGREGATION_TYPES.AVERAGE, comparator: '>', threshold: [], timeSize: 1, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/package_icon.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/package_icon.tsx index 8ba597a0d377e..de0dd75f635cf 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/package_icon.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/package_icon.tsx @@ -3,78 +3,12 @@ * 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, { useEffect, useMemo, useState } from 'react'; -import { ICON_TYPES, EuiIcon, EuiIconProps } from '@elastic/eui'; -import { PackageInfo, PackageListItem } from '../../../../common/types/models'; -import { useLinks } from '../sections/epm/hooks'; -import { epmRouteService } from '../../../../common/services'; -import { sendRequest } from '../hooks/use_request'; -import { GetInfoResponse } from '../types'; -type Package = PackageInfo | PackageListItem; +import React from 'react'; +import { EuiIcon, EuiIconProps } from '@elastic/eui'; +import { usePackageIconType, UsePackageIconType } from '../hooks'; -const CACHED_ICONS = new Map(); - -export const PackageIcon: React.FunctionComponent<{ - packageName: string; - version?: string; - icons?: Package['icons']; -} & Omit> = ({ packageName, version, icons, ...euiIconProps }) => { - const iconType = usePackageIcon(packageName, version, icons); +export const PackageIcon: React.FunctionComponent> = ({ packageName, version, icons, tryApi, ...euiIconProps }) => { + const iconType = usePackageIconType({ packageName, version, icons, tryApi }); return ; }; - -const usePackageIcon = (packageName: string, version?: string, icons?: Package['icons']) => { - const { toImage } = useLinks(); - const [iconType, setIconType] = useState(''); // FIXME: use `empty` icon during initialization - see: https://github.com/elastic/kibana/issues/60622 - const pkgKey = `${packageName}-${version ?? ''}`; - - // Generates an icon path or Eui Icon name based on an icon list from the package - // or by using the package name against logo icons from Eui - const fromInput = useMemo(() => { - return (iconList?: Package['icons']) => { - const svgIcons = iconList?.filter(iconDef => iconDef.type === 'image/svg+xml'); - const localIconSrc = Array.isArray(svgIcons) && svgIcons[0]?.src; - if (localIconSrc) { - CACHED_ICONS.set(pkgKey, toImage(localIconSrc)); - setIconType(CACHED_ICONS.get(pkgKey) as string); - return; - } - - const euiLogoIcon = ICON_TYPES.find(key => key.toLowerCase() === `logo${packageName}`); - if (euiLogoIcon) { - CACHED_ICONS.set(pkgKey, euiLogoIcon); - setIconType(euiLogoIcon); - return; - } - - CACHED_ICONS.set(pkgKey, 'package'); - setIconType('package'); - }; - }, [packageName, pkgKey, toImage]); - - useEffect(() => { - if (CACHED_ICONS.has(pkgKey)) { - setIconType(CACHED_ICONS.get(pkgKey) as string); - return; - } - - // Use API to see if package has icons defined - if (!icons && version) { - fromPackageInfo(pkgKey) - .catch(() => undefined) // ignore API errors - .then(fromInput); - } else { - fromInput(icons); - } - }, [icons, toImage, packageName, version, fromInput, pkgKey]); - - return iconType; -}; - -const fromPackageInfo = async (pkgKey: string) => { - const { data } = await sendRequest({ - path: epmRouteService.getInfoPath(pkgKey), - method: 'get', - }); - return data?.response?.icons; -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts index 5e0695bd3e305..66c7333150fb7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts @@ -9,6 +9,7 @@ export { useCore, CoreContext } from './use_core'; export { useConfig, ConfigContext } from './use_config'; export { useSetupDeps, useStartDeps, DepsContext } from './use_deps'; export { useLink } from './use_link'; +export { usePackageIconType, UsePackageIconType } from './use_package_icon_type'; export { usePagination, Pagination } from './use_pagination'; export { useDebounce } from './use_debounce'; export * from './use_request'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts new file mode 100644 index 0000000000000..5f231b5cc9ec9 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts @@ -0,0 +1,71 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { ICON_TYPES } from '@elastic/eui'; +import { PackageInfo, PackageListItem } from '../../../../common/types/models'; +import { useLinks } from '../sections/epm/hooks'; +import { sendGetPackageInfoByKey } from './index'; + +type Package = PackageInfo | PackageListItem; + +export interface UsePackageIconType { + packageName: Package['name']; + version: Package['version']; + icons?: Package['icons']; + tryApi?: boolean; // should it call API to try to find missing icons? +} + +const CACHED_ICONS = new Map(); + +export const usePackageIconType = ({ + packageName, + version, + icons: paramIcons, + tryApi = false, +}: UsePackageIconType) => { + const { toImage } = useLinks(); + const [iconList, setIconList] = useState(); + const [iconType, setIconType] = useState(''); // FIXME: use `empty` icon during initialization - see: https://github.com/elastic/kibana/issues/60622 + const pkgKey = `${packageName}-${version}`; + + // Generates an icon path or Eui Icon name based on an icon list from the package + // or by using the package name against logo icons from Eui + useEffect(() => { + if (CACHED_ICONS.has(pkgKey)) { + setIconType(CACHED_ICONS.get(pkgKey) || ''); + return; + } + const svgIcons = (paramIcons || iconList)?.filter(iconDef => iconDef.type === 'image/svg+xml'); + const localIconSrc = Array.isArray(svgIcons) && svgIcons[0]?.src; + if (localIconSrc) { + CACHED_ICONS.set(pkgKey, toImage(localIconSrc)); + setIconType(CACHED_ICONS.get(pkgKey) || ''); + return; + } + + const euiLogoIcon = ICON_TYPES.find(key => key.toLowerCase() === `logo${packageName}`); + if (euiLogoIcon) { + CACHED_ICONS.set(pkgKey, euiLogoIcon); + setIconType(euiLogoIcon); + return; + } + + if (tryApi && !paramIcons && !iconList) { + sendGetPackageInfoByKey(pkgKey) + .catch(error => undefined) // Ignore API errors + .then(res => { + CACHED_ICONS.delete(pkgKey); + setIconList(res?.data?.response?.icons); + }); + } + + CACHED_ICONS.set(pkgKey, 'package'); + setIconType('package'); + }, [paramIcons, pkgKey, toImage, iconList, packageName, iconType, tryApi]); + + return iconType; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_package.tsx index 0b48020c3cac1..cc7fc89ab8a80 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_package.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_package.tsx @@ -130,7 +130,15 @@ export const StepSelectPackage: React.FunctionComponent<{ return { label: title || name, key: pkgkey, - prepend: , + prepend: ( + + ), checked: selectedPkgKey === pkgkey ? 'on' : undefined, }; })} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx index 49285707457e1..87155afdc21be 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx @@ -150,6 +150,7 @@ export const DatasourcesTable: React.FunctionComponent = ({ packageName={datasource.package.name} version={datasource.package.version} size="m" + tryApi={true} /> )} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icon_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icon_panel.tsx index 7ce386ed56f5f..684b158b5da86 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icon_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icon_panel.tsx @@ -16,7 +16,8 @@ export function IconPanel({ iconType }: { iconType: IconType }) { text-align: center; vertical-align: middle; padding: ${props => props.theme.eui.spacerSizes.xl}; - svg { + svg, + img { height: ${props => props.theme.eui.euiKeyPadMenuSize}; width: ${props => props.theme.eui.euiKeyPadMenuSize}; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx index 4bc90c6a0f8fd..3239d7b90e3c3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiPage, EuiPageBody, EuiPageProps, ICON_TYPES } from '@elastic/eui'; +import { EuiPage, EuiPageBody, EuiPageProps } from '@elastic/eui'; import React, { Fragment, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; @@ -12,7 +12,7 @@ import { PackageInfo } from '../../../../types'; import { useSetPackageInstallStatus } from '../../hooks'; import { Content } from './content'; import { Header } from './header'; -import { sendGetPackageInfoByKey } from '../../../../hooks'; +import { sendGetPackageInfoByKey, usePackageIconType } from '../../../../hooks'; export const DEFAULT_PANEL: DetailViewPanelName = 'overview'; @@ -62,8 +62,8 @@ const FullWidthContent = styled(EuiPage)` type LayoutProps = PackageInfo & Pick & Pick; export function DetailLayout(props: LayoutProps) { - const { name, restrictWidth } = props; - const iconType = ICON_TYPES.find(key => key.toLowerCase() === `logo${name}`); + const { name: packageName, version, icons, restrictWidth } = props; + const iconType = usePackageIconType({ packageName, version, icons }); return ( diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index 41bd2476c99a1..ec270884e62b4 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -14,6 +14,7 @@ import { } from '../../constants'; import { AgentSOAttributes, Agent, AgentEventSOAttributes } from '../../types'; import { savedObjectToAgent } from './saved_objects'; +import { escapeSearchQueryPhrase } from '../saved_object'; export async function listAgents( soClient: SavedObjectsClientContract, @@ -72,14 +73,16 @@ export async function getAgentByAccessAPIKeyId( const response = await soClient.find({ type: AGENT_SAVED_OBJECT_TYPE, searchFields: ['access_api_key_id'], - search: accessAPIKeyId, + search: escapeSearchQueryPhrase(accessAPIKeyId), }); - const [agent] = response.saved_objects.map(savedObjectToAgent); if (!agent) { throw Boom.notFound('Agent not found'); } + if (agent.access_api_key_id !== accessAPIKeyId) { + throw new Error('Agent api key id is not matching'); + } if (!agent.active) { throw Boom.forbidden('Agent inactive'); } diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts index 329945b669f8f..57362e6b4b0de 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts @@ -8,6 +8,7 @@ import { SavedObjectsClientContract, SavedObject, KibanaRequest } from 'src/core import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { EnrollmentAPIKeySOAttributes, EnrollmentAPIKey } from '../../types'; import { createAPIKey } from './security'; +import { escapeSearchQueryPhrase } from '../saved_object'; export { invalidateAPIKey } from './security'; export * from './enrollment_api_key'; @@ -71,10 +72,14 @@ export async function getEnrollmentAPIKeyById( await soClient.find({ type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, searchFields: ['api_key_id'], - search: apiKeyId, + search: escapeSearchQueryPhrase(apiKeyId), }) ).saved_objects.map(_savedObjectToEnrollmentApiKey); + if (enrollmentAPIKey?.api_key_id !== apiKeyId) { + throw new Error('find enrollmentKeyById returned an incorrect key'); + } + return enrollmentAPIKey; } diff --git a/x-pack/plugins/ingest_manager/server/services/saved_object.test.ts b/x-pack/plugins/ingest_manager/server/services/saved_object.test.ts new file mode 100644 index 0000000000000..9eb5dccb76ac5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/saved_object.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { escapeSearchQueryPhrase } from './saved_object'; + +describe('Saved object service', () => { + describe('escapeSearchQueryPhrase', () => { + it('should return value between quotes', () => { + const res = escapeSearchQueryPhrase('-test'); + + expect(res).toEqual('"-test"'); + }); + + it('should escape quotes', () => { + const res = escapeSearchQueryPhrase('test1"test2'); + + expect(res).toEqual(`"test1\"test2"`); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/saved_object.ts b/x-pack/plugins/ingest_manager/server/services/saved_object.ts new file mode 100644 index 0000000000000..8fe7ffcdfc896 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/saved_object.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +/** + * Escape a value with double quote to use with saved object search + * Example: escapeSearchQueryPhrase('-test"toto') => '"-test\"toto""' + * @param val + */ +export function escapeSearchQueryPhrase(val: string): string { + return `"${val.replace(/["]/g, '"')}"`; +} diff --git a/x-pack/plugins/ml/common/util/es_utils.ts b/x-pack/plugins/ml/common/util/es_utils.ts index bed7ba8bc7736..ff632a60dd516 100644 --- a/x-pack/plugins/ml/common/util/es_utils.ts +++ b/x-pack/plugins/ml/common/util/es_utils.ts @@ -26,6 +26,7 @@ function isValidIndexNameLength(indexName: string) { // https://github.com/elastic/elasticsearch/blob/master/docs/reference/indices/create-index.asciidoc export function isValidIndexName(indexName: string) { return ( + typeof indexName === 'string' && // Lowercase only indexName === indexName.toLowerCase() && // Cannot include \, /, *, ?, ", <, >, |, space character, comma, #, : 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 28d8afbcd88cc..4f3d2b6a96490 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 @@ -233,6 +233,17 @@ export const validateAdvancedEditor = (state: State): State => { ), message: '', }); + } else if (destinationIndexPatternTitleExists && !createIndexPattern) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameExistsWarn', + { + defaultMessage: + 'An index with this destination index name already exists. Be aware that running this analytics job will modify this destination index.', + } + ), + message: '', + }); } else if (!destinationIndexNameValid) { state.advancedEditorMessages.push({ error: i18n.translate( @@ -276,6 +287,8 @@ export const validateAdvancedEditor = (state: State): State => { }); } + state.form.destinationIndexPatternTitleExists = destinationIndexPatternTitleExists; + state.isValid = maxDistinctValuesError === undefined && excludesValid && diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 44bfc0c5a472c..2478dbf7cf63d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -47,7 +47,8 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { const { refresh } = useRefreshAnalyticsList(); const { form, jobConfig, isAdvancedEditorEnabled } = state; - const { createIndexPattern, destinationIndex, jobId } = form; + const { createIndexPattern, jobId } = form; + let { destinationIndex } = form; const addRequestMessage = (requestMessage: FormMessage) => dispatch({ type: ACTION.ADD_REQUEST_MESSAGE, requestMessage }); @@ -90,9 +91,13 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { resetRequestMessages(); setIsModalButtonDisabled(true); - const analyticsJobConfig = isAdvancedEditorEnabled + const analyticsJobConfig = (isAdvancedEditorEnabled ? jobConfig - : getJobConfigFromFormState(form); + : getJobConfigFromFormState(form)) as DataFrameAnalyticsConfig; + + if (isAdvancedEditorEnabled) { + destinationIndex = analyticsJobConfig.dest.index; + } try { await ml.dataFrameAnalytics.createDataFrameAnalytics(jobId, analyticsJobConfig); diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts new file mode 100644 index 0000000000000..f40976f09f9c8 --- /dev/null +++ b/x-pack/test/accessibility/apps/home.ts @@ -0,0 +1,67 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'home']); + const a11y = getService('a11y'); + + describe('Kibana Home', () => { + before(async () => { + await PageObjects.common.navigateToApp('home'); + }); + + it('Kibana Home view', async () => { + await a11y.testAppSnapshot(); + }); + + it('all plugins view page meets a11y requirements', async () => { + await PageObjects.home.clickAllKibanaPlugins(); + await a11y.testAppSnapshot(); + }); + + it('visualize & explore details tab meets a11y requirements', async () => { + await PageObjects.home.clickVisualizeExplorePlugins(); + await a11y.testAppSnapshot(); + }); + + it('administrative detail tab meets a11y requirements', async () => { + await PageObjects.home.clickAdminPlugin(); + await a11y.testAppSnapshot(); + }); + + it('navigating to console app from administration tab meets a11y requirements', async () => { + await PageObjects.home.clickOnConsole(); + await a11y.testAppSnapshot(); + }); + + // issue: https://github.com/elastic/kibana/issues/38980 + it.skip('navigating back to home page from console meets a11y requirements', async () => { + await PageObjects.home.clickOnLogo(); + await a11y.testAppSnapshot(); + }); + + // Extra clickon logo step here will be removed after preceding test is fixed. + it('click on Add logs panel to open all log examples page meets a11y requirements ', async () => { + await PageObjects.home.clickOnLogo(); + await PageObjects.home.ClickOnLogsData(); + await a11y.testAppSnapshot(); + }); + + // issue - logo images are missing alt -text https://github.com/elastic/kibana/issues/62239 + it.skip('click on ActiveMQ logs panel to open tutorial meets a11y requirements', async () => { + await PageObjects.home.clickOnLogsTutorial(); + await a11y.testAppSnapshot(); + }); + + // https://github.com/elastic/kibana/issues/62239 + it.skip('click on cloud tutorial meets a11y requirements', async () => { + await PageObjects.home.clickOnCloudTutorial(); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index c8a31ab4ceba8..7bf6079cc6487 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -13,7 +13,11 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), - testFiles: [require.resolve('./apps/login_page'), require.resolve('./apps/grok_debugger')], + testFiles: [ + require.resolve('./apps/login_page'), + require.resolve('./apps/home'), + require.resolve('./apps/grok_debugger'), + ], pageObjects, services, diff --git a/x-pack/test/api_integration/apis/fleet/agents/acks.ts b/x-pack/test/api_integration/apis/fleet/agents/acks.ts index db925813b90c4..a2eba2c23c39d 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/acks.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/acks.ts @@ -18,8 +18,7 @@ export default function(providerContext: FtrProviderContext) { const supertest = getSupertestWithoutAuth(providerContext); let apiKey: { id: string; api_key: string }; - // FLAKY: https://github.com/elastic/kibana/issues/60471 - describe.skip('fleet_agents_acks', () => { + describe('fleet_agents_acks', () => { before(async () => { await esArchiver.loadIfNeeded('fleet/agents'); diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/data.json.gz b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/data.json.gz index c1a3c44cb8d8d..feb2af93b0fd1 100644 Binary files a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/data.json.gz and b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json index e0a7068e1149a..64dc395ab69a4 100644 --- a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json +++ b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json @@ -94,7 +94,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { @@ -454,7 +454,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { @@ -851,7 +851,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { @@ -1496,7 +1496,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { @@ -1689,7 +1689,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 347eb5e14d0a8..029af1ea06e4f 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -38,7 +38,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return createdAlert; } - describe('alerts', function() { + // FLAKY: https://github.com/elastic/kibana/issues/62472 + describe.skip('alerts', function() { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('alertsTab'); diff --git a/yarn.lock b/yarn.lock index 8176eab436afd..d9edb55a32039 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9651,7 +9651,7 @@ core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3, resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.1, core-js@^3.0.4, core-js@^3.2.1, core-js@^3.4.1, core-js@^3.6.4: +core-js@^3.0.1, core-js@^3.0.4, core-js@^3.4.1, core-js@^3.6.4: version "3.6.4" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647" integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw== @@ -24284,7 +24284,7 @@ react-router-redux@^4.0.8: resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.8.tgz#227403596b5151e182377dab835b5d45f0f8054e" integrity sha1-InQDWWtRUeGCN32rg1tdRfD4BU4= -react-router@5.1.2: +react-router@5.1.2, react-router@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418" integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==