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==