(
-
+
),
validate,
defaultActionMessage: '{{context.internalFullMessage}}',
diff --git a/x-pack/plugins/monitoring/public/alerts/panel.tsx b/x-pack/plugins/monitoring/public/alerts/panel.tsx
index eb3b6ff9da1be..99db6c8b3c945 100644
--- a/x-pack/plugins/monitoring/public/alerts/panel.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/panel.tsx
@@ -18,8 +18,7 @@ import {
EuiListGroupItem,
} from '@elastic/eui';
-import { CommonAlertStatus, CommonAlertState } from '../../common/types';
-import { AlertMessage } from '../../server/alerts/types';
+import { CommonAlertStatus, CommonAlertState, AlertMessage } from '../../common/types/alerts';
import { Legacy } from '../legacy_shims';
import { replaceTokens } from './lib/replace_tokens';
import { AlertsContextProvider } from '../../../triggers_actions_ui/public';
diff --git a/x-pack/plugins/monitoring/public/alerts/status.tsx b/x-pack/plugins/monitoring/public/alerts/status.tsx
index c1ad41fc8d763..53918807a4272 100644
--- a/x-pack/plugins/monitoring/public/alerts/status.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/status.tsx
@@ -7,9 +7,8 @@ import React from 'react';
import { EuiToolTip, EuiHealth } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { CommonAlertStatus } from '../../common/types';
+import { CommonAlertStatus, AlertMessage, AlertState } from '../../common/types/alerts';
import { AlertSeverity } from '../../common/enums';
-import { AlertMessage, AlertState } from '../../server/alerts/types';
import { AlertsBadge } from './badge';
import { isInSetupMode } from '../lib/setup_mode';
import { SetupModeContext } from '../components/setup_mode/setup_mode_context';
diff --git a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx
new file mode 100644
index 0000000000000..5e8e676448218
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx
@@ -0,0 +1,60 @@
+/*
+ * 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 { EuiSpacer } from '@elastic/eui';
+import { Expression, Props } from '../components/duration/expression';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
+import { CommonAlertParamDetails } from '../../../common/types/alerts';
+
+interface ThreadPoolTypes {
+ [key: string]: unknown;
+}
+
+interface ThreadPoolRejectionAlertDetails {
+ label: string;
+ paramDetails: CommonAlertParamDetails;
+}
+
+export function createThreadPoolRejectionsAlertType(
+ alertType: string,
+ threadPoolAlertDetails: ThreadPoolRejectionAlertDetails
+): AlertTypeModel {
+ return {
+ id: alertType,
+ name: threadPoolAlertDetails.label,
+ iconClass: 'bell',
+ alertParamsExpression: (props: Props) => (
+ <>
+
+
+ >
+ ),
+ validate: (inputValues: ThreadPoolTypes) => {
+ const errors: { [key: string]: string[] } = {};
+ const value = inputValues.threshold as number;
+ if (value < 0) {
+ const errStr = i18n.translate('xpack.monitoring.alerts.validation.lessThanZero', {
+ defaultMessage: 'This value can not be less than zero',
+ });
+ errors.threshold = [errStr];
+ }
+
+ if (!inputValues.duration) {
+ const errStr = i18n.translate('xpack.monitoring.alerts.validation.duration', {
+ defaultMessage: 'A valid duration is required.',
+ });
+ errors.duration = [errStr];
+ }
+
+ return { errors };
+ },
+ defaultActionMessage: '{{context.internalFullMessage}}',
+ requiresAppContext: true,
+ };
+}
diff --git a/x-pack/plugins/monitoring/public/angular/providers/private.js b/x-pack/plugins/monitoring/public/angular/providers/private.js
index 3a667037b2919..7709865432fe6 100644
--- a/x-pack/plugins/monitoring/public/angular/providers/private.js
+++ b/x-pack/plugins/monitoring/public/angular/providers/private.js
@@ -81,9 +81,9 @@
*
* @param {[type]} prov [description]
*/
-import _ from 'lodash';
+import { partial, uniqueId, isObject } from 'lodash';
-const nextId = _.partial(_.uniqueId, 'privateProvider#');
+const nextId = partial(uniqueId, 'privateProvider#');
function name(fn) {
return fn.name || fn.toString().split('\n').shift();
@@ -141,7 +141,7 @@ export function PrivateProvider() {
const context = {};
let instance = $injector.invoke(prov, context, locals);
- if (!_.isObject(instance)) instance = context;
+ if (!isObject(instance)) instance = context;
privPath.pop();
return instance;
@@ -155,7 +155,7 @@ export function PrivateProvider() {
if ($delegateId != null && $delegateProv != null) {
instance = instantiate(prov, {
- $decorate: _.partial(get, $delegateId, $delegateProv),
+ $decorate: partial(get, $delegateId, $delegateProv),
});
} else {
instance = instantiate(prov);
diff --git a/x-pack/plugins/monitoring/public/components/chart/chart_target.js b/x-pack/plugins/monitoring/public/components/chart/chart_target.js
index 9a590d803bb19..519964e4d5914 100644
--- a/x-pack/plugins/monitoring/public/components/chart/chart_target.js
+++ b/x-pack/plugins/monitoring/public/components/chart/chart_target.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
+import { get, isEqual, filter } from 'lodash';
import $ from 'jquery';
import React from 'react';
import { eventBus } from './event_bus';
@@ -50,12 +50,12 @@ export class ChartTarget extends React.Component {
}
UNSAFE_componentWillReceiveProps(newProps) {
- if (this.plot && !_.isEqual(newProps, this.props)) {
+ if (this.plot && !isEqual(newProps, this.props)) {
const { series, timeRange } = newProps;
const xaxisOptions = this.plot.getAxes().xaxis.options;
- xaxisOptions.min = _.get(timeRange, 'min');
- xaxisOptions.max = _.get(timeRange, 'max');
+ xaxisOptions.min = get(timeRange, 'min');
+ xaxisOptions.max = get(timeRange, 'max');
this.plot.setData(this.filterData(series, newProps.seriesToShow));
this.plot.setupGrid();
@@ -73,7 +73,7 @@ export class ChartTarget extends React.Component {
}
filterData(data, seriesToShow) {
- return _(data).filter(this.filterByShow(seriesToShow)).value();
+ return filter(data, this.filterByShow(seriesToShow));
}
async getOptions() {
@@ -128,7 +128,7 @@ export class ChartTarget extends React.Component {
this.handleThorPlotHover = (_event, pos, item, originalPlot) => {
if (this.plot !== originalPlot) {
// the crosshair is set for the original chart already
- this.plot.setCrosshair({ x: _.get(pos, 'x') });
+ this.plot.setCrosshair({ x: get(pos, 'x') });
}
this.props.updateLegend(pos, item);
};
diff --git a/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js b/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js
index eb32ee108e7b3..829994791f769 100644
--- a/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js
+++ b/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
+import { debounce, keys, has, includes, isFunction, difference, assign } from 'lodash';
import React from 'react';
import { getLastValue } from './get_last_value';
import { TimeseriesContainer } from './timeseries_container';
@@ -17,7 +17,7 @@ export class TimeseriesVisualization extends React.Component {
constructor(props) {
super(props);
- this.debouncedUpdateLegend = _.debounce(this.updateLegend, DEBOUNCE_SLOW_MS);
+ this.debouncedUpdateLegend = debounce(this.updateLegend, DEBOUNCE_SLOW_MS);
this.debouncedUpdateLegend = this.debouncedUpdateLegend.bind(this);
this.toggleFilter = this.toggleFilter.bind(this);
@@ -26,18 +26,18 @@ export class TimeseriesVisualization extends React.Component {
this.state = {
values: {},
- seriesToShow: _.keys(values),
+ seriesToShow: keys(values),
ignoreVisibilityUpdates: false,
};
}
filterLegend(id) {
- if (!_.has(this.state.values, id)) {
+ if (!has(this.state.values, id)) {
return [];
}
- const notAllShown = _.keys(this.state.values).length !== this.state.seriesToShow.length;
- const isCurrentlyShown = _.includes(this.state.seriesToShow, id);
+ const notAllShown = keys(this.state.values).length !== this.state.seriesToShow.length;
+ const isCurrentlyShown = includes(this.state.seriesToShow, id);
const seriesToShow = [];
if (notAllShown && isCurrentlyShown) {
@@ -59,7 +59,7 @@ export class TimeseriesVisualization extends React.Component {
toggleFilter(_event, id) {
const seriesToShow = this.filterLegend(id);
- if (_.isFunction(this.props.onFilter)) {
+ if (isFunction(this.props.onFilter)) {
this.props.onFilter(seriesToShow);
}
}
@@ -94,7 +94,7 @@ export class TimeseriesVisualization extends React.Component {
getValuesByX(this.props.series, pos.x, setValueCallback);
}
} else {
- _.assign(values, this.getLastValues());
+ assign(values, this.getLastValues());
}
this.setState({ values });
@@ -102,13 +102,13 @@ export class TimeseriesVisualization extends React.Component {
UNSAFE_componentWillReceiveProps(props) {
const values = this.getLastValues(props);
- const currentKeys = _.keys(this.state.values);
- const keys = _.keys(values);
- const diff = _.difference(keys, currentKeys);
+ const currentKeys = keys(this.state.values);
+ const valueKeys = keys(values);
+ const diff = difference(valueKeys, currentKeys);
const nextState = { values: values };
if (diff.length && !this.state.ignoreVisibilityUpdates) {
- nextState.seriesToShow = keys;
+ nextState.seriesToShow = valueKeys;
}
this.setState(nextState);
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js
index 0fe434afa2c88..7e85d62c4bbd6 100644
--- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js
+++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js
@@ -41,6 +41,8 @@ import {
ALERT_CLUSTER_HEALTH,
ALERT_CPU_USAGE,
ALERT_DISK_USAGE,
+ ALERT_THREAD_POOL_SEARCH_REJECTIONS,
+ ALERT_THREAD_POOL_WRITE_REJECTIONS,
ALERT_MEMORY_USAGE,
ALERT_NODES_CHANGED,
ALERT_ELASTICSEARCH_VERSION_MISMATCH,
@@ -162,6 +164,8 @@ const OVERVIEW_PANEL_ALERTS = [ALERT_CLUSTER_HEALTH, ALERT_LICENSE_EXPIRATION];
const NODES_PANEL_ALERTS = [
ALERT_CPU_USAGE,
ALERT_DISK_USAGE,
+ ALERT_THREAD_POOL_SEARCH_REJECTIONS,
+ ALERT_THREAD_POOL_WRITE_REJECTIONS,
ALERT_MEMORY_USAGE,
ALERT_NODES_CHANGED,
ALERT_ELASTICSEARCH_VERSION_MISMATCH,
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
index 41d3a579db5a2..61188487e2f99 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
@@ -27,7 +27,7 @@ import {
EuiHealth,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import _ from 'lodash';
+import { get } from 'lodash';
import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants';
import { FormattedMessage } from '@kbn/i18n/react';
import { ListingCallOut } from '../../setup_mode/listing_callout';
@@ -58,7 +58,7 @@ const getNodeTooltip = (node) => {
return null;
};
-const getSortHandler = (type) => (item) => _.get(item, [type, 'summary', 'lastVal']);
+const getSortHandler = (type) => (item) => get(item, [type, 'summary', 'lastVal']);
const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, alerts) => {
const cols = [];
@@ -87,7 +87,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler
let setupModeStatus = null;
if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) {
- const list = _.get(setupMode, 'data.byUuid', {});
+ const list = get(setupMode, 'data.byUuid', {});
const status = list[node.resolver] || {};
const instance = {
uuid: node.resolver,
@@ -396,7 +396,7 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear
setupMode.data.totalUniqueInstanceCount
) {
const finishMigrationAction =
- _.get(setupMode.meta, 'liveClusterUuid') === clusterUuid
+ get(setupMode.meta, 'liveClusterUuid') === clusterUuid
? setupMode.shortcutToFinishMigration
: setupMode.openFlyout;
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js
index 2c66d14a40605..5c8dca54894b4 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
+import { sortBy } from 'lodash';
import React from 'react';
import { Shard } from './shard';
import { i18n } from '@kbn/i18n';
@@ -36,7 +36,7 @@ export class Unassigned extends React.Component {
};
render() {
- const shards = _.sortBy(this.props.shards, 'shard').map(this.createShard);
+ const shards = sortBy(this.props.shards, 'shard').map(this.createShard);
return (
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js
index 47739b8fe31e8..a371f3e5ff40c 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js
@@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
+import { some } from 'lodash';
export function hasPrimaryChildren(item) {
- return _.some(item.children, { primary: true });
+ return some(item.children, { primary: true });
}
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js
index db9b7bacc3cdf..335c3d29a5b9e 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js
@@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
+import { each, isArray } from 'lodash';
export const _vents = {};
export const vents = {
vents: _vents,
on: function (id, cb) {
- if (!_.isArray(_vents[id])) {
+ if (!isArray(_vents[id])) {
_vents[id] = [];
}
_vents[id].push(cb);
@@ -22,7 +22,7 @@ export const vents = {
const args = Array.prototype.slice.call(arguments);
const id = args.shift();
if (_vents[id]) {
- _.each(_vents[id], function (cb) {
+ each(_vents[id], function (cb) {
cb.apply(null, args);
});
}
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js
index a9808ebc4c6ad..a04e2bcd1786e 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
+import { find, reduce, values, sortBy } from 'lodash';
import { decorateShards } from '../lib/decorate_shards';
export function indicesByNodes() {
@@ -39,7 +39,7 @@ export function indicesByNodes() {
return obj;
}
- let nodeObj = _.find(obj[index].children, { id: node });
+ let nodeObj = find(obj[index].children, { id: node });
if (!nodeObj) {
nodeObj = {
id: node,
@@ -55,7 +55,7 @@ export function indicesByNodes() {
return obj;
}
- const data = _.reduce(
+ const data = reduce(
decorateShards(shards, nodes),
function (obj, shard) {
obj = createIndex(obj, shard);
@@ -64,10 +64,11 @@ export function indicesByNodes() {
},
{}
);
-
- return _(data)
- .values()
- .sortBy((index) => [!index.unassignedPrimaries, /^\./.test(index.name), index.name])
- .value();
+ const dataValues = values(data);
+ return sortBy(dataValues, (index) => [
+ !index.unassignedPrimaries,
+ /^\./.test(index.name),
+ index.name,
+ ]);
};
}
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js
index 353e1c23d4bc1..f8dd5b6cb8e8d 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
+import { find, some, reduce, values, sortBy } from 'lodash';
import { hasPrimaryChildren } from '../lib/has_primary_children';
import { decorateShards } from '../lib/decorate_shards';
@@ -32,7 +32,7 @@ export function nodesByIndices() {
if (!obj[node]) {
createNode(obj, nodes[node], node);
}
- let indexObj = _.find(obj[node].children, { id: index });
+ let indexObj = find(obj[node].children, { id: index });
if (!indexObj) {
indexObj = {
id: index,
@@ -51,7 +51,7 @@ export function nodesByIndices() {
}
let data = {};
- if (_.some(shards, isUnassigned)) {
+ if (some(shards, isUnassigned)) {
data.unassigned = {
name: 'Unassigned',
master: false,
@@ -60,19 +60,15 @@ export function nodesByIndices() {
};
}
- data = _.reduce(decorateShards(shards, nodes), createIndexAddShard, data);
-
- return _(data)
- .values()
- .sortBy(function (node) {
- return [node.name !== 'Unassigned', !node.master, node.name];
- })
- .map(function (node) {
- if (node.name === 'Unassigned') {
- node.unassignedPrimaries = node.children.some(hasPrimaryChildren);
- }
- return node;
- })
- .value();
+ data = reduce(decorateShards(shards, nodes), createIndexAddShard, data);
+ const dataValues = values(data);
+ return sortBy(dataValues, function (node) {
+ return [node.name !== 'Unassigned', !node.master, node.name];
+ }).map(function (node) {
+ if (node.name === 'Unassigned') {
+ node.unassignedPrimaries = node.children.some(hasPrimaryChildren);
+ }
+ return node;
+ });
};
}
diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap
index deed4687e74f6..046f2bfa92247 100644
--- a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap
@@ -248,6 +248,7 @@ exports[`ExplainCollectionEnabled should explain about xpack.monitoring.collecti
type="button"
>
-
+
-
+
{
- const existingCurrent = _.find(clusters, { cluster_uuid: globalState.cluster_uuid });
+ const existingCurrent = find(clusters, { cluster_uuid: globalState.cluster_uuid });
if (existingCurrent) {
return existingCurrent;
}
- const firstCluster = _.first(clusters);
+ const firstCluster = first(clusters);
if (firstCluster && firstCluster.cluster_uuid) {
return firstCluster;
}
diff --git a/x-pack/plugins/monitoring/public/lib/route_init.js b/x-pack/plugins/monitoring/public/lib/route_init.js
index eebdfa8692f1a..97ff621ee3164 100644
--- a/x-pack/plugins/monitoring/public/lib/route_init.js
+++ b/x-pack/plugins/monitoring/public/lib/route_init.js
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
import { ajaxErrorHandlersProvider } from './ajax_error_handler';
import { isInSetupMode } from './setup_mode';
import { getClusterFromClusters } from './get_cluster_from_clusters';
@@ -13,7 +12,7 @@ export function routeInitProvider(Private, monitoringClusters, globalState, lice
const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider);
function isOnPage(hash) {
- return _.includes(window.location.hash, hash);
+ return window.location.hash.includes(hash);
}
/*
diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts
index 4c50abb40dd3d..a228c540761b8 100644
--- a/x-pack/plugins/monitoring/public/plugin.ts
+++ b/x-pack/plugins/monitoring/public/plugin.ts
@@ -22,11 +22,11 @@ import { UI_SETTINGS } from '../../../../src/plugins/data/public';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
import { MonitoringStartPluginDependencies, MonitoringConfig } from './types';
import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public';
-import { createCpuUsageAlertType } from './alerts/cpu_usage_alert';
-import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert';
-import { createLegacyAlertTypes } from './alerts/legacy_alert';
-import { createDiskUsageAlertType } from './alerts/disk_usage_alert';
-import { createMemoryUsageAlertType } from './alerts/memory_usage_alert';
+import {
+ ALERT_THREAD_POOL_SEARCH_REJECTIONS,
+ ALERT_THREAD_POOL_WRITE_REJECTIONS,
+ ALERT_DETAILS,
+} from '../common/constants';
interface MonitoringSetupPluginDependencies {
home?: HomePublicPluginSetup;
@@ -40,7 +40,7 @@ export class MonitoringPlugin
Plugin {
constructor(private initializerContext: PluginInitializerContext) {}
- public setup(
+ public async setup(
core: CoreSetup,
plugins: MonitoringSetupPluginDependencies
) {
@@ -73,16 +73,7 @@ export class MonitoringPlugin
});
}
- const { alertTypeRegistry } = plugins.triggersActionsUi;
- alertTypeRegistry.register(createCpuUsageAlertType());
- alertTypeRegistry.register(createDiskUsageAlertType());
- alertTypeRegistry.register(createMemoryUsageAlertType());
- alertTypeRegistry.register(createMissingMonitoringDataAlertType());
-
- const legacyAlertTypes = createLegacyAlertTypes();
- for (const legacyAlertType of legacyAlertTypes) {
- alertTypeRegistry.register(legacyAlertType);
- }
+ await this.registerAlertsAsync(plugins);
const app: App = {
id,
@@ -106,7 +97,6 @@ export class MonitoringPlugin
usageCollection: plugins.usageCollection,
};
- pluginsStart.kibanaLegacy.loadFontAwesome();
this.setInitialTimefilter(deps);
const monitoringApp = new AngularApp(deps);
@@ -154,4 +144,41 @@ export class MonitoringPlugin
['showCgroupMetricsLogstash', monitoring.ui.container.logstash.enabled],
];
}
+
+ private registerAlertsAsync = async (plugins: MonitoringSetupPluginDependencies) => {
+ const { createCpuUsageAlertType } = await import('./alerts/cpu_usage_alert');
+ const { createMissingMonitoringDataAlertType } = await import(
+ './alerts/missing_monitoring_data_alert'
+ );
+ const { createLegacyAlertTypes } = await import('./alerts/legacy_alert');
+ const { createDiskUsageAlertType } = await import('./alerts/disk_usage_alert');
+ const { createThreadPoolRejectionsAlertType } = await import(
+ './alerts/thread_pool_rejections_alert'
+ );
+ const { createMemoryUsageAlertType } = await import('./alerts/memory_usage_alert');
+
+ const {
+ triggersActionsUi: { alertTypeRegistry },
+ } = plugins;
+ alertTypeRegistry.register(createCpuUsageAlertType());
+ alertTypeRegistry.register(createDiskUsageAlertType());
+ alertTypeRegistry.register(createMemoryUsageAlertType());
+ alertTypeRegistry.register(createMissingMonitoringDataAlertType());
+ alertTypeRegistry.register(
+ createThreadPoolRejectionsAlertType(
+ ALERT_THREAD_POOL_SEARCH_REJECTIONS,
+ ALERT_DETAILS[ALERT_THREAD_POOL_SEARCH_REJECTIONS]
+ )
+ );
+ alertTypeRegistry.register(
+ createThreadPoolRejectionsAlertType(
+ ALERT_THREAD_POOL_WRITE_REJECTIONS,
+ ALERT_DETAILS[ALERT_THREAD_POOL_WRITE_REJECTIONS]
+ )
+ );
+ const legacyAlertTypes = createLegacyAlertTypes();
+ for (const legacyAlertType of legacyAlertTypes) {
+ alertTypeRegistry.register(legacyAlertType);
+ }
+ };
}
diff --git a/x-pack/plugins/monitoring/public/services/features.js b/x-pack/plugins/monitoring/public/services/features.js
index f98af10f8dfb4..5e29353e497d1 100644
--- a/x-pack/plugins/monitoring/public/services/features.js
+++ b/x-pack/plugins/monitoring/public/services/features.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
+import { has, isUndefined } from 'lodash';
export function featuresProvider($window) {
function getData() {
@@ -28,11 +28,11 @@ export function featuresProvider($window) {
function isEnabled(featureName, defaultSetting) {
const monitoringDataObj = getData();
- if (_.has(monitoringDataObj, featureName)) {
+ if (has(monitoringDataObj, featureName)) {
return monitoringDataObj[featureName];
}
- if (_.isUndefined(defaultSetting)) {
+ if (isUndefined(defaultSetting)) {
return false;
}
diff --git a/x-pack/plugins/monitoring/public/services/title.js b/x-pack/plugins/monitoring/public/services/title.js
index 0715f4dc9e0b6..91ef4c32f3b98 100644
--- a/x-pack/plugins/monitoring/public/services/title.js
+++ b/x-pack/plugins/monitoring/public/services/title.js
@@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
+import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { Legacy } from '../legacy_shims';
export function titleProvider($rootScope) {
return function changeTitle(cluster, suffix) {
- let clusterName = _.get(cluster, 'cluster_name');
+ let clusterName = get(cluster, 'cluster_name');
clusterName = clusterName ? `- ${clusterName}` : '';
suffix = suffix ? `- ${suffix}` : '';
$rootScope.$applyAsync(() => {
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js
index 8021ae7e5f63c..7e78170d1117f 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js
@@ -20,6 +20,8 @@ import { MonitoringViewBaseController } from '../../../base_controller';
import {
CODE_PATH_ELASTICSEARCH,
ALERT_CPU_USAGE,
+ ALERT_THREAD_POOL_SEARCH_REJECTIONS,
+ ALERT_THREAD_POOL_WRITE_REJECTIONS,
ALERT_MISSING_MONITORING_DATA,
ALERT_DISK_USAGE,
ALERT_MEMORY_USAGE,
@@ -76,6 +78,8 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', {
alertTypeIds: [
ALERT_CPU_USAGE,
ALERT_DISK_USAGE,
+ ALERT_THREAD_POOL_SEARCH_REJECTIONS,
+ ALERT_THREAD_POOL_WRITE_REJECTIONS,
ALERT_MEMORY_USAGE,
ALERT_MISSING_MONITORING_DATA,
],
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js
index 5164e93c266ca..586261eecb250 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js
@@ -21,6 +21,8 @@ import { MonitoringViewBaseController } from '../../base_controller';
import {
CODE_PATH_ELASTICSEARCH,
ALERT_CPU_USAGE,
+ ALERT_THREAD_POOL_SEARCH_REJECTIONS,
+ ALERT_THREAD_POOL_WRITE_REJECTIONS,
ALERT_MISSING_MONITORING_DATA,
ALERT_DISK_USAGE,
ALERT_MEMORY_USAGE,
@@ -60,6 +62,8 @@ uiRoutes.when('/elasticsearch/nodes/:node', {
alertTypeIds: [
ALERT_CPU_USAGE,
ALERT_DISK_USAGE,
+ ALERT_THREAD_POOL_SEARCH_REJECTIONS,
+ ALERT_THREAD_POOL_WRITE_REJECTIONS,
ALERT_MEMORY_USAGE,
ALERT_MISSING_MONITORING_DATA,
],
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js
index e69d572f9560b..3ec9c6235867b 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js
@@ -19,6 +19,8 @@ import {
ELASTICSEARCH_SYSTEM_ID,
CODE_PATH_ELASTICSEARCH,
ALERT_CPU_USAGE,
+ ALERT_THREAD_POOL_SEARCH_REJECTIONS,
+ ALERT_THREAD_POOL_WRITE_REJECTIONS,
ALERT_MISSING_MONITORING_DATA,
ALERT_DISK_USAGE,
ALERT_MEMORY_USAGE,
@@ -93,6 +95,8 @@ uiRoutes.when('/elasticsearch/nodes', {
alertTypeIds: [
ALERT_CPU_USAGE,
ALERT_DISK_USAGE,
+ ALERT_THREAD_POOL_SEARCH_REJECTIONS,
+ ALERT_THREAD_POOL_WRITE_REJECTIONS,
ALERT_MEMORY_USAGE,
ALERT_MISSING_MONITORING_DATA,
],
diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_common.ts b/x-pack/plugins/monitoring/server/alerts/alert_helpers.ts
similarity index 97%
rename from x-pack/plugins/monitoring/server/alerts/alerts_common.ts
rename to x-pack/plugins/monitoring/server/alerts/alert_helpers.ts
index 41c8bba17df0a..984746e59f06b 100644
--- a/x-pack/plugins/monitoring/server/alerts/alerts_common.ts
+++ b/x-pack/plugins/monitoring/server/alerts/alert_helpers.ts
@@ -5,7 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
-import { AlertMessageDocLinkToken } from './types';
+import { AlertMessageDocLinkToken } from '../../common/types/alerts';
import { AlertMessageTokenType } from '../../common/enums';
export class AlertingDefaults {
diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts
index f486061109b39..cc0423051f2aa 100644
--- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts
+++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts
@@ -60,9 +60,4 @@ describe('AlertsFactory', () => {
expect(alert).not.toBeNull();
expect(alert?.type).toBe(ALERT_CPU_USAGE);
});
-
- it('should get all', () => {
- const alerts = AlertsFactory.getAll();
- expect(alerts.length).toBe(10);
- });
});
diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts
index 22c41c9c60038..efd3d7d5e3b30 100644
--- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts
+++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts
@@ -8,6 +8,8 @@ import {
CpuUsageAlert,
MissingMonitoringDataAlert,
DiskUsageAlert,
+ ThreadPoolSearchRejectionsAlert,
+ ThreadPoolWriteRejectionsAlert,
MemoryUsageAlert,
NodesChangedAlert,
ClusterHealthAlert,
@@ -23,6 +25,8 @@ import {
ALERT_CPU_USAGE,
ALERT_MISSING_MONITORING_DATA,
ALERT_DISK_USAGE,
+ ALERT_THREAD_POOL_SEARCH_REJECTIONS,
+ ALERT_THREAD_POOL_WRITE_REJECTIONS,
ALERT_MEMORY_USAGE,
ALERT_NODES_CHANGED,
ALERT_LOGSTASH_VERSION_MISMATCH,
@@ -31,12 +35,14 @@ import {
} from '../../common/constants';
import { AlertsClient } from '../../../alerts/server';
-export const BY_TYPE = {
+const BY_TYPE = {
[ALERT_CLUSTER_HEALTH]: ClusterHealthAlert,
[ALERT_LICENSE_EXPIRATION]: LicenseExpirationAlert,
[ALERT_CPU_USAGE]: CpuUsageAlert,
[ALERT_MISSING_MONITORING_DATA]: MissingMonitoringDataAlert,
[ALERT_DISK_USAGE]: DiskUsageAlert,
+ [ALERT_THREAD_POOL_SEARCH_REJECTIONS]: ThreadPoolSearchRejectionsAlert,
+ [ALERT_THREAD_POOL_WRITE_REJECTIONS]: ThreadPoolWriteRejectionsAlert,
[ALERT_MEMORY_USAGE]: MemoryUsageAlert,
[ALERT_NODES_CHANGED]: NodesChangedAlert,
[ALERT_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchAlert,
diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts
index c92291cf72093..48b783a450807 100644
--- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts
@@ -28,14 +28,16 @@ import {
AlertData,
AlertInstanceState,
AlertEnableAction,
-} from './types';
+ CommonAlertFilter,
+ CommonAlertParams,
+ CommonBaseAlert,
+} from '../../common/types/alerts';
import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs';
import { fetchClusters } from '../lib/alerts/fetch_clusters';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants';
import { MonitoringConfig } from '../config';
import { AlertSeverity } from '../../common/enums';
-import { CommonAlertFilter, CommonAlertParams, CommonBaseAlert } from '../../common/types';
import { MonitoringLicenseService } from '../types';
import { mbSafeQuery } from '../lib/mb_safe_query';
import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index';
@@ -269,18 +271,18 @@ export class BaseAlert {
}
protected async fetchData(
- params: CommonAlertParams,
+ params: CommonAlertParams | unknown,
callCluster: any,
clusters: AlertCluster[],
uiSettings: IUiSettingsClient,
availableCcs: string[]
- ): Promise {
+ ): Promise> {
// Child should implement
throw new Error('Child classes must implement `fetchData`');
}
protected async processData(
- data: AlertData[],
+ data: Array,
clusters: AlertCluster[],
services: AlertServices,
logger: Logger,
@@ -365,15 +367,18 @@ export class BaseAlert {
};
}
- protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage {
+ protected getUiMessage(
+ alertState: AlertState | unknown,
+ item: AlertData | unknown
+ ): AlertMessage {
throw new Error('Child classes must implement `getUiMessage`');
}
protected executeActions(
instance: AlertInstance,
- instanceState: AlertInstanceState,
- item: AlertData,
- cluster: AlertCluster
+ instanceState: AlertInstanceState | unknown,
+ item: AlertData | unknown,
+ cluster?: AlertCluster | unknown
) {
throw new Error('Child classes must implement `executeActions`');
}
diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts
index 427dd2f86de00..1d3d36413ebc2 100644
--- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts
@@ -14,15 +14,15 @@ import {
AlertMessageLinkToken,
AlertInstanceState,
LegacyAlert,
-} from './types';
+ CommonAlertParams,
+} from '../../common/types/alerts';
import { AlertInstance } from '../../../alerts/server';
-import { INDEX_ALERTS, ALERT_CLUSTER_HEALTH } from '../../common/constants';
+import { INDEX_ALERTS, ALERT_CLUSTER_HEALTH, LEGACY_ALERT_DETAILS } from '../../common/constants';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
import { AlertMessageTokenType, AlertClusterHealthType } from '../../common/enums';
import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts';
import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity';
-import { CommonAlertParams } from '../../common/types';
-import { AlertingDefaults } from './alerts_common';
+import { AlertingDefaults } from './alert_helpers';
const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterHealth.redMessage', {
defaultMessage: 'Allocate missing primary and replica shards',
@@ -39,9 +39,7 @@ const WATCH_NAME = 'elasticsearch_cluster_status';
export class ClusterHealthAlert extends BaseAlert {
public type = ALERT_CLUSTER_HEALTH;
- public label = i18n.translate('xpack.monitoring.alerts.clusterHealth.label', {
- defaultMessage: 'Cluster health',
- });
+ public label = LEGACY_ALERT_DETAILS[ALERT_CLUSTER_HEALTH].label;
public isLegacy = true;
protected actionVariables = [
diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts
index 09133dadca162..55931e2996cbf 100644
--- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts
@@ -16,55 +16,36 @@ import {
AlertMessageTimeToken,
AlertMessageLinkToken,
AlertInstanceState,
-} from './types';
+ CommonAlertFilter,
+ CommonAlertNodeUuidFilter,
+ CommonAlertParams,
+} from '../../common/types/alerts';
import { AlertInstance, AlertServices } from '../../../alerts/server';
-import { INDEX_PATTERN_ELASTICSEARCH, ALERT_CPU_USAGE } from '../../common/constants';
+import {
+ INDEX_PATTERN_ELASTICSEARCH,
+ ALERT_CPU_USAGE,
+ ALERT_DETAILS,
+} from '../../common/constants';
import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
-import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums';
+import { AlertMessageTokenType, AlertSeverity } from '../../common/enums';
import { RawAlertInstance } from '../../../alerts/common';
import { parseDuration } from '../../../alerts/common/parse_duration';
-import {
- CommonAlertFilter,
- CommonAlertNodeUuidFilter,
- CommonAlertParams,
- CommonAlertParamDetail,
-} from '../../common/types';
-import { AlertingDefaults, createLink } from './alerts_common';
+import { AlertingDefaults, createLink } from './alert_helpers';
import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index';
-const DEFAULT_THRESHOLD = 85;
-const DEFAULT_DURATION = '5m';
-
interface CpuUsageParams {
threshold: number;
duration: string;
}
export class CpuUsageAlert extends BaseAlert {
- public static paramDetails = {
- threshold: {
- label: i18n.translate('xpack.monitoring.alerts.cpuUsage.paramDetails.threshold.label', {
- defaultMessage: `Notify when CPU is over`,
- }),
- type: AlertParamType.Percentage,
- } as CommonAlertParamDetail,
- duration: {
- label: i18n.translate('xpack.monitoring.alerts.cpuUsage.paramDetails.duration.label', {
- defaultMessage: `Look at the average over`,
- }),
- type: AlertParamType.Duration,
- } as CommonAlertParamDetail,
- };
-
public type = ALERT_CPU_USAGE;
- public label = i18n.translate('xpack.monitoring.alerts.cpuUsage.label', {
- defaultMessage: 'CPU Usage',
- });
+ public label = ALERT_DETAILS[ALERT_CPU_USAGE].label;
protected defaultParams: CpuUsageParams = {
- threshold: DEFAULT_THRESHOLD,
- duration: DEFAULT_DURATION,
+ threshold: 85,
+ duration: '5m',
};
protected actionVariables = [
diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts
index 34c640de79625..e54e736724357 100644
--- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts
@@ -15,43 +15,25 @@ import {
AlertMessageTimeToken,
AlertMessageLinkToken,
AlertInstanceState,
-} from './types';
+ CommonAlertFilter,
+ CommonAlertParams,
+} from '../../common/types/alerts';
import { AlertInstance, AlertServices } from '../../../alerts/server';
-import { INDEX_PATTERN_ELASTICSEARCH, ALERT_DISK_USAGE } from '../../common/constants';
+import {
+ INDEX_PATTERN_ELASTICSEARCH,
+ ALERT_DISK_USAGE,
+ ALERT_DETAILS,
+} from '../../common/constants';
import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
-import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums';
+import { AlertMessageTokenType, AlertSeverity } from '../../common/enums';
import { RawAlertInstance } from '../../../alerts/common';
-import { CommonAlertFilter, CommonAlertParams, CommonAlertParamDetail } from '../../common/types';
-import { AlertingDefaults, createLink } from './alerts_common';
+import { AlertingDefaults, createLink } from './alert_helpers';
import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index';
-interface ParamDetails {
- [key: string]: CommonAlertParamDetail;
-}
-
export class DiskUsageAlert extends BaseAlert {
- public static readonly PARAM_DETAILS: ParamDetails = {
- threshold: {
- label: i18n.translate('xpack.monitoring.alerts.diskUsage.paramDetails.threshold.label', {
- defaultMessage: `Notify when disk capacity is over`,
- }),
- type: AlertParamType.Percentage,
- },
- duration: {
- label: i18n.translate('xpack.monitoring.alerts.diskUsage.paramDetails.duration.label', {
- defaultMessage: `Look at the average over`,
- }),
- type: AlertParamType.Duration,
- },
- };
- public static paramDetails = DiskUsageAlert.PARAM_DETAILS;
- public static readonly TYPE = ALERT_DISK_USAGE;
- public static readonly LABEL = i18n.translate('xpack.monitoring.alerts.diskUsage.label', {
- defaultMessage: 'Disk Usage',
- });
- public type = DiskUsageAlert.TYPE;
- public label = DiskUsageAlert.LABEL;
+ public type = ALERT_DISK_USAGE;
+ public label = ALERT_DETAILS[ALERT_DISK_USAGE].label;
protected defaultParams = {
threshold: 80,
diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts
index f26b21f0c64c5..6412dcfde54bd 100644
--- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts
@@ -13,22 +13,24 @@ import {
AlertMessage,
AlertInstanceState,
LegacyAlert,
-} from './types';
+ CommonAlertParams,
+} from '../../common/types/alerts';
import { AlertInstance } from '../../../alerts/server';
-import { INDEX_ALERTS, ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants';
+import {
+ INDEX_ALERTS,
+ ALERT_ELASTICSEARCH_VERSION_MISMATCH,
+ LEGACY_ALERT_DETAILS,
+} from '../../common/constants';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
import { AlertSeverity } from '../../common/enums';
-import { CommonAlertParams } from '../../common/types';
import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts';
-import { AlertingDefaults } from './alerts_common';
+import { AlertingDefaults } from './alert_helpers';
const WATCH_NAME = 'elasticsearch_version_mismatch';
export class ElasticsearchVersionMismatchAlert extends BaseAlert {
public type = ALERT_ELASTICSEARCH_VERSION_MISMATCH;
- public label = i18n.translate('xpack.monitoring.alerts.elasticsearchVersionMismatch.label', {
- defaultMessage: 'Elasticsearch version mismatch',
- });
+ public label = LEGACY_ALERT_DETAILS[ALERT_ELASTICSEARCH_VERSION_MISMATCH].label;
public isLegacy = true;
protected actionVariables = [
diff --git a/x-pack/plugins/monitoring/server/alerts/index.ts b/x-pack/plugins/monitoring/server/alerts/index.ts
index 48254f2dec326..5fa718dfb34cd 100644
--- a/x-pack/plugins/monitoring/server/alerts/index.ts
+++ b/x-pack/plugins/monitoring/server/alerts/index.ts
@@ -8,6 +8,8 @@ export { BaseAlert } from './base_alert';
export { CpuUsageAlert } from './cpu_usage_alert';
export { MissingMonitoringDataAlert } from './missing_monitoring_data_alert';
export { DiskUsageAlert } from './disk_usage_alert';
+export { ThreadPoolSearchRejectionsAlert } from './thread_pool_search_rejections_alert';
+export { ThreadPoolWriteRejectionsAlert } from './thread_pool_write_rejections_alert';
export { MemoryUsageAlert } from './memory_usage_alert';
export { ClusterHealthAlert } from './cluster_health_alert';
export { LicenseExpirationAlert } from './license_expiration_alert';
@@ -15,4 +17,4 @@ export { NodesChangedAlert } from './nodes_changed_alert';
export { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert';
export { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert';
export { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert';
-export { AlertsFactory, BY_TYPE } from './alerts_factory';
+export { AlertsFactory } from './alerts_factory';
diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts
index 316f305603964..851a401635792 100644
--- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts
@@ -13,22 +13,24 @@ import {
AlertMessage,
AlertInstanceState,
LegacyAlert,
-} from './types';
+ CommonAlertParams,
+} from '../../common/types/alerts';
import { AlertInstance } from '../../../alerts/server';
-import { INDEX_ALERTS, ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants';
+import {
+ INDEX_ALERTS,
+ ALERT_KIBANA_VERSION_MISMATCH,
+ LEGACY_ALERT_DETAILS,
+} from '../../common/constants';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
import { AlertSeverity } from '../../common/enums';
-import { CommonAlertParams } from '../../common/types';
import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts';
-import { AlertingDefaults } from './alerts_common';
+import { AlertingDefaults } from './alert_helpers';
const WATCH_NAME = 'kibana_version_mismatch';
export class KibanaVersionMismatchAlert extends BaseAlert {
public type = ALERT_KIBANA_VERSION_MISMATCH;
- public label = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.label', {
- defaultMessage: 'Kibana version mismatch',
- });
+ public label = LEGACY_ALERT_DETAILS[ALERT_KIBANA_VERSION_MISMATCH].label;
public isLegacy = true;
protected actionVariables = [
diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts
index f1412ff0fc91a..e0396ee6673e8 100644
--- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts
@@ -16,27 +16,26 @@ import {
AlertMessageLinkToken,
AlertInstanceState,
LegacyAlert,
-} from './types';
+ CommonAlertParams,
+} from '../../common/types/alerts';
import { AlertInstance } from '../../../alerts/server';
import {
INDEX_ALERTS,
ALERT_LICENSE_EXPIRATION,
FORMAT_DURATION_TEMPLATE_SHORT,
+ LEGACY_ALERT_DETAILS,
} from '../../common/constants';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
import { AlertMessageTokenType } from '../../common/enums';
-import { CommonAlertParams } from '../../common/types';
import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts';
import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity';
-import { AlertingDefaults } from './alerts_common';
+import { AlertingDefaults } from './alert_helpers';
const WATCH_NAME = 'xpack_license_expiration';
export class LicenseExpirationAlert extends BaseAlert {
public type = ALERT_LICENSE_EXPIRATION;
- public label = i18n.translate('xpack.monitoring.alerts.licenseExpiration.label', {
- defaultMessage: 'License expiration',
- });
+ public label = LEGACY_ALERT_DETAILS[ALERT_LICENSE_EXPIRATION].label;
public isLegacy = true;
protected actionVariables = [
{
diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts
index 37515e32e591a..7f5c0ea40e36a 100644
--- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts
@@ -13,22 +13,24 @@ import {
AlertMessage,
AlertInstanceState,
LegacyAlert,
-} from './types';
+ CommonAlertParams,
+} from '../../common/types/alerts';
import { AlertInstance } from '../../../alerts/server';
-import { INDEX_ALERTS, ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants';
+import {
+ INDEX_ALERTS,
+ ALERT_LOGSTASH_VERSION_MISMATCH,
+ LEGACY_ALERT_DETAILS,
+} from '../../common/constants';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
import { AlertSeverity } from '../../common/enums';
-import { CommonAlertParams } from '../../common/types';
import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts';
-import { AlertingDefaults } from './alerts_common';
+import { AlertingDefaults } from './alert_helpers';
const WATCH_NAME = 'logstash_version_mismatch';
export class LogstashVersionMismatchAlert extends BaseAlert {
public type = ALERT_LOGSTASH_VERSION_MISMATCH;
- public label = i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.label', {
- defaultMessage: 'Logstash version mismatch',
- });
+ public label = LEGACY_ALERT_DETAILS[ALERT_LOGSTASH_VERSION_MISMATCH].label;
public isLegacy = true;
protected actionVariables = [
diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts
index 8dc707afab1e1..c37176764c020 100644
--- a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts
@@ -15,44 +15,26 @@ import {
AlertMessageTimeToken,
AlertMessageLinkToken,
AlertInstanceState,
-} from './types';
+ CommonAlertFilter,
+ CommonAlertParams,
+} from '../../common/types/alerts';
import { AlertInstance, AlertServices } from '../../../alerts/server';
-import { INDEX_PATTERN_ELASTICSEARCH, ALERT_MEMORY_USAGE } from '../../common/constants';
+import {
+ INDEX_PATTERN_ELASTICSEARCH,
+ ALERT_MEMORY_USAGE,
+ ALERT_DETAILS,
+} from '../../common/constants';
import { fetchMemoryUsageNodeStats } from '../lib/alerts/fetch_memory_usage_node_stats';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
-import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums';
+import { AlertMessageTokenType, AlertSeverity } from '../../common/enums';
import { RawAlertInstance } from '../../../alerts/common';
-import { CommonAlertFilter, CommonAlertParams, CommonAlertParamDetail } from '../../common/types';
-import { AlertingDefaults, createLink } from './alerts_common';
+import { AlertingDefaults, createLink } from './alert_helpers';
import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index';
import { parseDuration } from '../../../alerts/common/parse_duration';
-interface ParamDetails {
- [key: string]: CommonAlertParamDetail;
-}
-
export class MemoryUsageAlert extends BaseAlert {
- public static readonly PARAM_DETAILS: ParamDetails = {
- threshold: {
- label: i18n.translate('xpack.monitoring.alerts.memoryUsage.paramDetails.threshold.label', {
- defaultMessage: `Notify when memory usage is over`,
- }),
- type: AlertParamType.Percentage,
- },
- duration: {
- label: i18n.translate('xpack.monitoring.alerts.memoryUsage.paramDetails.duration.label', {
- defaultMessage: `Look at the average over`,
- }),
- type: AlertParamType.Duration,
- },
- };
- public static paramDetails = MemoryUsageAlert.PARAM_DETAILS;
- public static readonly TYPE = ALERT_MEMORY_USAGE;
- public static readonly LABEL = i18n.translate('xpack.monitoring.alerts.memoryUsage.label', {
- defaultMessage: 'Memory Usage (JVM)',
- });
- public type = MemoryUsageAlert.TYPE;
- public label = MemoryUsageAlert.LABEL;
+ public type = ALERT_MEMORY_USAGE;
+ public label = ALERT_DETAILS[ALERT_MEMORY_USAGE].label;
protected defaultParams = {
threshold: 85,
diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts
index 5b4542a4439ca..456ad92855f65 100644
--- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts
@@ -16,24 +16,22 @@ import {
AlertMissingData,
AlertMessageTimeToken,
AlertInstanceState,
-} from './types';
+ CommonAlertFilter,
+ CommonAlertParams,
+ CommonAlertStackProductFilter,
+ CommonAlertNodeUuidFilter,
+} from '../../common/types/alerts';
import { AlertInstance, AlertServices } from '../../../alerts/server';
import {
INDEX_PATTERN,
ALERT_MISSING_MONITORING_DATA,
INDEX_PATTERN_ELASTICSEARCH,
+ ALERT_DETAILS,
} from '../../common/constants';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
-import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums';
+import { AlertMessageTokenType, AlertSeverity } from '../../common/enums';
import { RawAlertInstance } from '../../../alerts/common';
import { parseDuration } from '../../../alerts/common/parse_duration';
-import {
- CommonAlertFilter,
- CommonAlertParams,
- CommonAlertParamDetail,
- CommonAlertStackProductFilter,
- CommonAlertNodeUuidFilter,
-} from '../../common/types';
import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index';
import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data';
import { getTypeLabelForStackProduct } from '../lib/alerts/get_type_label_for_stack_product';
@@ -41,7 +39,7 @@ import { getListingLinkForStackProduct } from '../lib/alerts/get_listing_link_fo
import { getStackProductLabel } from '../lib/alerts/get_stack_product_label';
import { fetchClusters } from '../lib/alerts/fetch_clusters';
import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs';
-import { AlertingDefaults, createLink } from './alerts_common';
+import { AlertingDefaults, createLink } from './alert_helpers';
const RESOLVED = i18n.translate('xpack.monitoring.alerts.missingData.resolved', {
defaultMessage: 'resolved',
@@ -62,27 +60,10 @@ interface MissingDataParams {
}
export class MissingMonitoringDataAlert extends BaseAlert {
- public static paramDetails = {
- duration: {
- label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.duration.label', {
- defaultMessage: `Notify if monitoring data is missing for the last`,
- }),
- type: AlertParamType.Duration,
- } as CommonAlertParamDetail,
- limit: {
- label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.limit.label', {
- defaultMessage: `looking back`,
- }),
- type: AlertParamType.Duration,
- } as CommonAlertParamDetail,
- };
-
public defaultThrottle: string = '6h';
public type = ALERT_MISSING_MONITORING_DATA;
- public label = i18n.translate('xpack.monitoring.alerts.missingData.label', {
- defaultMessage: 'Missing monitoring data',
- });
+ public label = ALERT_DETAILS[ALERT_MISSING_MONITORING_DATA].label;
protected defaultParams: MissingDataParams = {
duration: DEFAULT_DURATION,
diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts
index e03e6ea53ab4e..7b54ef629cba6 100644
--- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts
@@ -14,22 +14,20 @@ import {
AlertInstanceState,
LegacyAlert,
LegacyAlertNodesChangedList,
-} from './types';
+ CommonAlertParams,
+} from '../../common/types/alerts';
import { AlertInstance } from '../../../alerts/server';
-import { INDEX_ALERTS, ALERT_NODES_CHANGED } from '../../common/constants';
+import { INDEX_ALERTS, ALERT_NODES_CHANGED, LEGACY_ALERT_DETAILS } from '../../common/constants';
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
-import { CommonAlertParams } from '../../common/types';
import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts';
import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity';
-import { AlertingDefaults } from './alerts_common';
+import { AlertingDefaults } from './alert_helpers';
const WATCH_NAME = 'elasticsearch_nodes';
export class NodesChangedAlert extends BaseAlert {
public type = ALERT_NODES_CHANGED;
- public label = i18n.translate('xpack.monitoring.alerts.nodesChanged.label', {
- defaultMessage: 'Nodes changed',
- });
+ public label = LEGACY_ALERT_DETAILS[ALERT_NODES_CHANGED].label;
public isLegacy = true;
protected actionVariables = [
diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts
new file mode 100644
index 0000000000000..4905ae73b0545
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts
@@ -0,0 +1,312 @@
+/*
+ * 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 { IUiSettingsClient, Logger } from 'kibana/server';
+import { i18n } from '@kbn/i18n';
+import { BaseAlert } from './base_alert';
+import {
+ AlertData,
+ AlertCluster,
+ AlertMessage,
+ AlertThreadPoolRejectionsState,
+ AlertMessageTimeToken,
+ AlertMessageLinkToken,
+ CommonAlertFilter,
+ ThreadPoolRejectionsAlertParams,
+} from '../../common/types/alerts';
+import { AlertInstance, AlertServices } from '../../../alerts/server';
+import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants';
+import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats';
+import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
+import { AlertMessageTokenType, AlertSeverity } from '../../common/enums';
+import { Alert, RawAlertInstance } from '../../../alerts/common';
+import { AlertingDefaults, createLink } from './alert_helpers';
+import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index';
+
+type ActionVariables = Array<{ name: string; description: string }>;
+
+export class ThreadPoolRejectionsAlertBase extends BaseAlert {
+ protected static createActionVariables(type: string) {
+ return [
+ {
+ name: 'count',
+ description: i18n.translate(
+ 'xpack.monitoring.alerts.threadPoolRejections.actionVariables.count',
+ {
+ defaultMessage: 'The number of nodes reporting high thread pool {type} rejections.',
+ values: { type },
+ }
+ ),
+ },
+ ...Object.values(AlertingDefaults.ALERT_TYPE.context),
+ ];
+ }
+
+ protected defaultParams: ThreadPoolRejectionsAlertParams = {
+ threshold: 300,
+ duration: '5m',
+ };
+
+ constructor(
+ rawAlert: Alert | undefined = undefined,
+ public readonly type: string,
+ public readonly threadPoolType: string,
+ public readonly label: string,
+ public readonly actionVariables: ActionVariables
+ ) {
+ super(rawAlert);
+ }
+
+ protected async fetchData(
+ params: ThreadPoolRejectionsAlertParams,
+ callCluster: any,
+ clusters: AlertCluster[],
+ uiSettings: IUiSettingsClient,
+ availableCcs: string[]
+ ): Promise {
+ let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH);
+ if (availableCcs) {
+ esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs);
+ }
+
+ const { threshold, duration } = params;
+
+ const stats = await fetchThreadPoolRejectionStats(
+ callCluster,
+ clusters,
+ esIndexPattern,
+ this.config.ui.max_bucket_size,
+ this.threadPoolType,
+ duration
+ );
+
+ return stats.map((stat) => {
+ const { clusterUuid, nodeId, rejectionCount, ccs } = stat;
+
+ return {
+ instanceKey: `${clusterUuid}:${nodeId}`,
+ shouldFire: rejectionCount > threshold,
+ rejectionCount,
+ severity: AlertSeverity.Danger,
+ meta: stat,
+ clusterUuid,
+ ccs,
+ };
+ });
+ }
+
+ protected filterAlertInstance(alertInstance: RawAlertInstance, filters: CommonAlertFilter[]) {
+ const alertInstanceStates = alertInstance.state
+ ?.alertStates as AlertThreadPoolRejectionsState[];
+ const nodeUuid = filters?.find((filter) => filter.nodeUuid)?.nodeUuid;
+
+ if (!alertInstanceStates?.length || !nodeUuid) {
+ return true;
+ }
+
+ const nodeAlerts = alertInstanceStates.filter(({ nodeId }) => nodeId === nodeUuid);
+ return Boolean(nodeAlerts.length);
+ }
+
+ protected getUiMessage(
+ alertState: AlertThreadPoolRejectionsState,
+ rejectionCount: number
+ ): AlertMessage {
+ const { nodeName, nodeId } = alertState;
+ return {
+ text: i18n.translate('xpack.monitoring.alerts.threadPoolRejections.ui.firingMessage', {
+ defaultMessage: `Node #start_link{nodeName}#end_link is reporting {rejectionCount} {type} rejections at #absolute`,
+ values: {
+ nodeName,
+ type: this.threadPoolType,
+ rejectionCount,
+ },
+ }),
+ nextSteps: [
+ createLink(
+ i18n.translate(
+ 'xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.monitorThisNode',
+ {
+ defaultMessage: `#start_linkMonitor this node#end_link`,
+ }
+ ),
+ `elasticsearch/nodes/${nodeId}/advanced`,
+ AlertMessageTokenType.Link
+ ),
+ createLink(
+ i18n.translate(
+ 'xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.optimizeQueries',
+ {
+ defaultMessage: '#start_linkOptimize complex queries#end_link',
+ }
+ ),
+ `{elasticWebsiteUrl}blog/advanced-tuning-finding-and-fixing-slow-elasticsearch-queries`
+ ),
+ createLink(
+ i18n.translate('xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.addMoreNodes', {
+ defaultMessage: '#start_linkAdd more nodes#end_link',
+ }),
+ `{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html`
+ ),
+ createLink(
+ i18n.translate(
+ 'xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.resizeYourDeployment',
+ {
+ defaultMessage: '#start_linkResize your deployment (ECE)#end_link',
+ }
+ ),
+ `{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html`
+ ),
+ createLink(
+ i18n.translate(
+ 'xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.threadPoolSettings',
+ {
+ defaultMessage: '#start_linkThread pool settings#end_link',
+ }
+ ),
+ `{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html`
+ ),
+ ],
+ tokens: [
+ {
+ startToken: '#absolute',
+ type: AlertMessageTokenType.Time,
+ isAbsolute: true,
+ isRelative: false,
+ timestamp: alertState.ui.triggeredMS,
+ } as AlertMessageTimeToken,
+ {
+ startToken: '#start_link',
+ endToken: '#end_link',
+ type: AlertMessageTokenType.Link,
+ url: `elasticsearch/nodes/${nodeId}`,
+ } as AlertMessageLinkToken,
+ ],
+ };
+ }
+
+ protected executeActions(
+ instance: AlertInstance,
+ alertStates: AlertThreadPoolRejectionsState[],
+ cluster: AlertCluster
+ ) {
+ const type = this.threadPoolType;
+ const count = alertStates.length;
+ const { clusterName: clusterKnownName, clusterUuid } = cluster;
+ const clusterName = clusterKnownName || clusterUuid;
+ const shortActionText = i18n.translate(
+ 'xpack.monitoring.alerts.threadPoolRejections.shortAction',
+ {
+ defaultMessage: 'Verify thread pool {type} rejections across affected nodes.',
+ values: {
+ type,
+ },
+ }
+ );
+
+ const fullActionText = i18n.translate(
+ 'xpack.monitoring.alerts.threadPoolRejections.fullAction',
+ {
+ defaultMessage: 'View nodes',
+ }
+ );
+
+ const ccs = alertStates.find((state) => state.ccs)?.ccs;
+ const globalStateLink = this.createGlobalStateLink('elasticsearch/nodes', clusterUuid, ccs);
+
+ const action = `[${fullActionText}](${globalStateLink})`;
+ const internalShortMessage = i18n.translate(
+ 'xpack.monitoring.alerts.threadPoolRejections.firing.internalShortMessage',
+ {
+ defaultMessage: `Thread pool {type} rejections alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`,
+ values: {
+ count,
+ clusterName,
+ shortActionText,
+ type,
+ },
+ }
+ );
+ const internalFullMessage = i18n.translate(
+ 'xpack.monitoring.alerts.threadPoolRejections.firing.internalFullMessage',
+ {
+ defaultMessage: `Thread pool {type} rejections alert is firing for {count} node(s) in cluster: {clusterName}. {action}`,
+ values: {
+ count,
+ clusterName,
+ action,
+ type,
+ },
+ }
+ );
+
+ instance.scheduleActions('default', {
+ internalShortMessage,
+ internalFullMessage: this.isCloud ? internalShortMessage : internalFullMessage,
+ threadPoolType: type,
+ state: AlertingDefaults.ALERT_STATE.firing,
+ count,
+ clusterName,
+ action,
+ actionPlain: shortActionText,
+ });
+ }
+
+ protected async processData(
+ data: AlertData[],
+ clusters: AlertCluster[],
+ services: AlertServices,
+ logger: Logger,
+ state: { lastChecked?: number }
+ ) {
+ const currentUTC = +new Date();
+ for (const cluster of clusters) {
+ const nodes = data.filter((node) => node.clusterUuid === cluster.clusterUuid);
+ if (!nodes.length) {
+ continue;
+ }
+
+ const firingNodeUuids = nodes.filter((node) => node.shouldFire);
+
+ if (!firingNodeUuids.length) {
+ continue;
+ }
+
+ const instanceSuffix = firingNodeUuids.map((node) => node.meta.nodeId);
+
+ const instancePrefix = `${this.type}:${cluster.clusterUuid}:`;
+ const alertInstanceId = `${instancePrefix}:${instanceSuffix}`;
+ const alertInstance = services.alertInstanceFactory(alertInstanceId);
+ const newAlertStates: AlertThreadPoolRejectionsState[] = [];
+
+ for (const node of nodes) {
+ if (!node.shouldFire) {
+ continue;
+ }
+ const stat = node.meta as AlertThreadPoolRejectionsState;
+ const nodeState = this.getDefaultAlertState(
+ cluster,
+ node
+ ) as AlertThreadPoolRejectionsState;
+ const { nodeId, nodeName, rejectionCount } = stat;
+ nodeState.nodeId = nodeId;
+ nodeState.nodeName = nodeName;
+ nodeState.ui.triggeredMS = currentUTC;
+ nodeState.ui.isFiring = true;
+ nodeState.ui.severity = node.severity;
+ nodeState.ui.message = this.getUiMessage(nodeState, rejectionCount);
+ newAlertStates.push(nodeState);
+ }
+
+ alertInstance.replaceState({ alertStates: newAlertStates });
+ if (newAlertStates.length) {
+ this.executeActions(alertInstance, newAlertStates, cluster);
+ }
+ }
+
+ state.lastChecked = currentUTC;
+ return state;
+ }
+}
diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts
new file mode 100644
index 0000000000000..10df95c05ba3f
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { ThreadPoolRejectionsAlertBase } from './thread_pool_rejections_alert_base';
+import { ALERT_THREAD_POOL_SEARCH_REJECTIONS, ALERT_DETAILS } from '../../common/constants';
+import { Alert } from '../../../alerts/common';
+
+export class ThreadPoolSearchRejectionsAlert extends ThreadPoolRejectionsAlertBase {
+ private static TYPE = ALERT_THREAD_POOL_SEARCH_REJECTIONS;
+ private static THREAD_POOL_TYPE = 'search';
+ private static readonly LABEL = ALERT_DETAILS[ALERT_THREAD_POOL_SEARCH_REJECTIONS].label;
+ constructor(rawAlert?: Alert) {
+ super(
+ rawAlert,
+ ThreadPoolSearchRejectionsAlert.TYPE,
+ ThreadPoolSearchRejectionsAlert.THREAD_POOL_TYPE,
+ ThreadPoolSearchRejectionsAlert.LABEL,
+ ThreadPoolRejectionsAlertBase.createActionVariables(
+ ThreadPoolSearchRejectionsAlert.THREAD_POOL_TYPE
+ )
+ );
+ }
+}
diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts
new file mode 100644
index 0000000000000..d415515315b37
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { ThreadPoolRejectionsAlertBase } from './thread_pool_rejections_alert_base';
+import { ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_DETAILS } from '../../common/constants';
+import { Alert } from '../../../alerts/common';
+
+export class ThreadPoolWriteRejectionsAlert extends ThreadPoolRejectionsAlertBase {
+ private static TYPE = ALERT_THREAD_POOL_WRITE_REJECTIONS;
+ private static THREAD_POOL_TYPE = 'write';
+ private static readonly LABEL = ALERT_DETAILS[ALERT_THREAD_POOL_WRITE_REJECTIONS].label;
+ constructor(rawAlert?: Alert) {
+ super(
+ rawAlert,
+ ThreadPoolWriteRejectionsAlert.TYPE,
+ ThreadPoolWriteRejectionsAlert.THREAD_POOL_TYPE,
+ ThreadPoolWriteRejectionsAlert.LABEL,
+ ThreadPoolRejectionsAlertBase.createActionVariables(
+ ThreadPoolWriteRejectionsAlert.THREAD_POOL_TYPE
+ )
+ );
+ }
+}
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts
index d474338bce922..368a909279b8c 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash';
-import { AlertCluster } from '../../alerts/types';
+import { AlertCluster } from '../../../common/types/alerts';
interface RangeFilter {
[field: string]: {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts
index ecd324c083a8c..b38a32164223e 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts
@@ -6,7 +6,7 @@
import { get } from 'lodash';
import moment from 'moment';
import { NORMALIZED_DERIVATIVE_UNIT } from '../../../common/constants';
-import { AlertCluster, AlertCpuUsageNodeStats } from '../../alerts/types';
+import { AlertCluster, AlertCpuUsageNodeStats } from '../../../common/types/alerts';
interface NodeBucketESResponse {
key: string;
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts
index 6201204ebebe0..f00c42d708b16 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts
@@ -5,7 +5,7 @@
*/
import { get } from 'lodash';
-import { AlertCluster, AlertDiskUsageNodeStats } from '../../alerts/types';
+import { AlertCluster, AlertDiskUsageNodeStats } from '../../../common/types/alerts';
export async function fetchDiskUsageNodeStats(
callCluster: any,
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts
index fe01a1b921c2e..fbf7608a737ba 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash';
-import { LegacyAlert, AlertCluster, LegacyAlertMetadata } from '../../alerts/types';
+import { LegacyAlert, AlertCluster, LegacyAlertMetadata } from '../../../common/types/alerts';
export async function fetchLegacyAlerts(
callCluster: any,
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts
index c6843c3ed5f12..9a68b3afc7758 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts
@@ -5,7 +5,7 @@
*/
import { get } from 'lodash';
-import { AlertCluster, AlertMemoryUsageNodeStats } from '../../alerts/types';
+import { AlertCluster, AlertMemoryUsageNodeStats } from '../../../common/types/alerts';
export async function fetchMemoryUsageNodeStats(
callCluster: any,
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts
index 91fc05137a8c1..49307764e9f01 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash';
-import { AlertCluster, AlertMissingData } from '../../alerts/types';
+import { AlertCluster, AlertMissingData } from '../../../common/types/alerts';
import {
KIBANA_SYSTEM_ID,
BEATS_SYSTEM_ID,
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts
index 824eeab7245b4..c31ab91866b1d 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts
@@ -5,7 +5,7 @@
*/
import { fetchStatus } from './fetch_status';
-import { AlertUiState, AlertState } from '../../alerts/types';
+import { AlertUiState, AlertState } from '../../../common/types/alerts';
import { AlertSeverity } from '../../../common/enums';
import {
ALERT_CPU_USAGE,
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts
index ed49f42e4908c..ed860ee21344d 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts
@@ -4,10 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import moment from 'moment';
-import { AlertInstanceState } from '../../alerts/types';
+import { AlertInstanceState } from '../../../common/types/alerts';
import { AlertsClient } from '../../../../alerts/server';
import { AlertsFactory } from '../../alerts';
-import { CommonAlertStatus, CommonAlertState, CommonAlertFilter } from '../../../common/types';
+import {
+ CommonAlertStatus,
+ CommonAlertState,
+ CommonAlertFilter,
+} from '../../../common/types/alerts';
import { ALERTS } from '../../../common/constants';
import { MonitoringLicenseService } from '../../types';
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts
new file mode 100644
index 0000000000000..664ceb1d9411b
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts
@@ -0,0 +1,141 @@
+/*
+ * 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 { get } from 'lodash';
+import { AlertCluster, AlertThreadPoolRejectionsStats } from '../../../common/types/alerts';
+
+const invalidNumberValue = (value: number) => {
+ return isNaN(value) || value === undefined || value === null;
+};
+
+const getTopHits = (threadType: string, order: string) => ({
+ top_hits: {
+ sort: [
+ {
+ timestamp: {
+ order,
+ unmapped_type: 'long',
+ },
+ },
+ ],
+ _source: {
+ includes: [`node_stats.thread_pool.${threadType}.rejected`, 'source_node.name'],
+ },
+ size: 1,
+ },
+});
+
+export async function fetchThreadPoolRejectionStats(
+ callCluster: any,
+ clusters: AlertCluster[],
+ index: string,
+ size: number,
+ threadType: string,
+ duration: string
+): Promise {
+ const clustersIds = clusters.map((cluster) => cluster.clusterUuid);
+ const params = {
+ index,
+ filterPath: ['aggregations'],
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ {
+ terms: {
+ cluster_uuid: clustersIds,
+ },
+ },
+ {
+ term: {
+ type: 'node_stats',
+ },
+ },
+ {
+ range: {
+ timestamp: {
+ gte: `now-${duration}`,
+ },
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ clusters: {
+ terms: {
+ field: 'cluster_uuid',
+ size,
+ },
+ aggs: {
+ nodes: {
+ terms: {
+ field: 'source_node.uuid',
+ size,
+ },
+ aggs: {
+ most_recent: {
+ ...getTopHits(threadType, 'desc'),
+ },
+ least_recent: {
+ ...getTopHits(threadType, 'asc'),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const response = await callCluster('search', params);
+ const stats: AlertThreadPoolRejectionsStats[] = [];
+ const { buckets: clusterBuckets = [] } = response.aggregations.clusters;
+
+ if (!clusterBuckets.length) {
+ return stats;
+ }
+
+ for (const clusterBucket of clusterBuckets) {
+ for (const node of clusterBucket.nodes.buckets) {
+ const mostRecentDoc = get(node, 'most_recent.hits.hits[0]');
+ mostRecentDoc.timestamp = mostRecentDoc.sort[0];
+
+ const leastRecentDoc = get(node, 'least_recent.hits.hits[0]');
+ leastRecentDoc.timestamp = leastRecentDoc.sort[0];
+
+ if (!mostRecentDoc || mostRecentDoc.timestamp === leastRecentDoc.timestamp) {
+ continue;
+ }
+
+ const rejectedPath = `_source.node_stats.thread_pool.${threadType}.rejected`;
+ const newRejectionCount = Number(get(mostRecentDoc, rejectedPath));
+ const oldRejectionCount = Number(get(leastRecentDoc, rejectedPath));
+
+ if (invalidNumberValue(newRejectionCount) || invalidNumberValue(oldRejectionCount)) {
+ continue;
+ }
+
+ const rejectionCount =
+ oldRejectionCount > newRejectionCount
+ ? newRejectionCount
+ : newRejectionCount - oldRejectionCount;
+ const indexName = mostRecentDoc._index;
+ const nodeName = get(mostRecentDoc, '_source.source_node.name') || node.key;
+ const nodeStat = {
+ rejectionCount,
+ type: threadType,
+ clusterUuid: clusterBucket.key,
+ nodeId: node.key,
+ nodeName,
+ ccs: indexName.includes(':') ? indexName.split(':')[0] : null,
+ };
+ stats.push(nodeStat);
+ }
+ }
+ return stats;
+}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts
index d97bc34c2adb0..29a27ac3d05e7 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts
@@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema';
import { handleError } from '../../../../lib/errors';
import { RouteDependencies } from '../../../../types';
import { fetchStatus } from '../../../../lib/alerts/fetch_status';
-import { CommonAlertFilter } from '../../../../../common/types';
+import { CommonAlertFilter } from '../../../../../common/types/alerts';
export function alertStatusRoute(server: any, npRoute: RouteDependencies) {
npRoute.router.post(
diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx b/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx
index e04e8f050006a..6a05749df6d7a 100644
--- a/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx
+++ b/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx
@@ -13,7 +13,7 @@ describe('EmptySection', () => {
const section: ISection = {
id: 'apm',
title: 'APM',
- icon: 'logoAPM',
+ icon: 'logoObservability',
description: 'foo bar',
};
const { getByText, queryAllByText } = render();
@@ -26,7 +26,7 @@ describe('EmptySection', () => {
const section: ISection = {
id: 'apm',
title: 'APM',
- icon: 'logoAPM',
+ icon: 'logoObservability',
description: 'foo bar',
linkTitle: 'install agent',
href: 'https://www.elastic.co',
diff --git a/x-pack/plugins/observability/public/pages/home/section.ts b/x-pack/plugins/observability/public/pages/home/section.ts
index 8c87f17c16b3d..21bda1ac5bbba 100644
--- a/x-pack/plugins/observability/public/pages/home/section.ts
+++ b/x-pack/plugins/observability/public/pages/home/section.ts
@@ -24,7 +24,7 @@ export const appsSection: ISection[] = [
title: i18n.translate('xpack.observability.section.apps.apm.title', {
defaultMessage: 'APM',
}),
- icon: 'logoAPM',
+ icon: 'logoObservability',
description: i18n.translate('xpack.observability.section.apps.apm.description', {
defaultMessage:
'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.',
diff --git a/x-pack/plugins/observability/public/pages/overview/empty_section.ts b/x-pack/plugins/observability/public/pages/overview/empty_section.ts
index e13efbb8ffdd2..95b46cbb782d8 100644
--- a/x-pack/plugins/observability/public/pages/overview/empty_section.ts
+++ b/x-pack/plugins/observability/public/pages/overview/empty_section.ts
@@ -29,7 +29,7 @@ export const getEmptySections = ({ core }: { core: AppMountContext['core'] }): I
title: i18n.translate('xpack.observability.emptySection.apps.apm.title', {
defaultMessage: 'APM',
}),
- icon: 'logoAPM',
+ icon: 'logoObservability',
description: i18n.translate('xpack.observability.emptySection.apps.apm.description', {
defaultMessage:
'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.',
@@ -74,7 +74,7 @@ export const getEmptySections = ({ core }: { core: AppMountContext['core'] }): I
title: i18n.translate('xpack.observability.emptySection.apps.ux.title', {
defaultMessage: 'User Experience',
}),
- icon: 'logoAPM',
+ icon: 'logoObservability',
description: i18n.translate('xpack.observability.emptySection.apps.ux.description', {
defaultMessage:
'Performance is a distribution. Measure the experiences of all visitors to your web application and understand how to improve the experience for everyone.',
diff --git a/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx b/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx
index e6a61995ac78a..c6c0bee98a466 100644
--- a/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx
+++ b/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx
@@ -102,7 +102,6 @@ export function MainControls({ toggleRequestFlyout, isRequestFlyoutOpen, reset,
isOpen={isHelpOpen}
closePopover={() => setIsHelpOpen(false)}
panelPaddingSize="none"
- withTitle
anchorPosition="upLeft"
>
diff --git a/x-pack/plugins/painless_lab/public/styles/_index.scss b/x-pack/plugins/painless_lab/public/styles/_index.scss
index 0a6b84523424a..14a58fe4bdb8b 100644
--- a/x-pack/plugins/painless_lab/public/styles/_index.scss
+++ b/x-pack/plugins/painless_lab/public/styles/_index.scss
@@ -1,5 +1,4 @@
@import '@elastic/eui/src/global_styling/variables/header';
-@import '@elastic/eui/src/components/nav_drawer/variables';
/**
* This is a very brittle way of preventing the editor and other content from disappearing
@@ -51,16 +50,3 @@ $headerOffset: $euiHeaderHeightCompensation * 3;
// The panels container should adopt the height of the main container
height: 100%;
}
-
-/**
- * 1. Hack EUI so the bottom bar doesn't obscure the nav drawer flyout, but is also not obscured
- * by the main content area.
- */
-.painlessLab__bottomBar {
- z-index: 5; /* 1 */
- left: $euiNavDrawerWidthCollapsed;
-}
-
-.painlessLab__bottomBar-isNavDrawerLocked {
- left: $euiNavDrawerWidthExpanded;
-}
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap
index 4f54c2f9a1675..ff0caecf93a96 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap
+++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap
@@ -1270,6 +1270,7 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u
>
diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap b/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap
index 1124924fcdda2..f33e322cbaae4 100644
--- a/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap
+++ b/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap
@@ -71,14 +71,21 @@ exports[`OverwrittenSessionPage renders as expected 1`] = `
href="/mock-base-path/"
>
{
isOpen={isMenuOpen}
closePopover={() => setIsMenuOpen(false)}
panelPaddingSize="none"
- withTitle
anchorPosition="downLeft"
>
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx
index a07c2e1c14ac4..57828c50ca17f 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx
@@ -249,7 +249,7 @@ export class FeatureTable extends Component {
);
const extraAction = (
-
+
{feature.reserved.description}
);
@@ -320,6 +320,15 @@ export class FeatureTable extends Component {
options={options}
idSelected={`${feature.id}_${selectedPrivilegeId ?? NO_PRIVILEGE_VALUE}`}
onChange={this.onChange(feature.id)}
+ legend={i18n.translate('xpack.security.management.editRole.featureTable.actionLegendText', {
+ defaultMessage: '{featureName} feature privilege',
+ values: {
+ featureName: feature.name,
+ },
+ })}
+ style={{
+ minWidth: 200,
+ }}
/>
);
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.tsx
index 5432a50c1f0df..41e15387f9c47 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiCheckbox, EuiButtonGroup } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { NO_PRIVILEGE_VALUE } from '../constants';
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
import {
@@ -118,7 +119,7 @@ export const SubFeatureForm = (props: Props) => {
options={options}
idSelected={firstSelectedPrivilege?.id ?? NO_PRIVILEGE_VALUE}
isDisabled={props.disabled}
- onChange={(selectedPrivilegeId) => {
+ onChange={(selectedPrivilegeId: string) => {
// Deselect all privileges which belong to this mutually-exclusive group
const privilegesWithoutGroupEntries = props.selectedFeaturePrivileges.filter(
(sp) => !privilegeGroup.privileges.some((privilege) => privilege.id === sp)
@@ -130,6 +131,15 @@ export const SubFeatureForm = (props: Props) => {
props.onChange([...privilegesWithoutGroupEntries, selectedPrivilegeId]);
}
}}
+ legend={i18n.translate(
+ 'xpack.security.management.editRole.subFeatureForm.controlLegendText',
+ {
+ defaultMessage: '{subFeatureName} sub-feature privilege',
+ values: {
+ subFeatureName: props.subFeature.name,
+ },
+ }
+ )}
/>
);
}
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx
index 6601c6ae1f8d5..8254e2632aa9d 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx
@@ -193,7 +193,7 @@ describe('', () => {
const featurePrivilegeToggles = wrapper.find(EuiButtonGroup);
expect(featurePrivilegeToggles).toHaveLength(1);
- expect(featurePrivilegeToggles.find('button')).toHaveLength(3);
+ expect(featurePrivilegeToggles.find('input')).toHaveLength(3);
(featurePrivilegeToggles.props() as EuiButtonGroupProps).onChange('feature1_all', null);
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx
index 28bbd55c7d544..7ce878321185a 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx
@@ -222,6 +222,12 @@ export class PrivilegeSpaceForm extends Component {
idSelected={this.getDisplayedBasePrivilege()}
isDisabled={!hasSelectedSpaces}
onChange={this.onSpaceBasePrivilegeChange}
+ legend={i18n.translate(
+ 'xpack.security.management.editRole.spacePrivilegeForm.basePrivilegeControlLegend',
+ {
+ defaultMessage: 'Privileges for all features',
+ }
+ )}
/>
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 2910f02a187f4..767a2616a4c7e 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -179,3 +179,5 @@ export const showAllOthersBucket: string[] = [
'destination.ip',
'user.name',
];
+
+export const ENABLE_NEW_TIMELINE = false;
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
index 491f4f8952fd9..783f8be840b7f 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
@@ -55,7 +55,6 @@ import {
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
- RULE_ABOUT_DETAILS_HEADER_TOGGLE,
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
@@ -180,7 +179,7 @@ describe('Custom detection rules creation', () => {
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
- cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
+ cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
@@ -333,9 +332,7 @@ describe('Custom detection rules deletion and edition', () => {
getDetails(RISK_SCORE_DETAILS).should('have.text', editedRule.riskScore);
getDetails(TAGS_DETAILS).should('have.text', expectedEditedtags);
});
- cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE)
- .eq(INVESTIGATION_NOTES_TOGGLE)
- .click({ force: true });
+ cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', editedRule.note);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should(
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts
index bee4713ca7cda..3d4aaca8bb78f 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts
@@ -38,7 +38,6 @@ import {
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
- RULE_ABOUT_DETAILS_HEADER_TOGGLE,
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
@@ -142,7 +141,7 @@ describe('Detection rules, EQL', () => {
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
- cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
+ cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts
index e31fe2e9a3911..e905365d1bbb3 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts
@@ -41,7 +41,6 @@ import {
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RISK_SCORE_OVERRIDE_DETAILS,
- RULE_ABOUT_DETAILS_HEADER_TOGGLE,
RULE_NAME_HEADER,
RULE_NAME_OVERRIDE_DETAILS,
RULE_TYPE_DETAILS,
@@ -160,7 +159,7 @@ describe('Detection rules, override', () => {
});
});
});
- cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
+ cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts
index a6f974256f3e4..a9b43d82bb7fd 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts
@@ -38,7 +38,6 @@ import {
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
- RULE_ABOUT_DETAILS_HEADER_TOGGLE,
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
@@ -141,7 +140,7 @@ describe('Detection rules, threshold', () => {
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
- cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
+ cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts
index e40b81ed0e856..d72210dd3e083 100644
--- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts
@@ -34,7 +34,7 @@ export const INDEX_PATTERNS_DETAILS = 'Index patterns';
export const INVESTIGATION_NOTES_MARKDOWN = 'test markdown';
-export const INVESTIGATION_NOTES_TOGGLE = 1;
+export const INVESTIGATION_NOTES_TOGGLE = '[data-test-subj="stepAboutDetailsToggle-notes"]';
export const MACHINE_LEARNING_JOB_ID = '[data-test-subj="machineLearningJobId"]';
diff --git a/x-pack/plugins/security_solution/public/cases/components/case_status/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_status/index.tsx
index a37c9052c2ff3..2d3a7850eb0b6 100644
--- a/x-pack/plugins/security_solution/public/cases/components/case_status/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/case_status/index.tsx
@@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useCallback } from 'react';
import styled, { css } from 'styled-components';
import {
EuiBadge,
+ EuiButton,
EuiButtonEmpty,
- EuiButtonToggle,
EuiDescriptionList,
EuiDescriptionListDescription,
EuiDescriptionListTitle,
@@ -44,7 +44,7 @@ interface CaseStatusProps {
onRefresh: () => void;
status: string;
title: string;
- toggleStatusCase: (evt: unknown) => void;
+ toggleStatusCase: (status: boolean) => void;
value: string | null;
}
const CaseStatusComp: React.FC = ({
@@ -62,56 +62,62 @@ const CaseStatusComp: React.FC = ({
title,
toggleStatusCase,
value,
-}) => (
-
-
-
-
-
- {i18n.STATUS}
-
-
- {status}
-
-
+}) => {
+ const handleToggleStatusCase = useCallback(() => {
+ toggleStatusCase(!isSelected);
+ }, [toggleStatusCase, isSelected]);
+ return (
+
+
+
+
+
+ {i18n.STATUS}
+
+
+ {status}
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+ {i18n.CASE_REFRESH}
+
- {title}
-
-
-
+
+ {buttonLabel}
+
+
+
+
-
-
-
-
-
-
- {i18n.CASE_REFRESH}
-
-
-
-
-
-
-
-
-
-
-
-);
+
+
+ );
+};
export const CaseStatus = React.memo(CaseStatusComp);
diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx
index 58d7efb763ee2..5cb6ede0d9d21 100644
--- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx
@@ -183,9 +183,7 @@ describe('CaseView ', () => {
);
await waitFor(() => {
- wrapper
- .find('input[data-test-subj="toggle-case-status"]')
- .simulate('change', { target: { checked: true } });
+ wrapper.find('[data-test-subj="toggle-case-status"]').first().simulate('click');
expect(updateCaseProperty).toHaveBeenCalled();
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx
index 52cea10cfb275..7ee2b856f8786 100644
--- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx
@@ -5,7 +5,7 @@
*/
import {
- EuiButtonToggle,
+ EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingContent,
@@ -242,10 +242,10 @@ export const CaseComponent = React.memo(
);
const toggleStatusCase = useCallback(
- (e) =>
+ (nextStatus) =>
onUpdateField({
key: 'status',
- value: e.target.checked ? 'closed' : 'open',
+ value: nextStatus ? 'closed' : 'open',
}),
[onUpdateField]
);
@@ -307,6 +307,11 @@ export const CaseComponent = React.memo(
[allCasesLink]
);
+ const isSelected = useMemo(() => caseStatusData.isSelected, [caseStatusData]);
+ const handleToggleStatusCase = useCallback(() => {
+ toggleStatusCase(!isSelected);
+ }, [toggleStatusCase, isSelected]);
+
return (
<>
@@ -330,7 +335,7 @@ export const CaseComponent = React.memo(
disabled={!userCanCrud}
isLoading={isLoading && updateKey === 'status'}
onRefresh={handleRefresh}
- toggleStatusCase={toggleStatusCase}
+ toggleStatusCase={handleToggleStatusCase}
{...caseStatusData}
/>
@@ -358,15 +363,16 @@ export const CaseComponent = React.memo(
-
+ fill={caseStatusData.isSelected}
+ onClick={handleToggleStatusCase}
+ >
+ {caseStatusData.buttonLabel}
+
{hasDataToPush && (
diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap
index 3df8663324fdd..6331a2e02b219 100644
--- a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap
@@ -77,15 +77,6 @@ exports[`Paginated Table Component rendering it renders the default load more ta
"warning": "#ffce7a",
},
"euiButtonMinWidth": "112px",
- "euiButtonToggleBorderColor": "#343741",
- "euiButtonToggleTypes": Object {
- "danger": "#ff6666",
- "ghost": "#ffffff",
- "primary": "#1ba9f5",
- "secondary": "#7de2d1",
- "text": "#98a2b3",
- "warning": "#ffce7a",
- },
"euiButtonTypes": Object {
"danger": "#ff6666",
"ghost": "#ffffff",
@@ -340,15 +331,6 @@ exports[`Paginated Table Component rendering it renders the default load more ta
"small": "14px",
"xSmall": "12px",
},
- "euiNavDrawerBackgroundColor": "#1d1e24",
- "euiNavDrawerContractingDelay": "150ms",
- "euiNavDrawerExpandingDelay": "250ms",
- "euiNavDrawerExtendedDelay": "1000ms",
- "euiNavDrawerMenuAddedDelay": "90ms",
- "euiNavDrawerSideShadow": "2px 0 2px -1px rgba(0, 0, 0, 0.3)",
- "euiNavDrawerTopPosition": "49px",
- "euiNavDrawerWidthCollapsed": "48px",
- "euiNavDrawerWidthExpanded": "240px",
"euiPageBackgroundColor": "#1a1b20",
"euiPaletteColorBlind": Object {
"euiColorVis0": Object {
@@ -402,10 +384,22 @@ exports[`Paginated Table Component rendering it renders the default load more ta
"euiPopoverTranslateDistance": "8px",
"euiProgressColors": Object {
"accent": "#f990c0",
+ "customColor": "currentColor",
"danger": "#ff6666",
"primary": "#1ba9f5",
"secondary": "#7de2d1",
"subdued": "#81858f",
+ "success": "#7de2d1",
+ "vis0": "#54b399",
+ "vis1": "#6092c0",
+ "vis2": "#d36086",
+ "vis3": "#9170b8",
+ "vis4": "#ca8eae",
+ "vis5": "#d6bf57",
+ "vis6": "#b9a888",
+ "vis7": "#da8b45",
+ "vis8": "#aa6556",
+ "vis9": "#e7664c",
"warning": "#ffce7a",
},
"euiProgressSizes": Object {
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx
index 757319e7aa1ae..555e02d13d723 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx
@@ -104,8 +104,8 @@ describe('StepAboutRuleToggleDetails', () => {
);
expect(wrapper.find(EuiButtonGroup).exists()).toBeTruthy();
- expect(wrapper.find('EuiButtonToggle[id="details"]').at(0).prop('isSelected')).toBeTruthy();
- expect(wrapper.find('EuiButtonToggle[id="notes"]').at(0).prop('isSelected')).toBeFalsy();
+ expect(wrapper.find('#details').at(0).prop('isSelected')).toBeTruthy();
+ expect(wrapper.find('#notes').at(0).prop('isSelected')).toBeFalsy();
});
test('it allows users to toggle between "details" and "note"', () => {
@@ -122,16 +122,17 @@ describe('StepAboutRuleToggleDetails', () => {
);
- expect(wrapper.find('EuiButtonGroup[idSelected="details"]').exists()).toBeTruthy();
- expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeFalsy();
+ expect(wrapper.find('[idSelected="details"]').exists()).toBeTruthy();
+ expect(wrapper.find('[idSelected="notes"]').exists()).toBeFalsy();
wrapper
- .find('input[title="Investigation guide"]')
+ .find('[title="Investigation guide"]')
.at(0)
+ .find('input')
.simulate('change', { target: { value: 'notes' } });
- expect(wrapper.find('EuiButtonGroup[idSelected="details"]').exists()).toBeFalsy();
- expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy();
+ expect(wrapper.find('[idSelected="details"]').exists()).toBeFalsy();
+ expect(wrapper.find('[idSelected="notes"]').exists()).toBeTruthy();
});
test('it displays notes markdown when user toggles to "notes"', () => {
@@ -149,8 +150,9 @@ describe('StepAboutRuleToggleDetails', () => {
);
wrapper
- .find('input[title="Investigation guide"]')
+ .find('[title="Investigation guide"]')
.at(0)
+ .find('input')
.simulate('change', { target: { value: 'notes' } });
expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy();
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
index 52e9dc7e44ff7..fb98233bf8cc7 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
@@ -8,7 +8,7 @@ import {
EuiPanel,
EuiProgress,
EuiButtonGroup,
- EuiButtonGroupOption,
+ EuiButtonGroupOptionProps,
EuiSpacer,
EuiFlexItem,
EuiText,
@@ -46,14 +46,16 @@ const AboutContent = styled.div`
height: 100%;
`;
-const toggleOptions: EuiButtonGroupOption[] = [
+const toggleOptions: EuiButtonGroupOptionProps[] = [
{
id: 'details',
label: i18n.ABOUT_PANEL_DETAILS_TAB,
+ 'data-test-subj': 'stepAboutDetailsToggle-details',
},
{
id: 'notes',
label: i18n.ABOUT_PANEL_NOTES_TAB,
+ 'data-test-subj': 'stepAboutDetailsToggle-notes',
},
];
@@ -98,6 +100,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({
setToggleOption(val);
}}
data-test-subj="stepAboutDetailsToggle"
+ legend={i18n.ABOUT_CONTROL_LEGEND}
/>
)}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts
index e1b89b1ec8ce2..8a98697b523d7 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts
@@ -25,3 +25,10 @@ export const ABOUT_PANEL_NOTES_TAB = i18n.translate(
defaultMessage: 'Investigation guide',
}
);
+
+export const ABOUT_CONTROL_LEGEND = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.details.stepAboutRule.controlLegend',
+ {
+ defaultMessage: 'Viewing',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap
index 86cb203671ac2..c82b9cac8ab1f 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap
@@ -2653,7 +2653,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
class="euiFlexItem euiFlexItem--flexGrowZero"
>
);
});
diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/filters/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/filters/index.tsx
index af2c1523605d2..1bf608787fd7e 100644
--- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/filters/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/filters/index.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButtonGroup, EuiButtonGroupOption } from '@elastic/eui';
+import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { FilterMode } from '../types';
@@ -13,7 +13,7 @@ import * as i18n from '../translations';
const MY_RECENTLY_REPORTED_ID = 'myRecentlyReported';
-const toggleButtonIcons: EuiButtonGroupOption[] = [
+const toggleButtonIcons: EuiButtonGroupOptionProps[] = [
{
id: 'recentlyCreated',
label: i18n.RECENTLY_CREATED_CASES,
@@ -45,7 +45,15 @@ export const Filters = React.memo<{
[setFilterBy]
);
- return ;
+ return (
+
+ );
});
Filters.displayName = 'Filters';
diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/translations.ts b/x-pack/plugins/security_solution/public/overview/components/recent_cases/translations.ts
index f9b3e05ad9595..ff5585affb475 100644
--- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/translations.ts
+++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/translations.ts
@@ -41,3 +41,10 @@ export const VIEW_ALL_CASES = i18n.translate(
defaultMessage: 'View all cases',
}
);
+
+export const CASES_FILTER_CONTROL = i18n.translate(
+ 'xpack.securitySolution.recentCases.controlLegend',
+ {
+ defaultMessage: 'Cases filter',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/filters/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/filters/index.tsx
index 815768482781b..bd6f1271f3073 100644
--- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/filters/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/filters/index.tsx
@@ -4,14 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButtonGroup, EuiButtonGroupOption } from '@elastic/eui';
+import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui';
import React from 'react';
import { FilterMode } from '../types';
import * as i18n from '../translations';
-const toggleButtonIcons: EuiButtonGroupOption[] = [
+const toggleButtonIcons: EuiButtonGroupOptionProps[] = [
{
id: 'favorites',
label: i18n.FAVORITES,
@@ -35,6 +35,7 @@ export const Filters = React.memo<{
setFilterBy(f as FilterMode);
}}
isIconOnly
+ legend={i18n.TIMELINES_FILTER_CONTROL}
/>
));
diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/translations.ts b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/translations.ts
index 468773ae90790..998e333d727d2 100644
--- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/translations.ts
+++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/translations.ts
@@ -81,3 +81,10 @@ export const VIEW_ALL_TIMELINES = i18n.translate(
defaultMessage: 'View all timelines',
}
);
+
+export const TIMELINES_FILTER_CONTROL = i18n.translate(
+ 'xpack.securitySolution.recentTimelines.filterControlLegend',
+ {
+ defaultMessage: 'Timelines filter',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
index a711e7a1d0442..0737db7a00788 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
@@ -125,13 +125,27 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({
associateNote: (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })),
- updateDescription: ({ id, description }: { id: string; description: string }) =>
- dispatch(timelineActions.updateDescription({ id, description })),
+ updateDescription: ({
+ id,
+ description,
+ disableAutoSave,
+ }: {
+ id: string;
+ description: string;
+ disableAutoSave?: boolean;
+ }) => dispatch(timelineActions.updateDescription({ id, description, disableAutoSave })),
updateIsFavorite: ({ id, isFavorite }: { id: string; isFavorite: boolean }) =>
dispatch(timelineActions.updateIsFavorite({ id, isFavorite })),
updateNote: (note: Note) => dispatch(appActions.updateNote({ note })),
- updateTitle: ({ id, title }: { id: string; title: string }) =>
- dispatch(timelineActions.updateTitle({ id, title })),
+ updateTitle: ({
+ id,
+ title,
+ disableAutoSave,
+ }: {
+ id: string;
+ title: string;
+ disableAutoSave?: boolean;
+ }) => dispatch(timelineActions.updateTitle({ id, title, disableAutoSave })),
toggleLock: ({ linkToId }: { linkToId: InputsModelId }) =>
dispatch(inputsActions.toggleTimelineLinkTo({ linkToId })),
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap
index 10ad0123f7fc6..17c614bd2c83c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap
@@ -77,15 +77,6 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = `
"warning": "#ffce7a",
},
"euiButtonMinWidth": "112px",
- "euiButtonToggleBorderColor": "#343741",
- "euiButtonToggleTypes": Object {
- "danger": "#ff6666",
- "ghost": "#ffffff",
- "primary": "#1ba9f5",
- "secondary": "#7de2d1",
- "text": "#98a2b3",
- "warning": "#ffce7a",
- },
"euiButtonTypes": Object {
"danger": "#ff6666",
"ghost": "#ffffff",
@@ -340,15 +331,6 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = `
"small": "14px",
"xSmall": "12px",
},
- "euiNavDrawerBackgroundColor": "#1d1e24",
- "euiNavDrawerContractingDelay": "150ms",
- "euiNavDrawerExpandingDelay": "250ms",
- "euiNavDrawerExtendedDelay": "1000ms",
- "euiNavDrawerMenuAddedDelay": "90ms",
- "euiNavDrawerSideShadow": "2px 0 2px -1px rgba(0, 0, 0, 0.3)",
- "euiNavDrawerTopPosition": "49px",
- "euiNavDrawerWidthCollapsed": "48px",
- "euiNavDrawerWidthExpanded": "240px",
"euiPageBackgroundColor": "#1a1b20",
"euiPaletteColorBlind": Object {
"euiColorVis0": Object {
@@ -402,10 +384,22 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = `
"euiPopoverTranslateDistance": "8px",
"euiProgressColors": Object {
"accent": "#f990c0",
+ "customColor": "currentColor",
"danger": "#ff6666",
"primary": "#1ba9f5",
"secondary": "#7de2d1",
"subdued": "#81858f",
+ "success": "#7de2d1",
+ "vis0": "#54b399",
+ "vis1": "#6092c0",
+ "vis2": "#d36086",
+ "vis3": "#9170b8",
+ "vis4": "#ca8eae",
+ "vis5": "#d6bf57",
+ "vis6": "#b9a888",
+ "vis7": "#da8b45",
+ "vis8": "#aa6556",
+ "vis9": "#e7664c",
"warning": "#ffce7a",
},
"euiProgressSizes": Object {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx
index 0cd7032596f15..ff3df357f7337 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx
@@ -196,7 +196,6 @@ const AddDataProviderPopoverComponent: React.FC = (
isOpen={isAddFilterPopoverOpen}
closePopover={handleClosePopover}
anchorPosition="downLeft"
- withTitle
panelPaddingSize="none"
ownFocus={true}
repositionOnScroll
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx
new file mode 100644
index 0000000000000..e9dc312ee8d19
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow, mount } from 'enzyme';
+
+import { SaveTimelineButton } from './save_timeline_button';
+import { act } from '@testing-library/react-hooks';
+
+jest.mock('react-redux', () => {
+ const actual = jest.requireActual('react-redux');
+ return {
+ ...actual,
+ useDispatch: jest.fn(),
+ };
+});
+
+jest.mock('./title_and_description');
+
+describe('SaveTimelineButton', () => {
+ const props = {
+ timelineId: 'timeline-1',
+ showOverlay: false,
+ toolTip: 'tooltip message',
+ toggleSaveTimeline: jest.fn(),
+ onSaveTimeline: jest.fn(),
+ updateTitle: jest.fn(),
+ updateDescription: jest.fn(),
+ };
+ test('Show tooltip', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(true);
+ });
+
+ test('Hide tooltip', () => {
+ const testProps = {
+ ...props,
+ showOverlay: true,
+ };
+ const component = mount();
+ component.find('[data-test-subj="save-timeline-button-icon"]').first().simulate('click');
+
+ act(() => {
+ expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(
+ false
+ );
+ });
+ });
+
+ test('should show a button with pencil icon', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="save-timeline-button-icon"]').prop('iconType')).toEqual(
+ 'pencil'
+ );
+ });
+
+ test('should not show a modal when showOverlay equals false', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="save-timeline-modal"]').exists()).toEqual(false);
+ });
+
+ test('should show a modal when showOverlay equals true', () => {
+ const testProps = {
+ ...props,
+ showOverlay: true,
+ };
+ const component = mount();
+ component.find('[data-test-subj="save-timeline-button-icon"]').first().simulate('click');
+ act(() => {
+ expect(component.find('[data-test-subj="save-timeline-modal"]').exists()).toEqual(true);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx
new file mode 100644
index 0000000000000..476ef8d1dd5a1
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx
@@ -0,0 +1,87 @@
+/*
+ * 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 { EuiButtonIcon, EuiOverlayMask, EuiModal, EuiToolTip } from '@elastic/eui';
+
+import React, { useCallback, useMemo, useState } from 'react';
+import { useDispatch } from 'react-redux';
+import { timelineActions } from '../../../store/timeline';
+import { NOTES_PANEL_WIDTH } from '../properties/notes_size';
+
+import { TimelineTitleAndDescription } from './title_and_description';
+import { EDIT } from './translations';
+
+export interface SaveTimelineComponentProps {
+ timelineId: string;
+ toolTip?: string;
+}
+
+export const SaveTimelineButton = React.memo(
+ ({ timelineId, toolTip }) => {
+ const [showSaveTimelineOverlay, setShowSaveTimelineOverlay] = useState(false);
+ const onToggleSaveTimeline = useCallback(() => {
+ setShowSaveTimelineOverlay((prevShowSaveTimelineOverlay) => !prevShowSaveTimelineOverlay);
+ }, [setShowSaveTimelineOverlay]);
+
+ const dispatch = useDispatch();
+ const updateTitle = useCallback(
+ ({ id, title, disableAutoSave }: { id: string; title: string; disableAutoSave?: boolean }) =>
+ dispatch(timelineActions.updateTitle({ id, title, disableAutoSave })),
+ [dispatch]
+ );
+
+ const updateDescription = useCallback(
+ ({
+ id,
+ description,
+ disableAutoSave,
+ }: {
+ id: string;
+ description: string;
+ disableAutoSave?: boolean;
+ }) => dispatch(timelineActions.updateDescription({ id, description, disableAutoSave })),
+ [dispatch]
+ );
+
+ const saveTimelineButtonIcon = useMemo(
+ () => (
+
+ ),
+ [onToggleSaveTimeline]
+ );
+
+ return showSaveTimelineOverlay ? (
+ <>
+ {saveTimelineButtonIcon}
+
+
+
+
+
+ >
+ ) : (
+
+ {saveTimelineButtonIcon}
+
+ );
+ }
+);
+
+SaveTimelineButton.displayName = 'SaveTimelineButton';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx
new file mode 100644
index 0000000000000..bcc90a25d5789
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx
@@ -0,0 +1,261 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { TimelineTitleAndDescription } from './title_and_description';
+import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
+import { useCreateTimelineButton } from '../properties/use_create_timeline';
+import { TimelineType } from '../../../../../common/types/timeline';
+import * as i18n from './translations';
+
+jest.mock('../../../../common/hooks/use_selector', () => ({
+ useShallowEqualSelector: jest.fn(),
+}));
+
+jest.mock('../../../../timelines/store/timeline', () => ({
+ timelineSelectors: {
+ selectTimeline: jest.fn(),
+ },
+}));
+
+jest.mock('../properties/use_create_timeline', () => ({
+ useCreateTimelineButton: jest.fn(),
+}));
+
+jest.mock('react-redux', () => {
+ const actual = jest.requireActual('react-redux');
+ return {
+ ...actual,
+ useDispatch: jest.fn(),
+ };
+});
+
+describe('TimelineTitleAndDescription', () => {
+ describe('save timeline', () => {
+ const props = {
+ timelineId: 'timeline-1',
+ toggleSaveTimeline: jest.fn(),
+ onSaveTimeline: jest.fn(),
+ updateTitle: jest.fn(),
+ updateDescription: jest.fn(),
+ };
+
+ const mockGetButton = jest.fn().mockReturnValue();
+
+ beforeEach(() => {
+ (useShallowEqualSelector as jest.Mock).mockReturnValue({
+ description: '',
+ isSaving: true,
+ savedObjectId: null,
+ title: 'my timeline',
+ timelineType: TimelineType.default,
+ });
+ (useCreateTimelineButton as jest.Mock).mockReturnValue({
+ getButton: mockGetButton,
+ });
+ });
+
+ afterEach(() => {
+ (useShallowEqualSelector as jest.Mock).mockReset();
+ (useCreateTimelineButton as jest.Mock).mockReset();
+ mockGetButton.mockClear();
+ });
+
+ test('show proress bar while saving', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="progress-bar"]').exists()).toEqual(true);
+ });
+
+ test('Show correct header for save timeline modal', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual(
+ i18n.SAVE_TIMELINE
+ );
+ });
+
+ test('Show correct header for save timeline template modal', () => {
+ (useShallowEqualSelector as jest.Mock).mockReturnValue({
+ description: '',
+ isSaving: true,
+ savedObjectId: null,
+ title: 'my timeline',
+ timelineType: TimelineType.template,
+ });
+ const component = shallow();
+ expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual(
+ i18n.SAVE_TIMELINE_TEMPLATE
+ );
+ });
+
+ test('Show name field', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="save-timeline-name"]').exists()).toEqual(true);
+ });
+
+ test('Show description field', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="save-timeline-description"]').exists()).toEqual(true);
+ });
+
+ test('Show close button', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="close-button"]').exists()).toEqual(true);
+ });
+
+ test('Show saveButton', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true);
+ });
+ });
+
+ describe('update timeline', () => {
+ const props = {
+ timelineId: 'timeline-1',
+ toggleSaveTimeline: jest.fn(),
+ onSaveTimeline: jest.fn(),
+ updateTitle: jest.fn(),
+ updateDescription: jest.fn(),
+ };
+
+ const mockGetButton = jest.fn().mockReturnValue();
+
+ beforeEach(() => {
+ (useShallowEqualSelector as jest.Mock).mockReturnValue({
+ description: 'xxxx',
+ isSaving: true,
+ savedObjectId: '1234',
+ title: 'my timeline',
+ timelineType: TimelineType.default,
+ });
+ (useCreateTimelineButton as jest.Mock).mockReturnValue({
+ getButton: mockGetButton,
+ });
+ });
+
+ afterEach(() => {
+ (useShallowEqualSelector as jest.Mock).mockReset();
+ (useCreateTimelineButton as jest.Mock).mockReset();
+ mockGetButton.mockClear();
+ });
+
+ test('show proress bar while saving', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="progress-bar"]').exists()).toEqual(true);
+ });
+
+ test('Show correct header for save timeline modal', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual(
+ i18n.NAME_TIMELINE
+ );
+ });
+
+ test('Show correct header for save timeline template modal', () => {
+ (useShallowEqualSelector as jest.Mock).mockReturnValue({
+ description: 'xxxx',
+ isSaving: true,
+ savedObjectId: '1234',
+ title: 'my timeline',
+ timelineType: TimelineType.template,
+ });
+ const component = shallow();
+ expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual(
+ i18n.NAME_TIMELINE_TEMPLATE
+ );
+ });
+
+ test('Show name field', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="save-timeline-name"]').exists()).toEqual(true);
+ });
+
+ test('Show description field', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="save-timeline-description"]').exists()).toEqual(true);
+ });
+
+ test('Show saveButton', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true);
+ });
+ });
+
+ describe('showWarning', () => {
+ const props = {
+ timelineId: 'timeline-1',
+ toggleSaveTimeline: jest.fn(),
+ onSaveTimeline: jest.fn(),
+ updateTitle: jest.fn(),
+ updateDescription: jest.fn(),
+ showWarning: true,
+ };
+
+ const mockGetButton = jest.fn().mockReturnValue();
+
+ beforeEach(() => {
+ (useShallowEqualSelector as jest.Mock).mockReturnValue({
+ description: '',
+ isSaving: true,
+ savedObjectId: null,
+ title: 'my timeline',
+ timelineType: TimelineType.default,
+ showWarnging: true,
+ });
+ (useCreateTimelineButton as jest.Mock).mockReturnValue({
+ getButton: mockGetButton,
+ });
+ });
+
+ afterEach(() => {
+ (useShallowEqualSelector as jest.Mock).mockReset();
+ (useCreateTimelineButton as jest.Mock).mockReset();
+ mockGetButton.mockClear();
+ });
+
+ test('Show EuiCallOut', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="save-timeline-callout"]').exists()).toEqual(true);
+ });
+
+ test('Show discardTimelineButton', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="mock-discard-button"]').exists()).toEqual(true);
+ });
+
+ test('get discardTimelineButton with correct props', () => {
+ shallow();
+ expect(mockGetButton).toBeCalledWith({
+ title: i18n.DISCARD_TIMELINE,
+ outline: true,
+ iconType: '',
+ fill: false,
+ });
+ });
+
+ test('get discardTimelineTemplateButton with correct props', () => {
+ (useShallowEqualSelector as jest.Mock).mockReturnValue({
+ description: 'xxxx',
+ isSaving: true,
+ savedObjectId: null,
+ title: 'my timeline',
+ timelineType: TimelineType.template,
+ });
+ shallow();
+ expect(mockGetButton).toBeCalledWith({
+ title: i18n.DISCARD_TIMELINE_TEMPLATE,
+ outline: true,
+ iconType: '',
+ fill: false,
+ });
+ });
+
+ test('Show saveButton', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx
new file mode 100644
index 0000000000000..3597b26e2663a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx
@@ -0,0 +1,218 @@
+/*
+ * 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 {
+ EuiButton,
+ EuiFlexGroup,
+ EuiFormRow,
+ EuiFlexItem,
+ EuiModalBody,
+ EuiModalHeader,
+ EuiSpacer,
+ EuiProgress,
+ EuiCallOut,
+} from '@elastic/eui';
+import React, { useCallback, useEffect, useMemo, useRef } from 'react';
+import { useDispatch } from 'react-redux';
+import styled from 'styled-components';
+import { TimelineType } from '../../../../../common/types/timeline';
+import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
+import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline';
+import { TimelineInput } from '../../../store/timeline/actions';
+import { Description, Name, UpdateTitle, UpdateDescription } from '../properties/helpers';
+import { TIMELINE_TITLE, DESCRIPTION, OPTIONAL } from '../properties/translations';
+import { useCreateTimelineButton } from '../properties/use_create_timeline';
+import * as i18n from './translations';
+
+interface TimelineTitleAndDescriptionProps {
+ showWarning?: boolean;
+ timelineId: string;
+ toggleSaveTimeline: () => void;
+ updateTitle: UpdateTitle;
+ updateDescription: UpdateDescription;
+}
+
+const Wrapper = styled(EuiModalBody)`
+ .euiFormRow {
+ max-width: none;
+ }
+
+ .euiFormControlLayout {
+ max-width: none;
+ }
+
+ .euiFieldText {
+ max-width: none;
+ }
+`;
+
+Wrapper.displayName = 'Wrapper';
+
+const usePrevious = (value: unknown) => {
+ const ref = useRef();
+ useEffect(() => {
+ ref.current = value;
+ });
+ return ref.current;
+};
+
+// when showWarning equals to true,
+// the modal is used as a reminder for users to save / discard
+// the unsaved timeline / template
+export const TimelineTitleAndDescription = React.memo(
+ ({ timelineId, toggleSaveTimeline, updateTitle, updateDescription, showWarning }) => {
+ const timeline = useShallowEqualSelector((state) =>
+ timelineSelectors.selectTimeline(state, timelineId)
+ );
+
+ const { description, isSaving, savedObjectId, title, timelineType } = timeline;
+
+ const prevIsSaving = usePrevious(isSaving);
+ const dispatch = useDispatch();
+ const onSaveTimeline = useCallback(
+ (args: TimelineInput) => dispatch(timelineActions.saveTimeline(args)),
+ [dispatch]
+ );
+
+ const handleClick = useCallback(() => {
+ onSaveTimeline({
+ ...timeline,
+ id: timelineId,
+ });
+ }, [onSaveTimeline, timeline, timelineId]);
+
+ const { getButton } = useCreateTimelineButton({ timelineId, timelineType });
+
+ const discardTimelineButton = useMemo(
+ () =>
+ getButton({
+ title:
+ timelineType === TimelineType.template
+ ? i18n.DISCARD_TIMELINE_TEMPLATE
+ : i18n.DISCARD_TIMELINE,
+ outline: true,
+ iconType: '',
+ fill: false,
+ }),
+ [getButton, timelineType]
+ );
+
+ useEffect(() => {
+ if (!isSaving && prevIsSaving) {
+ toggleSaveTimeline();
+ }
+ }, [isSaving, prevIsSaving, toggleSaveTimeline]);
+
+ const modalHeader =
+ savedObjectId == null
+ ? timelineType === TimelineType.template
+ ? i18n.SAVE_TIMELINE_TEMPLATE
+ : i18n.SAVE_TIMELINE
+ : timelineType === TimelineType.template
+ ? i18n.NAME_TIMELINE_TEMPLATE
+ : i18n.NAME_TIMELINE;
+
+ const saveButtonTitle =
+ savedObjectId == null && showWarning
+ ? timelineType === TimelineType.template
+ ? i18n.SAVE_TIMELINE_TEMPLATE
+ : i18n.SAVE_TIMELINE
+ : i18n.SAVE;
+
+ const calloutMessage = useMemo(() => i18n.UNSAVED_TIMELINE_WARNING(timelineType), [
+ timelineType,
+ ]);
+
+ const descriptionLabel = savedObjectId == null ? `${DESCRIPTION} (${OPTIONAL})` : DESCRIPTION;
+
+ return (
+ <>
+ {isSaving && (
+
+ )}
+ {modalHeader}
+
+
+ {showWarning && (
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {savedObjectId == null && showWarning ? (
+ discardTimelineButton
+ ) : (
+
+ {i18n.CLOSE_MODAL}
+
+ )}
+
+
+
+ {saveButtonTitle}
+
+
+
+
+
+ >
+ );
+ }
+);
+
+TimelineTitleAndDescription.displayName = 'TimelineTitleAndDescription';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts
index 89ad11d75cae1..80aa719a3469d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts
@@ -5,6 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
+import { TimelineType, TimelineTypeLiteral } from '../../../../../common/types/timeline';
export const CALL_OUT_UNAUTHORIZED_MSG = i18n.translate(
'xpack.securitySolution.timeline.callOut.unauthorized.message.description',
@@ -21,3 +22,66 @@ export const CALL_OUT_IMMUTABLE = i18n.translate(
'This prebuilt timeline template cannot be modified. To make changes, please duplicate this template and make modifications to the duplicate template.',
}
);
+
+export const EDIT = i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.button', {
+ defaultMessage: 'edit',
+});
+
+export const SAVE_TIMELINE = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimeline.modal.header',
+ {
+ defaultMessage: 'Save Timeline',
+ }
+);
+
+export const SAVE_TIMELINE_TEMPLATE = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimelineTemplate.modal.header',
+ {
+ defaultMessage: 'Save Timeline Template',
+ }
+);
+
+export const SAVE = i18n.translate('xpack.securitySolution.timeline.nameTimeline.save.title', {
+ defaultMessage: 'Save',
+});
+
+export const NAME_TIMELINE = i18n.translate(
+ 'xpack.securitySolution.timeline.nameTimeline.modal.header',
+ {
+ defaultMessage: 'Name Timeline',
+ }
+);
+
+export const NAME_TIMELINE_TEMPLATE = i18n.translate(
+ 'xpack.securitySolution.timeline.nameTimelineTemplate.modal.header',
+ {
+ defaultMessage: 'Name Timeline Template',
+ }
+);
+
+export const DISCARD_TIMELINE = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimeline.modal.discard.title',
+ {
+ defaultMessage: 'Discard Timeline',
+ }
+);
+
+export const DISCARD_TIMELINE_TEMPLATE = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimelineTemplate.modal.discard.title',
+ {
+ defaultMessage: 'Discard Timeline Template',
+ }
+);
+
+export const CLOSE_MODAL = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimeline.modal.close.title',
+ {
+ defaultMessage: 'Close',
+ }
+);
+
+export const UNSAVED_TIMELINE_WARNING = (timelineType: TimelineTypeLiteral) =>
+ i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.warning.title', {
+ values: { timeline: timelineType === TimelineType.template ? 'timeline template' : 'timeline' },
+ defaultMessage: 'You have an unsaved {timeline}. Do you wish to save it?',
+ });
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx
index 887c2e1e825f8..dd0695e795397 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx
@@ -5,8 +5,10 @@
*/
import React from 'react';
import { mount, shallow } from 'enzyme';
-import { NewTimeline, NewTimelineProps } from './helpers';
+import { Description, Name, NewTimeline, NewTimelineProps } from './helpers';
import { useCreateTimelineButton } from './use_create_timeline';
+import * as i18n from './translations';
+import { TimelineType } from '../../../../../common/types/timeline';
jest.mock('./use_create_timeline', () => ({
useCreateTimelineButton: jest.fn(),
@@ -83,3 +85,72 @@ describe('NewTimeline', () => {
});
});
});
+
+describe('Description', () => {
+ const props = {
+ description: 'xxx',
+ timelineId: 'timeline-1',
+ updateDescription: jest.fn(),
+ };
+
+ test('should render tooltip', () => {
+ const component = shallow();
+ expect(
+ component.find('[data-test-subj="timeline-description-tool-tip"]').prop('content')
+ ).toEqual(i18n.DESCRIPTION_TOOL_TIP);
+ });
+
+ test('should not render textarea if isTextArea is false', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="timeline-description-textarea"]').exists()).toEqual(
+ false
+ );
+
+ expect(component.find('[data-test-subj="timeline-description"]').exists()).toEqual(true);
+ });
+
+ test('should render textarea if isTextArea is true', () => {
+ const testProps = {
+ ...props,
+ isTextArea: true,
+ };
+ const component = shallow();
+ expect(component.find('[data-test-subj="timeline-description-textarea"]').exists()).toEqual(
+ true
+ );
+ });
+});
+
+describe('Name', () => {
+ const props = {
+ timelineId: 'timeline-1',
+ timelineType: TimelineType.default,
+ title: 'xxx',
+ updateTitle: jest.fn(),
+ };
+
+ test('should render tooltip', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="timeline-title-tool-tip"]').prop('content')).toEqual(
+ i18n.TITLE
+ );
+ });
+
+ test('should render placeholder by timelineType - timeline', () => {
+ const component = shallow();
+ expect(component.find('[data-test-subj="timeline-title"]').prop('placeholder')).toEqual(
+ i18n.UNTITLED_TIMELINE
+ );
+ });
+
+ test('should render placeholder by timelineType - timeline template', () => {
+ const testProps = {
+ ...props,
+ timelineType: TimelineType.template,
+ };
+ const component = shallow();
+ expect(component.find('[data-test-subj="timeline-title"]').prop('placeholder')).toEqual(
+ i18n.UNTITLED_TEMPLATE
+ );
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
index a28f4240d3a2f..25039dbc9529a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
@@ -16,8 +16,9 @@ import {
EuiModal,
EuiOverlayMask,
EuiToolTip,
+ EuiTextArea,
} from '@elastic/eui';
-import React, { useCallback, useMemo } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import uuid from 'uuid';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
@@ -41,14 +42,22 @@ import { Notes } from '../../notes';
import { AssociateNote, UpdateNote } from '../../notes/helpers';
import { NOTES_PANEL_WIDTH } from './notes_size';
-import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles';
+import {
+ ButtonContainer,
+ DescriptionContainer,
+ LabelText,
+ NameField,
+ NameWrapper,
+ StyledStar,
+} from './styles';
import * as i18n from './translations';
-import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions';
+import { setInsertTimeline, showTimeline, TimelineInput } from '../../../store/timeline/actions';
import { useCreateTimelineButton } from './use_create_timeline';
export const historyToolTip = 'The chronological history of actions related to this timeline';
export const streamLiveToolTip = 'Update the Timeline as new data arrives';
export const newTimelineToolTip = 'Create a new timeline';
+export const TIMELINE_TITLE_CLASSNAME = 'timeline-title';
const NotesCountBadge = (styled(EuiBadge)`
margin-left: 5px;
@@ -66,8 +75,25 @@ type CreateTimeline = ({
timelineType?: TimelineTypeLiteral;
}) => void;
type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void;
-type UpdateTitle = ({ id, title }: { id: string; title: string }) => void;
-type UpdateDescription = ({ id, description }: { id: string; description: string }) => void;
+export type UpdateTitle = ({
+ id,
+ title,
+ disableAutoSave,
+}: {
+ id: string;
+ title: string;
+ disableAutoSave?: boolean;
+}) => void;
+export type UpdateDescription = ({
+ id,
+ description,
+ disableAutoSave,
+}: {
+ id: string;
+ description: string;
+ disableAutoSave?: boolean;
+}) => void;
+export type SaveTimeline = (args: TimelineInput) => void;
export const StarIcon = React.memo<{
isFavorite: boolean;
@@ -104,55 +130,146 @@ interface DescriptionProps {
description: string;
timelineId: string;
updateDescription: UpdateDescription;
+ isTextArea?: boolean;
+ disableAutoSave?: boolean;
+ disableTooltip?: boolean;
+ disabled?: boolean;
+ marginRight?: number;
}
export const Description = React.memo(
- ({ description, timelineId, updateDescription }) => (
-
-
- updateDescription({ id: timelineId, description: e.target.value })}
- placeholder={i18n.DESCRIPTION}
- spellCheck={true}
- value={description}
- />
+ ({
+ description,
+ timelineId,
+ updateDescription,
+ isTextArea = false,
+ disableAutoSave = false,
+ disableTooltip = false,
+ disabled = false,
+ marginRight,
+ }) => {
+ const onDescriptionChanged = useCallback(
+ (e) => {
+ updateDescription({ id: timelineId, description: e.target.value, disableAutoSave });
+ },
+ [updateDescription, disableAutoSave, timelineId]
+ );
+
+ const inputField = useMemo(
+ () =>
+ isTextArea ? (
+
+ ) : (
+
+ ),
+ [description, isTextArea, onDescriptionChanged, disabled]
+ );
+ return (
+
+ {disableTooltip ? (
+ inputField
+ ) : (
+
+ {inputField}
+
+ )}
-
- )
+ );
+ }
);
Description.displayName = 'Description';
interface NameProps {
+ autoFocus?: boolean;
+ disableAutoSave?: boolean;
+ disableTooltip?: boolean;
+ disabled?: boolean;
timelineId: string;
timelineType: TimelineType;
title: string;
updateTitle: UpdateTitle;
+ width?: string;
+ marginRight?: number;
}
-export const Name = React.memo(({ timelineId, timelineType, title, updateTitle }) => {
- const handleChange = useCallback((e) => updateTitle({ id: timelineId, title: e.target.value }), [
+export const Name = React.memo(
+ ({
+ autoFocus = false,
+ disableAutoSave = false,
+ disableTooltip = false,
+ disabled = false,
timelineId,
+ timelineType,
+ title,
updateTitle,
- ]);
+ width,
+ marginRight,
+ }) => {
+ const timelineNameRef = useRef(null);
+
+ const handleChange = useCallback(
+ (e) => updateTitle({ id: timelineId, title: e.target.value, disableAutoSave }),
+ [timelineId, updateTitle, disableAutoSave]
+ );
- return (
-
-
-
- );
-});
+ useEffect(() => {
+ if (autoFocus && timelineNameRef && timelineNameRef.current) {
+ timelineNameRef.current.focus();
+ }
+ }, [autoFocus]);
+
+ const nameField = useMemo(
+ () => (
+
+ ),
+ [handleChange, marginRight, timelineType, title, width, disabled]
+ );
+
+ return (
+
+ {disableTooltip ? (
+ nameField
+ ) : (
+
+ {nameField}
+
+ )}
+
+ );
+ }
+);
Name.displayName = 'Name';
interface NewCaseProps {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
index 19344a7fd7c9b..cdedca23e85af 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
@@ -92,6 +92,7 @@ const defaultProps = {
description: '',
getNotesByIds: jest.fn(),
noteIds: [],
+ saveTimeline: jest.fn(),
status: TimelineStatus.active,
timelineId: 'abc',
toggleLock: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
index 9eea95a0a9b1a..9df2b585449a0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
@@ -92,6 +92,7 @@ export const Properties = React.memo(
setShowTimelineModal(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+
const { Modal: AllCasesModal, onOpenModal: onOpenCaseModal } = useAllCasesModal({ timelineId });
const datePickerWidth = useMemo(
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx
index a3cd8802c36bc..6b181a5af7bf3 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx
@@ -16,6 +16,8 @@ import { SuperDatePicker } from '../../../../common/components/super_date_picker
import { TimelineTypeLiteral, TimelineStatusLiteral } from '../../../../../common/types/timeline';
import * as i18n from './translations';
+import { SaveTimelineButton } from '../header/save_timeline_button';
+import { ENABLE_NEW_TIMELINE } from '../../../../../common/constants';
type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void;
type UpdateTitle = ({ id, title }: { id: string; title: string }) => void;
@@ -122,6 +124,8 @@ export const PropertiesLeft = React.memo(
) : null}
+ {ENABLE_NEW_TIMELINE && }
+
{showNotesFromWidth ? (
(({ width }) => ({
`;
DatePicker.displayName = 'DatePicker';
-export const NameField = styled(EuiFieldText)`
- width: 150px;
- margin-right: 5px;
+export const NameField = styled(({ width, marginRight, ...rest }) => )`
+ width: ${({ width = '150px' }) => width};
+ margin-right: ${({ marginRight = 10 }) => marginRight} px;
+
+ .euiToolTipAnchor {
+ display: block;
+ }
`;
NameField.displayName = 'NameField';
-export const DescriptionContainer = styled.div`
+export const NameWrapper = styled.div`
+ .euiToolTipAnchor {
+ display: block;
+ }
+`;
+NameWrapper.displayName = 'NameWrapper';
+
+export const DescriptionContainer = styled.div<{ marginRight?: number }>`
animation: ${fadeInEffect} 0.3s;
- margin-right: 5px;
+ margin-right: ${({ marginRight = 5 }) => marginRight}px;
min-width: 150px;
+
+ .euiToolTipAnchor {
+ display: block;
+ }
`;
DescriptionContainer.displayName = 'DescriptionContainer';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts
index 1fc3b7b00f847..78d01b2d98ab3 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts
@@ -34,7 +34,7 @@ export const NOT_A_FAVORITE = i18n.translate(
export const TIMELINE_TITLE = i18n.translate(
'xpack.securitySolution.timeline.properties.timelineTitleAriaLabel',
{
- defaultMessage: 'Timeline title',
+ defaultMessage: 'Title',
}
);
@@ -194,3 +194,10 @@ export const UNLOCK_SYNC_MAIN_DATE_PICKER_ARIA = i18n.translate(
defaultMessage: 'Unlock date picker to global date picker',
}
);
+
+export const OPTIONAL = i18n.translate(
+ 'xpack.securitySolution.timeline.properties.timelineDescriptionOptional',
+ {
+ defaultMessage: 'Optional',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx
index c21592bed12e0..10b505da5c76f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx
@@ -63,6 +63,46 @@ describe('useCreateTimelineButton', () => {
});
});
+ test('getButton renders correct iconType - EuiButton', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () => useCreateTimelineButton({ timelineId: mockId, timelineType }),
+ { wrapper: wrapperContainer }
+ );
+ await waitForNextUpdate();
+
+ const button = result.current.getButton({
+ outline: true,
+ title: 'mock title',
+ iconType: 'pencil',
+ });
+ const wrapper = shallow(button);
+ expect(wrapper.find('[data-test-subj="timeline-new-with-border"]').prop('iconType')).toEqual(
+ 'pencil'
+ );
+ });
+ });
+
+ test('getButton renders correct filling - EuiButton', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () => useCreateTimelineButton({ timelineId: mockId, timelineType }),
+ { wrapper: wrapperContainer }
+ );
+ await waitForNextUpdate();
+
+ const button = result.current.getButton({
+ outline: true,
+ title: 'mock title',
+ fill: false,
+ });
+ const wrapper = shallow(button);
+ expect(wrapper.find('[data-test-subj="timeline-new-with-border"]').prop('fill')).toEqual(
+ false
+ );
+ });
+ });
+
test('getButton renders correct outline - EuiButtonEmpty', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx
index 28dd865c763ae..b4d168cc980b6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx
@@ -93,15 +93,28 @@ export const useCreateTimelineButton = ({
}, [createTimeline, timelineId, timelineType, closeGearMenu]);
const getButton = useCallback(
- ({ outline, title }: { outline?: boolean; title?: string }) => {
+ ({
+ outline,
+ title,
+ iconType = 'plusInCircle',
+ fill = true,
+ isDisabled = false,
+ }: {
+ outline?: boolean;
+ title?: string;
+ iconType?: string;
+ fill?: boolean;
+ isDisabled?: boolean;
+ }) => {
const buttonProps = {
- iconType: 'plusInCircle',
+ iconType,
onClick: handleButtonClick,
+ fill,
};
const dataTestSubjPrefix =
timelineType === TimelineType.template ? `template-timeline-new` : `timeline-new`;
return outline ? (
-
+
{title}
) : (
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
index 472e82426468e..c066de8af9f20 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
@@ -56,7 +56,7 @@ export const applyDeltaToColumnWidth = actionCreator<{
delta: number;
}>('APPLY_DELTA_TO_COLUMN_WIDTH');
-export const createTimeline = actionCreator<{
+export interface TimelineInput {
id: string;
dataProviders?: DataProvider[];
dateRange?: {
@@ -76,9 +76,13 @@ export const createTimeline = actionCreator<{
sort?: Sort;
showCheckboxes?: boolean;
timelineType?: TimelineTypeLiteral;
- templateTimelineId?: string;
- templateTimelineVersion?: number;
-}>('CREATE_TIMELINE');
+ templateTimelineId?: string | null;
+ templateTimelineVersion?: number | null;
+}
+
+export const saveTimeline = actionCreator('SAVE_TIMELINE');
+
+export const createTimeline = actionCreator('CREATE_TIMELINE');
export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT');
@@ -174,9 +178,11 @@ export const updateHighlightedDropAndProviderId = actionCreator<{
providerId: string;
}>('UPDATE_DROP_AND_PROVIDER');
-export const updateDescription = actionCreator<{ id: string; description: string }>(
- 'UPDATE_DESCRIPTION'
-);
+export const updateDescription = actionCreator<{
+ id: string;
+ description: string;
+ disableAutoSave?: boolean;
+}>('UPDATE_DESCRIPTION');
export const updateKqlMode = actionCreator<{ id: string; kqlMode: KqlMode }>('UPDATE_KQL_MODE');
@@ -205,7 +211,9 @@ export const updateItemsPerPageOptions = actionCreator<{
itemsPerPageOptions: number[];
}>('UPDATE_ITEMS_PER_PAGE_OPTIONS');
-export const updateTitle = actionCreator<{ id: string; title: string }>('UPDATE_TITLE');
+export const updateTitle = actionCreator<{ id: string; title: string; disableAutoSave?: boolean }>(
+ 'UPDATE_TITLE'
+);
export const updatePageIndex = actionCreator<{ id: string; activePage: number }>(
'UPDATE_PAGE_INDEX'
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
index cc8e856de1b16..d50de33412175 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
@@ -78,6 +78,7 @@ import {
createTimeline,
addTimeline,
showCallOutUnauthorizedMsg,
+ saveTimeline,
} from './actions';
import { ColumnHeaderOptions, TimelineModel } from './model';
import { epicPersistNote, timelineNoteActionsType } from './epic_note';
@@ -95,6 +96,7 @@ const timelineActionsType = [
dataProviderEdited.type,
removeColumn.type,
removeProvider.type,
+ saveTimeline.type,
setExcludedRowRendererIds.type,
setFilters.type,
setSavedQueryId.type,
@@ -179,11 +181,11 @@ export const createTimelineEpic = (): Epic<
} else if (
timelineActionsType.includes(action.type) &&
!timelineObj.isLoading &&
- isItAtimelineAction(timelineId)
+ isItAtimelineAction(timelineId) &&
+ !get('payload.disableAutoSave', action)
) {
return true;
}
- return false;
}),
debounceTime(500),
mergeMap(([action]) => {
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
index 1d956e02e7083..7c227f1c80610 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
@@ -389,7 +389,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: updateTimelineKqlMode({ id, kqlMode, timelineById: state.timelineById }),
}))
- .case(updateTitle, (state, { id, title }) => ({
+ .case(updateTitle, (state, { id, title, disableAutoSave }) => ({
...state,
timelineById: updateTimelineTitle({ id, title, timelineById: state.timelineById }),
}))
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx
index b4612c9df42ff..aecb3c02ef43e 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx
@@ -214,7 +214,6 @@ export const PolicyDetails: React.FunctionComponent = ({
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
panelPaddingSize="none"
- withTitle
anchorPosition="rightUp"
repositionOnScroll
>
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_retention_schedule/policy_retention_schedule.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_retention_schedule/policy_retention_schedule.tsx
index dc5de0b4295e8..583c8e4ef1dc9 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_retention_schedule/policy_retention_schedule.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_retention_schedule/policy_retention_schedule.tsx
@@ -176,7 +176,6 @@ export const PolicyRetentionSchedule: React.FunctionComponent = ({
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
panelPaddingSize="none"
- withTitle
anchorPosition="rightUp"
repositionOnScroll
>
diff --git a/x-pack/plugins/spaces/public/nav_control/__snapshots__/nav_control_popover.test.tsx.snap b/x-pack/plugins/spaces/public/nav_control/__snapshots__/nav_control_popover.test.tsx.snap
index 22d65f4600e05..e02e81e497806 100644
--- a/x-pack/plugins/spaces/public/nav_control/__snapshots__/nav_control_popover.test.tsx.snap
+++ b/x-pack/plugins/spaces/public/nav_control/__snapshots__/nav_control_popover.test.tsx.snap
@@ -26,7 +26,6 @@ exports[`NavControlPopover renders without crashing 1`] = `
ownFocus={true}
panelPaddingSize="none"
repositionOnScroll={true}
- withTitle={true}
>
{
anchorPosition={this.props.anchorPosition}
panelPaddingSize="none"
repositionOnScroll={true}
- withTitle={this.props.anchorPosition.includes('down')}
ownFocus
>
{element}
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx
index 7f6c23dddb9fc..8a41ea81407e4 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx
@@ -11,7 +11,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
- EuiButtonToggle,
+ EuiButton,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { FilterAggConfigRange } from '../types';
@@ -60,18 +60,17 @@ export const FilterRangeForm: FilterAggConfigRange['aggTypeConfig']['FilterAggFo
onChange={(e) => {
updateConfig({ from: e.target.value === '' ? undefined : Number(e.target.value) });
}}
- // @ts-ignore
step="any"
prepend={
- |