=
+ cloneDeep(packagePolicyDoc);
+
+ const input = updatedPackagePolicyDoc.attributes.inputs[0];
+ const memory = {
+ mode: 'off',
+ // This value is based on license.
+ // For the migration, we add 'true', our license watcher will correct it, if needed, when the app starts.
+ supported: true,
+ };
+ const memoryPopup = {
+ message: '',
+ enabled: false,
+ };
+ if (input && input.config) {
+ const policy = input.config.policy.value;
+
+ policy.mac.memory_protection = memory;
+ policy.mac.popup.memory_protection = memoryPopup;
+ policy.linux.memory_protection = memory;
+ policy.linux.popup.memory_protection = memoryPopup;
+ }
+
+ return updatedPackagePolicyDoc;
+};
diff --git a/x-pack/plugins/fleet/server/services/epm/registry/requests.ts b/x-pack/plugins/fleet/server/services/epm/registry/requests.ts
index 40943aa0cffff..ed6df5f6459ec 100644
--- a/x-pack/plugins/fleet/server/services/epm/registry/requests.ts
+++ b/x-pack/plugins/fleet/server/services/epm/registry/requests.ts
@@ -97,7 +97,6 @@ export function getFetchOptions(targetUrl: string): RequestInit | undefined {
logger.debug(`Using ${proxyUrl} as proxy for ${targetUrl}`);
return {
- // @ts-expect-error The types exposed by 'HttpsProxyAgent' isn't up to date with 'Agent'
agent: getProxyAgent({ proxyUrl, targetUrl }),
};
}
diff --git a/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts b/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts
index 52c1c71446d64..b27248a3cb933 100644
--- a/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts
+++ b/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts
@@ -7,9 +7,12 @@
import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks';
-import { upgradeManagedPackagePolicies } from './managed_package_policies';
+import type { Installation, PackageInfo } from '../../common';
+import { AUTO_UPDATE_PACKAGES } from '../../common';
+
+import { shouldUpgradePolicies, upgradeManagedPackagePolicies } from './managed_package_policies';
import { packagePolicyService } from './package_policy';
-import { getPackageInfo } from './epm/packages';
+import { getPackageInfo, getInstallation } from './epm/packages';
jest.mock('./package_policy');
jest.mock('./epm/packages');
@@ -24,11 +27,12 @@ jest.mock('./app_context', () => {
};
});
-describe('managed package policies', () => {
+describe('upgradeManagedPackagePolicies', () => {
afterEach(() => {
(packagePolicyService.get as jest.Mock).mockReset();
(packagePolicyService.getUpgradeDryRunDiff as jest.Mock).mockReset();
(getPackageInfo as jest.Mock).mockReset();
+ (getInstallation as jest.Mock).mockReset();
(packagePolicyService.upgrade as jest.Mock).mockReset();
});
@@ -50,7 +54,7 @@ describe('managed package policies', () => {
package: {
name: 'non-managed-package',
title: 'Non-Managed Package',
- version: '0.0.1',
+ version: '1.0.0',
},
};
}
@@ -74,6 +78,11 @@ describe('managed package policies', () => {
})
);
+ (getInstallation as jest.Mock).mockResolvedValueOnce({
+ id: 'test-installation',
+ version: '0.0.1',
+ });
+
await upgradeManagedPackagePolicies(soClient, esClient, ['non-managed-package-id']);
expect(packagePolicyService.upgrade).not.toBeCalled();
@@ -121,6 +130,11 @@ describe('managed package policies', () => {
})
);
+ (getInstallation as jest.Mock).mockResolvedValueOnce({
+ id: 'test-installation',
+ version: '1.0.0',
+ });
+
await upgradeManagedPackagePolicies(soClient, esClient, ['managed-package-id']);
expect(packagePolicyService.upgrade).toBeCalledWith(soClient, esClient, ['managed-package-id']);
@@ -172,6 +186,11 @@ describe('managed package policies', () => {
})
);
+ (getInstallation as jest.Mock).mockResolvedValueOnce({
+ id: 'test-installation',
+ version: '1.0.0',
+ });
+
const result = await upgradeManagedPackagePolicies(soClient, esClient, [
'conflicting-package-policy',
]);
@@ -206,3 +225,133 @@ describe('managed package policies', () => {
});
});
});
+
+describe('shouldUpgradePolicies', () => {
+ describe('package is marked as AUTO_UPDATE', () => {
+ describe('keep_policies_up_to_date is true', () => {
+ it('returns false', () => {
+ const packageInfo = {
+ version: '1.0.0',
+ keepPoliciesUpToDate: true,
+ name: AUTO_UPDATE_PACKAGES[0].name,
+ };
+
+ const installedPackage = {
+ version: '1.0.0',
+ };
+
+ const result = shouldUpgradePolicies(
+ packageInfo as PackageInfo,
+ installedPackage as Installation
+ );
+
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('keep_policies_up_to_date is false', () => {
+ it('returns false', () => {
+ const packageInfo = {
+ version: '1.0.0',
+ keepPoliciesUpToDate: false,
+ name: AUTO_UPDATE_PACKAGES[0].name,
+ };
+
+ const installedPackage = {
+ version: '1.0.0',
+ };
+
+ const result = shouldUpgradePolicies(
+ packageInfo as PackageInfo,
+ installedPackage as Installation
+ );
+
+ expect(result).toBe(false);
+ });
+ });
+ });
+
+ describe('package policy is up-to-date', () => {
+ describe('keep_policies_up_to_date is true', () => {
+ it('returns false', () => {
+ const packageInfo = {
+ version: '1.0.0',
+ keepPoliciesUpToDate: true,
+ };
+
+ const installedPackage = {
+ version: '1.0.0',
+ };
+
+ const result = shouldUpgradePolicies(
+ packageInfo as PackageInfo,
+ installedPackage as Installation
+ );
+
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('keep_policies_up_to_date is false', () => {
+ it('returns false', () => {
+ const packageInfo = {
+ version: '1.0.0',
+ keepPoliciesUpToDate: false,
+ };
+
+ const installedPackage = {
+ version: '1.0.0',
+ };
+
+ const result = shouldUpgradePolicies(
+ packageInfo as PackageInfo,
+ installedPackage as Installation
+ );
+
+ expect(result).toBe(false);
+ });
+ });
+ });
+
+ describe('package policy is out-of-date', () => {
+ describe('keep_policies_up_to_date is true', () => {
+ it('returns true', () => {
+ const packageInfo = {
+ version: '1.0.0',
+ keepPoliciesUpToDate: true,
+ };
+
+ const installedPackage = {
+ version: '1.1.0',
+ };
+
+ const result = shouldUpgradePolicies(
+ packageInfo as PackageInfo,
+ installedPackage as Installation
+ );
+
+ expect(result).toBe(true);
+ });
+ });
+
+ describe('keep_policies_up_to_date is false', () => {
+ it('returns false', () => {
+ const packageInfo = {
+ version: '1.0.0',
+ keepPoliciesUpToDate: false,
+ };
+
+ const installedPackage = {
+ version: '1.1.0',
+ };
+
+ const result = shouldUpgradePolicies(
+ packageInfo as PackageInfo,
+ installedPackage as Installation
+ );
+
+ expect(result).toBe(false);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/fleet/server/services/managed_package_policies.ts b/x-pack/plugins/fleet/server/services/managed_package_policies.ts
index 25e2482892712..306725ae01953 100644
--- a/x-pack/plugins/fleet/server/services/managed_package_policies.ts
+++ b/x-pack/plugins/fleet/server/services/managed_package_policies.ts
@@ -6,9 +6,13 @@
*/
import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server';
+import semverGte from 'semver/functions/gte';
-import type { UpgradePackagePolicyDryRunResponseItem } from '../../common';
-import { AUTO_UPDATE_PACKAGES } from '../../common';
+import type {
+ Installation,
+ PackageInfo,
+ UpgradePackagePolicyDryRunResponseItem,
+} from '../../common';
import { appContextService } from './app_context';
import { getInstallation, getPackageInfo } from './epm/packages';
@@ -16,7 +20,7 @@ import { packagePolicyService } from './package_policy';
export interface UpgradeManagedPackagePoliciesResult {
packagePolicyId: string;
- diff: UpgradePackagePolicyDryRunResponseItem['diff'];
+ diff?: UpgradePackagePolicyDryRunResponseItem['diff'];
errors: any;
}
@@ -49,15 +53,16 @@ export const upgradeManagedPackagePolicies = async (
pkgName: packagePolicy.package.name,
});
- const isPolicyVersionAlignedWithInstalledVersion =
- packageInfo.version === installedPackage?.version;
+ if (!installedPackage) {
+ results.push({
+ packagePolicyId,
+ errors: [`${packagePolicy.package.name} is not installed`],
+ });
- const shouldUpgradePolicies =
- !isPolicyVersionAlignedWithInstalledVersion &&
- (AUTO_UPDATE_PACKAGES.some((pkg) => pkg.name === packageInfo.name) ||
- packageInfo.keepPoliciesUpToDate);
+ continue;
+ }
- if (shouldUpgradePolicies) {
+ if (shouldUpgradePolicies(packageInfo, installedPackage)) {
// Since upgrades don't report diffs/errors, we need to perform a dry run first in order
// to notify the user of any granular policy upgrade errors that occur during Fleet's
// preconfiguration check
@@ -91,3 +96,15 @@ export const upgradeManagedPackagePolicies = async (
return results;
};
+
+export function shouldUpgradePolicies(
+ packageInfo: PackageInfo,
+ installedPackage: Installation
+): boolean {
+ const isPolicyVersionGteInstalledVersion = semverGte(
+ packageInfo.version,
+ installedPackage.version
+ );
+
+ return !isPolicyVersionGteInstalledVersion && !!packageInfo.keepPoliciesUpToDate;
+}
diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx
index 63e6c277ed710..5bdd95ef0b874 100644
--- a/x-pack/plugins/fleet/server/types/index.tsx
+++ b/x-pack/plugins/fleet/server/types/index.tsx
@@ -96,3 +96,4 @@ export interface BulkActionResult {
export * from './models';
export * from './rest_spec';
export * from './extensions';
+export { FleetRequestHandler, FleetRequestHandlerContext } from './request_context';
diff --git a/x-pack/plugins/fleet/server/types/request_context.ts b/x-pack/plugins/fleet/server/types/request_context.ts
new file mode 100644
index 0000000000000..a3b414119b685
--- /dev/null
+++ b/x-pack/plugins/fleet/server/types/request_context.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type {
+ KibanaResponseFactory,
+ RequestHandler,
+ RequestHandlerContext,
+ RouteMethod,
+ SavedObjectsClientContract,
+} from '../../../../../src/core/server';
+
+/** @internal */
+export interface FleetRequestHandlerContext extends RequestHandlerContext {
+ fleet: {
+ epm: {
+ /**
+ * Saved Objects client configured to use kibana_system privileges instead of end-user privileges. Should only be
+ * used by routes that have additional privilege checks for authorization (such as requiring superuser).
+ */
+ readonly internalSoClient: SavedObjectsClientContract;
+ };
+ };
+}
+
+/**
+ * Convenience type for request handlers in Fleet that includes the FleetRequestHandlerContext type
+ * @internal
+ */
+export type FleetRequestHandler<
+ P = unknown,
+ Q = unknown,
+ B = unknown,
+ Method extends RouteMethod = any,
+ ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory
+> = RequestHandler;
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts
index 270d5b2ec1079..94e2f1b3163ed 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts
@@ -22,7 +22,8 @@ import { stubWebWorker } from '@kbn/test/jest';
import { createMemoryHistory } from 'history';
stubWebWorker();
-describe('', () => {
+// unhandled promise rejection https://github.com/elastic/kibana/issues/112699
+describe.skip('', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
let testBed: IndicesTestBed;
diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx
index 0e5ef1ca78033..2b0aaf5175b88 100644
--- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx
+++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx
@@ -83,7 +83,6 @@ class WithKueryAutocompletionComponent extends React.Component<
query: expression,
selectionStart: cursorPosition,
selectionEnd: cursorPosition,
- // @ts-expect-error (until data service updates to new types)
indexPatterns: [indexPattern],
boolFilter: [],
})) || [];
diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts
index bba3ac7e8a9ca..edf7654deb7b7 100644
--- a/x-pack/plugins/lens/common/constants.ts
+++ b/x-pack/plugins/lens/common/constants.ts
@@ -17,7 +17,10 @@ export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations';
export const BASE_API_URL = '/api/lens';
export const LENS_EDIT_BY_VALUE = 'edit_by_value';
-export const layerTypes: Record = { DATA: 'data', THRESHOLD: 'threshold' };
+export const layerTypes: Record = {
+ DATA: 'data',
+ REFERENCELINE: 'referenceLine',
+};
export function getBasePath() {
return `#/`;
diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts b/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts
index 9ff1b5a4dc3f7..0b9667353706d 100644
--- a/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts
+++ b/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts
@@ -173,24 +173,24 @@ export const yAxisConfig: ExpressionFunctionDefinition<
lineStyle: {
types: ['string'],
options: ['solid', 'dotted', 'dashed'],
- help: 'The style of the threshold line',
+ help: 'The style of the reference line',
},
lineWidth: {
types: ['number'],
- help: 'The width of the threshold line',
+ help: 'The width of the reference line',
},
icon: {
types: ['string'],
- help: 'An optional icon used for threshold lines',
+ help: 'An optional icon used for reference lines',
},
iconPosition: {
types: ['string'],
options: ['auto', 'above', 'below', 'left', 'right'],
- help: 'The placement of the icon for the threshold line',
+ help: 'The placement of the icon for the reference line',
},
textVisibility: {
types: ['boolean'],
- help: 'Visibility of the label on the threshold line',
+ help: 'Visibility of the label on the reference line',
},
fill: {
types: ['string'],
diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts
index 659d3c0eced26..38e198c01e730 100644
--- a/x-pack/plugins/lens/common/types.ts
+++ b/x-pack/plugins/lens/common/types.ts
@@ -61,4 +61,4 @@ export interface CustomPaletteParams {
export type RequiredPaletteParamTypes = Required;
-export type LayerType = 'data' | 'threshold';
+export type LayerType = 'data' | 'referenceLine';
diff --git a/x-pack/plugins/lens/public/assets/chart_bar_threshold.tsx b/x-pack/plugins/lens/public/assets/chart_bar_reference_line.tsx
similarity index 97%
rename from x-pack/plugins/lens/public/assets/chart_bar_threshold.tsx
rename to x-pack/plugins/lens/public/assets/chart_bar_reference_line.tsx
index 88e0a46b5538c..447641540a284 100644
--- a/x-pack/plugins/lens/public/assets/chart_bar_threshold.tsx
+++ b/x-pack/plugins/lens/public/assets/chart_bar_reference_line.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { EuiIconProps } from '@elastic/eui';
-export const LensIconChartBarThreshold = ({
+export const LensIconChartBarReferenceLine = ({
title,
titleId,
...props
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx
index 61d37d4cc9fed..a8436edb63f63 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx
@@ -280,7 +280,7 @@ describe('ConfigPanel', () => {
instance.update();
act(() => {
instance
- .find(`[data-test-subj="lnsLayerAddButton-${layerTypes.THRESHOLD}"]`)
+ .find(`[data-test-subj="lnsLayerAddButton-${layerTypes.REFERENCELINE}"]`)
.first()
.simulate('click');
});
@@ -301,8 +301,8 @@ describe('ConfigPanel', () => {
props.activeVisualization.getSupportedLayers = jest.fn(() => [
{ type: layerTypes.DATA, label: 'Data Layer' },
{
- type: layerTypes.THRESHOLD,
- label: 'Threshold layer',
+ type: layerTypes.REFERENCELINE,
+ label: 'Reference layer',
},
]);
datasourceMap.testDatasource.initializeDimension = jest.fn();
@@ -331,8 +331,8 @@ describe('ConfigPanel', () => {
],
},
{
- type: layerTypes.THRESHOLD,
- label: 'Threshold layer',
+ type: layerTypes.REFERENCELINE,
+ label: 'Reference layer',
},
]);
datasourceMap.testDatasource.initializeDimension = jest.fn();
@@ -349,8 +349,8 @@ describe('ConfigPanel', () => {
props.activeVisualization.getSupportedLayers = jest.fn(() => [
{ type: layerTypes.DATA, label: 'Data Layer' },
{
- type: layerTypes.THRESHOLD,
- label: 'Threshold layer',
+ type: layerTypes.REFERENCELINE,
+ label: 'Reference layer',
initialDimensions: [
{
groupId: 'testGroup',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
index 8286ab492f14d..93718c88b251c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
@@ -26,6 +26,7 @@ import {
insertOrReplaceColumn,
replaceColumn,
updateColumnParam,
+ updateDefaultLabels,
resetIncomplete,
FieldBasedIndexPatternColumn,
canTransition,
@@ -151,13 +152,27 @@ export function DimensionEditor(props: DimensionEditorProps) {
const addStaticValueColumn = (prevLayer = props.state.layers[props.layerId]) => {
if (selectedColumn?.operationType !== staticValueOperationName) {
trackUiEvent(`indexpattern_dimension_operation_static_value`);
- return insertOrReplaceColumn({
+ const layer = insertOrReplaceColumn({
layer: prevLayer,
indexPattern: currentIndexPattern,
columnId,
op: staticValueOperationName,
visualizationGroups: dimensionGroups,
});
+ const value = props.activeData?.[layerId]?.rows[0]?.[columnId];
+ // replace the default value with the one from the active data
+ if (value != null) {
+ return updateDefaultLabels(
+ updateColumnParam({
+ layer,
+ columnId,
+ paramName: 'value',
+ value: props.activeData?.[layerId]?.rows[0]?.[columnId],
+ }),
+ currentIndexPattern
+ );
+ }
+ return layer;
}
return prevLayer;
};
@@ -173,7 +188,18 @@ export function DimensionEditor(props: DimensionEditorProps) {
if (temporaryStaticValue) {
setTemporaryState('none');
}
- return setStateWrapper(setter, { forceRender: true });
+ if (typeof setter === 'function') {
+ return setState(
+ (prevState) => {
+ const layer = setter(addStaticValueColumn(prevState.layers[layerId]));
+ return mergeLayer({ state: prevState, layerId, newLayer: layer });
+ },
+ {
+ isDimensionComplete: true,
+ forceRender: true,
+ }
+ );
+ }
};
const ParamEditor = getParamEditor(
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
index bf4b10de386a1..9315b61adcc54 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
@@ -1206,11 +1206,11 @@ describe('IndexPattern Data Source suggestions', () => {
const modifiedState: IndexPatternPrivateState = {
...initialState,
layers: {
- thresholdLayer: {
+ referenceLineLayer: {
indexPatternId: '1',
- columnOrder: ['threshold'],
+ columnOrder: ['referenceLine'],
columns: {
- threshold: {
+ referenceLine: {
dataType: 'number',
isBucketed: false,
label: 'Static Value: 0',
@@ -1251,10 +1251,10 @@ describe('IndexPattern Data Source suggestions', () => {
modifiedState,
'1',
documentField,
- (layerId) => layerId !== 'thresholdLayer'
+ (layerId) => layerId !== 'referenceLineLayer'
)
);
- // should ignore the threshold layer
+ // should ignore the referenceLine layer
expect(suggestions).toContainEqual(
expect.objectContaining({
table: expect.objectContaining({
@@ -1704,7 +1704,7 @@ describe('IndexPattern Data Source suggestions', () => {
);
});
- it('adds date histogram over default time field for tables without time dimension and a threshold', async () => {
+ it('adds date histogram over default time field for tables without time dimension and a referenceLine', async () => {
const initialState = testInitialState();
const state: IndexPatternPrivateState = {
...initialState,
@@ -1738,11 +1738,11 @@ describe('IndexPattern Data Source suggestions', () => {
},
},
},
- threshold: {
+ referenceLine: {
indexPatternId: '2',
- columnOrder: ['thresholda'],
+ columnOrder: ['referenceLineA'],
columns: {
- thresholda: {
+ referenceLineA: {
label: 'My Op',
customLabel: true,
dataType: 'number',
@@ -1758,7 +1758,7 @@ describe('IndexPattern Data Source suggestions', () => {
expect(
getSuggestionSubset(
- getDatasourceSuggestionsFromCurrentState(state, (layerId) => layerId !== 'threshold')
+ getDatasourceSuggestionsFromCurrentState(state, (layerId) => layerId !== 'referenceLine')
)
).toContainEqual(
expect.objectContaining({
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts
index d68fd8b9555f9..8b1eaeb109d9b 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts
@@ -21,7 +21,7 @@ describe('utils', () => {
describe('checkForDataLayerType', () => {
it('should return an error if the layer is of the wrong type', () => {
- expect(checkForDataLayerType(layerTypes.THRESHOLD, 'Operation')).toEqual([
+ expect(checkForDataLayerType(layerTypes.REFERENCELINE, 'Operation')).toEqual([
'Operation is disabled for this type of layer.',
]);
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts
index 87c4355c1dc9f..0ec7ad046ac2a 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts
@@ -24,7 +24,7 @@ export const buildLabelFunction =
};
export function checkForDataLayerType(layerType: LayerType, name: string) {
- if (layerType === layerTypes.THRESHOLD) {
+ if (layerType === layerTypes.REFERENCELINE) {
return [
i18n.translate('xpack.lens.indexPattern.calculations.layerDataType', {
defaultMessage: '{name} is disabled for this type of layer.',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx
index 0a6620eecf308..1c574fe69611c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx
@@ -80,14 +80,14 @@ describe('static_value', () => {
};
});
- function getLayerWithStaticValue(newValue: string): IndexPatternLayer {
+ function getLayerWithStaticValue(newValue: string | null | undefined): IndexPatternLayer {
return {
...layer,
columns: {
...layer.columns,
col2: {
...layer.columns.col2,
- label: `Static value: ${newValue}`,
+ label: `Static value: ${newValue ?? String(newValue)}`,
params: {
value: newValue,
},
@@ -155,8 +155,9 @@ describe('static_value', () => {
).toBeUndefined();
});
- it('should return error for invalid values', () => {
- for (const value of ['NaN', 'Infinity', 'string']) {
+ it.each(['NaN', 'Infinity', 'string'])(
+ 'should return error for invalid values: %s',
+ (value) => {
expect(
staticValueOperation.getErrorMessage!(
getLayerWithStaticValue(value),
@@ -165,6 +166,16 @@ describe('static_value', () => {
)
).toEqual(expect.arrayContaining([expect.stringMatching('is not a valid number')]));
}
+ );
+
+ it.each([null, undefined])('should return no error for: %s', (value) => {
+ expect(
+ staticValueOperation.getErrorMessage!(
+ getLayerWithStaticValue(value),
+ 'col2',
+ createMockedIndexPattern()
+ )
+ ).toBe(undefined);
});
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx
index a76c5f64d1750..26be4e7b114da 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx
@@ -61,7 +61,7 @@ export const staticValueOperation: OperationDefinition<
getErrorMessage(layer, columnId) {
const column = layer.columns[columnId] as StaticValueIndexPatternColumn;
- return !isValidNumber(column.params.value)
+ return column.params.value != null && !isValidNumber(column.params.value)
? [
i18n.translate('xpack.lens.indexPattern.staticValueError', {
defaultMessage: 'The static value of {value} is not a valid number',
@@ -176,10 +176,7 @@ export const staticValueOperation: OperationDefinition<
// Pick the data from the current activeData (to be used when the current operation is not static_value)
const activeDataValue =
- activeData &&
- activeData[layerId] &&
- activeData[layerId]?.rows?.length === 1 &&
- activeData[layerId].rows[0][columnId];
+ activeData?.[layerId]?.rows?.length === 1 && activeData[layerId].rows[0][columnId];
const fallbackValue =
currentColumn?.operationType !== 'static_value' && activeDataValue != null
@@ -206,7 +203,7 @@ export const staticValueOperation: OperationDefinition<
{i18n.translate('xpack.lens.indexPattern.staticValue.label', {
- defaultMessage: 'Threshold value',
+ defaultMessage: 'Reference line value',
})}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
index b3b98e5054aa6..9f3cba89ce17b 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
@@ -1406,7 +1406,7 @@ export function isOperationAllowedAsReference({
// Labels need to be updated when columns are added because reference-based column labels
// are sometimes copied into the parents
-function updateDefaultLabels(
+export function updateDefaultLabels(
layer: IndexPatternLayer,
indexPattern: IndexPattern
): IndexPatternLayer {
diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts
index 30238507c3566..5ed6ec052a0da 100644
--- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts
@@ -24,7 +24,7 @@ interface LayerColorConfig {
layerType: LayerType;
}
-export const defaultThresholdColor = euiLightVars.euiColorDarkShade;
+export const defaultReferenceLineColor = euiLightVars.euiColorDarkShade;
export type ColorAssignments = Record<
string,
@@ -117,11 +117,11 @@ export function getAccessorColorConfig(
triggerIcon: 'disabled',
};
}
- if (layer.layerType === layerTypes.THRESHOLD) {
+ if (layer.layerType === layerTypes.REFERENCELINE) {
return {
columnId: accessor as string,
triggerIcon: 'color',
- color: currentYConfig?.color || defaultThresholdColor,
+ color: currentYConfig?.color || defaultReferenceLineColor,
};
}
const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layer.layerId]);
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx
index af2995fb65b71..9c272202e4565 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx
@@ -330,7 +330,7 @@ function sampleArgs() {
return { data, args };
}
-function sampleArgsWithThreshold(thresholdValue: number = 150) {
+function sampleArgsWithReferenceLine(value: number = 150) {
const { data, args } = sampleArgs();
return {
@@ -338,16 +338,16 @@ function sampleArgsWithThreshold(thresholdValue: number = 150) {
...data,
tables: {
...data.tables,
- threshold: {
+ referenceLine: {
type: 'datatable',
columns: [
{
- id: 'threshold-a',
+ id: 'referenceLine-a',
meta: { params: { id: 'number' }, type: 'number' },
name: 'Static value',
},
],
- rows: [{ 'threshold-a': thresholdValue }],
+ rows: [{ 'referenceLine-a': value }],
},
},
} as LensMultiTable,
@@ -356,16 +356,16 @@ function sampleArgsWithThreshold(thresholdValue: number = 150) {
layers: [
...args.layers,
{
- layerType: layerTypes.THRESHOLD,
- accessors: ['threshold-a'],
- layerId: 'threshold',
+ layerType: layerTypes.REFERENCELINE,
+ accessors: ['referenceLine-a'],
+ layerId: 'referenceLine',
seriesType: 'line',
xScaleType: 'linear',
yScaleType: 'linear',
palette: mockPaletteOutput,
isHistogram: false,
hide: true,
- yConfig: [{ axisMode: 'left', forAccessor: 'threshold-a', type: 'lens_xy_yConfig' }],
+ yConfig: [{ axisMode: 'left', forAccessor: 'referenceLine-a', type: 'lens_xy_yConfig' }],
},
],
} as XYArgs,
@@ -874,8 +874,8 @@ describe('xy_expression', () => {
});
});
- test('it does include threshold values when in full extent mode', () => {
- const { data, args } = sampleArgsWithThreshold();
+ test('it does include referenceLine values when in full extent mode', () => {
+ const { data, args } = sampleArgsWithReferenceLine();
const component = shallow(
);
expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({
@@ -885,8 +885,8 @@ describe('xy_expression', () => {
});
});
- test('it should ignore threshold values when set to custom extents', () => {
- const { data, args } = sampleArgsWithThreshold();
+ test('it should ignore referenceLine values when set to custom extents', () => {
+ const { data, args } = sampleArgsWithReferenceLine();
const component = shallow(
{
});
});
- test('it should work for negative values in thresholds', () => {
- const { data, args } = sampleArgsWithThreshold(-150);
+ test('it should work for negative values in referenceLines', () => {
+ const { data, args } = sampleArgsWithReferenceLine(-150);
const component = shallow();
expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
index 7aee537ebbedd..36f1b92b8a1f4 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
@@ -64,10 +64,10 @@ import { getXDomain, XyEndzones } from './x_domain';
import { getLegendAction } from './get_legend_action';
import {
computeChartMargins,
- getThresholdRequiredPaddings,
- ThresholdAnnotations,
-} from './expression_thresholds';
-import { computeOverallDataDomain } from './threshold_helpers';
+ getReferenceLineRequiredPaddings,
+ ReferenceLineAnnotations,
+} from './expression_reference_lines';
+import { computeOverallDataDomain } from './reference_line_helpers';
declare global {
interface Window {
@@ -264,7 +264,9 @@ export function XYChart({
const icon: IconType = layers.length > 0 ? getIconForSeriesType(layers[0].seriesType) : 'bar';
return ;
}
- const thresholdLayers = layers.filter((layer) => layer.layerType === layerTypes.THRESHOLD);
+ const referenceLineLayers = layers.filter(
+ (layer) => layer.layerType === layerTypes.REFERENCELINE
+ );
// use formatting hint of first x axis column to format ticks
const xAxisColumn = data.tables[filteredLayers[0].layerId].columns.find(
@@ -332,7 +334,7 @@ export function XYChart({
left: yAxesConfiguration.find(({ groupId }) => groupId === 'left'),
right: yAxesConfiguration.find(({ groupId }) => groupId === 'right'),
};
- const thresholdPaddings = getThresholdRequiredPaddings(thresholdLayers, yAxesMap);
+ const referenceLinePaddings = getReferenceLineRequiredPaddings(referenceLineLayers, yAxesMap);
const getYAxesTitles = (
axisSeries: Array<{ layer: string; accessor: string }>,
@@ -364,9 +366,9 @@ export function XYChart({
? args.labelsOrientation?.yRight || 0
: args.labelsOrientation?.yLeft || 0,
padding:
- thresholdPaddings[groupId] != null
+ referenceLinePaddings[groupId] != null
? {
- inner: thresholdPaddings[groupId],
+ inner: referenceLinePaddings[groupId],
}
: undefined,
},
@@ -377,9 +379,9 @@ export function XYChart({
: axisTitlesVisibilitySettings?.yLeft,
// if labels are not visible add the padding to the title
padding:
- !tickVisible && thresholdPaddings[groupId] != null
+ !tickVisible && referenceLinePaddings[groupId] != null
? {
- inner: thresholdPaddings[groupId],
+ inner: referenceLinePaddings[groupId],
}
: undefined,
},
@@ -406,10 +408,10 @@ export function XYChart({
max = extent.upperBound ?? NaN;
}
} else {
- const axisHasThreshold = thresholdLayers.some(({ yConfig }) =>
+ const axisHasReferenceLine = referenceLineLayers.some(({ yConfig }) =>
yConfig?.some(({ axisMode }) => axisMode === axis.groupId)
);
- if (!fit && axisHasThreshold) {
+ if (!fit && axisHasReferenceLine) {
// Remove this once the chart will support automatic annotation fit for other type of charts
const { min: computedMin, max: computedMax } = computeOverallDataDomain(
filteredLayers,
@@ -421,7 +423,7 @@ export function XYChart({
max = Math.max(computedMax, max || 0);
min = Math.min(computedMin, min || 0);
}
- for (const { layerId, yConfig } of thresholdLayers) {
+ for (const { layerId, yConfig } of referenceLineLayers) {
const table = data.tables[layerId];
for (const { axisMode, forAccessor } of yConfig || []) {
if (axis.groupId === axisMode) {
@@ -575,11 +577,11 @@ export function XYChart({
legend: {
labelOptions: { maxLines: legend.shouldTruncate ? legend?.maxLines ?? 1 : 0 },
},
- // if not title or labels are shown for axes, add some padding if required by threshold markers
+ // if not title or labels are shown for axes, add some padding if required by reference line markers
chartMargins: {
...chartTheme.chartPaddings,
...computeChartMargins(
- thresholdPaddings,
+ referenceLinePaddings,
tickLabelsVisibilitySettings,
axisTitlesVisibilitySettings,
yAxesMap,
@@ -622,13 +624,15 @@ export function XYChart({
visible: tickLabelsVisibilitySettings?.x,
rotation: labelsOrientation?.x,
padding:
- thresholdPaddings.bottom != null ? { inner: thresholdPaddings.bottom } : undefined,
+ referenceLinePaddings.bottom != null
+ ? { inner: referenceLinePaddings.bottom }
+ : undefined,
},
axisTitle: {
visible: axisTitlesVisibilitySettings.x,
padding:
- !tickLabelsVisibilitySettings?.x && thresholdPaddings.bottom != null
- ? { inner: thresholdPaddings.bottom }
+ !tickLabelsVisibilitySettings?.x && referenceLinePaddings.bottom != null
+ ? { inner: referenceLinePaddings.bottom }
: undefined,
},
}}
@@ -911,9 +915,9 @@ export function XYChart({
}
})
)}
- {thresholdLayers.length ? (
-
) : null}
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.scss b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.scss
new file mode 100644
index 0000000000000..07946b52b0000
--- /dev/null
+++ b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.scss
@@ -0,0 +1,18 @@
+.lnsXyDecorationRotatedWrapper {
+ display: inline-block;
+ overflow: hidden;
+ line-height: 1.5;
+
+ .lnsXyDecorationRotatedWrapper__label {
+ display: inline-block;
+ white-space: nowrap;
+ transform: translate(0, 100%) rotate(-90deg);
+ transform-origin: 0 0;
+
+ &::after {
+ content: '';
+ float: left;
+ margin-top: 100%;
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx
similarity index 81%
rename from x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx
rename to x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx
index 67e994b734b84..42e02871026df 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import './expression_thresholds.scss';
+import './expression_reference_lines.scss';
import React from 'react';
import { groupBy } from 'lodash';
import { EuiIcon } from '@elastic/eui';
@@ -15,59 +15,59 @@ import type { FieldFormat } from 'src/plugins/field_formats/common';
import { euiLightVars } from '@kbn/ui-shared-deps-src/theme';
import type { LayerArgs, YConfig } from '../../common/expressions';
import type { LensMultiTable } from '../../common/types';
-import { hasIcon } from './xy_config_panel/threshold_panel';
+import { hasIcon } from './xy_config_panel/reference_line_panel';
-const THRESHOLD_MARKER_SIZE = 20;
+const REFERENCE_LINE_MARKER_SIZE = 20;
export const computeChartMargins = (
- thresholdPaddings: Partial>,
+ referenceLinePaddings: Partial>,
labelVisibility: Partial>,
titleVisibility: Partial>,
axesMap: Record<'left' | 'right', unknown>,
isHorizontal: boolean
) => {
const result: Partial> = {};
- if (!labelVisibility?.x && !titleVisibility?.x && thresholdPaddings.bottom) {
+ if (!labelVisibility?.x && !titleVisibility?.x && referenceLinePaddings.bottom) {
const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom';
- result[placement] = thresholdPaddings.bottom;
+ result[placement] = referenceLinePaddings.bottom;
}
if (
- thresholdPaddings.left &&
+ referenceLinePaddings.left &&
(isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft))
) {
const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left';
- result[placement] = thresholdPaddings.left;
+ result[placement] = referenceLinePaddings.left;
}
if (
- thresholdPaddings.right &&
+ referenceLinePaddings.right &&
(isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight))
) {
const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right';
- result[placement] = thresholdPaddings.right;
+ result[placement] = referenceLinePaddings.right;
}
// there's no top axis, so just check if a margin has been computed
- if (thresholdPaddings.top) {
+ if (referenceLinePaddings.top) {
const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top';
- result[placement] = thresholdPaddings.top;
+ result[placement] = referenceLinePaddings.top;
}
return result;
};
-// Note: it does not take into consideration whether the threshold is in view or not
-export const getThresholdRequiredPaddings = (
- thresholdLayers: LayerArgs[],
+// Note: it does not take into consideration whether the reference line is in view or not
+export const getReferenceLineRequiredPaddings = (
+ referenceLineLayers: LayerArgs[],
axesMap: Record<'left' | 'right', unknown>
) => {
// collect all paddings for the 4 axis: if any text is detected double it.
const paddings: Partial> = {};
const icons: Partial> = {};
- thresholdLayers.forEach((layer) => {
+ referenceLineLayers.forEach((layer) => {
layer.yConfig?.forEach(({ axisMode, icon, iconPosition, textVisibility }) => {
if (axisMode && (hasIcon(icon) || textVisibility)) {
const placement = getBaseIconPlacement(iconPosition, axisMode, axesMap);
paddings[placement] = Math.max(
paddings[placement] || 0,
- THRESHOLD_MARKER_SIZE * (textVisibility ? 2 : 1) // double the padding size if there's text
+ REFERENCE_LINE_MARKER_SIZE * (textVisibility ? 2 : 1) // double the padding size if there's text
);
icons[placement] = (icons[placement] || 0) + (hasIcon(icon) ? 1 : 0);
}
@@ -77,7 +77,7 @@ export const getThresholdRequiredPaddings = (
// if no icon is present for the placement, just reduce the padding
(Object.keys(paddings) as Position[]).forEach((placement) => {
if (!icons[placement]) {
- paddings[placement] = THRESHOLD_MARKER_SIZE;
+ paddings[placement] = REFERENCE_LINE_MARKER_SIZE;
}
});
@@ -133,7 +133,7 @@ function getMarkerBody(label: string | undefined, isHorizontal: boolean) {
}
if (isHorizontal) {
return (
-
+
{label}
);
@@ -142,13 +142,13 @@ function getMarkerBody(label: string | undefined, isHorizontal: boolean) {
{label}
@@ -180,32 +180,32 @@ function getMarkerToShow(
}
}
-export const ThresholdAnnotations = ({
- thresholdLayers,
+export const ReferenceLineAnnotations = ({
+ layers,
data,
formatters,
paletteService,
syncColors,
axesMap,
isHorizontal,
- thresholdPaddingMap,
+ paddingMap,
}: {
- thresholdLayers: LayerArgs[];
+ layers: LayerArgs[];
data: LensMultiTable;
formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>;
paletteService: PaletteRegistry;
syncColors: boolean;
axesMap: Record<'left' | 'right', boolean>;
isHorizontal: boolean;
- thresholdPaddingMap: Partial
>;
+ paddingMap: Partial>;
}) => {
return (
<>
- {thresholdLayers.flatMap((thresholdLayer) => {
- if (!thresholdLayer.yConfig) {
+ {layers.flatMap((layer) => {
+ if (!layer.yConfig) {
return [];
}
- const { columnToLabel, yConfig: yConfigs, layerId } = thresholdLayer;
+ const { columnToLabel, yConfig: yConfigs, layerId } = layer;
const columnToLabelMap: Record = columnToLabel
? JSON.parse(columnToLabel)
: {};
@@ -218,6 +218,9 @@ export const ThresholdAnnotations = ({
);
const groupedByDirection = groupBy(yConfigByValue, 'fill');
+ if (groupedByDirection.below) {
+ groupedByDirection.below.reverse();
+ }
return yConfigByValue.flatMap((yConfig, i) => {
// Find the formatter for the given axis
@@ -240,7 +243,7 @@ export const ThresholdAnnotations = ({
);
// the padding map is built for vertical chart
const hasReducedPadding =
- thresholdPaddingMap[markerPositionVertical] === THRESHOLD_MARKER_SIZE;
+ paddingMap[markerPositionVertical] === REFERENCE_LINE_MARKER_SIZE;
const props = {
groupId,
@@ -306,7 +309,7 @@ export const ThresholdAnnotations = ({
const indexFromSameType = groupedByDirection[yConfig.fill].findIndex(
({ forAccessor }) => forAccessor === yConfig.forAccessor
);
- const shouldCheckNextThreshold =
+ const shouldCheckNextReferenceLine =
indexFromSameType < groupedByDirection[yConfig.fill].length - 1;
annotations.push(
{
+ const nextValue =
+ !isFillAbove && shouldCheckNextReferenceLine
+ ? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor]
+ : undefined;
if (yConfig.axisMode === 'bottom') {
return {
coordinates: {
- x0: isFillAbove ? row[yConfig.forAccessor] : undefined,
+ x0: isFillAbove ? row[yConfig.forAccessor] : nextValue,
y0: undefined,
- x1: isFillAbove
- ? shouldCheckNextThreshold
- ? row[
- groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor
- ]
- : undefined
- : row[yConfig.forAccessor],
+ x1: isFillAbove ? nextValue : row[yConfig.forAccessor],
y1: undefined,
},
header: columnToLabelMap[yConfig.forAccessor],
@@ -336,15 +337,9 @@ export const ThresholdAnnotations = ({
return {
coordinates: {
x0: undefined,
- y0: isFillAbove ? row[yConfig.forAccessor] : undefined,
+ y0: isFillAbove ? row[yConfig.forAccessor] : nextValue,
x1: undefined,
- y1: isFillAbove
- ? shouldCheckNextThreshold
- ? row[
- groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor
- ]
- : undefined
- : row[yConfig.forAccessor],
+ y1: isFillAbove ? nextValue : row[yConfig.forAccessor],
},
header: columnToLabelMap[yConfig.forAccessor],
details:
diff --git a/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.test.ts b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts
similarity index 99%
rename from x-pack/plugins/lens/public/xy_visualization/threshold_helpers.test.ts
rename to x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts
index d7286de0316d6..9dacc12c68d65 100644
--- a/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts
@@ -7,7 +7,7 @@
import { XYLayerConfig } from '../../common/expressions';
import { FramePublicAPI } from '../types';
-import { computeOverallDataDomain, getStaticValue } from './threshold_helpers';
+import { computeOverallDataDomain, getStaticValue } from './reference_line_helpers';
function getActiveData(json: Array<{ id: string; rows: Array> }>) {
return json.reduce((memo, { id, rows }) => {
@@ -25,7 +25,7 @@ function getActiveData(json: Array<{ id: string; rows: Array);
}
-describe('threshold helpers', () => {
+describe('reference_line helpers', () => {
describe('getStaticValue', () => {
const hasDateHistogram = () => false;
const hasAllNumberHistogram = () => true;
diff --git a/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx
similarity index 83%
rename from x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx
rename to x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx
index 8bf5f84b15bad..71ce2d0ea2082 100644
--- a/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx
@@ -15,17 +15,17 @@ import { isPercentageSeries, isStackedChart } from './state_helpers';
import type { XYState } from './types';
import { checkScaleOperation } from './visualization_helpers';
-export interface ThresholdBase {
+export interface ReferenceLineBase {
label: 'x' | 'yRight' | 'yLeft';
}
/**
- * Return the threshold layers groups to show based on multiple criteria:
+ * Return the reference layers groups to show based on multiple criteria:
* * what groups are current defined in data layers
- * * what existing threshold are currently defined in data thresholds
+ * * what existing reference line are currently defined in reference layers
*/
-export function getGroupsToShow(
- thresholdLayers: T[],
+export function getGroupsToShow(
+ referenceLayers: T[],
state: XYState | undefined,
datasourceLayers: Record,
tables: Record | undefined
@@ -37,16 +37,16 @@ export function getGroupsToShow layerType === layerTypes.DATA
);
const groupsAvailable = getGroupsAvailableInData(dataLayers, datasourceLayers, tables);
- return thresholdLayers
+ return referenceLayers
.filter(({ label, config }: T) => groupsAvailable[label] || config?.length)
.map((layer) => ({ ...layer, valid: groupsAvailable[layer.label] }));
}
/**
- * Returns the threshold layers groups to show based on what groups are current defined in data layers.
+ * Returns the reference layers groups to show based on what groups are current defined in data layers.
*/
-export function getGroupsRelatedToData(
- thresholdLayers: T[],
+export function getGroupsRelatedToData(
+ referenceLayers: T[],
state: XYState | undefined,
datasourceLayers: Record,
tables: Record | undefined
@@ -58,7 +58,7 @@ export function getGroupsRelatedToData(
({ layerType = layerTypes.DATA }) => layerType === layerTypes.DATA
);
const groupsAvailable = getGroupsAvailableInData(dataLayers, datasourceLayers, tables);
- return thresholdLayers.filter(({ label }: T) => groupsAvailable[label]);
+ return referenceLayers.filter(({ label }: T) => groupsAvailable[label]);
}
/**
* Returns a dictionary with the groups filled in all the data layers
@@ -90,7 +90,7 @@ export function getStaticValue(
return fallbackValue;
}
- // filter and organize data dimensions into threshold groups
+ // filter and organize data dimensions into reference layer groups
// now pick the columnId in the active data
const {
dataLayers: filteredLayers,
@@ -128,29 +128,22 @@ function getAccessorCriteriaForGroup(
...rest,
accessors: [xAccessor] as string[],
})),
- // need the untouched ones for some checks later on
+ // need the untouched ones to check if there are invalid layers from the filtered ones
+ // to perform the checks the original accessor structure needs to be accessed
untouchedDataLayers: filteredDataLayers,
accessors: filteredDataLayers.map(({ xAccessor }) => xAccessor) as string[],
};
}
- case 'yLeft': {
- const { left } = groupAxesByType(dataLayers, activeData);
- const leftIds = new Set(left.map(({ layer }) => layer));
- const filteredDataLayers = dataLayers.filter(({ layerId }) => leftIds.has(layerId));
- return {
- dataLayers: filteredDataLayers,
- untouchedDataLayers: filteredDataLayers,
- accessors: left.map(({ accessor }) => accessor),
- };
- }
+ case 'yLeft':
case 'yRight': {
- const { right } = groupAxesByType(dataLayers, activeData);
- const rightIds = new Set(right.map(({ layer }) => layer));
+ const prop = groupId === 'yLeft' ? 'left' : 'right';
+ const { [prop]: axis } = groupAxesByType(dataLayers, activeData);
+ const rightIds = new Set(axis.map(({ layer }) => layer));
const filteredDataLayers = dataLayers.filter(({ layerId }) => rightIds.has(layerId));
return {
dataLayers: filteredDataLayers,
untouchedDataLayers: filteredDataLayers,
- accessors: right.map(({ accessor }) => accessor),
+ accessors: axis.map(({ accessor }) => accessor),
};
}
}
@@ -224,11 +217,11 @@ function computeStaticValueForGroup(
activeData: NonNullable,
minZeroOrNegativeBase: boolean = true
) {
- const defaultThresholdFactor = 3 / 4;
+ const defaultReferenceLineFactor = 3 / 4;
if (dataLayers.length && accessorIds.length) {
if (dataLayers.some(({ seriesType }) => isPercentageSeries(seriesType))) {
- return defaultThresholdFactor;
+ return defaultReferenceLineFactor;
}
const { min, max } = computeOverallDataDomain(dataLayers, accessorIds, activeData);
@@ -237,7 +230,7 @@ function computeStaticValueForGroup(
// Custom axis bounds can go below 0, so consider also lower values than 0
const finalMinValue = minZeroOrNegativeBase ? Math.min(0, min) : min;
const interval = max - finalMinValue;
- return Number((finalMinValue + interval * defaultThresholdFactor).toFixed(2));
+ return Number((finalMinValue + interval * defaultReferenceLineFactor).toFixed(2));
}
}
}
diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts
index d174fb831c2dc..7d1bd64abe906 100644
--- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts
@@ -13,7 +13,7 @@ import { Operation } from '../types';
import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
import { layerTypes } from '../../common';
import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks';
-import { defaultThresholdColor } from './color_assignment';
+import { defaultReferenceLineColor } from './color_assignment';
describe('#toExpression', () => {
const xyVisualization = getXyVisualization({
@@ -338,8 +338,8 @@ describe('#toExpression', () => {
yConfig: [{ forAccessor: 'a' }],
},
{
- layerId: 'threshold',
- layerType: layerTypes.THRESHOLD,
+ layerId: 'referenceLine',
+ layerType: layerTypes.REFERENCELINE,
seriesType: 'area',
splitAccessor: 'd',
xAccessor: 'a',
@@ -348,7 +348,7 @@ describe('#toExpression', () => {
},
],
},
- { ...frame.datasourceLayers, threshold: mockDatasource.publicAPIMock }
+ { ...frame.datasourceLayers, referenceLine: mockDatasource.publicAPIMock }
) as Ast;
function getYConfigColorForLayer(ast: Ast, index: number) {
@@ -356,6 +356,6 @@ describe('#toExpression', () => {
.chain[0].arguments.color;
}
expect(getYConfigColorForLayer(expression, 0)).toEqual([]);
- expect(getYConfigColorForLayer(expression, 1)).toEqual([defaultThresholdColor]);
+ expect(getYConfigColorForLayer(expression, 1)).toEqual([defaultReferenceLineColor]);
});
});
diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
index 96ea9b84dd983..1cd0bab48cd68 100644
--- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
@@ -13,8 +13,8 @@ import { OperationMetadata, DatasourcePublicAPI } from '../types';
import { getColumnToLabelMap } from './state_helpers';
import type { ValidLayer, XYLayerConfig } from '../../common/expressions';
import { layerTypes } from '../../common';
-import { hasIcon } from './xy_config_panel/threshold_panel';
-import { defaultThresholdColor } from './color_assignment';
+import { hasIcon } from './xy_config_panel/reference_line_panel';
+import { defaultReferenceLineColor } from './color_assignment';
export const getSortedAccessors = (datasource: DatasourcePublicAPI, layer: XYLayerConfig) => {
const originalOrder = datasource
@@ -59,7 +59,7 @@ export function toPreviewExpression(
layers: state.layers.map((layer) =>
layer.layerType === layerTypes.DATA
? { ...layer, hide: true }
- : // cap the threshold line to 1px
+ : // cap the reference line to 1px
{
...layer,
hide: true,
@@ -338,8 +338,8 @@ export const buildExpression = (
forAccessor: [yConfig.forAccessor],
axisMode: yConfig.axisMode ? [yConfig.axisMode] : [],
color:
- layer.layerType === layerTypes.THRESHOLD
- ? [yConfig.color || defaultThresholdColor]
+ layer.layerType === layerTypes.REFERENCELINE
+ ? [yConfig.color || defaultReferenceLineColor]
: yConfig.color
? [yConfig.color]
: [],
diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts
index 8052b0d593215..01fbbd892a118 100644
--- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts
@@ -319,7 +319,7 @@ describe('xy_visualization', () => {
});
});
- it('should add a dimension to a threshold layer', () => {
+ it('should add a dimension to a reference layer', () => {
expect(
xyVisualization.setDimension({
frame,
@@ -327,20 +327,20 @@ describe('xy_visualization', () => {
...exampleState(),
layers: [
{
- layerId: 'threshold',
- layerType: layerTypes.THRESHOLD,
+ layerId: 'referenceLine',
+ layerType: layerTypes.REFERENCELINE,
seriesType: 'line',
accessors: [],
},
],
},
- layerId: 'threshold',
- groupId: 'xThreshold',
+ layerId: 'referenceLine',
+ groupId: 'xReferenceLine',
columnId: 'newCol',
}).layers[0]
).toEqual({
- layerId: 'threshold',
- layerType: layerTypes.THRESHOLD,
+ layerId: 'referenceLine',
+ layerType: layerTypes.REFERENCELINE,
seriesType: 'line',
accessors: ['newCol'],
yConfig: [
@@ -538,15 +538,15 @@ describe('xy_visualization', () => {
expect(ops.filter(filterOperations).map((x) => x.dataType)).toEqual(['number']);
});
- describe('thresholds', () => {
+ describe('reference lines', () => {
beforeEach(() => {
frame.datasourceLayers = {
first: mockDatasource.publicAPIMock,
- threshold: mockDatasource.publicAPIMock,
+ referenceLine: mockDatasource.publicAPIMock,
};
});
- function getStateWithBaseThreshold(): State {
+ function getStateWithBaseReferenceLine(): State {
return {
...exampleState(),
layers: [
@@ -559,8 +559,8 @@ describe('xy_visualization', () => {
accessors: ['a'],
},
{
- layerId: 'threshold',
- layerType: layerTypes.THRESHOLD,
+ layerId: 'referenceLine',
+ layerType: layerTypes.REFERENCELINE,
seriesType: 'line',
accessors: [],
yConfig: [{ axisMode: 'left', forAccessor: 'a' }],
@@ -570,28 +570,28 @@ describe('xy_visualization', () => {
}
it('should support static value', () => {
- const state = getStateWithBaseThreshold();
+ const state = getStateWithBaseReferenceLine();
state.layers[0].accessors = [];
state.layers[1].yConfig = undefined;
expect(
xyVisualization.getConfiguration({
- state: getStateWithBaseThreshold(),
+ state: getStateWithBaseReferenceLine(),
frame,
- layerId: 'threshold',
+ layerId: 'referenceLine',
}).supportStaticValue
).toBeTruthy();
});
- it('should return no threshold groups for a empty data layer', () => {
- const state = getStateWithBaseThreshold();
+ it('should return no referenceLine groups for a empty data layer', () => {
+ const state = getStateWithBaseReferenceLine();
state.layers[0].accessors = [];
state.layers[1].yConfig = undefined;
const options = xyVisualization.getConfiguration({
state,
frame,
- layerId: 'threshold',
+ layerId: 'referenceLine',
}).groups;
expect(options).toHaveLength(0);
@@ -599,37 +599,37 @@ describe('xy_visualization', () => {
it('should return a group for the vertical left axis', () => {
const options = xyVisualization.getConfiguration({
- state: getStateWithBaseThreshold(),
+ state: getStateWithBaseReferenceLine(),
frame,
- layerId: 'threshold',
+ layerId: 'referenceLine',
}).groups;
expect(options).toHaveLength(1);
- expect(options[0].groupId).toBe('yThresholdLeft');
+ expect(options[0].groupId).toBe('yReferenceLineLeft');
});
it('should return a group for the vertical right axis', () => {
- const state = getStateWithBaseThreshold();
+ const state = getStateWithBaseReferenceLine();
state.layers[0].yConfig = [{ axisMode: 'right', forAccessor: 'a' }];
state.layers[1].yConfig![0].axisMode = 'right';
const options = xyVisualization.getConfiguration({
state,
frame,
- layerId: 'threshold',
+ layerId: 'referenceLine',
}).groups;
expect(options).toHaveLength(1);
- expect(options[0].groupId).toBe('yThresholdRight');
+ expect(options[0].groupId).toBe('yReferenceLineRight');
});
- it('should compute no groups for thresholds when the only data accessor available is a date histogram', () => {
- const state = getStateWithBaseThreshold();
+ it('should compute no groups for referenceLines when the only data accessor available is a date histogram', () => {
+ const state = getStateWithBaseReferenceLine();
state.layers[0].xAccessor = 'b';
state.layers[0].accessors = [];
state.layers[1].yConfig = []; // empty the configuration
// set the xAccessor as date_histogram
- frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => {
+ frame.datasourceLayers.referenceLine.getOperationForColumnId = jest.fn((accessor) => {
if (accessor === 'b') {
return {
dataType: 'date',
@@ -644,19 +644,19 @@ describe('xy_visualization', () => {
const options = xyVisualization.getConfiguration({
state,
frame,
- layerId: 'threshold',
+ layerId: 'referenceLine',
}).groups;
expect(options).toHaveLength(0);
});
it('should mark horizontal group is invalid when xAccessor is changed to a date histogram', () => {
- const state = getStateWithBaseThreshold();
+ const state = getStateWithBaseReferenceLine();
state.layers[0].xAccessor = 'b';
state.layers[0].accessors = [];
state.layers[1].yConfig![0].axisMode = 'bottom';
// set the xAccessor as date_histogram
- frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => {
+ frame.datasourceLayers.referenceLine.getOperationForColumnId = jest.fn((accessor) => {
if (accessor === 'b') {
return {
dataType: 'date',
@@ -671,19 +671,19 @@ describe('xy_visualization', () => {
const options = xyVisualization.getConfiguration({
state,
frame,
- layerId: 'threshold',
+ layerId: 'referenceLine',
}).groups;
expect(options[0]).toEqual(
expect.objectContaining({
invalid: true,
- groupId: 'xThreshold',
+ groupId: 'xReferenceLine',
})
);
});
it('should return groups in a specific order (left, right, bottom)', () => {
- const state = getStateWithBaseThreshold();
+ const state = getStateWithBaseReferenceLine();
state.layers[0].xAccessor = 'c';
state.layers[0].accessors = ['a', 'b'];
// invert them on purpose
@@ -697,7 +697,7 @@ describe('xy_visualization', () => {
{ forAccessor: 'a', axisMode: 'left' },
];
// set the xAccessor as number histogram
- frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => {
+ frame.datasourceLayers.referenceLine.getOperationForColumnId = jest.fn((accessor) => {
if (accessor === 'c') {
return {
dataType: 'number',
@@ -712,21 +712,21 @@ describe('xy_visualization', () => {
const [left, right, bottom] = xyVisualization.getConfiguration({
state,
frame,
- layerId: 'threshold',
+ layerId: 'referenceLine',
}).groups;
- expect(left.groupId).toBe('yThresholdLeft');
- expect(right.groupId).toBe('yThresholdRight');
- expect(bottom.groupId).toBe('xThreshold');
+ expect(left.groupId).toBe('yReferenceLineLeft');
+ expect(right.groupId).toBe('yReferenceLineRight');
+ expect(bottom.groupId).toBe('xReferenceLine');
});
it('should ignore terms operation for xAccessor', () => {
- const state = getStateWithBaseThreshold();
+ const state = getStateWithBaseReferenceLine();
state.layers[0].xAccessor = 'b';
state.layers[0].accessors = [];
state.layers[1].yConfig = []; // empty the configuration
// set the xAccessor as top values
- frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => {
+ frame.datasourceLayers.referenceLine.getOperationForColumnId = jest.fn((accessor) => {
if (accessor === 'b') {
return {
dataType: 'string',
@@ -741,19 +741,19 @@ describe('xy_visualization', () => {
const options = xyVisualization.getConfiguration({
state,
frame,
- layerId: 'threshold',
+ layerId: 'referenceLine',
}).groups;
expect(options).toHaveLength(0);
});
it('should mark horizontal group is invalid when accessor is changed to a terms operation', () => {
- const state = getStateWithBaseThreshold();
+ const state = getStateWithBaseReferenceLine();
state.layers[0].xAccessor = 'b';
state.layers[0].accessors = [];
state.layers[1].yConfig![0].axisMode = 'bottom';
// set the xAccessor as date_histogram
- frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => {
+ frame.datasourceLayers.referenceLine.getOperationForColumnId = jest.fn((accessor) => {
if (accessor === 'b') {
return {
dataType: 'string',
@@ -768,13 +768,13 @@ describe('xy_visualization', () => {
const options = xyVisualization.getConfiguration({
state,
frame,
- layerId: 'threshold',
+ layerId: 'referenceLine',
}).groups;
expect(options[0]).toEqual(
expect.objectContaining({
invalid: true,
- groupId: 'xThreshold',
+ groupId: 'xReferenceLine',
})
);
});
@@ -813,20 +813,20 @@ describe('xy_visualization', () => {
},
};
- const state = getStateWithBaseThreshold();
+ const state = getStateWithBaseReferenceLine();
state.layers[0].accessors = ['yAccessorId', 'yAccessorId2'];
state.layers[1].yConfig = []; // empty the configuration
const options = xyVisualization.getConfiguration({
state,
frame: { ...frame, activeData: tables },
- layerId: 'threshold',
+ layerId: 'referenceLine',
}).groups;
expect(options).toEqual(
expect.arrayContaining([
- expect.objectContaining({ groupId: 'yThresholdLeft' }),
- expect.objectContaining({ groupId: 'yThresholdRight' }),
+ expect.objectContaining({ groupId: 'yReferenceLineLeft' }),
+ expect.objectContaining({ groupId: 'yReferenceLineRight' }),
])
);
});
diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
index 4e279d2e0026d..db1a2aeffb670 100644
--- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx
@@ -27,14 +27,14 @@ import { LensIconChartMixedXy } from '../assets/chart_mixed_xy';
import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal';
import { getAccessorColorConfig, getColorAssignments } from './color_assignment';
import { getColumnToLabelMap } from './state_helpers';
-import { LensIconChartBarThreshold } from '../assets/chart_bar_threshold';
+import { LensIconChartBarReferenceLine } from '../assets/chart_bar_reference_line';
import { generateId } from '../id_generator';
import {
getGroupsAvailableInData,
getGroupsRelatedToData,
getGroupsToShow,
getStaticValue,
-} from './threshold_helpers';
+} from './reference_line_helpers';
import {
checkScaleOperation,
checkXAccessorCompatibility,
@@ -194,17 +194,17 @@ export const getXyVisualization = ({
},
getSupportedLayers(state, frame) {
- const thresholdGroupIds = [
+ const referenceLineGroupIds = [
{
- id: 'yThresholdLeft',
+ id: 'yReferenceLineLeft',
label: 'yLeft' as const,
},
{
- id: 'yThresholdRight',
+ id: 'yReferenceLineRight',
label: 'yRight' as const,
},
{
- id: 'xThreshold',
+ id: 'xReferenceLine',
label: 'x' as const,
},
];
@@ -220,8 +220,8 @@ export const getXyVisualization = ({
'number',
frame?.datasourceLayers || {}
);
- const thresholdGroups = getGroupsRelatedToData(
- thresholdGroupIds,
+ const referenceLineGroups = getGroupsRelatedToData(
+ referenceLineGroupIds,
state,
frame?.datasourceLayers || {},
frame?.activeData
@@ -236,22 +236,22 @@ export const getXyVisualization = ({
icon: LensIconChartMixedXy,
},
{
- type: layerTypes.THRESHOLD,
- label: i18n.translate('xpack.lens.xyChart.addThresholdLayerLabel', {
- defaultMessage: 'Add threshold layer',
+ type: layerTypes.REFERENCELINE,
+ label: i18n.translate('xpack.lens.xyChart.addReferenceLineLayerLabel', {
+ defaultMessage: 'Add reference layer',
}),
- icon: LensIconChartBarThreshold,
+ icon: LensIconChartBarReferenceLine,
disabled:
!filledDataLayers.length ||
(!dataLayers.some(layerHasNumberHistogram) &&
dataLayers.every(({ accessors }) => !accessors.length)),
tooltipContent: filledDataLayers.length
? undefined
- : i18n.translate('xpack.lens.xyChart.addThresholdLayerLabelDisabledHelp', {
- defaultMessage: 'Add some data to enable threshold layer',
+ : i18n.translate('xpack.lens.xyChart.addReferenceLineLayerLabelDisabledHelp', {
+ defaultMessage: 'Add some data to enable reference layer',
}),
initialDimensions: state
- ? thresholdGroups.map(({ id, label }) => ({
+ ? referenceLineGroups.map(({ id, label }) => ({
groupId: id,
columnId: generateId(),
dataType: 'number',
@@ -318,25 +318,25 @@ export const getXyVisualization = ({
);
const groupsToShow = getGroupsToShow(
[
- // When a threshold layer panel is added, a static threshold should automatically be included by default
+ // When a reference layer panel is added, a static reference line should automatically be included by default
// in the first available axis, in the following order: vertical left, vertical right, horizontal.
{
config: left,
- id: 'yThresholdLeft',
+ id: 'yReferenceLineLeft',
label: 'yLeft',
- dataTestSubj: 'lnsXY_yThresholdLeftPanel',
+ dataTestSubj: 'lnsXY_yReferenceLineLeftPanel',
},
{
config: right,
- id: 'yThresholdRight',
+ id: 'yReferenceLineRight',
label: 'yRight',
- dataTestSubj: 'lnsXY_yThresholdRightPanel',
+ dataTestSubj: 'lnsXY_yReferenceLineRightPanel',
},
{
config: bottom,
- id: 'xThreshold',
+ id: 'xReferenceLine',
label: 'x',
- dataTestSubj: 'lnsXY_xThresholdPanel',
+ dataTestSubj: 'lnsXY_xReferenceLinePanel',
},
],
state,
@@ -346,9 +346,9 @@ export const getXyVisualization = ({
return {
supportFieldFormat: false,
supportStaticValue: true,
- // Each thresholds layer panel will have sections for each available axis
+ // Each reference lines layer panel will have sections for each available axis
// (horizontal axis, vertical axis left, vertical axis right).
- // Only axes that support numeric thresholds should be shown
+ // Only axes that support numeric reference lines should be shown
groups: groupsToShow.map(({ config = [], id, label, dataTestSubj, valid }) => ({
groupId: id,
groupLabel: getAxisName(label, { isHorizontal }),
@@ -363,10 +363,16 @@ export const getXyVisualization = ({
enableDimensionEditor: true,
dataTestSubj,
invalid: !valid,
- invalidMessage: i18n.translate('xpack.lens.configure.invalidThresholdDimension', {
- defaultMessage:
- 'This threshold is assigned to an axis that no longer exists. You may move this threshold to another available axis or remove it.',
- }),
+ invalidMessage:
+ label === 'x'
+ ? i18n.translate('xpack.lens.configure.invalidBottomReferenceLineDimension', {
+ defaultMessage:
+ 'This reference line is assigned to an axis that no longer exists or is no longer valid. You may move this reference line to another available axis or remove it.',
+ })
+ : i18n.translate('xpack.lens.configure.invalidReferenceLineDimension', {
+ defaultMessage:
+ 'This reference line is assigned to an axis that no longer exists. You may move this reference line to another available axis or remove it.',
+ }),
requiresPreviousColumnOnDuplicate: true,
})),
};
@@ -439,7 +445,7 @@ export const getXyVisualization = ({
newLayer.splitAccessor = columnId;
}
- if (newLayer.layerType === layerTypes.THRESHOLD) {
+ if (newLayer.layerType === layerTypes.REFERENCELINE) {
newLayer.accessors = [...newLayer.accessors.filter((a) => a !== columnId), columnId];
const hasYConfig = newLayer.yConfig?.some(({ forAccessor }) => forAccessor === columnId);
const previousYConfig = previousColumn
@@ -454,9 +460,9 @@ export const getXyVisualization = ({
// but keep the new group & id config
forAccessor: columnId,
axisMode:
- groupId === 'xThreshold'
+ groupId === 'xReferenceLine'
? 'bottom'
- : groupId === 'yThresholdRight'
+ : groupId === 'yReferenceLineRight'
? 'right'
: 'left',
},
@@ -491,9 +497,9 @@ export const getXyVisualization = ({
}
let newLayers = prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l));
- // // check if there's any threshold layer and pull it off if all data layers have no dimensions set
+ // check if there's any reference layer and pull it off if all data layers have no dimensions set
const layersByType = groupBy(newLayers, ({ layerType }) => layerType);
- // // check for data layers if they all still have xAccessors
+ // check for data layers if they all still have xAccessors
const groupsAvailable = getGroupsAvailableInData(
layersByType[layerTypes.DATA],
frame.datasourceLayers,
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx
index 516adbf585b9f..e3e53126015eb 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx
@@ -16,7 +16,7 @@ import { State } from '../types';
import { FormatFactory, layerTypes } from '../../../common';
import { getSeriesColor } from '../state_helpers';
import {
- defaultThresholdColor,
+ defaultReferenceLineColor,
getAccessorColorConfig,
getColorAssignments,
} from '../color_assignment';
@@ -60,8 +60,8 @@ export const ColorPicker = ({
const overwriteColor = getSeriesColor(layer, accessor);
const currentColor = useMemo(() => {
if (overwriteColor || !frame.activeData) return overwriteColor;
- if (layer.layerType === layerTypes.THRESHOLD) {
- return defaultThresholdColor;
+ if (layer.layerType === layerTypes.REFERENCELINE) {
+ return defaultReferenceLineColor;
}
const datasource = frame.datasourceLayers[layer.layerId];
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx
index 41d00e2eef32a..e18ea18c30fb0 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx
@@ -24,7 +24,7 @@ import type {
FramePublicAPI,
} from '../../types';
import { State, visualizationTypes, XYState } from '../types';
-import type { FormatFactory } from '../../../common';
+import { FormatFactory, layerTypes } from '../../../common';
import {
SeriesType,
YAxisMode,
@@ -39,7 +39,7 @@ import { getAxesConfiguration, GroupsConfiguration } from '../axes_configuration
import { VisualOptionsPopover } from './visual_options_popover';
import { getScaleType } from '../to_expression';
import { ColorPicker } from './color_picker';
-import { ThresholdPanel } from './threshold_panel';
+import { ReferenceLinePanel } from './reference_line_panel';
import { PalettePicker, TooltipWrapper } from '../../shared_components';
type UnwrapArray = T extends Array ? P : T;
@@ -564,8 +564,8 @@ export function DimensionEditor(
);
}
- if (layer.layerType === 'threshold') {
- return ;
+ if (layer.layerType === layerTypes.REFERENCELINE) {
+ return ;
}
return (
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx
index dde4de0dd4bc3..d81979f603943 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx
@@ -17,7 +17,7 @@ import { isHorizontalChart, isHorizontalSeries } from '../state_helpers';
import { trackUiEvent } from '../../lens_ui_telemetry';
import { StaticHeader } from '../../shared_components';
import { ToolbarButton } from '../../../../../../src/plugins/kibana_react/public';
-import { LensIconChartBarThreshold } from '../../assets/chart_bar_threshold';
+import { LensIconChartBarReferenceLine } from '../../assets/chart_bar_reference_line';
import { updateLayer } from '.';
export function LayerHeader(props: VisualizationLayerWidgetProps) {
@@ -29,13 +29,13 @@ export function LayerHeader(props: VisualizationLayerWidgetProps) {
if (!layer) {
return null;
}
- // if it's a threshold just draw a static text
- if (layer.layerType === layerTypes.THRESHOLD) {
+ // if it's a reference line just draw a static text
+ if (layer.layerType === layerTypes.REFERENCELINE) {
return (
);
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx
similarity index 71%
rename from x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx
rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx
index 7c31d72e6cbde..7b9fd01e540fe 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx
@@ -30,53 +30,55 @@ import { TooltipWrapper, useDebouncedValue } from '../../shared_components';
const icons = [
{
value: 'none',
- label: i18n.translate('xpack.lens.xyChart.thresholds.noIconLabel', { defaultMessage: 'None' }),
+ label: i18n.translate('xpack.lens.xyChart.referenceLine.noIconLabel', {
+ defaultMessage: 'None',
+ }),
},
{
value: 'asterisk',
- label: i18n.translate('xpack.lens.xyChart.thresholds.asteriskIconLabel', {
+ label: i18n.translate('xpack.lens.xyChart.referenceLine.asteriskIconLabel', {
defaultMessage: 'Asterisk',
}),
},
{
value: 'bell',
- label: i18n.translate('xpack.lens.xyChart.thresholds.bellIconLabel', {
+ label: i18n.translate('xpack.lens.xyChart.referenceLine.bellIconLabel', {
defaultMessage: 'Bell',
}),
},
{
value: 'bolt',
- label: i18n.translate('xpack.lens.xyChart.thresholds.boltIconLabel', {
+ label: i18n.translate('xpack.lens.xyChart.referenceLine.boltIconLabel', {
defaultMessage: 'Bolt',
}),
},
{
value: 'bug',
- label: i18n.translate('xpack.lens.xyChart.thresholds.bugIconLabel', {
+ label: i18n.translate('xpack.lens.xyChart.referenceLine.bugIconLabel', {
defaultMessage: 'Bug',
}),
},
{
value: 'editorComment',
- label: i18n.translate('xpack.lens.xyChart.thresholds.commentIconLabel', {
+ label: i18n.translate('xpack.lens.xyChart.referenceLine.commentIconLabel', {
defaultMessage: 'Comment',
}),
},
{
value: 'alert',
- label: i18n.translate('xpack.lens.xyChart.thresholds.alertIconLabel', {
+ label: i18n.translate('xpack.lens.xyChart.referenceLine.alertIconLabel', {
defaultMessage: 'Alert',
}),
},
{
value: 'flag',
- label: i18n.translate('xpack.lens.xyChart.thresholds.flagIconLabel', {
+ label: i18n.translate('xpack.lens.xyChart.referenceLine.flagIconLabel', {
defaultMessage: 'Flag',
}),
},
{
value: 'tag',
- label: i18n.translate('xpack.lens.xyChart.thresholds.tagIconLabel', {
+ label: i18n.translate('xpack.lens.xyChart.referenceLine.tagIconLabel', {
defaultMessage: 'Tag',
}),
},
@@ -116,20 +118,57 @@ const IconSelect = ({
);
};
-function getIconPositionOptions({
- isHorizontal,
- axisMode,
-}: {
+interface LabelConfigurationOptions {
isHorizontal: boolean;
axisMode: YConfig['axisMode'];
-}) {
+}
+
+function getFillPositionOptions({ isHorizontal, axisMode }: LabelConfigurationOptions) {
+ const aboveLabel = i18n.translate('xpack.lens.xyChart.referenceLineFill.above', {
+ defaultMessage: 'Above',
+ });
+ const belowLabel = i18n.translate('xpack.lens.xyChart.referenceLineFill.below', {
+ defaultMessage: 'Below',
+ });
+ const beforeLabel = i18n.translate('xpack.lens.xyChart.referenceLineFill.before', {
+ defaultMessage: 'Before',
+ });
+ const afterLabel = i18n.translate('xpack.lens.xyChart.referenceLineFill.after', {
+ defaultMessage: 'After',
+ });
+
+ const aboveOptionLabel = axisMode !== 'bottom' && !isHorizontal ? aboveLabel : afterLabel;
+ const belowOptionLabel = axisMode !== 'bottom' && !isHorizontal ? belowLabel : beforeLabel;
+
+ return [
+ {
+ id: `${idPrefix}none`,
+ label: i18n.translate('xpack.lens.xyChart.referenceLineFill.none', {
+ defaultMessage: 'None',
+ }),
+ 'data-test-subj': 'lnsXY_referenceLine_fill_none',
+ },
+ {
+ id: `${idPrefix}above`,
+ label: aboveOptionLabel,
+ 'data-test-subj': 'lnsXY_referenceLine_fill_above',
+ },
+ {
+ id: `${idPrefix}below`,
+ label: belowOptionLabel,
+ 'data-test-subj': 'lnsXY_referenceLine_fill_below',
+ },
+ ];
+}
+
+function getIconPositionOptions({ isHorizontal, axisMode }: LabelConfigurationOptions) {
const options = [
{
id: `${idPrefix}auto`,
- label: i18n.translate('xpack.lens.xyChart.thresholdMarker.auto', {
+ label: i18n.translate('xpack.lens.xyChart.referenceLineMarker.auto', {
defaultMessage: 'Auto',
}),
- 'data-test-subj': 'lnsXY_markerPosition_auto',
+ 'data-test-subj': 'lnsXY_referenceLine_markerPosition_auto',
},
];
const topLabel = i18n.translate('xpack.lens.xyChart.markerPosition.above', {
@@ -149,12 +188,12 @@ function getIconPositionOptions({
{
id: `${idPrefix}above`,
label: isHorizontal ? rightLabel : topLabel,
- 'data-test-subj': 'lnsXY_markerPosition_above',
+ 'data-test-subj': 'lnsXY_referenceLine_markerPosition_above',
},
{
id: `${idPrefix}below`,
label: isHorizontal ? leftLabel : bottomLabel,
- 'data-test-subj': 'lnsXY_markerPosition_below',
+ 'data-test-subj': 'lnsXY_referenceLine_markerPosition_below',
},
];
if (isHorizontal) {
@@ -168,12 +207,12 @@ function getIconPositionOptions({
{
id: `${idPrefix}left`,
label: isHorizontal ? bottomLabel : leftLabel,
- 'data-test-subj': 'lnsXY_markerPosition_left',
+ 'data-test-subj': 'lnsXY_referenceLine_markerPosition_left',
},
{
id: `${idPrefix}right`,
label: isHorizontal ? topLabel : rightLabel,
- 'data-test-subj': 'lnsXY_markerPosition_right',
+ 'data-test-subj': 'lnsXY_referenceLine_markerPosition_right',
},
];
if (isHorizontal) {
@@ -188,7 +227,7 @@ export function hasIcon(icon: string | undefined): icon is string {
return icon != null && icon !== 'none';
}
-export const ThresholdPanel = (
+export const ReferenceLinePanel = (
props: VisualizationDimensionEditorProps & {
formatFactory: FormatFactory;
paletteService: PaletteRegistry;
@@ -232,18 +271,18 @@ export const ThresholdPanel = (
return (
<>
{
setYConfig({ forAccessor: accessor, textVisibility: !currentYConfig?.textVisibility });
@@ -253,7 +292,7 @@ export const ThresholdPanel = (
@@ -268,15 +307,18 @@ export const ThresholdPanel = (
display="columnCompressed"
fullWidth
isDisabled={!hasIcon(currentYConfig?.icon) && !currentYConfig?.textVisibility}
- label={i18n.translate('xpack.lens.xyChart.thresholdMarker.position', {
+ label={i18n.translate('xpack.lens.xyChart.referenceLineMarker.position', {
defaultMessage: 'Decoration position',
})}
>
@@ -372,41 +414,19 @@ export const ThresholdPanel = (
{
const newMode = id.replace(idPrefix, '') as FillStyle;
@@ -440,7 +460,7 @@ const LineThicknessSlider = ({
return (
{
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
showEventFilters: false,
+ showHostIsolationExceptions: false,
showTrustedApps: false,
})
);
@@ -86,6 +87,7 @@ describe('useExceptionLists', () => {
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
showEventFilters: false,
+ showHostIsolationExceptions: false,
showTrustedApps: false,
})
);
@@ -127,6 +129,7 @@ describe('useExceptionLists', () => {
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
showEventFilters: false,
+ showHostIsolationExceptions: false,
showTrustedApps: true,
})
);
@@ -137,7 +140,7 @@ describe('useExceptionLists', () => {
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
- '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
+ '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)',
http: mockKibanaHttpService,
namespaceTypes: 'single,agnostic',
pagination: { page: 1, perPage: 20 },
@@ -163,6 +166,7 @@ describe('useExceptionLists', () => {
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
showEventFilters: false,
+ showHostIsolationExceptions: false,
showTrustedApps: false,
})
);
@@ -173,7 +177,7 @@ describe('useExceptionLists', () => {
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
- '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)',
http: mockKibanaHttpService,
namespaceTypes: 'single,agnostic',
pagination: { page: 1, perPage: 20 },
@@ -199,6 +203,7 @@ describe('useExceptionLists', () => {
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
showEventFilters: true,
+ showHostIsolationExceptions: false,
showTrustedApps: false,
})
);
@@ -209,7 +214,7 @@ describe('useExceptionLists', () => {
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
- '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)',
http: mockKibanaHttpService,
namespaceTypes: 'single,agnostic',
pagination: { page: 1, perPage: 20 },
@@ -235,6 +240,7 @@ describe('useExceptionLists', () => {
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
showEventFilters: false,
+ showHostIsolationExceptions: false,
showTrustedApps: false,
})
);
@@ -245,7 +251,81 @@ describe('useExceptionLists', () => {
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
- '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)',
+ http: mockKibanaHttpService,
+ namespaceTypes: 'single,agnostic',
+ pagination: { page: 1, perPage: 20 },
+ signal: new AbortController().signal,
+ });
+ });
+ });
+
+ test('fetches host isolation exceptions lists if "hostIsolationExceptionsFilter" is true', async () => {
+ const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
+
+ await act(async () => {
+ const { waitForNextUpdate } = renderHook(() =>
+ useExceptionLists({
+ errorMessage: 'Uh oh',
+ filterOptions: {},
+ http: mockKibanaHttpService,
+ initialPagination: {
+ page: 1,
+ perPage: 20,
+ total: 0,
+ },
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
+ showEventFilters: false,
+ showHostIsolationExceptions: true,
+ showTrustedApps: false,
+ })
+ );
+ // NOTE: First `waitForNextUpdate` is initialization
+ // Second call applies the params
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
+ filters:
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions* OR exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)',
+ http: mockKibanaHttpService,
+ namespaceTypes: 'single,agnostic',
+ pagination: { page: 1, perPage: 20 },
+ signal: new AbortController().signal,
+ });
+ });
+ });
+
+ test('does not fetch host isolation exceptions lists if "showHostIsolationExceptions" is false', async () => {
+ const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
+
+ await act(async () => {
+ const { waitForNextUpdate } = renderHook(() =>
+ useExceptionLists({
+ errorMessage: 'Uh oh',
+ filterOptions: {},
+ http: mockKibanaHttpService,
+ initialPagination: {
+ page: 1,
+ perPage: 20,
+ total: 0,
+ },
+ namespaceTypes: ['single', 'agnostic'],
+ notifications: mockKibanaNotificationsService,
+ showEventFilters: false,
+ showHostIsolationExceptions: false,
+ showTrustedApps: false,
+ })
+ );
+ // NOTE: First `waitForNextUpdate` is initialization
+ // Second call applies the params
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
+ filters:
+ '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)',
http: mockKibanaHttpService,
namespaceTypes: 'single,agnostic',
pagination: { page: 1, perPage: 20 },
@@ -274,6 +354,7 @@ describe('useExceptionLists', () => {
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
showEventFilters: false,
+ showHostIsolationExceptions: false,
showTrustedApps: false,
})
);
@@ -284,7 +365,7 @@ describe('useExceptionLists', () => {
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
- '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)',
+ '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)',
http: mockKibanaHttpService,
namespaceTypes: 'single,agnostic',
pagination: { page: 1, perPage: 20 },
@@ -318,6 +399,7 @@ describe('useExceptionLists', () => {
namespaceTypes,
notifications,
showEventFilters,
+ showHostIsolationExceptions: false,
showTrustedApps,
}),
{
@@ -333,6 +415,7 @@ describe('useExceptionLists', () => {
namespaceTypes: ['single'],
notifications: mockKibanaNotificationsService,
showEventFilters: false,
+ showHostIsolationExceptions: false,
showTrustedApps: false,
},
}
@@ -354,6 +437,7 @@ describe('useExceptionLists', () => {
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
showEventFilters: false,
+ showHostIsolationExceptions: false,
showTrustedApps: false,
});
// NOTE: Only need one call here because hook already initilaized
@@ -382,6 +466,7 @@ describe('useExceptionLists', () => {
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
showEventFilters: false,
+ showHostIsolationExceptions: false,
showTrustedApps: false,
})
);
@@ -421,6 +506,7 @@ describe('useExceptionLists', () => {
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
showEventFilters: false,
+ showHostIsolationExceptions: false,
showTrustedApps: false,
})
);
diff --git a/x-pack/plugins/maps/server/index.ts b/x-pack/plugins/maps/server/index.ts
index 4e5dfcf1f94c4..6a846bc245824 100644
--- a/x-pack/plugins/maps/server/index.ts
+++ b/x-pack/plugins/maps/server/index.ts
@@ -34,6 +34,7 @@ export const config: PluginConfigDescriptor = {
return completeConfig;
}
addDeprecation({
+ configPath: 'xpack.maps.showMapVisualizationTypes',
message: i18n.translate('xpack.maps.deprecation.showMapVisualizationTypes.message', {
defaultMessage:
'xpack.maps.showMapVisualizationTypes is deprecated and is no longer used',
@@ -59,6 +60,7 @@ export const config: PluginConfigDescriptor = {
return completeConfig;
}
addDeprecation({
+ configPath: 'map.proxyElasticMapsServiceInMaps',
documentationUrl: `https://www.elastic.co/guide/en/kibana/${branch}/maps-connect-to-ems.html#elastic-maps-server`,
message: i18n.translate('xpack.maps.deprecation.proxyEMS.message', {
defaultMessage: 'map.proxyElasticMapsServiceInMaps is deprecated and is no longer used',
@@ -86,6 +88,7 @@ export const config: PluginConfigDescriptor = {
return completeConfig;
}
addDeprecation({
+ configPath: 'map.regionmap',
message: i18n.translate('xpack.maps.deprecation.regionmap.message', {
defaultMessage: 'map.regionmap is deprecated and is no longer used',
}),
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json
index 123232935df05..f8feaef3be5f8 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json
@@ -1,29 +1,29 @@
{
"id": "apm_transaction",
"title": "APM",
- "description": "Detect anomalies in transactions from your APM services for metric data.",
+ "description": "Detect anomalies in transactions from your APM services.",
"type": "Transaction data",
"logoFile": "logo.json",
- "defaultIndexPattern": "apm-*-metric,metrics-apm*",
+ "defaultIndexPattern": "apm-*-transaction",
"query": {
"bool": {
"filter": [
- { "term": { "processor.event": "metric" } },
- { "term": { "metricset.name": "transaction" } }
+ { "term": { "processor.event": "transaction" } },
+ { "exists": { "field": "transaction.duration" } }
]
}
},
"jobs": [
{
- "id": "apm_metrics",
- "file": "apm_metrics.json"
+ "id": "high_mean_transaction_duration",
+ "file": "high_mean_transaction_duration.json"
}
],
"datafeeds": [
{
- "id": "datafeed-apm_metrics",
- "file": "datafeed_apm_metrics.json",
- "job_id": "apm_metrics"
+ "id": "datafeed-high_mean_transaction_duration",
+ "file": "datafeed_high_mean_transaction_duration.json",
+ "job_id": "high_mean_transaction_duration"
}
]
}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_metrics.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_metrics.json
deleted file mode 100644
index d5092f3ffc553..0000000000000
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_metrics.json
+++ /dev/null
@@ -1,53 +0,0 @@
-{
- "job_type": "anomaly_detector",
- "groups": [
- "apm"
- ],
- "description": "Detects anomalies in transaction duration, throughput and error percentage for metric data.",
- "analysis_config": {
- "bucket_span": "15m",
- "summary_count_field_name" : "doc_count",
- "detectors" : [
- {
- "detector_description" : "high duration by transaction type for an APM service",
- "function" : "high_mean",
- "field_name" : "transaction_duration",
- "by_field_name" : "transaction.type",
- "partition_field_name" : "service.name"
- },
- {
- "detector_description" : "transactions per minute for an APM service",
- "function" : "mean",
- "field_name" : "transactions_per_min",
- "by_field_name" : "transaction.type",
- "partition_field_name" : "service.name"
- },
- {
- "detector_description" : "percent failed for an APM service",
- "function" : "high_mean",
- "field_name" : "transaction_failure_percentage",
- "by_field_name" : "transaction.type",
- "partition_field_name" : "service.name"
- }
- ],
- "influencers" : [
- "transaction.type",
- "service.name"
- ]
- },
- "analysis_limits": {
- "model_memory_limit": "32mb"
- },
- "data_description": {
- "time_field" : "@timestamp",
- "time_format" : "epoch_ms"
- },
- "model_plot_config": {
- "enabled" : true,
- "annotations_enabled" : true
- },
- "results_index_name" : "custom-apm",
- "custom_settings": {
- "created_by": "ml-module-apm-transaction"
- }
-}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_metrics.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_metrics.json
deleted file mode 100644
index ba45582252cd7..0000000000000
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_metrics.json
+++ /dev/null
@@ -1,95 +0,0 @@
-{
- "job_id": "JOB_ID",
- "indices": [
- "INDEX_PATTERN_NAME"
- ],
- "chunking_config" : {
- "mode" : "off"
- },
- "query": {
- "bool": {
- "filter": [
- { "term": { "processor.event": "metric" } },
- { "term": { "metricset.name": "transaction" } }
- ]
- }
- },
- "aggregations" : {
- "buckets" : {
- "composite" : {
- "size" : 5000,
- "sources" : [
- {
- "date" : {
- "date_histogram" : {
- "field" : "@timestamp",
- "fixed_interval" : "90s"
- }
- }
- },
- {
- "transaction.type" : {
- "terms" : {
- "field" : "transaction.type"
- }
- }
- },
- {
- "service.name" : {
- "terms" : {
- "field" : "service.name"
- }
- }
- }
- ]
- },
- "aggs" : {
- "@timestamp" : {
- "max" : {
- "field" : "@timestamp"
- }
- },
- "transactions_per_min" : {
- "rate" : {
- "unit" : "minute"
- }
- },
- "transaction_duration" : {
- "avg" : {
- "field" : "transaction.duration.histogram"
- }
- },
- "error_count" : {
- "filter" : {
- "term" : {
- "event.outcome" : "failure"
- }
- },
- "aggs" : {
- "actual_error_count" : {
- "value_count" : {
- "field" : "event.outcome"
- }
- }
- }
- },
- "success_count" : {
- "filter" : {
- "term" : {
- "event.outcome" : "success"
- }
- }
- },
- "transaction_failure_percentage" : {
- "bucket_script" : {
- "buckets_path" : {
- "failure_count" : "error_count>_count",
- "success_count" : "success_count>_count"
- },
- "script" : "if ((params.failure_count + params.success_count)==0){return 0;}else{return params.failure_count/(params.failure_count + params.success_count);}"
- }
- }
- }
- }
- }
-}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json
new file mode 100644
index 0000000000000..d312577902f51
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json
@@ -0,0 +1,14 @@
+{
+ "job_id": "JOB_ID",
+ "indices": [
+ "INDEX_PATTERN_NAME"
+ ],
+ "query": {
+ "bool": {
+ "filter": [
+ { "term": { "processor.event": "transaction" } },
+ { "exists": { "field": "transaction.duration.us" } }
+ ]
+ }
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json
new file mode 100644
index 0000000000000..77284cb275cd8
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json
@@ -0,0 +1,35 @@
+{
+ "job_type": "anomaly_detector",
+ "groups": [
+ "apm"
+ ],
+ "description": "Detect transaction duration anomalies across transaction types for your APM services.",
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "high duration by transaction type for an APM service",
+ "function": "high_mean",
+ "field_name": "transaction.duration.us",
+ "by_field_name": "transaction.type",
+ "partition_field_name": "service.name"
+ }
+ ],
+ "influencers": [
+ "transaction.type",
+ "service.name"
+ ]
+ },
+ "analysis_limits": {
+ "model_memory_limit": "32mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "model_plot_config": {
+ "enabled": true
+ },
+ "custom_settings": {
+ "created_by": "ml-module-apm-transaction"
+ }
+}
diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/beats_template.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/beats_template.tsx
index 3bab01af8ceb7..7a070c735bbea 100644
--- a/x-pack/plugins/monitoring/public/application/pages/beats/beats_template.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/beats/beats_template.tsx
@@ -23,6 +23,7 @@ export const BeatsTemplate: React.FC = ({ instance, ...props
defaultMessage: 'Overview',
}),
route: '/beats',
+ testSubj: 'beatsOverviewPage',
});
tabs.push({
id: 'instances',
@@ -30,6 +31,7 @@ export const BeatsTemplate: React.FC = ({ instance, ...props
defaultMessage: 'Instances',
}),
route: '/beats/beats',
+ testSubj: 'beatsListingPage',
});
} else {
tabs.push({
diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx
index 489ad110c40fd..4611f17159621 100644
--- a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx
@@ -71,12 +71,7 @@ export const BeatsInstancesPage: React.FC = ({ clusters }) => {
]);
return (
-
+
= ({ clusters }) => {
};
return (
-
- {renderOverview(data)}
+
+ {renderOverview(data)}
);
};
diff --git a/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx b/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx
index 26072f53f4752..e798e7d74ad38 100644
--- a/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx
@@ -220,12 +220,10 @@ async function executeCheck(checker: SettingsChecker, http: { fetch: any }): Pro
return { found, reason };
} catch (err: any) {
- const { data } = err;
-
return {
error: true,
found: false,
- errorReason: data,
+ errorReason: err.body,
};
}
}
diff --git a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap
index b6d117fb3269c..8852d104fe00a 100644
--- a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap
@@ -3,7 +3,13 @@
exports[`NoData should show a default message if reason is unknown 1`] = `
+
+ No monitoring data found.
+
+
+ No monitoring data found.
+
{
+ return
{children};
+ };
+
if (isCloudEnabled) {
return (
-
+
-
+
);
}
if (useInternalCollection) {
return (
-
+
+
+
+
+
+
-
+
);
}
return (
-
+
+
+
+
+
+
-
+
);
}
diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/we_tried.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/we_tried.test.js.snap
index 482e6e97115b7..4e1209a5fd2aa 100644
--- a/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/we_tried.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/we_tried.test.js.snap
@@ -1,15 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WeTried should render "we tried" message 1`] = `
-Array [
+
We couldn't activate monitoring
-
,
+
,
+ />
@@ -19,6 +21,6 @@ Array [
If data is in your cluster, your monitoring dashboards will show up here.
-
,
-]
+
+
`;
diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.js
index 17e171451e3a3..37504f5842a74 100644
--- a/x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.js
+++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.js
@@ -5,13 +5,13 @@
* 2.0.
*/
-import React, { Fragment } from 'react';
+import React from 'react';
import { EuiText, EuiHorizontalRule, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export function WeTried() {
return (
-
+
-
+
);
}
diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts
index 5e02767fea98a..4fe5d9c765734 100644
--- a/x-pack/plugins/monitoring/server/config.test.ts
+++ b/x-pack/plugins/monitoring/server/config.test.ts
@@ -107,7 +107,7 @@ describe('config schema', () => {
"index": "metricbeat-*",
},
"min_interval_seconds": 10,
- "render_react_app": false,
+ "render_react_app": true,
"show_license_expiration": true,
},
}
diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts
index 5c2bdc1424f93..ddbfd480a9f4e 100644
--- a/x-pack/plugins/monitoring/server/config.ts
+++ b/x-pack/plugins/monitoring/server/config.ts
@@ -51,7 +51,7 @@ export const configSchema = schema.object({
}),
min_interval_seconds: schema.number({ defaultValue: 10 }),
show_license_expiration: schema.boolean({ defaultValue: true }),
- render_react_app: schema.boolean({ defaultValue: false }),
+ render_react_app: schema.boolean({ defaultValue: true }),
}),
kibana: schema.object({
collection: schema.object({
diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts
index 3554abd569581..9dec7b105f2f6 100644
--- a/x-pack/plugins/monitoring/server/deprecations.ts
+++ b/x-pack/plugins/monitoring/server/deprecations.ts
@@ -55,6 +55,7 @@ export const deprecations = ({
const legacyKey = get(config, `xpack.monitoring.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}`);
if (emailNotificationsEnabled && !updatedKey && !legacyKey) {
addDeprecation({
+ configPath: `cluster_alerts.email_notifications.enabled`,
message: `Config key [${fromPath}.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}] will be required for email notifications to work in 8.0."`,
correctiveActions: {
manualSteps: [
@@ -70,6 +71,7 @@ export const deprecations = ({
if (es) {
if (es.username === 'elastic') {
addDeprecation({
+ configPath: 'elasticsearch.username',
message: `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.`,
correctiveActions: {
manualSteps: [`Replace [${fromPath}.username] from "elastic" to "kibana_system".`],
@@ -77,6 +79,7 @@ export const deprecations = ({
});
} else if (es.username === 'kibana') {
addDeprecation({
+ configPath: 'elasticsearch.username',
message: `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.`,
correctiveActions: {
manualSteps: [`Replace [${fromPath}.username] from "kibana" to "kibana_system".`],
@@ -91,6 +94,7 @@ export const deprecations = ({
if (ssl) {
if (ssl.key !== undefined && ssl.certificate === undefined) {
addDeprecation({
+ configPath: 'elasticsearch.ssl.key',
message: `Setting [${fromPath}.key] without [${fromPath}.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`,
correctiveActions: {
manualSteps: [
@@ -100,6 +104,7 @@ export const deprecations = ({
});
} else if (ssl.certificate !== undefined && ssl.key === undefined) {
addDeprecation({
+ configPath: 'elasticsearch.ssl.certificate',
message: `Setting [${fromPath}.certificate] without [${fromPath}.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`,
correctiveActions: {
manualSteps: [
diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts
index a45c084131a4d..1eeafb4e0c513 100644
--- a/x-pack/plugins/reporting/server/config/index.ts
+++ b/x-pack/plugins/reporting/server/config/index.ts
@@ -25,6 +25,7 @@ export const config: PluginConfigDescriptor = {
const reporting = get(settings, fromPath);
if (reporting?.index) {
addDeprecation({
+ configPath: `${fromPath}.index`,
title: i18n.translate('xpack.reporting.deprecations.reportingIndex.title', {
defaultMessage: 'Setting "{fromPath}.index" is deprecated',
values: { fromPath },
@@ -51,6 +52,7 @@ export const config: PluginConfigDescriptor = {
if (reporting?.roles?.enabled !== false) {
addDeprecation({
+ configPath: `${fromPath}.roles.enabled`,
level: 'warning',
title: i18n.translate('xpack.reporting.deprecations.reportingRoles.title', {
defaultMessage: 'Setting "{fromPath}.roles" is deprecated',
diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts
index 4303de6a3ef56..b5258d91485f7 100644
--- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts
@@ -25,7 +25,7 @@ describe('headers', () => {
logger
);
await expect(getDecryptedHeaders()).rejects.toMatchInlineSnapshot(
- `[Error: Failed to decrypt report job data. Please ensure that xpack.reporting.encryptionKey is set and re-generate this report. Error: Invalid IV length]`
+ `[Error: Failed to decrypt report job data. Please ensure that xpack.reporting.encryptionKey is set and re-generate this report. TypeError: Invalid initialization vector]`
);
});
diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts
index 5032eaab46e84..b045f4872fcb0 100644
--- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts
@@ -303,7 +303,9 @@ describe('CSV Execute Job', function () {
});
await expect(
runTask('job123', jobParams, cancellationToken, stream)
- ).rejects.toMatchInlineSnapshot(`[TypeError: Cannot read property 'indexOf' of undefined]`);
+ ).rejects.toMatchInlineSnapshot(
+ `[TypeError: Cannot read properties of undefined (reading 'indexOf')]`
+ );
expect(mockEsClient.clearScroll).toHaveBeenCalledWith(
expect.objectContaining({ body: { scroll_id: lastScrollId } })
diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts
index 634df081d77d7..d4b18a2e1b296 100644
--- a/x-pack/plugins/security/server/config_deprecations.test.ts
+++ b/x-pack/plugins/security/server/config_deprecations.test.ts
@@ -17,6 +17,7 @@ const deprecationContext = configDeprecationsMock.createContext();
const applyConfigDeprecations = (settings: Record = {}) => {
const deprecations = securityConfigDeprecationProvider(configDeprecationFactory);
const deprecationMessages: string[] = [];
+ const configPaths: string[] = [];
const { config: migrated } = applyDeprecations(
settings,
deprecations.map((deprecation) => ({
@@ -25,10 +26,13 @@ const applyConfigDeprecations = (settings: Record = {}) => {
context: deprecationContext,
})),
() =>
- ({ message }) =>
- deprecationMessages.push(message)
+ ({ message, configPath }) => {
+ deprecationMessages.push(message);
+ configPaths.push(configPath);
+ }
);
return {
+ configPaths,
messages: deprecationMessages,
migrated,
};
@@ -356,12 +360,14 @@ describe('Config Deprecations', () => {
},
},
};
- const { messages } = applyConfigDeprecations(cloneDeep(config));
+ const { messages, configPaths } = applyConfigDeprecations(cloneDeep(config));
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"xpack.security.authc.providers.saml..maxRedirectURLSize\\" is no longer used.",
]
`);
+
+ expect(configPaths).toEqual(['xpack.security.authc.providers.saml.saml1.maxRedirectURLSize']);
});
it(`warns when 'xpack.security.authc.providers' is an array of strings`, () => {
diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts
index ce9eb76fb1dc8..c8c8e64648c4b 100644
--- a/x-pack/plugins/security/server/config_deprecations.ts
+++ b/x-pack/plugins/security/server/config_deprecations.ts
@@ -35,6 +35,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({
const legacyAuditLoggerEnabled = !settings?.xpack?.security?.audit?.appender;
if (auditLoggingEnabled && legacyAuditLoggerEnabled) {
addDeprecation({
+ configPath: 'xpack.security.audit.appender',
title: i18n.translate('xpack.security.deprecations.auditLoggerTitle', {
defaultMessage: 'The legacy audit logger is deprecated',
}),
@@ -59,6 +60,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({
(settings, fromPath, addDeprecation) => {
if (Array.isArray(settings?.xpack?.security?.authc?.providers)) {
addDeprecation({
+ configPath: 'xpack.security.authc.providers',
title: i18n.translate('xpack.security.deprecations.authcProvidersTitle', {
defaultMessage:
'Defining "xpack.security.authc.providers" as an array of provider types is deprecated',
@@ -92,6 +94,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({
if (hasProviderType('basic') && hasProviderType('token')) {
addDeprecation({
+ configPath: 'xpack.security.authc.providers',
title: i18n.translate('xpack.security.deprecations.basicAndTokenProvidersTitle', {
defaultMessage:
'Both "basic" and "token" authentication providers are enabled in "xpack.security.authc.providers"',
@@ -119,8 +122,13 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({
string,
any
>;
- if (Object.values(samlProviders).find((provider) => !!provider.maxRedirectURLSize)) {
+
+ const foundProvider = Object.entries(samlProviders).find(
+ ([_, provider]) => !!provider.maxRedirectURLSize
+ );
+ if (foundProvider) {
addDeprecation({
+ configPath: `xpack.security.authc.providers.saml.${foundProvider[0]}.maxRedirectURLSize`,
title: i18n.translate('xpack.security.deprecations.maxRedirectURLSizeTitle', {
defaultMessage:
'"xpack.security.authc.providers.saml..maxRedirectURLSize" is deprecated',
@@ -143,6 +151,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({
(settings, fromPath, addDeprecation, { branch }) => {
if ('enabled' in (settings?.xpack?.security || {})) {
addDeprecation({
+ configPath: 'xpack.security.enabled',
title: i18n.translate('xpack.security.deprecations.enabledTitle', {
defaultMessage: 'Setting "xpack.security.enabled" is deprecated',
}),
@@ -169,6 +178,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({
(settings, fromPath, addDeprecation, { branch }) => {
if (settings?.xpack?.security?.session?.idleTimeout === undefined) {
addDeprecation({
+ configPath: 'xpack.security.session.idleTimeout',
level: 'warning',
title: i18n.translate('xpack.security.deprecations.idleTimeoutTitle', {
defaultMessage: 'The "xpack.security.session.idleTimeout" default is changing',
@@ -192,6 +202,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({
if (settings?.xpack?.security?.session?.lifespan === undefined) {
addDeprecation({
+ configPath: 'xpack.security.session.lifespan',
level: 'warning',
title: i18n.translate('xpack.security.deprecations.lifespanTitle', {
defaultMessage: 'The "xpack.security.session.lifespan" default is changing',
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
index 942aed4166595..2da3a604478fc 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
@@ -75,6 +75,10 @@ export const policyFactory = (): PolicyConfig => {
mode: ProtectionModes.prevent,
supported: true,
},
+ memory_protection: {
+ mode: ProtectionModes.prevent,
+ supported: true,
+ },
popup: {
malware: {
message: '',
@@ -84,6 +88,10 @@ export const policyFactory = (): PolicyConfig => {
message: '',
enabled: true,
},
+ memory_protection: {
+ message: '',
+ enabled: true,
+ },
},
logging: {
file: 'info',
@@ -102,6 +110,10 @@ export const policyFactory = (): PolicyConfig => {
mode: ProtectionModes.prevent,
supported: true,
},
+ memory_protection: {
+ mode: ProtectionModes.prevent,
+ supported: true,
+ },
popup: {
malware: {
message: '',
@@ -111,6 +123,10 @@ export const policyFactory = (): PolicyConfig => {
message: '',
enabled: true,
},
+ memory_protection: {
+ message: '',
+ enabled: true,
+ },
},
logging: {
file: 'info',
@@ -167,12 +183,20 @@ export const policyFactoryWithoutPaidFeatures = (
mode: ProtectionModes.off,
supported: false,
},
+ memory_protection: {
+ mode: ProtectionModes.off,
+ supported: false,
+ },
popup: {
...policy.mac.popup,
malware: {
message: '',
enabled: true,
},
+ memory_protection: {
+ message: '',
+ enabled: false,
+ },
behavior_protection: {
message: '',
enabled: false,
@@ -185,12 +209,20 @@ export const policyFactoryWithoutPaidFeatures = (
mode: ProtectionModes.off,
supported: false,
},
+ memory_protection: {
+ mode: ProtectionModes.off,
+ supported: false,
+ },
popup: {
...policy.linux.popup,
malware: {
message: '',
enabled: true,
},
+ memory_protection: {
+ message: '',
+ enabled: false,
+ },
behavior_protection: {
message: '',
enabled: false,
@@ -229,6 +261,10 @@ export const policyFactoryWithSupportedFeatures = (
...policy.windows.behavior_protection,
supported: true,
},
+ memory_protection: {
+ ...policy.mac.memory_protection,
+ supported: true,
+ },
},
linux: {
...policy.linux,
@@ -236,6 +272,10 @@ export const policyFactoryWithSupportedFeatures = (
...policy.windows.behavior_protection,
supported: true,
},
+ memory_protection: {
+ ...policy.linux.memory_protection,
+ supported: true,
+ },
},
};
};
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
index 297b1d2442c78..2fee3e4c39d1d 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
@@ -942,6 +942,7 @@ export interface PolicyConfig {
};
malware: ProtectionFields;
behavior_protection: ProtectionFields & SupportedFields;
+ memory_protection: ProtectionFields & SupportedFields;
popup: {
malware: {
message: string;
@@ -951,6 +952,10 @@ export interface PolicyConfig {
message: string;
enabled: boolean;
};
+ memory_protection: {
+ message: string;
+ enabled: boolean;
+ };
};
logging: {
file: string;
@@ -965,6 +970,7 @@ export interface PolicyConfig {
};
malware: ProtectionFields;
behavior_protection: ProtectionFields & SupportedFields;
+ memory_protection: ProtectionFields & SupportedFields;
popup: {
malware: {
message: string;
@@ -974,6 +980,10 @@ export interface PolicyConfig {
message: string;
enabled: boolean;
};
+ memory_protection: {
+ message: string;
+ enabled: boolean;
+ };
};
logging: {
file: string;
@@ -1004,14 +1014,14 @@ export interface UIPolicyConfig {
*/
mac: Pick<
PolicyConfig['mac'],
- 'malware' | 'events' | 'popup' | 'advanced' | 'behavior_protection'
+ 'malware' | 'events' | 'popup' | 'advanced' | 'behavior_protection' | 'memory_protection'
>;
/**
* Linux-specific policy configuration that is supported via the UI
*/
linux: Pick<
PolicyConfig['linux'],
- 'malware' | 'events' | 'popup' | 'advanced' | 'behavior_protection'
+ 'malware' | 'events' | 'popup' | 'advanced' | 'behavior_protection' | 'memory_protection'
>;
}
diff --git a/x-pack/plugins/security_solution/common/license/policy_config.test.ts b/x-pack/plugins/security_solution/common/license/policy_config.test.ts
index 725d6ba74afd0..e08a096be82ef 100644
--- a/x-pack/plugins/security_solution/common/license/policy_config.test.ts
+++ b/x-pack/plugins/security_solution/common/license/policy_config.test.ts
@@ -84,6 +84,10 @@ describe('policy_config and licenses', () => {
// memory protection
policy.windows.memory_protection.mode = ProtectionModes.prevent;
policy.windows.memory_protection.supported = true;
+ policy.mac.memory_protection.mode = ProtectionModes.prevent;
+ policy.mac.memory_protection.supported = true;
+ policy.linux.memory_protection.mode = ProtectionModes.prevent;
+ policy.linux.memory_protection.supported = true;
// behavior protection
policy.windows.behavior_protection.mode = ProtectionModes.prevent;
policy.windows.behavior_protection.supported = true;
@@ -104,6 +108,10 @@ describe('policy_config and licenses', () => {
// memory protection
policy.windows.popup.memory_protection.enabled = true;
policy.windows.memory_protection.supported = true;
+ policy.mac.popup.memory_protection.enabled = true;
+ policy.mac.memory_protection.supported = true;
+ policy.linux.popup.memory_protection.enabled = true;
+ policy.linux.memory_protection.supported = true;
// behavior protection
policy.windows.popup.behavior_protection.enabled = true;
policy.windows.behavior_protection.supported = true;
@@ -157,6 +165,8 @@ describe('policy_config and licenses', () => {
it('blocks memory_protection to be turned on for Gold and below licenses', () => {
const policy = policyFactoryWithoutPaidFeatures();
policy.windows.memory_protection.mode = ProtectionModes.prevent;
+ policy.mac.memory_protection.mode = ProtectionModes.prevent;
+ policy.linux.memory_protection.mode = ProtectionModes.prevent;
let valid = isEndpointPolicyValidForLicense(policy, Gold);
expect(valid).toBeFalsy();
@@ -167,6 +177,9 @@ describe('policy_config and licenses', () => {
it('blocks memory_protection notification to be turned on for Gold and below licenses', () => {
const policy = policyFactoryWithoutPaidFeatures();
policy.windows.popup.memory_protection.enabled = true;
+ policy.mac.popup.memory_protection.enabled = true;
+ policy.linux.popup.memory_protection.enabled = true;
+
let valid = isEndpointPolicyValidForLicense(policy, Gold);
expect(valid).toBeFalsy();
@@ -177,6 +190,8 @@ describe('policy_config and licenses', () => {
it('allows memory_protection notification message changes with a Platinum license', () => {
const policy = policyFactory();
policy.windows.popup.memory_protection.message = 'BOOM';
+ policy.mac.popup.memory_protection.message = 'BOOM';
+ policy.linux.popup.memory_protection.message = 'BOOM';
const valid = isEndpointPolicyValidForLicense(policy, Platinum);
expect(valid).toBeTruthy();
});
@@ -184,6 +199,8 @@ describe('policy_config and licenses', () => {
it('blocks memory_protection notification message changes for Gold and below licenses', () => {
const policy = policyFactory();
policy.windows.popup.memory_protection.message = 'BOOM';
+ policy.mac.popup.memory_protection.message = 'BOOM';
+ policy.linux.popup.memory_protection.message = 'BOOM';
let valid = isEndpointPolicyValidForLicense(policy, Gold);
expect(valid).toBeFalsy();
@@ -280,10 +297,26 @@ describe('policy_config and licenses', () => {
policy.windows.popup.memory_protection.enabled = false;
policy.windows.popup.memory_protection.message = popupMessage;
+ policy.linux.memory_protection.mode = ProtectionModes.detect;
+ policy.linux.popup.memory_protection.enabled = false;
+ policy.linux.popup.memory_protection.message = popupMessage;
+
+ policy.mac.memory_protection.mode = ProtectionModes.detect;
+ policy.mac.popup.memory_protection.enabled = false;
+ policy.mac.popup.memory_protection.message = popupMessage;
+
const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Platinum);
expect(retPolicy.windows.memory_protection.mode).toEqual(ProtectionModes.detect);
expect(retPolicy.windows.popup.memory_protection.enabled).toBeFalsy();
expect(retPolicy.windows.popup.memory_protection.message).toEqual(popupMessage);
+
+ expect(retPolicy.linux.memory_protection.mode).toEqual(ProtectionModes.detect);
+ expect(retPolicy.linux.popup.memory_protection.enabled).toBeFalsy();
+ expect(retPolicy.linux.popup.memory_protection.message).toEqual(popupMessage);
+
+ expect(retPolicy.mac.memory_protection.mode).toEqual(ProtectionModes.detect);
+ expect(retPolicy.mac.popup.memory_protection.enabled).toBeFalsy();
+ expect(retPolicy.mac.popup.memory_protection.message).toEqual(popupMessage);
});
it('does not change any behavior fields with a Platinum license', () => {
@@ -356,6 +389,8 @@ describe('policy_config and licenses', () => {
const policy = policyFactory(); // what we will modify, and should be reset
const popupMessage = 'WOOP WOOP';
policy.windows.popup.memory_protection.message = popupMessage;
+ policy.mac.popup.memory_protection.message = popupMessage;
+ policy.linux.popup.memory_protection.message = popupMessage;
const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Gold);
@@ -367,10 +402,28 @@ describe('policy_config and licenses', () => {
);
expect(retPolicy.windows.popup.memory_protection.message).not.toEqual(popupMessage);
+ expect(retPolicy.mac.memory_protection.mode).toEqual(defaults.mac.memory_protection.mode);
+ expect(retPolicy.mac.popup.memory_protection.enabled).toEqual(
+ defaults.mac.popup.memory_protection.enabled
+ );
+ expect(retPolicy.mac.popup.memory_protection.message).not.toEqual(popupMessage);
+
+ expect(retPolicy.linux.memory_protection.mode).toEqual(defaults.linux.memory_protection.mode);
+ expect(retPolicy.linux.popup.memory_protection.enabled).toEqual(
+ defaults.linux.popup.memory_protection.enabled
+ );
+ expect(retPolicy.linux.popup.memory_protection.message).not.toEqual(popupMessage);
+
// need to invert the test, since it could be either value
expect(['', DefaultPolicyRuleNotificationMessage]).toContain(
retPolicy.windows.popup.memory_protection.message
);
+ expect(['', DefaultPolicyRuleNotificationMessage]).toContain(
+ retPolicy.mac.popup.memory_protection.message
+ );
+ expect(['', DefaultPolicyRuleNotificationMessage]).toContain(
+ retPolicy.linux.popup.memory_protection.message
+ );
});
it('resets Platinum-paid behavior_protection fields for lower license tiers', () => {
@@ -445,24 +498,40 @@ describe('policy_config and licenses', () => {
const defaults = policyFactoryWithoutPaidFeatures(); // reference
const policy = policyFactory(); // what we will modify, and should be reset
policy.windows.memory_protection.supported = true;
+ policy.mac.memory_protection.supported = true;
+ policy.linux.memory_protection.supported = true;
const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Gold);
expect(retPolicy.windows.memory_protection.supported).toEqual(
defaults.windows.memory_protection.supported
);
+ expect(retPolicy.mac.memory_protection.supported).toEqual(
+ defaults.mac.memory_protection.supported
+ );
+ expect(retPolicy.linux.memory_protection.supported).toEqual(
+ defaults.linux.memory_protection.supported
+ );
});
it('sets memory_protection supported field to true when license is at Platinum', () => {
const defaults = policyFactoryWithSupportedFeatures(); // reference
const policy = policyFactory(); // what we will modify, and should be reset
policy.windows.memory_protection.supported = false;
+ policy.mac.memory_protection.supported = false;
+ policy.linux.memory_protection.supported = false;
const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Platinum);
expect(retPolicy.windows.memory_protection.supported).toEqual(
defaults.windows.memory_protection.supported
);
+ expect(retPolicy.mac.memory_protection.supported).toEqual(
+ defaults.mac.memory_protection.supported
+ );
+ expect(retPolicy.linux.memory_protection.supported).toEqual(
+ defaults.linux.memory_protection.supported
+ );
});
it('sets behavior_protection supported field to false when license is below Platinum', () => {
const defaults = policyFactoryWithoutPaidFeatures(); // reference
diff --git a/x-pack/plugins/security_solution/common/license/policy_config.ts b/x-pack/plugins/security_solution/common/license/policy_config.ts
index a05478eef8eba..7342968a380b1 100644
--- a/x-pack/plugins/security_solution/common/license/policy_config.ts
+++ b/x-pack/plugins/security_solution/common/license/policy_config.ts
@@ -87,7 +87,9 @@ function isEndpointMemoryPolicyValidForLicense(policy: PolicyConfig, license: IL
const defaults = policyFactoryWithSupportedFeatures();
// only platinum or higher may enable memory protection
if (
- policy.windows.memory_protection.supported !== defaults.windows.memory_protection.supported
+ policy.windows.memory_protection.supported !== defaults.windows.memory_protection.supported ||
+ policy.mac.memory_protection.supported !== defaults.mac.memory_protection.supported ||
+ policy.linux.memory_protection.supported !== defaults.linux.memory_protection.supported
) {
return false;
}
@@ -101,25 +103,39 @@ function isEndpointMemoryPolicyValidForLicense(policy: PolicyConfig, license: IL
// only platinum or higher may enable memory_protection
const defaults = policyFactoryWithoutPaidFeatures();
- if (policy.windows.memory_protection.mode !== defaults.windows.memory_protection.mode) {
+ if (
+ policy.windows.memory_protection.mode !== defaults.windows.memory_protection.mode ||
+ policy.mac.memory_protection.mode !== defaults.mac.memory_protection.mode ||
+ policy.linux.memory_protection.mode !== defaults.linux.memory_protection.mode
+ ) {
return false;
}
if (
policy.windows.popup.memory_protection.enabled !==
- defaults.windows.popup.memory_protection.enabled
+ defaults.windows.popup.memory_protection.enabled ||
+ policy.mac.popup.memory_protection.enabled !== defaults.mac.popup.memory_protection.enabled ||
+ policy.linux.popup.memory_protection.enabled !== defaults.linux.popup.memory_protection.enabled
) {
return false;
}
if (
- policy.windows.popup.memory_protection.message !== '' &&
- policy.windows.popup.memory_protection.message !== DefaultPolicyRuleNotificationMessage
+ (policy.windows.popup.memory_protection.message !== '' &&
+ policy.windows.popup.memory_protection.message !== DefaultPolicyRuleNotificationMessage) ||
+ (policy.mac.popup.memory_protection.message !== '' &&
+ policy.mac.popup.memory_protection.message !== DefaultPolicyRuleNotificationMessage) ||
+ (policy.linux.popup.memory_protection.message !== '' &&
+ policy.linux.popup.memory_protection.message !== DefaultPolicyRuleNotificationMessage)
) {
return false;
}
- if (policy.windows.memory_protection.supported !== defaults.windows.memory_protection.supported) {
+ if (
+ policy.windows.memory_protection.supported !== defaults.windows.memory_protection.supported ||
+ policy.mac.memory_protection.supported !== defaults.mac.memory_protection.supported ||
+ policy.linux.memory_protection.supported !== defaults.linux.memory_protection.supported
+ ) {
return false;
}
return true;
diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts
index 0a815705f5b21..c9660668f488b 100644
--- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts
@@ -58,7 +58,7 @@ export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="selectedShowBulkActions
export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-button"]';
-export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="attach-alert-to-case-button"]';
+export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="add-existing-case-menu-item"]';
export const ALERT_COUNT_TABLE_FIRST_ROW_COUNT =
'[data-test-subj="alertsCountTable"] tr:nth-child(1) td:nth-child(2) .euiTableCellContent__text';
diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json
index a76b942e555bc..80613ae12f51b 100644
--- a/x-pack/plugins/security_solution/kibana.json
+++ b/x-pack/plugins/security_solution/kibana.json
@@ -38,8 +38,7 @@
"lens",
"lists",
"home",
- "telemetry",
- "telemetryManagementSection"
+ "telemetry"
],
"server": true,
"ui": true,
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.test.ts
index 8a678be0616b9..ba806da195461 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.test.ts
@@ -6,7 +6,8 @@
*/
import { navTabs } from '../../../app/home/home_navigations';
-import { getTitle } from './helpers';
+import { getTitle, isQueryStateEmpty } from './helpers';
+import { CONSTANTS } from './constants';
describe('Helpers Url_State', () => {
describe('getTitle', () => {
@@ -31,4 +32,58 @@ describe('Helpers Url_State', () => {
expect(result).toEqual('');
});
});
+
+ describe('isQueryStateEmpty', () => {
+ test('returns true if queryState is undefined', () => {
+ const result = isQueryStateEmpty(undefined, CONSTANTS.savedQuery);
+ expect(result).toBeTruthy();
+ });
+
+ test('returns true if queryState is null', () => {
+ const result = isQueryStateEmpty(null, CONSTANTS.savedQuery);
+ expect(result).toBeTruthy();
+ });
+
+ test('returns true if url key is "query" and queryState is empty string', () => {
+ const result = isQueryStateEmpty({}, CONSTANTS.appQuery);
+ expect(result).toBeTruthy();
+ });
+
+ test('returns false if url key is "query" and queryState is not empty', () => {
+ const result = isQueryStateEmpty(
+ { query: { query: '*:*' }, language: 'kuery' },
+ CONSTANTS.appQuery
+ );
+ expect(result).toBeFalsy();
+ });
+
+ test('returns true if url key is "filters" and queryState is empty', () => {
+ const result = isQueryStateEmpty([], CONSTANTS.filters);
+ expect(result).toBeTruthy();
+ });
+
+ test('returns false if url key is "filters" and queryState is not empty', () => {
+ const result = isQueryStateEmpty(
+ [{ query: { query: '*:*' }, meta: { key: '123' } }],
+ CONSTANTS.filters
+ );
+ expect(result).toBeFalsy();
+ });
+
+ // TODO: Is this a bug, or intended?
+ test('returns false if url key is "timeline" and queryState is empty', () => {
+ const result = isQueryStateEmpty({}, CONSTANTS.timeline);
+ expect(result).toBeFalsy();
+ });
+
+ test('returns true if url key is "timeline" and queryState id is empty string', () => {
+ const result = isQueryStateEmpty({ id: '', isOpen: true }, CONSTANTS.timeline);
+ expect(result).toBeTruthy();
+ });
+
+ test('returns false if url key is "timeline" and queryState is not empty', () => {
+ const result = isQueryStateEmpty({ id: '123', isOpen: true }, CONSTANTS.timeline);
+ expect(result).toBeFalsy();
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
index ba09ed914dc68..5b6bb0400ccdf 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
@@ -199,8 +199,11 @@ export const updateTimerangeUrl = (
return timeRange;
};
-export const isQueryStateEmpty = (queryState: ValueUrlState | null, urlKey: KeyUrlState) =>
- queryState === null ||
+export const isQueryStateEmpty = (
+ queryState: ValueUrlState | undefined | null,
+ urlKey: KeyUrlState
+): boolean =>
+ queryState == null ||
(urlKey === CONSTANTS.appQuery && isEmpty((queryState as Query).query)) ||
(urlKey === CONSTANTS.filters && isEmpty(queryState)) ||
(urlKey === CONSTANTS.timeline && (queryState as TimelineUrl).id === '');
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts
index 4dbcd515db4c5..5d6744de9dbe3 100644
--- a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts
@@ -27,14 +27,9 @@ export const track: TrackFn = (type, event, count) => {
};
export const initTelemetry = (
- {
- usageCollection,
- telemetryManagementSection,
- }: Pick,
+ { usageCollection }: Pick,
appId: string
) => {
- telemetryManagementSection?.toggleSecuritySolutionExample(true);
-
_track = usageCollection?.reportUiCounter?.bind(null, appId) ?? noop;
};
diff --git a/x-pack/plugins/security_solution/public/common/mock/utils.ts b/x-pack/plugins/security_solution/public/common/mock/utils.ts
index b1851fd055b33..0bafdc4fad1e8 100644
--- a/x-pack/plugins/security_solution/public/common/mock/utils.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/utils.ts
@@ -21,11 +21,12 @@ import { mockGlobalState } from './global_state';
import { TimelineState } from '../../timelines/store/timeline/types';
import { defaultHeaders } from '../../timelines/components/timeline/body/column_headers/default_headers';
-interface Global extends NodeJS.Global {
+type GlobalThis = typeof globalThis;
+interface Global extends GlobalThis {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- window?: any;
+ window: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- document?: any;
+ document: any;
}
export const globalNode: Global = global;
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts
index cb5a23e711974..a835628fae6cf 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts
@@ -19,6 +19,9 @@ export const alertsStackByOptions: AlertsStackByOption[] = [
{ text: 'signal.rule.name', value: 'signal.rule.name' },
{ text: 'source.ip', value: 'source.ip' },
{ text: 'user.name', value: 'user.name' },
+ { text: 'process.name', value: 'process.name' },
+ { text: 'file.name', value: 'file.name' },
+ { text: 'hash.sha256', value: 'hash.sha256' },
];
export const DEFAULT_STACK_BY_FIELD = 'signal.rule.name';
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts
index 833c05bfc7a79..f561c3f6faa21 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts
@@ -21,4 +21,7 @@ export type AlertsStackByField =
| 'signal.rule.type'
| 'signal.rule.name'
| 'source.ip'
- | 'user.name';
+ | 'user.name'
+ | 'process.name'
+ | 'file.name'
+ | 'hash.sha256';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
index 5c2d5f5d62b5c..8528d64b7261d 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
@@ -85,6 +85,7 @@ export const ExceptionListsTable = React.memo(() => {
notifications,
showTrustedApps: false,
showEventFilters: false,
+ showHostIsolationExceptions: false,
});
const [loadingTableInfo, exceptionListsWithRuleRefs, exceptionsListsRef] = useAllExceptionLists({
exceptionLists: exceptions ?? [],
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
index 7167b07c7da5d..774b9463bed69 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
@@ -300,10 +300,8 @@ const RuleDetailsPageComponent: React.FC = ({
}, [rule, spacesApi]);
const getLegacyUrlConflictCallout = useMemo(() => {
- const outcome = rule?.outcome;
- if (rule != null && spacesApi && outcome === 'conflict') {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const aliasTargetId = rule?.alias_target_id!; // This is always defined if outcome === 'conflict'
+ if (rule?.alias_target_id != null && spacesApi && rule.outcome === 'conflict') {
+ const aliasTargetId = rule.alias_target_id;
// We have resolved to one rule, but there is another one with a legacy URL associated with this page. Display a
// callout with a warning for the user, and provide a way for them to navigate to the other rule.
const otherRulePath = `rules/id/${aliasTargetId}${window.location.search}${window.location.hash}`;
diff --git a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx
index c96deabfa245a..37b1646319f3f 100644
--- a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx
@@ -67,7 +67,7 @@ export const AdministrationListPage: FC
diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx
index bde1961dd782d..50500a789fd4e 100644
--- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx
@@ -63,7 +63,8 @@ describe.each([
);
});
- it('should display dates in expected format', () => {
+ // FLAKY https://github.com/elastic/kibana/issues/113892
+ it.skip('should display dates in expected format', () => {
render();
expect(renderResult.getByTestId('testCard-header-updated').textContent).toEqual(
diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx
index ddee8e13f069d..d7db249475df7 100644
--- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx
@@ -5,50 +5,78 @@
* 2.0.
*/
-import { mount } from 'enzyme';
import React from 'react';
+import { act, fireEvent } from '@testing-library/react';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint';
+import {
+ EndpointPrivileges,
+ useEndpointPrivileges,
+} from '../../../common/components/user_privileges/use_endpoint_privileges';
+import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
-import { SearchExceptions } from '.';
+import { SearchExceptions, SearchExceptionsProps } from '.';
+jest.mock('../../../common/components/user_privileges/use_endpoint_privileges');
let onSearchMock: jest.Mock;
+const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock;
-interface EuiFieldSearchPropsFake {
- onSearch(value: string): void;
-}
-
+// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699
describe('Search exceptions', () => {
+ let appTestContext: AppContextTestRender;
+ let renderResult: ReturnType;
+ let render: (
+ props?: Partial
+ ) => ReturnType;
+
+ const loadedUserEndpointPrivilegesState = (
+ endpointOverrides: Partial = {}
+ ): EndpointPrivileges => ({
+ loading: false,
+ canAccessFleet: true,
+ canAccessEndpointManagement: true,
+ isPlatinumPlus: false,
+ ...endpointOverrides,
+ });
+
beforeEach(() => {
onSearchMock = jest.fn();
+ appTestContext = createAppRootMockRenderer();
+
+ render = (overrideProps = {}) => {
+ const props: SearchExceptionsProps = {
+ placeholder: 'search test',
+ onSearch: onSearchMock,
+ ...overrideProps,
+ };
+
+ renderResult = appTestContext.render();
+ return renderResult;
+ };
+
+ mockUseEndpointPrivileges.mockReturnValue(loadedUserEndpointPrivilegesState());
});
- const getElement = (defaultValue: string = '') => (
-
- );
+ afterAll(() => {
+ mockUseEndpointPrivileges.mockReset();
+ });
it('should have a default value', () => {
const expectedDefaultValue = 'this is a default value';
- const element = mount(getElement(expectedDefaultValue));
- const defaultValue = element
- .find('[data-test-subj="searchField"]')
- .first()
- .props().defaultValue;
- expect(defaultValue).toBe(expectedDefaultValue);
+ const element = render({ defaultValue: expectedDefaultValue });
+
+ expect(element.getByDisplayValue(expectedDefaultValue)).not.toBeNull();
});
it('should dispatch search action when submit search field', () => {
const expectedDefaultValue = 'this is a default value';
- const element = mount(getElement());
+ const element = render();
expect(onSearchMock).toHaveBeenCalledTimes(0);
- const searchFieldProps = element
- .find('[data-test-subj="searchField"]')
- .first()
- .props() as EuiFieldSearchPropsFake;
- searchFieldProps.onSearch(expectedDefaultValue);
+ act(() => {
+ fireEvent.change(element.getByTestId('searchField'), {
+ target: { value: expectedDefaultValue },
+ });
+ });
expect(onSearchMock).toHaveBeenCalledTimes(1);
expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue, '', '');
@@ -56,11 +84,42 @@ describe('Search exceptions', () => {
it('should dispatch search action when click on button', () => {
const expectedDefaultValue = 'this is a default value';
- const element = mount(getElement(expectedDefaultValue));
+ const element = render({ defaultValue: expectedDefaultValue });
expect(onSearchMock).toHaveBeenCalledTimes(0);
- element.find('[data-test-subj="searchButton"]').first().simulate('click');
+ act(() => {
+ fireEvent.click(element.getByTestId('searchButton'));
+ });
+
expect(onSearchMock).toHaveBeenCalledTimes(1);
expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue, '', '');
});
+
+ it('should hide refresh button', () => {
+ const element = render({ hideRefreshButton: true });
+
+ expect(element.queryByTestId('searchButton')).toBeNull();
+ });
+
+ it('should hide policies selector when no license', () => {
+ const generator = new EndpointDocGenerator('policy-list');
+ const policy = generator.generatePolicyPackagePolicy();
+ mockUseEndpointPrivileges.mockReturnValue(
+ loadedUserEndpointPrivilegesState({ isPlatinumPlus: false })
+ );
+ const element = render({ policyList: [policy], hasPolicyFilter: true });
+
+ expect(element.queryByTestId('policiesSelectorButton')).toBeNull();
+ });
+
+ it('should display policies selector when right license', () => {
+ const generator = new EndpointDocGenerator('policy-list');
+ const policy = generator.generatePolicyPackagePolicy();
+ mockUseEndpointPrivileges.mockReturnValue(
+ loadedUserEndpointPrivilegesState({ isPlatinumPlus: true })
+ );
+ const element = render({ policyList: [policy], hasPolicyFilter: true });
+
+ expect(element.queryByTestId('policiesSelectorButton')).not.toBeNull();
+ });
});
diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx
index 2b7b2e6b66884..1f3eab5db2947 100644
--- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx
@@ -19,6 +19,7 @@ export interface SearchExceptionsProps {
policyList?: ImmutableArray;
defaultExcludedPolicies?: string;
defaultIncludedPolicies?: string;
+ hideRefreshButton?: boolean;
onSearch(query: string, includedPolicies?: string, excludedPolicies?: string): void;
}
@@ -31,6 +32,7 @@ export const SearchExceptions = memo(
policyList,
defaultIncludedPolicies,
defaultExcludedPolicies,
+ hideRefreshButton = false,
}) => {
const { isPlatinumPlus } = useEndpointPrivileges();
const [query, setQuery] = useState(defaultValue);
@@ -101,13 +103,15 @@ export const SearchExceptions = memo(
) : null}
-
-
- {i18n.translate('xpack.securitySolution.management.search.button', {
- defaultMessage: 'Refresh',
- })}
-
-
+ {!hideRefreshButton ? (
+
+
+ {i18n.translate('xpack.securitySolution.management.search.button', {
+ defaultMessage: 'Refresh',
+ })}
+
+
+ ) : null}
);
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
index 15d0684a2864b..43fa4e104067f 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
@@ -61,7 +61,8 @@ jest.mock('../../../../common/lib/kibana');
type EndpointListStore = Store, Immutable>;
-describe('endpoint list middleware', () => {
+// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699
+describe.skip('endpoint list middleware', () => {
const getKibanaServicesMock = KibanaServices.get as jest.Mock;
let fakeCoreStart: jest.Mocked;
let depsStart: DepsStartMock;
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts
index 4d7ca74ca19f8..eb134c4413ae8 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts
@@ -746,4 +746,48 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [
}
),
},
+ {
+ key: 'mac.advanced.memory_protection.memory_scan_collect_sample',
+ first_supported_version: '7.16',
+ documentation: i18n.translate(
+ 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.memory_protection.memory_scan_collect_sample',
+ {
+ defaultMessage:
+ 'Collect 4MB of memory surrounding detected malicious memory regions. Default: false. Enabling this value may significantly increase the amount of data stored in Elasticsearch.',
+ }
+ ),
+ },
+ {
+ key: 'mac.advanced.memory_protection.memory_scan',
+ first_supported_version: '7.16',
+ documentation: i18n.translate(
+ 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.memory_protection.memory_scan',
+ {
+ defaultMessage:
+ 'Enable scanning for malicious memory regions as a part of memory protection. Default: true.',
+ }
+ ),
+ },
+ {
+ key: 'linux.advanced.memory_protection.memory_scan_collect_sample',
+ first_supported_version: '7.16',
+ documentation: i18n.translate(
+ 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.memory_protection.memory_scan_collect_sample',
+ {
+ defaultMessage:
+ 'Collect 4MB of memory surrounding detected malicious memory regions. Default: false. Enabling this value may significantly increase the amount of data stored in Elasticsearch.',
+ }
+ ),
+ },
+ {
+ key: 'linux.advanced.memory_protection.memory_scan',
+ first_supported_version: '7.16',
+ documentation: i18n.translate(
+ 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.memory_protection.memory_scan',
+ {
+ defaultMessage:
+ 'Enable scanning for malicious memory regions as a part of memory protection. Default: true.',
+ }
+ ),
+ },
];
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
index e0c4ee2600588..da390fc1187b0 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
@@ -314,6 +314,7 @@ describe('policy details: ', () => {
events: { process: true, file: true, network: true },
malware: { mode: 'prevent' },
behavior_protection: { mode: 'off', supported: false },
+ memory_protection: { mode: 'off', supported: false },
popup: {
malware: {
enabled: true,
@@ -323,6 +324,10 @@ describe('policy details: ', () => {
enabled: false,
message: '',
},
+ memory_protection: {
+ enabled: false,
+ message: '',
+ },
},
logging: { file: 'info' },
},
@@ -331,6 +336,7 @@ describe('policy details: ', () => {
logging: { file: 'info' },
malware: { mode: 'prevent' },
behavior_protection: { mode: 'off', supported: false },
+ memory_protection: { mode: 'off', supported: false },
popup: {
malware: {
enabled: true,
@@ -340,6 +346,10 @@ describe('policy details: ', () => {
enabled: false,
message: '',
},
+ memory_protection: {
+ enabled: false,
+ message: '',
+ },
},
},
},
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts
index 5f612f4f4e6f6..784565b5d8e1d 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts
@@ -57,6 +57,14 @@ export const policySettingsMiddlewareRunner: MiddlewareRunner = async (
policyItem.inputs[0].config.policy.value.windows.popup.memory_protection.message =
DefaultPolicyRuleNotificationMessage;
}
+ if (policyItem.inputs[0].config.policy.value.mac.popup.memory_protection.message === '') {
+ policyItem.inputs[0].config.policy.value.mac.popup.memory_protection.message =
+ DefaultPolicyRuleNotificationMessage;
+ }
+ if (policyItem.inputs[0].config.policy.value.linux.popup.memory_protection.message === '') {
+ policyItem.inputs[0].config.policy.value.linux.popup.memory_protection.message =
+ DefaultPolicyRuleNotificationMessage;
+ }
if (
policyItem.inputs[0].config.policy.value.windows.popup.behavior_protection.message === ''
) {
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts
index 40d77f5869b67..6729f8094b840 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts
@@ -154,6 +154,7 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel
events: mac.events,
malware: mac.malware,
behavior_protection: mac.behavior_protection,
+ memory_protection: mac.memory_protection,
popup: mac.popup,
},
linux: {
@@ -161,6 +162,7 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel
events: linux.events,
malware: linux.malware,
behavior_protection: linux.behavior_protection,
+ memory_protection: linux.memory_protection,
popup: linux.popup,
},
};
@@ -220,7 +222,7 @@ export const totalLinuxEvents = (state: PolicyDetailsState): number => {
return 0;
};
-/** Returns the number of selected liinux eventing configurations */
+/** Returns the number of selected linux eventing configurations */
export const selectedLinuxEvents = (state: PolicyDetailsState): number => {
const config = policyConfig(state);
if (config) {
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts
index 283c3afb573b6..ad06f027542df 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts
@@ -175,8 +175,8 @@ export type PolicyProtection =
UIPolicyConfig['windows'],
'malware' | 'ransomware' | 'memory_protection' | 'behavior_protection'
>
- | keyof Pick
- | keyof Pick;
+ | keyof Pick
+ | keyof Pick;
export type MacPolicyProtection = keyof Pick;
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx
index 3292bc0c44cb9..c176ce9cacd43 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx
@@ -23,6 +23,7 @@ describe('Policy Details', () => {
const generator = new EndpointDocGenerator();
let history: AppContextTestRender['history'];
let coreStart: AppContextTestRender['coreStart'];
+ let middlewareSpy: AppContextTestRender['middlewareSpy'];
let http: typeof coreStart.http;
let render: (ui: Parameters[0]) => ReturnType;
let policyPackagePolicy: ReturnType;
@@ -32,13 +33,20 @@ describe('Policy Details', () => {
const appContextMockRenderer = createAppRootMockRenderer();
const AppWrapper = appContextMockRenderer.AppWrapper;
- ({ history, coreStart } = appContextMockRenderer);
+ ({ history, coreStart, middlewareSpy } = appContextMockRenderer);
render = (ui) => mount(ui, { wrappingComponent: AppWrapper });
http = coreStart.http;
});
describe('when displayed with invalid id', () => {
+ let releaseApiFailure: () => void;
+
beforeEach(() => {
+ http.get.mockImplementation(async () => {
+ await new Promise((_, reject) => {
+ releaseApiFailure = reject.bind(null, new Error('policy not found'));
+ });
+ });
history.push(policyDetailsPathUrl);
policyView = render();
});
@@ -46,7 +54,19 @@ describe('Policy Details', () => {
it('should NOT display timeline', async () => {
expect(policyView.find('flyoutOverlay')).toHaveLength(0);
});
+
+ it('should show loader followed by error message', async () => {
+ expect(policyView.find('EuiLoadingSpinner').length).toBe(1);
+ releaseApiFailure();
+ await middlewareSpy.waitForAction('serverFailedToReturnPolicyDetailsData');
+ policyView.update();
+ const callout = policyView.find('EuiCallOut');
+ expect(callout).toHaveLength(1);
+ expect(callout.prop('color')).toEqual('danger');
+ expect(callout.text()).toEqual('policy not found');
+ });
});
+
describe('when displayed with valid id', () => {
let asyncActions: Promise = Promise.resolve();
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx
index 660dda6493c39..65308012df080 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx
@@ -8,8 +8,14 @@
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { useLocation } from 'react-router-dom';
+import { EuiCallOut, EuiLoadingSpinner, EuiPageTemplate } from '@elastic/eui';
import { usePolicyDetailsSelector } from './policy_hooks';
-import { policyDetails, agentStatusSummary } from '../store/policy_details/selectors';
+import {
+ policyDetails,
+ agentStatusSummary,
+ isLoading,
+ apiError,
+} from '../store/policy_details/selectors';
import { AgentsSummary } from './agents_summary';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { PolicyTabs } from './tabs';
@@ -33,6 +39,8 @@ export const PolicyDetails = React.memo(() => {
const { getAppUrl } = useAppUrl();
// Store values
+ const loading = usePolicyDetailsSelector(isLoading);
+ const policyApiError = usePolicyDetailsSelector(apiError);
const policyItem = usePolicyDetailsSelector(policyDetails);
const policyAgentStatusSummary = usePolicyDetailsSelector(agentStatusSummary);
@@ -81,22 +89,48 @@ export const PolicyDetails = React.memo(() => {
);
+ const pageBody: React.ReactNode = useMemo(() => {
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (policyApiError) {
+ return (
+
+
+ {policyApiError?.message}
+
+
+ );
+ }
+
+ // TODO: Remove this and related code when removing FF
+ if (isTrustedAppsByPolicyEnabled) {
+ return ;
+ }
+
+ return ;
+ }, [isTrustedAppsByPolicyEnabled, loading, policyApiError]);
+
return (
- {isTrustedAppsByPolicyEnabled ? (
-
- ) : (
- // TODO: Remove this and related code when removing FF
-
- )}
+ {pageBody}
);
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx
index 87c16e411c702..650bf6115c9d9 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx
@@ -30,7 +30,6 @@ describe('Policy Form Layout', () => {
const generator = new EndpointDocGenerator();
let history: AppContextTestRender['history'];
let coreStart: AppContextTestRender['coreStart'];
- let middlewareSpy: AppContextTestRender['middlewareSpy'];
let http: typeof coreStart.http;
let render: (ui: Parameters[0]) => ReturnType;
let policyPackagePolicy: ReturnType;
@@ -40,7 +39,7 @@ describe('Policy Form Layout', () => {
const appContextMockRenderer = createAppRootMockRenderer();
const AppWrapper = appContextMockRenderer.AppWrapper;
- ({ history, coreStart, middlewareSpy } = appContextMockRenderer);
+ ({ history, coreStart } = appContextMockRenderer);
render = (ui) => mount(ui, { wrappingComponent: AppWrapper });
http = coreStart.http;
});
@@ -52,33 +51,6 @@ describe('Policy Form Layout', () => {
jest.clearAllMocks();
});
- describe('when displayed with invalid id', () => {
- let releaseApiFailure: () => void;
- beforeEach(() => {
- http.get.mockImplementation(async () => {
- await new Promise((_, reject) => {
- releaseApiFailure = reject.bind(null, new Error('policy not found'));
- });
- });
- history.push(policyDetailsPathUrl);
- policyFormLayoutView = render();
- });
-
- it('should NOT display timeline', async () => {
- expect(policyFormLayoutView.find('flyoutOverlay')).toHaveLength(0);
- });
-
- it('should show loader followed by error message', async () => {
- expect(policyFormLayoutView.find('EuiLoadingSpinner').length).toBe(1);
- releaseApiFailure();
- await middlewareSpy.waitForAction('serverFailedToReturnPolicyDetailsData');
- policyFormLayoutView.update();
- const callout = policyFormLayoutView.find('EuiCallOut');
- expect(callout).toHaveLength(1);
- expect(callout.prop('color')).toEqual('danger');
- expect(callout.text()).toEqual('policy not found');
- });
- });
describe('when displayed with valid id', () => {
let asyncActions: Promise = Promise.resolve();
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx
index 4573b15b8fabc..bae2c21242d97 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx
@@ -11,7 +11,6 @@ import {
EuiFlexItem,
EuiButton,
EuiButtonEmpty,
- EuiCallOut,
EuiLoadingSpinner,
EuiBottomBar,
EuiSpacer,
@@ -27,7 +26,6 @@ import {
agentStatusSummary,
updateStatus,
isLoading,
- apiError,
} from '../../../store/policy_details/selectors';
import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public';
@@ -58,7 +56,6 @@ export const PolicyFormLayout = React.memo(() => {
const policyAgentStatusSummary = usePolicyDetailsSelector(agentStatusSummary);
const policyUpdateStatus = usePolicyDetailsSelector(updateStatus);
const isPolicyLoading = usePolicyDetailsSelector(isLoading);
- const policyApiError = usePolicyDetailsSelector(apiError);
// Local state
const [showConfirm, setShowConfirm] = useState(false);
@@ -137,13 +134,7 @@ export const PolicyFormLayout = React.memo(() => {
if (!policyItem) {
return (
- {isPolicyLoading ? (
-
- ) : policyApiError ? (
-
- {policyApiError?.message}
-
- ) : null}
+ {isPolicyLoading ? : null}
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx
index 792664f3e6f25..2f47d52e37bf6 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx
@@ -23,7 +23,7 @@ import { ProtectionSwitch } from '../components/protection_switch';
* which will configure for all relevant OSes.
*/
export const MemoryProtection = React.memo(() => {
- const OSes: Immutable = [OS.windows];
+ const OSes: Immutable = [OS.windows, OS.mac, OS.linux];
const protection = 'memory_protection';
const protectionLabel = i18n.translate(
'xpack.securitySolution.endpoint.policy.protections.memory',
@@ -36,7 +36,7 @@ export const MemoryProtection = React.memo(() => {
type={i18n.translate('xpack.securitySolution.endpoint.policy.details.memory_protection', {
defaultMessage: 'Memory threat',
})}
- supportedOss={[OperatingSystem.WINDOWS]}
+ supportedOss={[OperatingSystem.WINDOWS, OperatingSystem.MAC, OperatingSystem.LINUX]}
dataTestSubj="memoryProtectionsForm"
rightCorner={
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx
index 0ccdf9bcb388d..ee52e1210a481 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx
@@ -10,6 +10,7 @@ import { EuiEmptyPrompt, EuiButton, EuiPageTemplate, EuiLink } from '@elastic/eu
import { FormattedMessage } from '@kbn/i18n/react';
import { usePolicyDetailsNavigateCallback } from '../../policy_hooks';
import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks';
+import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges';
interface CommonProps {
policyId: string;
@@ -17,6 +18,7 @@ interface CommonProps {
}
export const PolicyTrustedAppsEmptyUnassigned = memo(({ policyId, policyName }) => {
+ const { isPlatinumPlus } = useEndpointPrivileges();
const navigateCallback = usePolicyDetailsNavigateCallback();
const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName);
const onClickPrimaryButtonHandler = useCallback(
@@ -47,12 +49,21 @@ export const PolicyTrustedAppsEmptyUnassigned = memo(({ policyId, p
/>
}
actions={[
-
-
- ,
+ ...(isPlatinumPlus
+ ? [
+
+
+ ,
+ ]
+ : []),
// eslint-disable-next-line @elastic/eui/href-or-on-click
{
defaultMessage: 'Search trusted applications',
}
)}
+ hideRefreshButton
/>
@@ -218,7 +219,7 @@ export const PolicyTrustedAppsFlyout = React.memo(() => {
title={
}
/>
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx
index 5d5d36d41aaf8..d46775d38834b 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx
@@ -19,8 +19,20 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../..
import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing';
import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data';
import { policyListApiPathHandlers } from '../../../store/test_mock_utils';
+import { licenseService } from '../../../../../../common/hooks/use_license';
jest.mock('../../../../trusted_apps/service');
+jest.mock('../../../../../../common/hooks/use_license', () => {
+ const licenseServiceInstance = {
+ isPlatinumPlus: jest.fn(),
+ };
+ return {
+ licenseService: licenseServiceInstance,
+ useLicense: () => {
+ return licenseServiceInstance;
+ },
+ };
+});
let mockedContext: AppContextTestRender;
let waitForAction: MiddlewareActionSpyHelper['waitForAction'];
@@ -30,7 +42,8 @@ let coreStart: AppContextTestRender['coreStart'];
let http: typeof coreStart.http;
const generator = new EndpointDocGenerator();
-describe('Policy trusted apps layout', () => {
+// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699
+describe.skip('Policy trusted apps layout', () => {
beforeEach(() => {
mockedContext = createAppRootMockRenderer();
http = mockedContext.coreStart.http;
@@ -106,4 +119,31 @@ describe('Policy trusted apps layout', () => {
expect(component.getByTestId('policyDetailsTrustedAppsCount')).not.toBeNull();
});
+
+ it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => {
+ (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
+ const component = render();
+ mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234'));
+
+ await waitForAction('assignedTrustedAppsListStateChanged');
+
+ mockedContext.store.dispatch({
+ type: 'policyArtifactsDeosAnyTrustedAppExists',
+ payload: createLoadedResourceState(true),
+ });
+ expect(component.queryByTestId('assign-ta-button')).toBeNull();
+ });
+ it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => {
+ (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
+ TrustedAppsHttpServiceMock.mockImplementation(() => {
+ return {
+ getTrustedAppsList: () => getMockListResponse(),
+ };
+ });
+ const component = render();
+ mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234'));
+
+ await waitForAction('assignedTrustedAppsListStateChanged');
+ expect(component.queryByTestId('assignTrustedAppButton')).toBeNull();
+ });
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx
index 64e40e330ad2b..2421602f4e5af 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx
@@ -25,6 +25,7 @@ import {
import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks';
import { PolicyTrustedAppsFlyout } from '../flyout';
import { PolicyTrustedAppsList } from '../list/policy_trusted_apps_list';
+import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges';
export const PolicyTrustedAppsLayout = React.memo(() => {
const location = usePolicyDetailsSelector(getCurrentArtifactsLocation);
@@ -33,6 +34,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => {
const policyItem = usePolicyDetailsSelector(policyDetails);
const navigateCallback = usePolicyDetailsNavigateCallback();
const hasAssignedTrustedApps = usePolicyDetailsSelector(doesPolicyHaveTrustedApps);
+ const { isPlatinumPlus } = useEndpointPrivileges();
const showListFlyout = location.show === 'list';
@@ -41,6 +43,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => {
navigateCallback({
show: 'list',
@@ -88,7 +91,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => {
- {assignTrustedAppButton}
+ {isPlatinumPlus && assignTrustedAppButton}
) : null}
{
)}
- {showListFlyout ? : null}
+ {isPlatinumPlus && showListFlyout ? : null}
) : null;
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx
index ff94e3befe8c8..316b70064d9db 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx
@@ -21,6 +21,13 @@ import {
} from '../../../../../state';
import { fireEvent, within, act, waitFor } from '@testing-library/react';
import { APP_ID } from '../../../../../../../common/constants';
+import {
+ EndpointPrivileges,
+ useEndpointPrivileges,
+} from '../../../../../../common/components/user_privileges/use_endpoint_privileges';
+
+jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges');
+const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock;
describe('when rendering the PolicyTrustedAppsList', () => {
// The index (zero based) of the card created by the generator that is policy specific
@@ -32,6 +39,16 @@ describe('when rendering the PolicyTrustedAppsList', () => {
let mockedApis: ReturnType
;
let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction'];
+ const loadedUserEndpointPrivilegesState = (
+ endpointOverrides: Partial = {}
+ ): EndpointPrivileges => ({
+ loading: false,
+ canAccessFleet: true,
+ canAccessEndpointManagement: true,
+ isPlatinumPlus: true,
+ ...endpointOverrides,
+ });
+
const getCardByIndexPosition = (cardIndex: number = 0) => {
const card = renderResult.getAllByTestId('policyTrustedAppsGrid-card')[cardIndex];
@@ -66,8 +83,12 @@ describe('when rendering the PolicyTrustedAppsList', () => {
);
};
+ afterAll(() => {
+ mockUseEndpointPrivileges.mockReset();
+ });
beforeEach(() => {
appTestContext = createAppRootMockRenderer();
+ mockUseEndpointPrivileges.mockReturnValue(loadedUserEndpointPrivilegesState());
mockedApis = policyDetailsPageAllApiHttpMocks(appTestContext.coreStart.http);
appTestContext.setExperimentalFlag({ trustedAppsByPolicyEnabled: true });
@@ -297,4 +318,16 @@ describe('when rendering the PolicyTrustedAppsList', () => {
})
);
});
+
+ it('does not show remove option in actions menu if license is downgraded to gold or below', async () => {
+ await render();
+ mockUseEndpointPrivileges.mockReturnValue(
+ loadedUserEndpointPrivilegesState({
+ isPlatinumPlus: false,
+ })
+ );
+ await toggleCardActionMenu(POLICY_SPECIFIC_CARD_INDEX);
+
+ expect(renderResult.queryByTestId('policyTrustedAppsGrid-removeAction')).toBeNull();
+ });
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx
index 5d6c9731c7070..8ab2f5fd465e0 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx
@@ -38,6 +38,7 @@ import { ContextMenuItemNavByRouterProps } from '../../../../../components/conte
import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card';
import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator';
import { RemoveTrustedAppFromPolicyModal } from './remove_trusted_app_from_policy_modal';
+import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges';
const DATA_TEST_SUBJ = 'policyTrustedAppsGrid';
@@ -46,6 +47,7 @@ export const PolicyTrustedAppsList = memo(() => {
const toasts = useToasts();
const history = useHistory();
const { getAppUrl } = useAppUrl();
+ const { isPlatinumPlus } = useEndpointPrivileges();
const policyId = usePolicyDetailsSelector(policyIdFromParams);
const hasTrustedApps = usePolicyDetailsSelector(doesPolicyHaveTrustedApps);
const isLoading = usePolicyDetailsSelector(isPolicyTrustedAppListLoading);
@@ -132,44 +134,50 @@ export const PolicyTrustedAppsList = memo(() => {
return byIdPolicies;
}, {});
+ const fullDetailsAction: ArtifactCardGridCardComponentProps['actions'] = [
+ {
+ icon: 'controlsHorizontal',
+ children: i18n.translate(
+ 'xpack.securitySolution.endpoint.policy.trustedApps.list.viewAction',
+ { defaultMessage: 'View full details' }
+ ),
+ href: getAppUrl({ appId: APP_ID, path: viewUrlPath }),
+ navigateAppId: APP_ID,
+ navigateOptions: { path: viewUrlPath },
+ 'data-test-subj': getTestId('viewFullDetailsAction'),
+ },
+ ];
const thisTrustedAppCardProps: ArtifactCardGridCardComponentProps = {
expanded: Boolean(isCardExpanded[trustedApp.id]),
- actions: [
- {
- icon: 'controlsHorizontal',
- children: i18n.translate(
- 'xpack.securitySolution.endpoint.policy.trustedApps.list.viewAction',
- { defaultMessage: 'View full details' }
- ),
- href: getAppUrl({ appId: APP_ID, path: viewUrlPath }),
- navigateAppId: APP_ID,
- navigateOptions: { path: viewUrlPath },
- 'data-test-subj': getTestId('viewFullDetailsAction'),
- },
- {
- icon: 'trash',
- children: i18n.translate(
- 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeAction',
- { defaultMessage: 'Remove from policy' }
- ),
- onClick: () => {
- setTrustedAppsForRemoval([trustedApp]);
- setShowRemovalModal(true);
- },
- disabled: isGlobal,
- toolTipContent: isGlobal
- ? i18n.translate(
- 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeActionNotAllowed',
- {
- defaultMessage:
- 'Globally applied trusted applications cannot be removed from policy.',
- }
- )
- : undefined,
- toolTipPosition: 'top',
- 'data-test-subj': getTestId('removeAction'),
- },
- ],
+ actions: isPlatinumPlus
+ ? [
+ ...fullDetailsAction,
+ {
+ icon: 'trash',
+ children: i18n.translate(
+ 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeAction',
+ { defaultMessage: 'Remove from policy' }
+ ),
+ onClick: () => {
+ setTrustedAppsForRemoval([trustedApp]);
+ setShowRemovalModal(true);
+ },
+ disabled: isGlobal,
+ toolTipContent: isGlobal
+ ? i18n.translate(
+ 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeActionNotAllowed',
+ {
+ defaultMessage:
+ 'Globally applied trusted applications cannot be removed from policy.',
+ }
+ )
+ : undefined,
+ toolTipPosition: 'top',
+ 'data-test-subj': getTestId('removeAction'),
+ },
+ ]
+ : fullDetailsAction,
+
policies: assignedPoliciesMenuItems,
};
@@ -177,7 +185,7 @@ export const PolicyTrustedAppsList = memo(() => {
}
return newCardProps;
- }, [allPoliciesById, getAppUrl, getTestId, isCardExpanded, trustedAppItems]);
+ }, [allPoliciesById, getAppUrl, getTestId, isCardExpanded, trustedAppItems, isPlatinumPlus]);
const provideCardProps = useCallback['cardComponentProps']>(
(item) => {
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index 371b68e9bec8e..4885538269264 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -97,7 +97,6 @@ export class Plugin implements IPlugin {
+ const defaultProps = {
+ eventType: 'all' as TimelineEventsType,
+ onChangeEventTypeAndIndexesName: jest.fn(),
+ };
+ const initialPatterns = [
+ ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline].selectedPatterns,
+ mockGlobalState.sourcerer.signalIndexName,
+ ];
+ const { storage } = createSecuritySolutionStorageMock();
+ const state = {
+ ...mockGlobalState,
+ sourcerer: {
+ ...mockGlobalState.sourcerer,
+ kibanaIndexPatterns: [
+ { id: '1234', title: 'auditbeat-*' },
+ { id: '9100', title: 'filebeat-*' },
+ { id: '9100', title: 'auditbeat-*,filebeat-*' },
+ { id: '5678', title: 'auditbeat-*,.siem-signals-default' },
+ ],
+ configIndexPatterns:
+ mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline].selectedPatterns,
+ signalIndexName: mockGlobalState.sourcerer.signalIndexName,
+ sourcererScopes: {
+ ...mockGlobalState.sourcerer.sourcererScopes,
+ [SourcererScopeName.timeline]: {
+ ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
+ loading: false,
+ selectedPatterns: ['filebeat-*'],
+ },
+ },
+ },
+ };
+ const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+ });
+ it('renders', () => {
+ const wrapper = render(
+
+
+
+ );
+ fireEvent.click(wrapper.getByTestId('sourcerer-timeline-trigger'));
+ expect(wrapper.getByTestId('timeline-sourcerer').textContent).toEqual(
+ initialPatterns.sort().join('')
+ );
+ });
+ it('correctly filters options', () => {
+ const wrapper = render(
+
+
+
+ );
+ fireEvent.click(wrapper.getByTestId('sourcerer-timeline-trigger'));
+ fireEvent.click(wrapper.getByTestId('comboBoxToggleListButton'));
+ const optionNodes = wrapper.getAllByTestId('sourcerer-option');
+ expect(optionNodes.length).toBe(9);
+ });
+ it('reset button works', () => {
+ const wrapper = render(
+
+
+
+ );
+ fireEvent.click(wrapper.getByTestId('sourcerer-timeline-trigger'));
+ expect(wrapper.getByTestId('timeline-sourcerer').textContent).toEqual('filebeat-*');
+
+ fireEvent.click(wrapper.getByTestId('sourcerer-reset'));
+ expect(wrapper.getByTestId('timeline-sourcerer').textContent).toEqual(
+ initialPatterns.sort().join('')
+ );
+ fireEvent.click(wrapper.getByTestId('comboBoxToggleListButton'));
+ const optionNodes = wrapper.getAllByTestId('sourcerer-option');
+ expect(optionNodes.length).toBe(2);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx
index 5682bdb91ff58..dbe04eccac521 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx
@@ -144,7 +144,7 @@ const PickEventTypeComponents: React.FC = ({
...kibanaIndexPatterns.map((kip) => kip.title),
signalIndexName,
].reduce>>((acc, index) => {
- if (index != null && !acc.some((o) => o.label.includes(index))) {
+ if (index != null && !acc.some((o) => o.label === index)) {
return [...acc, { label: index, value: index }];
}
return acc;
@@ -153,16 +153,15 @@ const PickEventTypeComponents: React.FC = ({
);
const renderOption = useCallback(
- (option) => {
- const { value } = option;
+ ({ value }) => {
if (kibanaIndexPatterns.some((kip) => kip.title === value)) {
return (
- <>
+
{value}
- >
+
);
}
- return <>{value}>;
+ return {value};
},
[kibanaIndexPatterns]
);
@@ -193,14 +192,14 @@ const PickEventTypeComponents: React.FC = ({
setFilterEventType(filter);
if (filter === 'all') {
setSelectedOptions(
- [...configIndexPatterns, signalIndexName ?? ''].map((indexSelected) => ({
+ [...configIndexPatterns.sort(), signalIndexName ?? ''].map((indexSelected) => ({
label: indexSelected,
value: indexSelected,
}))
);
} else if (filter === 'raw') {
setSelectedOptions(
- configIndexPatterns.map((indexSelected) => ({
+ configIndexPatterns.sort().map((indexSelected) => ({
label: indexSelected,
value: indexSelected,
}))
@@ -240,14 +239,8 @@ const PickEventTypeComponents: React.FC = ({
}, [filterEventType, onChangeEventTypeAndIndexesName, selectedOptions]);
const resetDataSources = useCallback(() => {
- setSelectedOptions(
- sourcererScope.selectedPatterns.map((indexSelected) => ({
- label: indexSelected,
- value: indexSelected,
- }))
- );
- setFilterEventType(eventType);
- }, [eventType, sourcererScope.selectedPatterns]);
+ onChangeFilter('all');
+ }, [onChangeFilter]);
const comboBox = useMemo(
() => (
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 3750bc22ddc69..95ad6c5d44ca3 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
@@ -37,7 +37,9 @@ export const {
setSelected,
setTGridSelectAll,
toggleDetailPanel,
+ updateColumnOrder,
updateColumns,
+ updateColumnWidth,
updateIsLoading,
updateItemsPerPage,
updateItemsPerPageOptions,
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx
index 01bc589393d2e..131f255b5a7a7 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx
@@ -25,7 +25,9 @@ import {
removeColumn,
upsertColumn,
applyDeltaToColumnWidth,
+ updateColumnOrder,
updateColumns,
+ updateColumnWidth,
updateItemsPerPage,
updateSort,
} from './actions';
@@ -168,4 +170,35 @@ describe('epicLocalStorage', () => {
);
await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled());
});
+
+ it('persists updates to the column order to local storage', async () => {
+ shallow(
+
+
+
+ );
+ store.dispatch(
+ updateColumnOrder({
+ columnIds: ['event.severity', '@timestamp', 'event.category'],
+ id: 'test',
+ })
+ );
+ await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled());
+ });
+
+ it('persists updates to the column width to local storage', async () => {
+ shallow(
+
+
+
+ );
+ store.dispatch(
+ updateColumnWidth({
+ columnId: 'event.severity',
+ id: 'test',
+ width: 123,
+ })
+ );
+ await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled());
+ });
});
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts
index 9a889e9ec1af8..6c4ebf91b7adf 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts
@@ -19,6 +19,8 @@ import {
applyDeltaToColumnWidth,
setExcludedRowRendererIds,
updateColumns,
+ updateColumnOrder,
+ updateColumnWidth,
updateItemsPerPage,
updateSort,
} from './actions';
@@ -30,6 +32,8 @@ const timelineActionTypes = [
upsertColumn.type,
applyDeltaToColumnWidth.type,
updateColumns.type,
+ updateColumnOrder.type,
+ updateColumnWidth.type,
updateItemsPerPage.type,
updateSort.type,
setExcludedRowRendererIds.type,
diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts
index e595b905b998e..bee0e9b3a3d1d 100644
--- a/x-pack/plugins/security_solution/public/types.ts
+++ b/x-pack/plugins/security_solution/public/types.ts
@@ -15,7 +15,6 @@ import { NewsfeedPublicPluginStart } from '../../../../src/plugins/newsfeed/publ
import { Start as InspectorStart } from '../../../../src/plugins/inspector/public';
import { UiActionsStart } from '../../../../src/plugins/ui_actions/public';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
-import { TelemetryManagementSectionPluginSetup } from '../../../../src/plugins/telemetry_management_section/public';
import { Storage } from '../../../../src/plugins/kibana_utils/public';
import { FleetStart } from '../../fleet/public';
import { PluginStart as ListsPluginStart } from '../../lists/public';
@@ -49,7 +48,6 @@ export interface SetupPlugins {
security: SecurityPluginSetup;
triggersActionsUi: TriggersActionsSetup;
usageCollection?: UsageCollectionSetup;
- telemetryManagementSection?: TelemetryManagementSectionPluginSetup;
ml?: MlPluginSetup;
}
diff --git a/x-pack/plugins/security_solution/server/deprecation_privileges/index.test.ts b/x-pack/plugins/security_solution/server/deprecation_privileges/index.test.ts
index f61940387c413..213beb6207184 100644
--- a/x-pack/plugins/security_solution/server/deprecation_privileges/index.test.ts
+++ b/x-pack/plugins/security_solution/server/deprecation_privileges/index.test.ts
@@ -473,8 +473,8 @@ describe('deprecations', () => {
},
"deprecationType": "feature",
"level": "warning",
- "message": "The \\"Security\\" feature will be split into two separate features in 8.0. The \\"first_role\\" role grants access to this feature, and it needs to be updated before you upgrade Kibana. This will ensure that users have access to the same features after the upgrade.",
- "title": "The \\"first_role\\" role needs to be updated",
+ "message": "The Security feature will be split into the Security and Cases features in 8.0. The \\"first_role\\" role grants access to the Security feature only. Update the role to also grant access to the Cases feature.",
+ "title": "The Security feature is changing, and the \\"first_role\\" role requires an update",
},
]
`);
@@ -608,8 +608,8 @@ describe('deprecations', () => {
},
"deprecationType": "feature",
"level": "warning",
- "message": "The \\"Security\\" feature will be split into two separate features in 8.0. The \\"second_role\\" role grants access to this feature, and it needs to be updated before you upgrade Kibana. This will ensure that users have access to the same features after the upgrade.",
- "title": "The \\"second_role\\" role needs to be updated",
+ "message": "The Security feature will be split into the Security and Cases features in 8.0. The \\"second_role\\" role grants access to the Security feature only. Update the role to also grant access to the Cases feature.",
+ "title": "The Security feature is changing, and the \\"second_role\\" role requires an update",
},
]
`);
@@ -772,13 +772,13 @@ describe('deprecations', () => {
});
const response = await getDeprecations(mockContext);
expect(response).toEqual([
- expect.objectContaining({ title: 'The "role_siem_all" role needs to be updated' }),
- expect.objectContaining({ title: 'The "role_siem_read" role needs to be updated' }),
+ expect.objectContaining({ message: expect.stringMatching(/"role_siem_all"/) }),
+ expect.objectContaining({ message: expect.stringMatching(/"role_siem_read"/) }),
expect.objectContaining({
- title: 'The "role_siem_minimal_all_cases_all_cases_read" role needs to be updated',
+ message: expect.stringMatching(/"role_siem_minimal_all_cases_all_cases_read"/),
}),
expect.objectContaining({
- title: 'The "role_siem_minimal_read_cases_read" role needs to be updated',
+ message: expect.stringMatching(/"role_siem_minimal_read_cases_read"/),
}),
// the fifth_role and sixth_role have been filtered out because they do not grant access to Cases
]);
diff --git a/x-pack/plugins/security_solution/server/deprecation_privileges/index.ts b/x-pack/plugins/security_solution/server/deprecation_privileges/index.ts
index 803231b236cbd..b56583d26261f 100644
--- a/x-pack/plugins/security_solution/server/deprecation_privileges/index.ts
+++ b/x-pack/plugins/security_solution/server/deprecation_privileges/index.ts
@@ -119,7 +119,8 @@ export const registerPrivilegeDeprecations = ({
title: i18n.translate(
'xpack.securitySolution.privilegeDeprecations.casesSubFeaturePrivileges.title',
{
- defaultMessage: 'The "{roleName}" role needs to be updated',
+ defaultMessage:
+ 'The Security feature is changing, and the "{roleName}" role requires an update',
values: { roleName },
}
),
@@ -127,7 +128,7 @@ export const registerPrivilegeDeprecations = ({
'xpack.securitySolution.privilegeDeprecations.casesSubFeaturePrivileges.message',
{
defaultMessage:
- 'The "Security" feature will be split into two separate features in 8.0. The "{roleName}" role grants access to this feature, and it needs to be updated before you upgrade Kibana. This will ensure that users have access to the same features after the upgrade.',
+ 'The Security feature will be split into the Security and Cases features in 8.0. The "{roleName}" role grants access to the Security feature only. Update the role to also grant access to the Cases feature.',
values: { roleName },
}
),
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts
index 2e5e331b71b00..81f229c636bd8 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { elasticsearchServiceMock } from 'src/core/server/mocks';
+import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks';
import { alertsMock } from '../../../../../alerting/server/mocks';
import { scheduleThrottledNotificationActions } from './schedule_throttle_notification_actions';
import {
@@ -19,8 +19,10 @@ jest.mock('./schedule_notification_actions', () => ({
describe('schedule_throttle_notification_actions', () => {
let notificationRuleParams: NotificationRuleTypeParams;
+ let logger: ReturnType;
beforeEach(() => {
+ logger = loggingSystemMock.createLogger();
(scheduleNotificationActions as jest.Mock).mockReset();
notificationRuleParams = {
author: ['123'],
@@ -82,6 +84,38 @@ describe('schedule_throttle_notification_actions', () => {
),
alertInstance: alertsMock.createAlertInstanceFactory(),
notificationRuleParams,
+ logger,
+ signals: [],
+ });
+
+ expect(scheduleNotificationActions as jest.Mock).toHaveBeenCalled();
+ });
+
+ it('should call "scheduleNotificationActions" if the signals length is 1 or greater', async () => {
+ await scheduleThrottledNotificationActions({
+ throttle: '1d',
+ startedAt: new Date('2021-08-24T19:19:22.094Z'),
+ id: '123',
+ kibanaSiemAppUrl: 'http://www.example.com',
+ outputIndex: 'output-123',
+ ruleId: 'rule-123',
+ esClient: elasticsearchServiceMock.createElasticsearchClient(
+ elasticsearchServiceMock.createSuccessTransportRequestPromise({
+ hits: {
+ hits: [],
+ total: 0,
+ },
+ })
+ ),
+ alertInstance: alertsMock.createAlertInstanceFactory(),
+ notificationRuleParams,
+ logger,
+ signals: [
+ {
+ _id: '123',
+ index: '123',
+ },
+ ],
});
expect(scheduleNotificationActions as jest.Mock).toHaveBeenCalled();
@@ -105,6 +139,8 @@ describe('schedule_throttle_notification_actions', () => {
),
alertInstance: alertsMock.createAlertInstanceFactory(),
notificationRuleParams,
+ logger,
+ signals: [],
});
expect(scheduleNotificationActions as jest.Mock).not.toHaveBeenCalled();
@@ -132,6 +168,8 @@ describe('schedule_throttle_notification_actions', () => {
),
alertInstance: alertsMock.createAlertInstanceFactory(),
notificationRuleParams,
+ logger,
+ signals: [],
});
expect(scheduleNotificationActions as jest.Mock).not.toHaveBeenCalled();
@@ -161,6 +199,8 @@ describe('schedule_throttle_notification_actions', () => {
),
alertInstance: alertsMock.createAlertInstanceFactory(),
notificationRuleParams,
+ logger,
+ signals: [],
});
expect((scheduleNotificationActions as jest.Mock).mock.calls[0][0].resultsLink).toMatch(
@@ -174,4 +214,384 @@ describe('schedule_throttle_notification_actions', () => {
})
);
});
+
+ it('should log debug information when passing through in expected format and no error messages', async () => {
+ await scheduleThrottledNotificationActions({
+ throttle: '1d',
+ startedAt: new Date('2021-08-24T19:19:22.094Z'),
+ id: '123',
+ kibanaSiemAppUrl: 'http://www.example.com',
+ outputIndex: 'output-123',
+ ruleId: 'rule-123',
+ esClient: elasticsearchServiceMock.createElasticsearchClient(
+ elasticsearchServiceMock.createSuccessTransportRequestPromise({
+ hits: {
+ hits: [
+ {
+ _source: {},
+ },
+ ],
+ total: 1,
+ },
+ })
+ ),
+ alertInstance: alertsMock.createAlertInstanceFactory(),
+ notificationRuleParams,
+ logger,
+ signals: [],
+ });
+ // We only test the first part since it has date math using math
+ expect(logger.debug.mock.calls[0][0]).toMatch(
+ /The notification throttle resultsLink created is/
+ );
+ expect(logger.debug.mock.calls[1][0]).toEqual(
+ 'The notification throttle query result size before deconflicting duplicates is: 1. The notification throttle passed in signals size before deconflicting duplicates is: 0. The deconflicted size and size of the signals sent into throttle notification is: 1. The signals count from results size is: 1. The final signals count being sent to the notification is: 1.'
+ );
+ // error should not have been called in this case.
+ expect(logger.error).not.toHaveBeenCalled();
+ });
+
+ it('should log error information if "throttle" is an invalid string', async () => {
+ await scheduleThrottledNotificationActions({
+ throttle: 'invalid',
+ startedAt: new Date('2021-08-24T19:19:22.094Z'),
+ id: '123',
+ kibanaSiemAppUrl: 'http://www.example.com',
+ outputIndex: 'output-123',
+ ruleId: 'rule-123',
+ esClient: elasticsearchServiceMock.createElasticsearchClient(
+ elasticsearchServiceMock.createSuccessTransportRequestPromise({
+ hits: {
+ hits: [
+ {
+ _source: {},
+ },
+ ],
+ total: 1,
+ },
+ })
+ ),
+ alertInstance: alertsMock.createAlertInstanceFactory(),
+ notificationRuleParams,
+ logger,
+ signals: [],
+ });
+
+ expect(logger.error).toHaveBeenCalledWith(
+ 'The notification throttle "from" and/or "to" range values could not be constructed as valid. Tried to construct the values of "from": now-invalid "to": 2021-08-24T19:19:22.094Z. This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected.'
+ );
+ });
+
+ it('should count correctly if it does a deconflict', async () => {
+ await scheduleThrottledNotificationActions({
+ throttle: '1d',
+ startedAt: new Date('2021-08-24T19:19:22.094Z'),
+ id: '123',
+ kibanaSiemAppUrl: 'http://www.example.com',
+ outputIndex: 'output-123',
+ ruleId: 'rule-123',
+ esClient: elasticsearchServiceMock.createElasticsearchClient(
+ elasticsearchServiceMock.createSuccessTransportRequestPromise({
+ hits: {
+ hits: [
+ {
+ _index: 'index-123',
+ _id: 'id-123',
+ _source: {
+ test: 123,
+ },
+ },
+ {
+ _index: 'index-456',
+ _id: 'id-456',
+ _source: {
+ test: 456,
+ },
+ },
+ ],
+ total: 2,
+ },
+ })
+ ),
+ alertInstance: alertsMock.createAlertInstanceFactory(),
+ notificationRuleParams,
+ logger,
+ signals: [
+ {
+ _index: 'index-456',
+ _id: 'id-456',
+ test: 456,
+ },
+ ],
+ });
+ expect(scheduleNotificationActions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ signalsCount: 2,
+ signals: [
+ {
+ _id: 'id-456',
+ _index: 'index-456',
+ test: 456,
+ },
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ test: 123,
+ },
+ ],
+ ruleParams: notificationRuleParams,
+ })
+ );
+ });
+
+ it('should count correctly if it does not do a deconflict', async () => {
+ await scheduleThrottledNotificationActions({
+ throttle: '1d',
+ startedAt: new Date('2021-08-24T19:19:22.094Z'),
+ id: '123',
+ kibanaSiemAppUrl: 'http://www.example.com',
+ outputIndex: 'output-123',
+ ruleId: 'rule-123',
+ esClient: elasticsearchServiceMock.createElasticsearchClient(
+ elasticsearchServiceMock.createSuccessTransportRequestPromise({
+ hits: {
+ hits: [
+ {
+ _index: 'index-123',
+ _id: 'id-123',
+ _source: {
+ test: 123,
+ },
+ },
+ {
+ _index: 'index-456',
+ _id: 'id-456',
+ _source: {
+ test: 456,
+ },
+ },
+ ],
+ total: 2,
+ },
+ })
+ ),
+ alertInstance: alertsMock.createAlertInstanceFactory(),
+ notificationRuleParams,
+ logger,
+ signals: [
+ {
+ _index: 'index-789',
+ _id: 'id-789',
+ test: 456,
+ },
+ ],
+ });
+ expect(scheduleNotificationActions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ signalsCount: 3,
+ signals: [
+ {
+ _id: 'id-789',
+ _index: 'index-789',
+ test: 456,
+ },
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ test: 123,
+ },
+ {
+ _id: 'id-456',
+ _index: 'index-456',
+ test: 456,
+ },
+ ],
+ ruleParams: notificationRuleParams,
+ })
+ );
+ });
+
+ it('should count total hit with extra total elements', async () => {
+ await scheduleThrottledNotificationActions({
+ throttle: '1d',
+ startedAt: new Date('2021-08-24T19:19:22.094Z'),
+ id: '123',
+ kibanaSiemAppUrl: 'http://www.example.com',
+ outputIndex: 'output-123',
+ ruleId: 'rule-123',
+ esClient: elasticsearchServiceMock.createElasticsearchClient(
+ elasticsearchServiceMock.createSuccessTransportRequestPromise({
+ hits: {
+ hits: [
+ {
+ _index: 'index-123',
+ _id: 'id-123',
+ _source: {
+ test: 123,
+ },
+ },
+ ],
+ total: 20, // total can be different from the actual return values so we have to ensure we count these.
+ },
+ })
+ ),
+ alertInstance: alertsMock.createAlertInstanceFactory(),
+ notificationRuleParams,
+ logger,
+ signals: [
+ {
+ _index: 'index-789',
+ _id: 'id-789',
+ test: 456,
+ },
+ ],
+ });
+ expect(scheduleNotificationActions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ signalsCount: 21,
+ signals: [
+ {
+ _id: 'id-789',
+ _index: 'index-789',
+ test: 456,
+ },
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ test: 123,
+ },
+ ],
+ ruleParams: notificationRuleParams,
+ })
+ );
+ });
+
+ it('should count correctly if it does a deconflict and the total has extra values', async () => {
+ await scheduleThrottledNotificationActions({
+ throttle: '1d',
+ startedAt: new Date('2021-08-24T19:19:22.094Z'),
+ id: '123',
+ kibanaSiemAppUrl: 'http://www.example.com',
+ outputIndex: 'output-123',
+ ruleId: 'rule-123',
+ esClient: elasticsearchServiceMock.createElasticsearchClient(
+ elasticsearchServiceMock.createSuccessTransportRequestPromise({
+ hits: {
+ hits: [
+ {
+ _index: 'index-123',
+ _id: 'id-123',
+ _source: {
+ test: 123,
+ },
+ },
+ {
+ _index: 'index-456',
+ _id: 'id-456',
+ _source: {
+ test: 456,
+ },
+ },
+ ],
+ total: 20, // total can be different from the actual return values so we have to ensure we count these.
+ },
+ })
+ ),
+ alertInstance: alertsMock.createAlertInstanceFactory(),
+ notificationRuleParams,
+ logger,
+ signals: [
+ {
+ _index: 'index-456',
+ _id: 'id-456',
+ test: 456,
+ },
+ ],
+ });
+ expect(scheduleNotificationActions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ signalsCount: 20,
+ signals: [
+ {
+ _id: 'id-456',
+ _index: 'index-456',
+ test: 456,
+ },
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ test: 123,
+ },
+ ],
+ ruleParams: notificationRuleParams,
+ })
+ );
+ });
+
+ it('should add extra count element if it has signals added', async () => {
+ await scheduleThrottledNotificationActions({
+ throttle: '1d',
+ startedAt: new Date('2021-08-24T19:19:22.094Z'),
+ id: '123',
+ kibanaSiemAppUrl: 'http://www.example.com',
+ outputIndex: 'output-123',
+ ruleId: 'rule-123',
+ esClient: elasticsearchServiceMock.createElasticsearchClient(
+ elasticsearchServiceMock.createSuccessTransportRequestPromise({
+ hits: {
+ hits: [
+ {
+ _index: 'index-123',
+ _id: 'id-123',
+ _source: {
+ test: 123,
+ },
+ },
+ {
+ _index: 'index-456',
+ _id: 'id-456',
+ _source: {
+ test: 456,
+ },
+ },
+ ],
+ total: 20, // total can be different from the actual return values so we have to ensure we count these.
+ },
+ })
+ ),
+ alertInstance: alertsMock.createAlertInstanceFactory(),
+ notificationRuleParams,
+ logger,
+ signals: [
+ {
+ _index: 'index-789',
+ _id: 'id-789',
+ test: 789,
+ },
+ ],
+ });
+ expect(scheduleNotificationActions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ signalsCount: 21, // should be 1 more than the total since we pushed in an extra signal
+ signals: [
+ {
+ _id: 'id-789',
+ _index: 'index-789',
+ test: 789,
+ },
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ test: 123,
+ },
+ {
+ _id: 'id-456',
+ _index: 'index-456',
+ test: 456,
+ },
+ ],
+ ruleParams: notificationRuleParams,
+ })
+ );
+ });
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts
index 5dd583d47b403..5bf18496e6375 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts
@@ -5,11 +5,11 @@
* 2.0.
*/
-import { ElasticsearchClient, SavedObject } from 'src/core/server';
+import { ElasticsearchClient, SavedObject, Logger } from 'src/core/server';
import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils';
import { AlertInstance } from '../../../../../alerting/server';
import { RuleParams } from '../schemas/rule_schemas';
-import { getNotificationResultsLink } from '../notifications/utils';
+import { deconflictSignalsAndResults, getNotificationResultsLink } from '../notifications/utils';
import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE } from '../../../../common/constants';
import { getSignals } from '../notifications/get_signals';
import {
@@ -18,8 +18,25 @@ import {
} from './schedule_notification_actions';
import { AlertAttributes } from '../signals/types';
+interface ScheduleThrottledNotificationActionsOptions {
+ id: SavedObject['id'];
+ startedAt: Date;
+ throttle: AlertAttributes['throttle'];
+ kibanaSiemAppUrl: string | undefined;
+ outputIndex: RuleParams['outputIndex'];
+ ruleId: RuleParams['ruleId'];
+ esClient: ElasticsearchClient;
+ alertInstance: AlertInstance;
+ notificationRuleParams: NotificationRuleTypeParams;
+ signals: unknown[];
+ logger: Logger;
+}
+
/**
* Schedules a throttled notification action for executor rules.
+ * NOTE: It's important that since this is throttled that you call this in _ALL_ cases including error conditions or results being empty or not a success.
+ * If you do not call this within your rule executor then this will cause a "reset" and will stop "throttling" and the next call will cause an immediate action
+ * to be sent through the system.
* @param throttle The throttle which is the alerting saved object throttle
* @param startedAt When the executor started at
* @param id The id the alert which caused the notifications
@@ -40,17 +57,9 @@ export const scheduleThrottledNotificationActions = async ({
esClient,
alertInstance,
notificationRuleParams,
-}: {
- id: SavedObject['id'];
- startedAt: Date;
- throttle: AlertAttributes['throttle'];
- kibanaSiemAppUrl: string | undefined;
- outputIndex: RuleParams['outputIndex'];
- ruleId: RuleParams['ruleId'];
- esClient: ElasticsearchClient;
- alertInstance: AlertInstance;
- notificationRuleParams: NotificationRuleTypeParams;
-}): Promise => {
+ signals,
+ logger,
+}: ScheduleThrottledNotificationActionsOptions): Promise => {
const fromInMs = parseScheduleDates(`now-${throttle}`);
const toInMs = parseScheduleDates(startedAt.toISOString());
@@ -62,6 +71,22 @@ export const scheduleThrottledNotificationActions = async ({
kibanaSiemAppUrl,
});
+ logger.debug(
+ [
+ `The notification throttle resultsLink created is: ${resultsLink}.`,
+ ' Notification throttle is querying the results using',
+ ` "from:" ${fromInMs.valueOf()}`,
+ ' "to":',
+ ` ${toInMs.valueOf()}`,
+ ' "size":',
+ ` ${DEFAULT_RULE_NOTIFICATION_QUERY_SIZE}`,
+ ' "index":',
+ ` ${outputIndex}`,
+ ' "ruleId":',
+ ` ${ruleId}`,
+ ].join('')
+ );
+
const results = await getSignals({
from: `${fromInMs.valueOf()}`,
to: `${toInMs.valueOf()}`,
@@ -71,18 +96,56 @@ export const scheduleThrottledNotificationActions = async ({
esClient,
});
- const signalsCount =
+ // This will give us counts up to the max of 10k from tracking total hits.
+ const signalsCountFromResults =
typeof results.hits.total === 'number' ? results.hits.total : results.hits.total.value;
- const signals = results.hits.hits.map((hit) => hit._source);
- if (results.hits.hits.length !== 0) {
+ const resultsFlattened = results.hits.hits.map((hit) => {
+ return {
+ _id: hit._id,
+ _index: hit._index,
+ ...hit._source,
+ };
+ });
+
+ const deconflicted = deconflictSignalsAndResults({
+ logger,
+ signals,
+ querySignals: resultsFlattened,
+ });
+
+ // Difference of how many deconflicted results we have to subtract from our signals count.
+ const deconflictedDiff = resultsFlattened.length + signals.length - deconflicted.length;
+
+ // Subtract any deconflicted differences from the total count.
+ const signalsCount = signalsCountFromResults + signals.length - deconflictedDiff;
+ logger.debug(
+ [
+ `The notification throttle query result size before deconflicting duplicates is: ${resultsFlattened.length}.`,
+ ` The notification throttle passed in signals size before deconflicting duplicates is: ${signals.length}.`,
+ ` The deconflicted size and size of the signals sent into throttle notification is: ${deconflicted.length}.`,
+ ` The signals count from results size is: ${signalsCountFromResults}.`,
+ ` The final signals count being sent to the notification is: ${signalsCount}.`,
+ ].join('')
+ );
+
+ if (deconflicted.length !== 0) {
scheduleNotificationActions({
alertInstance,
signalsCount,
- signals,
+ signals: deconflicted,
resultsLink,
ruleParams: notificationRuleParams,
});
}
+ } else {
+ logger.error(
+ [
+ 'The notification throttle "from" and/or "to" range values could not be constructed as valid. Tried to construct the values of',
+ ` "from": now-${throttle}`,
+ ` "to": ${startedAt.toISOString()}.`,
+ ' This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected.',
+ ].join('')
+ );
}
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.test.ts
index 5a667616a9a39..2da7a0398bd3f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.test.ts
@@ -5,18 +5,384 @@
* 2.0.
*/
-import { getNotificationResultsLink } from './utils';
+import { SearchHit } from '@elastic/elasticsearch/api/types';
+import { loggingSystemMock } from 'src/core/server/mocks';
+import { SignalSource } from '../signals/types';
+import { deconflictSignalsAndResults, getNotificationResultsLink } from './utils';
describe('utils', () => {
- it('getNotificationResultsLink', () => {
- const resultLink = getNotificationResultsLink({
- kibanaSiemAppUrl: 'http://localhost:5601/app/security',
- id: 'notification-id',
- from: '00000',
- to: '1111',
- });
- expect(resultLink).toEqual(
- `http://localhost:5601/app/security/detections/rules/id/notification-id?timerange=(global:(linkTo:!(timeline),timerange:(from:00000,kind:absolute,to:1111)),timeline:(linkTo:!(global),timerange:(from:00000,kind:absolute,to:1111)))`
- );
+ let logger = loggingSystemMock.create().get('security_solution');
+
+ beforeEach(() => {
+ logger = loggingSystemMock.create().get('security_solution');
+ });
+
+ describe('getNotificationResultsLink', () => {
+ test('it returns expected link', () => {
+ const resultLink = getNotificationResultsLink({
+ kibanaSiemAppUrl: 'http://localhost:5601/app/security',
+ id: 'notification-id',
+ from: '00000',
+ to: '1111',
+ });
+ expect(resultLink).toEqual(
+ `http://localhost:5601/app/security/detections/rules/id/notification-id?timerange=(global:(linkTo:!(timeline),timerange:(from:00000,kind:absolute,to:1111)),timeline:(linkTo:!(global),timerange:(from:00000,kind:absolute,to:1111)))`
+ );
+ });
+ });
+
+ describe('deconflictSignalsAndResults', () => {
+ type FuncReturn = ReturnType;
+
+ test('given no signals and no query results it returns an empty array', () => {
+ expect(
+ deconflictSignalsAndResults({ logger, querySignals: [], signals: [] })
+ ).toEqual([]);
+ });
+
+ test('given an empty signal and a single query result it returns the query result in the array', () => {
+ const querySignals: Array> = [
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ _source: {
+ test: '123',
+ },
+ },
+ ];
+ expect(
+ deconflictSignalsAndResults({ logger, querySignals, signals: [] })
+ ).toEqual(querySignals);
+ });
+
+ test('given a single signal and an empty query result it returns the query result in the array', () => {
+ const signals: Array> = [
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ _source: {
+ test: '123',
+ },
+ },
+ ];
+ expect(
+ deconflictSignalsAndResults({ logger, querySignals: [], signals })
+ ).toEqual(signals);
+ });
+
+ test('given a signal and a different query result it returns both combined together', () => {
+ const querySignals: Array> = [
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ _source: {
+ test: '123',
+ },
+ },
+ ];
+ const signals: Array> = [
+ {
+ _id: 'id-789',
+ _index: 'index-456',
+ _source: {
+ test: '456',
+ },
+ },
+ ];
+ expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([
+ ...signals,
+ ...querySignals,
+ ]);
+ });
+
+ test('given a duplicate in querySignals it returns both combined together without the duplicate', () => {
+ const querySignals: Array> = [
+ {
+ _id: 'id-123',
+ _index: 'index-123', // This should only show up once and not be duplicated twice
+ _source: {
+ test: '123',
+ },
+ },
+ {
+ _index: 'index-890',
+ _id: 'id-890',
+ _source: {
+ test: '890',
+ },
+ },
+ ];
+ const signals: Array> = [
+ {
+ _id: 'id-123', // This should only show up once and not be duplicated twice
+ _index: 'index-123',
+ _source: {
+ test: '123',
+ },
+ },
+ {
+ _id: 'id-789',
+ _index: 'index-456',
+ _source: {
+ test: '456',
+ },
+ },
+ ];
+ expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ _source: {
+ test: '123',
+ },
+ },
+ {
+ _id: 'id-789',
+ _index: 'index-456',
+ _source: {
+ test: '456',
+ },
+ },
+ {
+ _id: 'id-890',
+ _index: 'index-890',
+ _source: {
+ test: '890',
+ },
+ },
+ ]);
+ });
+
+ test('given a duplicate in signals it returns both combined together without the duplicate', () => {
+ const signals: Array> = [
+ {
+ _id: 'id-123',
+ _index: 'index-123', // This should only show up once and not be duplicated twice
+ _source: {
+ test: '123',
+ },
+ },
+ {
+ _index: 'index-890',
+ _id: 'id-890',
+ _source: {
+ test: '890',
+ },
+ },
+ ];
+ const querySignals: Array> = [
+ {
+ _id: 'id-123', // This should only show up once and not be duplicated twice
+ _index: 'index-123',
+ _source: {
+ test: '123',
+ },
+ },
+ {
+ _id: 'id-789',
+ _index: 'index-456',
+ _source: {
+ test: '456',
+ },
+ },
+ ];
+ expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ _source: { test: '123' },
+ },
+ {
+ _id: 'id-890',
+ _index: 'index-890',
+ _source: { test: '890' },
+ },
+ {
+ _id: 'id-789',
+ _index: 'index-456',
+ _source: { test: '456' },
+ },
+ ]);
+ });
+
+ test('does not give a duplicate in signals if they are only different by their index', () => {
+ const signals: Array> = [
+ {
+ _id: 'id-123',
+ _index: 'index-123-a', // This is only different by index
+ _source: {
+ test: '123',
+ },
+ },
+ {
+ _index: 'index-890',
+ _id: 'id-890',
+ _source: {
+ test: '890',
+ },
+ },
+ ];
+ const querySignals: Array> = [
+ {
+ _id: 'id-123', // This is only different by index
+ _index: 'index-123-b',
+ _source: {
+ test: '123',
+ },
+ },
+ {
+ _id: 'id-789',
+ _index: 'index-456',
+ _source: {
+ test: '456',
+ },
+ },
+ ];
+ expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([
+ ...signals,
+ ...querySignals,
+ ]);
+ });
+
+ test('it logs a debug statement when it sees a duplicate and returns nothing if both are identical', () => {
+ const querySignals: Array> = [
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ _source: {
+ test: '123',
+ },
+ },
+ ];
+ const signals: Array> = [
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ _source: {
+ test: '456',
+ },
+ },
+ ];
+ expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ _source: {
+ test: '456',
+ },
+ },
+ ]);
+ expect(logger.debug).toHaveBeenCalledWith(
+ 'Notification throttle removing duplicate signal and query result found of "_id": id-123, "_index": index-123'
+ );
+ });
+
+ test('it logs an error statement if it sees a signal missing an "_id" for an uncommon reason and returns both documents', () => {
+ const querySignals: Array> = [
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ _source: {
+ test: '123',
+ },
+ },
+ ];
+ const signals: unknown[] = [
+ {
+ _index: 'index-123',
+ _source: {
+ test: '456',
+ },
+ },
+ ];
+ expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([
+ ...signals,
+ ...querySignals,
+ ]);
+ expect(logger.error).toHaveBeenCalledWith(
+ 'Notification throttle cannot determine if we can de-conflict as either the passed in signal or the results query has a null value for either "_id" or "_index". Expect possible duplications in your alerting actions. Passed in signals "_id": undefined. Passed in signals "_index": index-123. Passed in query "result._id": id-123. Passed in query "result._index": index-123.'
+ );
+ });
+
+ test('it logs an error statement if it sees a signal missing a "_index" for an uncommon reason and returns both documents', () => {
+ const querySignals: Array> = [
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ _source: {
+ test: '123',
+ },
+ },
+ ];
+ const signals: unknown[] = [
+ {
+ _id: 'id-123',
+ _source: {
+ test: '456',
+ },
+ },
+ ];
+ expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([
+ ...signals,
+ ...querySignals,
+ ]);
+ expect(logger.error).toHaveBeenCalledWith(
+ 'Notification throttle cannot determine if we can de-conflict as either the passed in signal or the results query has a null value for either "_id" or "_index". Expect possible duplications in your alerting actions. Passed in signals "_id": id-123. Passed in signals "_index": undefined. Passed in query "result._id": id-123. Passed in query "result._index": index-123.'
+ );
+ });
+
+ test('it logs an error statement if it sees a querySignals missing an "_id" for an uncommon reason and returns both documents', () => {
+ const querySignals: Array> = [
+ {
+ _index: 'index-123',
+ _source: {
+ test: '123',
+ },
+ },
+ ] as unknown[] as Array>;
+ const signals: unknown[] = [
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ _source: {
+ test: '456',
+ },
+ },
+ ];
+ expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([
+ ...signals,
+ ...querySignals,
+ ]);
+ expect(logger.error).toHaveBeenCalledWith(
+ 'Notification throttle cannot determine if we can de-conflict as either the passed in signal or the results query has a null value for either "_id" or "_index". Expect possible duplications in your alerting actions. Passed in signals "_id": id-123. Passed in signals "_index": index-123. Passed in query "result._id": undefined. Passed in query "result._index": index-123.'
+ );
+ });
+
+ test('it logs an error statement if it sees a querySignals missing a "_index" for an uncommon reason and returns both documents', () => {
+ const querySignals: Array> = [
+ {
+ _id: 'id-123',
+ _source: {
+ test: '123',
+ },
+ },
+ ] as unknown[] as Array>;
+ const signals: unknown[] = [
+ {
+ _id: 'id-123',
+ _index: 'index-123',
+ _source: {
+ test: '456',
+ },
+ },
+ ];
+ expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([
+ ...signals,
+ ...querySignals,
+ ]);
+ expect(logger.error).toHaveBeenCalledWith(
+ 'Notification throttle cannot determine if we can de-conflict as either the passed in signal or the results query has a null value for either "_id" or "_index". Expect possible duplications in your alerting actions. Passed in signals "_id": id-123. Passed in signals "_index": index-123. Passed in query "result._id": id-123. Passed in query "result._index": undefined.'
+ );
+ });
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.ts
index 4c4bac7da6a62..c8fc6febe4d0f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.ts
@@ -5,7 +5,9 @@
* 2.0.
*/
+import { Logger } from 'src/core/server';
import { APP_PATH } from '../../../../common/constants';
+import { SignalSearchResponse } from '../signals/types';
export const getNotificationResultsLink = ({
kibanaSiemAppUrl = APP_PATH,
@@ -22,3 +24,54 @@ export const getNotificationResultsLink = ({
return `${kibanaSiemAppUrl}/detections/rules/id/${id}?timerange=(global:(linkTo:!(timeline),timerange:(from:${from},kind:absolute,to:${to})),timeline:(linkTo:!(global),timerange:(from:${from},kind:absolute,to:${to})))`;
};
+
+interface DeconflictOptions {
+ signals: unknown[];
+ querySignals: SignalSearchResponse['hits']['hits'];
+ logger: Logger;
+}
+
+/**
+ * Given a signals array of unknown that at least has a '_id' and '_index' this will deconflict it with a results.
+ * @param signals The signals array to deconflict with results
+ * @param results The results to deconflict with the signals
+ * @param logger The logger to log results
+ */
+export const deconflictSignalsAndResults = ({
+ signals,
+ querySignals,
+ logger,
+}: DeconflictOptions): unknown[] => {
+ const querySignalsFiltered = querySignals.filter((result) => {
+ return !signals.find((signal) => {
+ const { _id, _index } = signal as { _id?: string; _index?: string };
+ if (_id == null || _index == null || result._id == null || result._index == null) {
+ logger.error(
+ [
+ 'Notification throttle cannot determine if we can de-conflict as either the passed in signal or the results query has a null value for either "_id" or "_index".',
+ ' Expect possible duplications in your alerting actions.',
+ ` Passed in signals "_id": ${_id}.`,
+ ` Passed in signals "_index": ${_index}.`,
+ ` Passed in query "result._id": ${result._id}.`,
+ ` Passed in query "result._index": ${result._index}.`,
+ ].join('')
+ );
+ return false;
+ } else {
+ if (result._id === _id && result._index === _index) {
+ logger.debug(
+ [
+ 'Notification throttle removing duplicate signal and query result found of',
+ ` "_id": ${_id},`,
+ ` "_index": ${_index}`,
+ ].join('')
+ );
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+ });
+ return [...signals, ...querySignalsFiltered];
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.test.ts
index 8414aa93c7984..7dd05c5122a61 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.test.ts
@@ -20,6 +20,8 @@ describe('legacy_migrations', () => {
test('it migrates both a "ruleAlertId" and a actions array with 1 element into the references array', () => {
const doc = {
attributes: {
+ ruleThrottle: '1d',
+ alertThrottle: '1d',
ruleAlertId: '123',
actions: [
{
@@ -37,6 +39,8 @@ describe('legacy_migrations', () => {
)
).toEqual({
attributes: {
+ ruleThrottle: '1d',
+ alertThrottle: '1d',
actions: [
{
actionRef: 'action_0',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts
index 8a52d3a13f065..aa85898e94a33 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts
@@ -245,7 +245,7 @@ export const legacyMigrateRuleAlertId = (
return {
...doc,
attributes: {
- ...attributesWithoutRuleAlertId.attributes,
+ ...attributesWithoutRuleAlertId,
actions: actionsWithRef,
},
references: [...existingReferences, ...alertReferences, ...actionsReferences],
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts
index 77981d92b2ba7..0ad416e86e31a 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts
@@ -111,6 +111,12 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
let result = createResultObject(state);
+ const notificationRuleParams: NotificationRuleTypeParams = {
+ ...params,
+ name: name as string,
+ id: ruleSO.id as string,
+ } as unknown as NotificationRuleTypeParams;
+
// check if rule has permissions to access given index pattern
// move this collection of lines into a function in utils
// so that we can use it in create rules route, bulk, etc.
@@ -296,12 +302,6 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
const createdSignalsCount = result.createdSignals.length;
if (actions.length) {
- const notificationRuleParams: NotificationRuleTypeParams = {
- ...params,
- name: name as string,
- id: ruleSO.id as string,
- } as unknown as NotificationRuleTypeParams;
-
const fromInMs = parseScheduleDates(`now-${interval}`)?.format('x');
const toInMs = parseScheduleDates('now')?.format('x');
const resultsLink = getNotificationResultsLink({
@@ -328,6 +328,8 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
ruleId,
esClient: services.scopedClusterClient.asCurrentUser,
notificationRuleParams,
+ signals: result.createdSignals,
+ logger,
});
} else if (createdSignalsCount) {
const alertInstance = services.alertInstanceFactory(alertId);
@@ -372,6 +374,21 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
)
);
} else {
+ // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early
+ await scheduleThrottledNotificationActions({
+ alertInstance: services.alertInstanceFactory(alertId),
+ throttle: ruleSO.attributes.throttle,
+ startedAt,
+ id: ruleSO.id,
+ kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined)
+ ?.kibana_siem_app_url,
+ outputIndex: ruleDataClient.indexName,
+ ruleId,
+ esClient: services.scopedClusterClient.asCurrentUser,
+ notificationRuleParams,
+ signals: result.createdSignals,
+ logger,
+ });
const errorMessage = buildRuleMessage(
'Bulk Indexing of signals failed:',
truncateMessageList(result.errors).join()
@@ -389,6 +406,22 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
});
}
} catch (error) {
+ // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early
+ await scheduleThrottledNotificationActions({
+ alertInstance: services.alertInstanceFactory(alertId),
+ throttle: ruleSO.attributes.throttle,
+ startedAt,
+ id: ruleSO.id,
+ kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined)
+ ?.kibana_siem_app_url,
+ outputIndex: ruleDataClient.indexName,
+ ruleId,
+ esClient: services.scopedClusterClient.asCurrentUser,
+ notificationRuleParams,
+ signals: result.createdSignals,
+ logger,
+ });
+
const errorMessage = error.message ?? '(no error message given)';
const message = buildRuleMessage(
'An error occurred during rule execution:',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
index c2923b566175e..88b276358a705 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
@@ -35,6 +35,7 @@ import { allowedExperimentalValues } from '../../../../common/experimental_featu
import { scheduleNotificationActions } from '../notifications/schedule_notification_actions';
import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
+import { scheduleThrottledNotificationActions } from '../notifications/schedule_throttle_notification_actions';
import { eventLogServiceMock } from '../../../../../event_log/server/mocks';
import { createMockConfig } from '../routes/__mocks__';
@@ -58,7 +59,7 @@ jest.mock('@kbn/securitysolution-io-ts-utils', () => {
parseScheduleDates: jest.fn(),
};
});
-
+jest.mock('../notifications/schedule_throttle_notification_actions');
const mockRuleExecutionLogClient = ruleExecutionLogClientMock.create();
jest.mock('../rule_execution_log/rule_execution_log_client', () => ({
@@ -200,6 +201,7 @@ describe('signal_rule_alert_type', () => {
});
mockRuleExecutionLogClient.logStatusChange.mockClear();
+ (scheduleThrottledNotificationActions as jest.Mock).mockClear();
});
describe('executor', () => {
@@ -520,5 +522,28 @@ describe('signal_rule_alert_type', () => {
})
);
});
+
+ it('should call scheduleThrottledNotificationActions if result is false to prevent the throttle from being reset', async () => {
+ const result: SearchAfterAndBulkCreateReturnType = {
+ success: false,
+ warning: false,
+ searchAfterTimes: [],
+ bulkCreateTimes: [],
+ lastLookBackDate: null,
+ createdSignalsCount: 0,
+ createdSignals: [],
+ warningMessages: [],
+ errors: ['Error that bubbled up.'],
+ };
+ (queryExecutor as jest.Mock).mockResolvedValue(result);
+ await alert.executor(payload);
+ expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call scheduleThrottledNotificationActions if an error was thrown to prevent the throttle from being reset', async () => {
+ (queryExecutor as jest.Mock).mockRejectedValue({});
+ await alert.executor(payload);
+ expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(1);
+ });
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
index 2094264cbf15f..4e98bee83aeb5 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
@@ -172,6 +172,12 @@ export const signalRulesAlertType = ({
newStatus: RuleExecutionStatus['going to run'],
});
+ const notificationRuleParams: NotificationRuleTypeParams = {
+ ...params,
+ name,
+ id: savedObject.id,
+ };
+
// check if rule has permissions to access given index pattern
// move this collection of lines into a function in utils
// so that we can use it in create rules route, bulk, etc.
@@ -396,12 +402,6 @@ export const signalRulesAlertType = ({
if (result.success) {
if (actions.length) {
- const notificationRuleParams: NotificationRuleTypeParams = {
- ...params,
- name,
- id: savedObject.id,
- };
-
const fromInMs = parseScheduleDates(`now-${interval}`)?.format('x');
const toInMs = parseScheduleDates('now')?.format('x');
const resultsLink = getNotificationResultsLink({
@@ -426,8 +426,10 @@ export const signalRulesAlertType = ({
?.kibana_siem_app_url,
outputIndex,
ruleId,
+ signals: result.createdSignals,
esClient: services.scopedClusterClient.asCurrentUser,
notificationRuleParams,
+ logger,
});
} else if (result.createdSignalsCount) {
const alertInstance = services.alertInstanceFactory(alertId);
@@ -471,6 +473,22 @@ export const signalRulesAlertType = ({
)
);
} else {
+ // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early
+ await scheduleThrottledNotificationActions({
+ alertInstance: services.alertInstanceFactory(alertId),
+ throttle: savedObject.attributes.throttle,
+ startedAt,
+ id: savedObject.id,
+ kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined)
+ ?.kibana_siem_app_url,
+ outputIndex,
+ ruleId,
+ signals: result.createdSignals,
+ esClient: services.scopedClusterClient.asCurrentUser,
+ notificationRuleParams,
+ logger,
+ });
+
const errorMessage = buildRuleMessage(
'Bulk Indexing of signals failed:',
truncateMessageList(result.errors).join()
@@ -488,6 +506,21 @@ export const signalRulesAlertType = ({
});
}
} catch (error) {
+ // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early
+ await scheduleThrottledNotificationActions({
+ alertInstance: services.alertInstanceFactory(alertId),
+ throttle: savedObject.attributes.throttle,
+ startedAt,
+ id: savedObject.id,
+ kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined)
+ ?.kibana_siem_app_url,
+ outputIndex,
+ ruleId,
+ signals: result.createdSignals,
+ esClient: services.scopedClusterClient.asCurrentUser,
+ notificationRuleParams,
+ logger,
+ });
const errorMessage = error.message ?? '(no error message given)';
const message = buildRuleMessage(
'An error occurred during rule execution:',
diff --git a/x-pack/plugins/spaces/server/config.ts b/x-pack/plugins/spaces/server/config.ts
index 4abaf4f0ca569..a8c3a1c6223da 100644
--- a/x-pack/plugins/spaces/server/config.ts
+++ b/x-pack/plugins/spaces/server/config.ts
@@ -28,6 +28,7 @@ export function createConfig$(context: PluginInitializerContext) {
const disabledDeprecation: ConfigDeprecation = (config, fromPath, addDeprecation) => {
if ('enabled' in (config?.xpack?.spaces || {})) {
addDeprecation({
+ configPath: 'xpack.spaces.enabled',
title: i18n.translate('xpack.spaces.deprecations.enabledTitle', {
defaultMessage: 'Setting "xpack.spaces.enabled" is deprecated',
}),
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts
index 111fda3bdaca8..2a98a4670f2b5 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts
@@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
-import { Logger } from 'src/core/server';
+import { Logger, SavedObjectReference } from 'src/core/server';
import { STACK_ALERTS_FEATURE_ID } from '../../../common';
import { getGeoContainmentExecutor } from './geo_containment';
import {
@@ -15,14 +15,37 @@ import {
AlertTypeState,
AlertInstanceState,
AlertInstanceContext,
+ RuleParamsAndRefs,
AlertTypeParams,
} from '../../../../alerting/server';
import { Query } from '../../../../../../src/plugins/data/common/query';
-export const GEO_CONTAINMENT_ID = '.geo-containment';
export const ActionGroupId = 'Tracked entity contained';
export const RecoveryActionGroupId = 'notGeoContained';
+export const GEO_CONTAINMENT_ID = '.geo-containment';
+export interface GeoContainmentParams extends AlertTypeParams {
+ index: string;
+ indexId: string;
+ geoField: string;
+ entity: string;
+ dateField: string;
+ boundaryType: string;
+ boundaryIndexTitle: string;
+ boundaryIndexId: string;
+ boundaryGeoField: string;
+ boundaryNameField?: string;
+ indexQuery?: Query;
+ boundaryIndexQuery?: Query;
+}
+export type GeoContainmentExtractedParams = Omit<
+ GeoContainmentParams,
+ 'indexId' | 'boundaryIndexId'
+> & {
+ indexRefName: string;
+ boundaryIndexRefName: string;
+};
+
const actionVariableContextEntityIdLabel = i18n.translate(
'xpack.stackAlerts.geoContainment.actionVariableContextEntityIdLabel',
{
@@ -103,20 +126,6 @@ export const ParamsSchema = schema.object({
boundaryIndexQuery: schema.maybe(schema.any({})),
});
-export interface GeoContainmentParams extends AlertTypeParams {
- index: string;
- indexId: string;
- geoField: string;
- entity: string;
- dateField: string;
- boundaryType: string;
- boundaryIndexTitle: string;
- boundaryIndexId: string;
- boundaryGeoField: string;
- boundaryNameField?: string;
- indexQuery?: Query;
- boundaryIndexQuery?: Query;
-}
export interface GeoContainmentState extends AlertTypeState {
shapesFilters: Record;
shapesIdsNamesMap: Record;
@@ -140,7 +149,7 @@ export interface GeoContainmentInstanceContext extends AlertInstanceContext {
export type GeoContainmentAlertType = AlertType<
GeoContainmentParams,
- never, // Only use if defining useSavedObjectReferences hook
+ GeoContainmentExtractedParams,
GeoContainmentState,
GeoContainmentInstanceState,
GeoContainmentInstanceContext,
@@ -148,6 +157,56 @@ export type GeoContainmentAlertType = AlertType<
typeof RecoveryActionGroupId
>;
+export function extractEntityAndBoundaryReferences(params: GeoContainmentParams): {
+ params: GeoContainmentExtractedParams;
+ references: SavedObjectReference[];
+} {
+ const { indexId, boundaryIndexId, ...otherParams } = params;
+
+ // Reference names omit the `param:`-prefix. This is handled by the alerting framework already
+ const references = [
+ {
+ name: `tracked_index_${indexId}`,
+ type: 'index-pattern',
+ id: indexId as string,
+ },
+ {
+ name: `boundary_index_${boundaryIndexId}`,
+ type: 'index-pattern',
+ id: boundaryIndexId as string,
+ },
+ ];
+ return {
+ params: {
+ ...otherParams,
+ indexRefName: `tracked_index_${indexId}`,
+ boundaryIndexRefName: `boundary_index_${boundaryIndexId}`,
+ },
+ references,
+ };
+}
+
+export function injectEntityAndBoundaryIds(
+ params: GeoContainmentExtractedParams,
+ references: SavedObjectReference[]
+): GeoContainmentParams {
+ const { indexRefName, boundaryIndexRefName, ...otherParams } = params;
+ const { id: indexId = null } = references.find((ref) => ref.name === indexRefName) || {};
+ const { id: boundaryIndexId = null } =
+ references.find((ref) => ref.name === boundaryIndexRefName) || {};
+ if (!indexId) {
+ throw new Error(`Index "${indexId}" not found in references array`);
+ }
+ if (!boundaryIndexId) {
+ throw new Error(`Boundary index "${boundaryIndexId}" not found in references array`);
+ }
+ return {
+ ...otherParams,
+ indexId,
+ boundaryIndexId,
+ } as GeoContainmentParams;
+}
+
export function getAlertType(logger: Logger): GeoContainmentAlertType {
const alertTypeName = i18n.translate('xpack.stackAlerts.geoContainment.alertTypeTitle', {
defaultMessage: 'Tracking containment',
@@ -179,5 +238,18 @@ export function getAlertType(logger: Logger): GeoContainmentAlertType {
actionVariables,
minimumLicenseRequired: 'gold',
isExportable: true,
+ useSavedObjectReferences: {
+ extractReferences: (
+ params: GeoContainmentParams
+ ): RuleParamsAndRefs => {
+ return extractEntityAndBoundaryReferences(params);
+ },
+ injectReferences: (
+ params: GeoContainmentExtractedParams,
+ references: SavedObjectReference[]
+ ) => {
+ return injectEntityAndBoundaryIds(params, references);
+ },
+ },
};
}
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts
index 21a536dd474ba..f227ae4fc23cc 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts
@@ -12,13 +12,14 @@ import { executeEsQueryFactory, getShapesFilters, OTHER_CATEGORY } from './es_qu
import { AlertServices } from '../../../../alerting/server';
import {
ActionGroupId,
- GEO_CONTAINMENT_ID,
GeoContainmentInstanceState,
GeoContainmentAlertType,
GeoContainmentInstanceContext,
GeoContainmentState,
} from './alert_type';
+import { GEO_CONTAINMENT_ID } from './alert_type';
+
export type LatestEntityLocation = GeoContainmentInstanceState;
// Flatten agg results and get latest locations for each entity
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts
index 023ea168a77d2..195ffb97bd81f 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts
@@ -8,7 +8,6 @@
import { Logger } from 'src/core/server';
import { AlertingSetup } from '../../types';
import {
- GeoContainmentParams,
GeoContainmentState,
GeoContainmentInstanceState,
GeoContainmentInstanceContext,
@@ -17,6 +16,8 @@ import {
RecoveryActionGroupId,
} from './alert_type';
+import { GeoContainmentExtractedParams, GeoContainmentParams } from './alert_type';
+
interface RegisterParams {
logger: Logger;
alerting: AlertingSetup;
@@ -26,7 +27,7 @@ export function register(params: RegisterParams) {
const { logger, alerting } = params;
alerting.registerType<
GeoContainmentParams,
- never, // Only use if defining useSavedObjectReferences hook
+ GeoContainmentExtractedParams,
GeoContainmentState,
GeoContainmentInstanceState,
GeoContainmentInstanceContext,
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts
index e8f699eb06161..9fc382240d0be 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts
@@ -6,7 +6,12 @@
*/
import { loggingSystemMock } from '../../../../../../../src/core/server/mocks';
-import { getAlertType, GeoContainmentParams } from '../alert_type';
+import {
+ getAlertType,
+ injectEntityAndBoundaryIds,
+ GeoContainmentParams,
+ extractEntityAndBoundaryReferences,
+} from '../alert_type';
describe('alertType', () => {
const logger = loggingSystemMock.create().get();
@@ -43,4 +48,94 @@ describe('alertType', () => {
expect(alertType.validate?.params?.validate(params)).toBeTruthy();
});
+
+ test('injectEntityAndBoundaryIds', () => {
+ expect(
+ injectEntityAndBoundaryIds(
+ {
+ boundaryGeoField: 'geometry',
+ boundaryIndexRefName: 'boundary_index_boundaryid',
+ boundaryIndexTitle: 'boundary*',
+ boundaryType: 'entireIndex',
+ dateField: '@timestamp',
+ entity: 'vehicle_id',
+ geoField: 'geometry',
+ index: 'foo*',
+ indexRefName: 'tracked_index_foobar',
+ },
+ [
+ {
+ id: 'foreign',
+ name: 'foobar',
+ type: 'foreign',
+ },
+ {
+ id: 'foobar',
+ name: 'tracked_index_foobar',
+ type: 'index-pattern',
+ },
+ {
+ id: 'foreignToo',
+ name: 'boundary_index_shouldbeignored',
+ type: 'index-pattern',
+ },
+ {
+ id: 'boundaryid',
+ name: 'boundary_index_boundaryid',
+ type: 'index-pattern',
+ },
+ ]
+ )
+ ).toEqual({
+ index: 'foo*',
+ indexId: 'foobar',
+ geoField: 'geometry',
+ entity: 'vehicle_id',
+ dateField: '@timestamp',
+ boundaryType: 'entireIndex',
+ boundaryIndexTitle: 'boundary*',
+ boundaryIndexId: 'boundaryid',
+ boundaryGeoField: 'geometry',
+ });
+ });
+
+ test('extractEntityAndBoundaryReferences', () => {
+ expect(
+ extractEntityAndBoundaryReferences({
+ index: 'foo*',
+ indexId: 'foobar',
+ geoField: 'geometry',
+ entity: 'vehicle_id',
+ dateField: '@timestamp',
+ boundaryType: 'entireIndex',
+ boundaryIndexTitle: 'boundary*',
+ boundaryIndexId: 'boundaryid',
+ boundaryGeoField: 'geometry',
+ })
+ ).toEqual({
+ params: {
+ boundaryGeoField: 'geometry',
+ boundaryIndexRefName: 'boundary_index_boundaryid',
+ boundaryIndexTitle: 'boundary*',
+ boundaryType: 'entireIndex',
+ dateField: '@timestamp',
+ entity: 'vehicle_id',
+ geoField: 'geometry',
+ index: 'foo*',
+ indexRefName: 'tracked_index_foobar',
+ },
+ references: [
+ {
+ id: 'foobar',
+ name: 'tracked_index_foobar',
+ type: 'index-pattern',
+ },
+ {
+ id: 'boundaryid',
+ name: 'boundary_index_boundaryid',
+ type: 'index-pattern',
+ },
+ ],
+ });
+ });
});
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts
index 364c484a02080..8b78441d174b2 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts
@@ -17,11 +17,8 @@ import {
getGeoContainmentExecutor,
} from '../geo_containment';
import { OTHER_CATEGORY } from '../es_query_builder';
-import {
- GeoContainmentInstanceContext,
- GeoContainmentInstanceState,
- GeoContainmentParams,
-} from '../alert_type';
+import { GeoContainmentInstanceContext, GeoContainmentInstanceState } from '../alert_type';
+import type { GeoContainmentParams } from '../alert_type';
const alertInstanceFactory =
(contextKeys: unknown[], testAlertActionArr: unknown[]) => (instanceId: string) => {
diff --git a/x-pack/plugins/stack_alerts/server/index.ts b/x-pack/plugins/stack_alerts/server/index.ts
index 9491f3e646c70..1ac774a2d6c3f 100644
--- a/x-pack/plugins/stack_alerts/server/index.ts
+++ b/x-pack/plugins/stack_alerts/server/index.ts
@@ -18,6 +18,7 @@ export const config: PluginConfigDescriptor = {
const stackAlerts = get(settings, fromPath);
if (stackAlerts?.enabled === false || stackAlerts?.enabled === true) {
addDeprecation({
+ configPath: 'xpack.stack_alerts.enabled',
message: `"xpack.stack_alerts.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`,
correctiveActions: {
manualSteps: [`Remove "xpack.stack_alerts.enabled" from your kibana configs.`],
diff --git a/x-pack/plugins/stack_alerts/server/plugin.test.ts b/x-pack/plugins/stack_alerts/server/plugin.test.ts
index b9263553173d2..b2bf076eaf49d 100644
--- a/x-pack/plugins/stack_alerts/server/plugin.test.ts
+++ b/x-pack/plugins/stack_alerts/server/plugin.test.ts
@@ -11,7 +11,8 @@ import { alertsMock } from '../../alerting/server/mocks';
import { featuresPluginMock } from '../../features/server/mocks';
import { BUILT_IN_ALERTS_FEATURE } from './feature';
-describe('AlertingBuiltins Plugin', () => {
+// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699
+describe.skip('AlertingBuiltins Plugin', () => {
describe('setup()', () => {
let context: ReturnType;
let plugin: AlertingBuiltinsPlugin;
diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts
index 84bee044c4de9..2a360fc1a1d90 100644
--- a/x-pack/plugins/task_manager/server/index.ts
+++ b/x-pack/plugins/task_manager/server/index.ts
@@ -49,6 +49,7 @@ export const config: PluginConfigDescriptor = {
const taskManager = get(settings, fromPath);
if (taskManager?.index) {
addDeprecation({
+ configPath: `${fromPath}.index`,
documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy',
message: `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`,
correctiveActions: {
@@ -61,6 +62,7 @@ export const config: PluginConfigDescriptor = {
}
if (taskManager?.max_workers > MAX_WORKERS_LIMIT) {
addDeprecation({
+ configPath: `${fromPath}.max_workers`,
message: `setting "${fromPath}.max_workers" (${taskManager?.max_workers}) greater than ${MAX_WORKERS_LIMIT} is deprecated. Values greater than ${MAX_WORKERS_LIMIT} will not be supported starting in 8.0.`,
correctiveActions: {
manualSteps: [
@@ -75,6 +77,7 @@ export const config: PluginConfigDescriptor = {
const taskManager = get(settings, fromPath);
if (taskManager?.enabled === false || taskManager?.enabled === true) {
addDeprecation({
+ configPath: 'xpack.task_manager.enabled',
message: `"xpack.task_manager.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`,
correctiveActions: {
manualSteps: [`Remove "xpack.task_manager.enabled" from your kibana configs.`],
diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx
index af19a6b7cdb74..30181a96aa70b 100644
--- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx
+++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx
@@ -32,7 +32,7 @@ const AddToCaseActionComponent: React.FC = ({
{userCanCrud && (
= ({
{userCanCrud && (
= ({
id: 0,
items: [
{
- icon: ,
- name: i18n.HIDE_COLUMN,
+ icon: ,
+ name: i18n.REMOVE_COLUMN,
onClick: () => {
dispatch(tGridActions.removeColumn({ id: timelineId, columnId: header.id }));
handleClosePopOverTrigger();
diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx
index 2e684b9eda989..47fcb8c8e1509 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx
@@ -98,6 +98,7 @@ describe('helpers', () => {
describe('getColumnHeaders', () => {
// additional properties used by `EuiDataGrid`:
const actions = {
+ showHide: false,
showSortAsc: true,
showSortDesc: true,
};
diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx
index c658000e6d331..66ec3ec1c399f 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx
@@ -27,6 +27,7 @@ import { allowSorting } from '../helpers';
const defaultActions: EuiDataGridColumnActions = {
showSortAsc: true,
showSortDesc: true,
+ showHide: false,
};
const getAllBrowserFields = (browserFields: BrowserFields): Array> =>
diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts
index 2d4fbcbd54cfa..202eef8d675b8 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts
+++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts
@@ -23,10 +23,6 @@ export const FULL_SCREEN = i18n.translate('xpack.timelines.timeline.fullScreenBu
defaultMessage: 'Full screen',
});
-export const HIDE_COLUMN = i18n.translate('xpack.timelines.timeline.hideColumnLabel', {
- defaultMessage: 'Hide column',
-});
-
export const SORT_AZ = i18n.translate('xpack.timelines.timeline.sortAZLabel', {
defaultMessage: 'Sort A-Z',
});
diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx
index 50764af3c7f2f..5a7ae6e407b0b 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx
@@ -6,9 +6,11 @@
*/
import React from 'react';
+import { fireEvent, render, screen } from '@testing-library/react';
import { BodyComponent, StatefulBodyProps } from '.';
import { Sort } from './sort';
+import { REMOVE_COLUMN } from './column_headers/translations';
import { Direction } from '../../../../common/search_strategy';
import { useMountAppended } from '../../utils/use_mount_appended';
import { defaultHeaders, mockBrowserFields, mockTimelineData, TestProviders } from '../../../mock';
@@ -273,4 +275,57 @@ describe('Body', () => {
.find((c) => c.id === 'signal.rule.risk_score')?.cellActions
).toBeUndefined();
});
+
+ test('it does NOT render switches for hiding columns in the `EuiDataGrid` `Columns` popover', async () => {
+ render(
+
+
+
+ );
+
+ // Click the `EuidDataGrid` `Columns` button to open the popover:
+ fireEvent.click(screen.getByTestId('dataGridColumnSelectorButton'));
+
+ // `EuiDataGrid` renders switches for hiding in the `Columns` popover when `showColumnSelector.allowHide` is `true`
+ const switches = await screen.queryAllByRole('switch');
+
+ expect(switches.length).toBe(0); // no switches are rendered, because `allowHide` is `false`
+ });
+
+ test('it dispatches the `REMOVE_COLUMN` action when a user clicks `Remove column` in the column header popover', async () => {
+ render(
+
+
+
+ );
+
+ // click the `@timestamp` column header to display the popover
+ fireEvent.click(screen.getByText('@timestamp'));
+
+ // click the `Remove column` action in the popover
+ fireEvent.click(await screen.getByText(REMOVE_COLUMN));
+
+ expect(mockDispatch).toBeCalledWith({
+ payload: { columnId: '@timestamp', id: 'timeline-test' },
+ type: 'x-pack/timelines/t-grid/REMOVE_COLUMN',
+ });
+ });
+
+ test('it dispatches the `UPDATE_COLUMN_WIDTH` action when a user resizes a column', async () => {
+ render(
+
+
+
+ );
+
+ // simulate resizing the column
+ fireEvent.mouseDown(screen.getAllByTestId('dataGridColumnResizer')[0]);
+ fireEvent.mouseMove(screen.getAllByTestId('dataGridColumnResizer')[0]);
+ fireEvent.mouseUp(screen.getAllByTestId('dataGridColumnResizer')[0]);
+
+ expect(mockDispatch).toBeCalledWith({
+ payload: { columnId: '@timestamp', id: 'timeline-test', width: NaN },
+ type: 'x-pack/timelines/t-grid/UPDATE_COLUMN_WIDTH',
+ });
+ });
});
diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx
index 619571a0c8e81..9e43c16fd5e6f 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx
@@ -75,6 +75,7 @@ import { ViewSelection } from '../event_rendered_view/selector';
import { EventRenderedView } from '../event_rendered_view';
import { useDataGridHeightHack } from './height_hack';
import { Filter } from '../../../../../../../src/plugins/data/public';
+import { REMOVE_COLUMN } from './column_headers/translations';
const StatefulAlertStatusBulkActions = lazy(
() => import('../toolbar/bulk_actions/alert_status_bulk_actions')
@@ -497,7 +498,7 @@ export const BodyComponent = React.memo(
showFullScreenSelector: false,
}
: {
- showColumnSelector: { allowHide: true, allowReorder: true },
+ showColumnSelector: { allowHide: false, allowReorder: true },
showSortSelector: true,
showFullScreenSelector: true,
}),
@@ -559,13 +560,32 @@ export const BodyComponent = React.memo(
[columnHeaders, dispatch, id, loadPage]
);
- const [visibleColumns, setVisibleColumns] = useState(() =>
- columnHeaders.map(({ id: cid }) => cid)
- ); // initializes to the full set of columns
+ const visibleColumns = useMemo(() => columnHeaders.map(({ id: cid }) => cid), [columnHeaders]); // the full set of columns
- useEffect(() => {
- setVisibleColumns(columnHeaders.map(({ id: cid }) => cid));
- }, [columnHeaders]);
+ const onColumnResize = useCallback(
+ ({ columnId, width }: { columnId: string; width: number }) => {
+ dispatch(
+ tGridActions.updateColumnWidth({
+ columnId,
+ id,
+ width,
+ })
+ );
+ },
+ [dispatch, id]
+ );
+
+ const onSetVisibleColumns = useCallback(
+ (newVisibleColumns: string[]) => {
+ dispatch(
+ tGridActions.updateColumnOrder({
+ columnIds: newVisibleColumns,
+ id,
+ })
+ );
+ },
+ [dispatch, id]
+ );
const setEventsLoading = useCallback(
({ eventIds, isLoading: loading }) => {
@@ -654,6 +674,19 @@ export const BodyComponent = React.memo(
return {
...header,
+ actions: {
+ ...header.actions,
+ additional: [
+ {
+ iconType: 'cross',
+ label: REMOVE_COLUMN,
+ onClick: () => {
+ dispatch(tGridActions.removeColumn({ id, columnId: header.id }));
+ },
+ size: 'xs',
+ },
+ ],
+ },
...(hasCellActions(header.id)
? {
cellActions:
@@ -663,7 +696,7 @@ export const BodyComponent = React.memo(
: {}),
};
}),
- [columnHeaders, defaultCellActions, browserFields, data, pageSize, id]
+ [columnHeaders, defaultCellActions, browserFields, data, pageSize, id, dispatch]
);
const renderTGridCellValue = useMemo(() => {
@@ -761,7 +794,7 @@ export const BodyComponent = React.memo(
data-test-subj="body-data-grid"
aria-label={i18n.TGRID_BODY_ARIA_LABEL}
columns={columnsWithCellActions}
- columnVisibility={{ visibleColumns, setVisibleColumns }}
+ columnVisibility={{ visibleColumns, setVisibleColumns: onSetVisibleColumns }}
gridStyle={gridStyle}
leadingControlColumns={leadingTGridControlColumns}
trailingControlColumns={trailingTGridControlColumns}
@@ -769,6 +802,7 @@ export const BodyComponent = React.memo(
rowCount={totalItems}
renderCellValue={renderTGridCellValue}
sorting={{ columns: sortingColumns, onSort }}
+ onColumnResize={onColumnResize}
pagination={{
pageIndex: activePage,
pageSize,
diff --git a/x-pack/plugins/timelines/public/store/t_grid/actions.ts b/x-pack/plugins/timelines/public/store/t_grid/actions.ts
index a039a236fb186..feab12b616c78 100644
--- a/x-pack/plugins/timelines/public/store/t_grid/actions.ts
+++ b/x-pack/plugins/timelines/public/store/t_grid/actions.ts
@@ -32,6 +32,17 @@ export const applyDeltaToColumnWidth = actionCreator<{
delta: number;
}>('APPLY_DELTA_TO_COLUMN_WIDTH');
+export const updateColumnOrder = actionCreator<{
+ columnIds: string[];
+ id: string;
+}>('UPDATE_COLUMN_ORDER');
+
+export const updateColumnWidth = actionCreator<{
+ columnId: string;
+ id: string;
+ width: number;
+}>('UPDATE_COLUMN_WIDTH');
+
export type ToggleDetailPanel = TimelineExpandedDetailType & {
tabType?: TimelineTabs;
timelineId: string;
diff --git a/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx b/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx
index 121e5bda78ed8..1e1fbe290a115 100644
--- a/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx
+++ b/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx
@@ -7,7 +7,11 @@
import { SortColumnTimeline } from '../../../common';
import { tGridDefaults } from './defaults';
-import { setInitializeTgridSettings } from './helpers';
+import {
+ setInitializeTgridSettings,
+ updateTGridColumnOrder,
+ updateTGridColumnWidth,
+} from './helpers';
import { mockGlobalState } from '../../mock/global_state';
import { TGridModelSettings } from '.';
@@ -57,3 +61,112 @@ describe('setInitializeTgridSettings', () => {
expect(result).toBe(timelineById);
});
});
+
+describe('updateTGridColumnOrder', () => {
+ test('it returns the columns in the new expected order', () => {
+ const originalIdOrder = defaultTimelineById.test.columns.map((x) => x.id); // ['@timestamp', 'event.severity', 'event.category', '...']
+
+ // the new order swaps the positions of the first and second columns:
+ const newIdOrder = [originalIdOrder[1], originalIdOrder[0], ...originalIdOrder.slice(2)]; // ['event.severity', '@timestamp', 'event.category', '...']
+
+ expect(
+ updateTGridColumnOrder({
+ columnIds: newIdOrder,
+ id: 'test',
+ timelineById: defaultTimelineById,
+ })
+ ).toEqual({
+ ...defaultTimelineById,
+ test: {
+ ...defaultTimelineById.test,
+ columns: [
+ defaultTimelineById.test.columns[1], // event.severity
+ defaultTimelineById.test.columns[0], // @timestamp
+ ...defaultTimelineById.test.columns.slice(2), // all remaining columns
+ ],
+ },
+ });
+ });
+
+ test('it omits unknown column IDs when re-ordering columns', () => {
+ const originalIdOrder = defaultTimelineById.test.columns.map((x) => x.id); // ['@timestamp', 'event.severity', 'event.category', '...']
+ const unknownColumId = 'does.not.exist';
+ const newIdOrder = [originalIdOrder[0], unknownColumId, ...originalIdOrder.slice(1)]; // ['@timestamp', 'does.not.exist', 'event.severity', 'event.category', '...']
+
+ expect(
+ updateTGridColumnOrder({
+ columnIds: newIdOrder,
+ id: 'test',
+ timelineById: defaultTimelineById,
+ })
+ ).toEqual({
+ ...defaultTimelineById,
+ test: {
+ ...defaultTimelineById.test,
+ },
+ });
+ });
+
+ test('it returns an empty collection of columns if none of the new column IDs are found', () => {
+ const newIdOrder = ['this.id.does.NOT.exist', 'this.id.also.does.NOT.exist']; // all unknown IDs
+
+ expect(
+ updateTGridColumnOrder({
+ columnIds: newIdOrder,
+ id: 'test',
+ timelineById: defaultTimelineById,
+ })
+ ).toEqual({
+ ...defaultTimelineById,
+ test: {
+ ...defaultTimelineById.test,
+ columns: [], // <-- empty, because none of the new column IDs match the old IDs
+ },
+ });
+ });
+});
+
+describe('updateTGridColumnWidth', () => {
+ test("it updates (only) the specified column's width", () => {
+ const columnId = '@timestamp';
+ const width = 1234;
+
+ const expectedUpdatedColumn = {
+ ...defaultTimelineById.test.columns[0], // @timestamp
+ initialWidth: width,
+ };
+
+ expect(
+ updateTGridColumnWidth({
+ columnId,
+ id: 'test',
+ timelineById: defaultTimelineById,
+ width,
+ })
+ ).toEqual({
+ ...defaultTimelineById,
+ test: {
+ ...defaultTimelineById.test,
+ columns: [expectedUpdatedColumn, ...defaultTimelineById.test.columns.slice(1)],
+ },
+ });
+ });
+
+ test('it is a noop if the the specified column is unknown', () => {
+ const unknownColumId = 'does.not.exist';
+
+ expect(
+ updateTGridColumnWidth({
+ columnId: unknownColumId,
+ id: 'test',
+ timelineById: defaultTimelineById,
+ width: 90210,
+ })
+ ).toEqual({
+ ...defaultTimelineById,
+ test: {
+ ...defaultTimelineById.test,
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/timelines/public/store/t_grid/helpers.ts b/x-pack/plugins/timelines/public/store/t_grid/helpers.ts
index f7b0d86f88621..34de86d32a9b2 100644
--- a/x-pack/plugins/timelines/public/store/t_grid/helpers.ts
+++ b/x-pack/plugins/timelines/public/store/t_grid/helpers.ts
@@ -8,6 +8,7 @@
import { omit, union } from 'lodash/fp';
import { isEmpty } from 'lodash';
+import { EuiDataGridColumn } from '@elastic/eui';
import type { ToggleDetailPanel } from './actions';
import { TGridPersistInput, TimelineById, TimelineId } from './types';
import type { TGridModel, TGridModelSettings } from './model';
@@ -232,6 +233,63 @@ export const applyDeltaToTimelineColumnWidth = ({
};
};
+type Columns = Array<
+ Pick & ColumnHeaderOptions
+>;
+
+export const updateTGridColumnOrder = ({
+ columnIds,
+ id,
+ timelineById,
+}: {
+ columnIds: string[];
+ id: string;
+ timelineById: TimelineById;
+}): TimelineById => {
+ const timeline = timelineById[id];
+
+ const columns = columnIds.reduce((acc, cid) => {
+ const columnIndex = timeline.columns.findIndex((c) => c.id === cid);
+
+ return columnIndex !== -1 ? [...acc, timeline.columns[columnIndex]] : acc;
+ }, []);
+
+ return {
+ ...timelineById,
+ [id]: {
+ ...timeline,
+ columns,
+ },
+ };
+};
+
+export const updateTGridColumnWidth = ({
+ columnId,
+ id,
+ timelineById,
+ width,
+}: {
+ columnId: string;
+ id: string;
+ timelineById: TimelineById;
+ width: number;
+}): TimelineById => {
+ const timeline = timelineById[id];
+
+ const columns = timeline.columns.map((x) => ({
+ ...x,
+ initialWidth: x.id === columnId ? width : x.initialWidth,
+ }));
+
+ return {
+ ...timelineById,
+ [id]: {
+ ...timeline,
+ columns,
+ },
+ };
+};
+
interface UpdateTimelineColumnsParams {
id: string;
columns: ColumnHeaderOptions[];
diff --git a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts
index d29240d5658db..d3af1dc4e9b30 100644
--- a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts
+++ b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts
@@ -23,7 +23,9 @@ import {
setSelected,
setTimelineUpdatedAt,
toggleDetailPanel,
+ updateColumnOrder,
updateColumns,
+ updateColumnWidth,
updateIsLoading,
updateItemsPerPage,
updateItemsPerPageOptions,
@@ -40,6 +42,8 @@ import {
setDeletedTimelineEvents,
setLoadingTimelineEvents,
setSelectedTimelineEvents,
+ updateTGridColumnOrder,
+ updateTGridColumnWidth,
updateTimelineColumns,
updateTimelineItemsPerPage,
updateTimelinePerPageOptions,
@@ -91,6 +95,23 @@ export const tGridReducer = reducerWithInitialState(initialTGridState)
timelineById: state.timelineById,
}),
}))
+ .case(updateColumnOrder, (state, { id, columnIds }) => ({
+ ...state,
+ timelineById: updateTGridColumnOrder({
+ columnIds,
+ id,
+ timelineById: state.timelineById,
+ }),
+ }))
+ .case(updateColumnWidth, (state, { id, columnId, width }) => ({
+ ...state,
+ timelineById: updateTGridColumnWidth({
+ columnId,
+ id,
+ timelineById: state.timelineById,
+ width,
+ }),
+ }))
.case(removeColumn, (state, { id, columnId }) => ({
...state,
timelineById: removeTimelineColumn({
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index a563079a101c3..52446ddc4a89f 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -4619,8 +4619,6 @@
"telemetry.provideUsageStatisticsTitle": "使用統計を提供",
"telemetry.readOurUsageDataPrivacyStatementLinkText": "プライバシーポリシー",
"telemetry.securityData": "Endpoint Security データ",
- "telemetry.seeExampleOfClusterData": "収集する {clusterData} の例をご覧ください。",
- "telemetry.seeExampleOfClusterDataAndEndpointSecuity": "当社が収集する{clusterData}および{endpointSecurityData}の例をご覧ください。",
"telemetry.telemetryBannerDescription": "Elastic Stackの改善にご協力ください使用状況データの収集は現在無効です。使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。",
"telemetry.telemetryConfigAndLinkDescription": "使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。",
"telemetry.telemetryConfigDescription": "基本的な機能の利用状況に関する統計情報を提供して、Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。",
@@ -10810,7 +10808,6 @@
"xpack.fleet.enrollmentInstructions.moreInstructionsLink": "Elastic エージェントドキュメント",
"xpack.fleet.enrollmentInstructions.moreInstructionsText": "RPM/DEB デプロイの手順については、{link}を参照してください。",
"xpack.fleet.enrollmentInstructions.platformSelectAriaLabel": "プラットフォーム",
- "xpack.fleet.enrollmentInstructions.platformSelectLabel": "プラットフォーム",
"xpack.fleet.enrollmentInstructions.troubleshootingLink": "トラブルシューティングガイド",
"xpack.fleet.enrollmentInstructions.troubleshootingText": "接続の問題が発生している場合は、{link}を参照してください。",
"xpack.fleet.enrollmentStepAgentPolicy.enrollmentTokenSelectLabel": "登録トークン",
@@ -10907,7 +10904,6 @@
"xpack.fleet.fleetServerSetup.generateServiceTokenDescription": "サービストークンは、Elasticsearchに書き込むためのFleetサーバーアクセス権を付与します。",
"xpack.fleet.fleetServerSetup.installAgentDescription": "エージェントディレクトリから、適切なクイックスタートコマンドをコピーして実行し、生成されたトークンと自己署名証明書を使用して、ElasticエージェントをFleetサーバーとして起動します。本番デプロイで独自の証明書を使用する手順については、{userGuideLink}を参照してください。すべてのコマンドには管理者権限が必要です。",
"xpack.fleet.fleetServerSetup.platformSelectAriaLabel": "プラットフォーム",
- "xpack.fleet.fleetServerSetup.platformSelectLabel": "プラットフォーム",
"xpack.fleet.fleetServerSetup.productionText": "本番運用",
"xpack.fleet.fleetServerSetup.quickStartText": "クイックスタート",
"xpack.fleet.fleetServerSetup.saveServiceTokenDescription": "サービストークン情報を保存します。これは1回だけ表示されます。",
@@ -24617,7 +24613,6 @@
"xpack.timelines.timeline.fieldTooltip": "フィールド",
"xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "列を削除",
"xpack.timelines.timeline.fullScreenButton": "全画面",
- "xpack.timelines.timeline.hideColumnLabel": "列を非表示",
"xpack.timelines.timeline.openedAlertFailedToastMessage": "アラートを開けませんでした",
"xpack.timelines.timeline.openSelectedTitle": "選択した項目を開く",
"xpack.timelines.timeline.properties.timelineToggleButtonAriaLabel": "タイムライン {title} を{isOpen, select, false {開く} true {閉じる} other {切り替える}}",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 4781e206cac20..10cfb10a01445 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -4665,8 +4665,6 @@
"telemetry.provideUsageStatisticsTitle": "提供使用情况统计",
"telemetry.readOurUsageDataPrivacyStatementLinkText": "隐私声明",
"telemetry.securityData": "终端安全数据",
- "telemetry.seeExampleOfClusterData": "查看我们收集的{clusterData}的示例。",
- "telemetry.seeExampleOfClusterDataAndEndpointSecuity": "查看我们收集的{clusterData}和 {endpointSecurityData}示例。",
"telemetry.telemetryBannerDescription": "想帮助我们改进 Elastic Stack?数据使用情况收集当前已禁用。启用使用情况数据收集可帮助我们管理并改善产品和服务。有关更多详情,请参阅我们的{privacyStatementLink}。",
"telemetry.telemetryConfigAndLinkDescription": "启用使用情况数据收集可帮助我们管理并改善产品和服务。有关更多详情,请参阅我们的{privacyStatementLink}。",
"telemetry.telemetryConfigDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。",
@@ -10925,7 +10923,6 @@
"xpack.fleet.enrollmentInstructions.moreInstructionsLink": "Elastic 代理文档",
"xpack.fleet.enrollmentInstructions.moreInstructionsText": "有关 RPM/DEB 部署说明,请参见 {link}。",
"xpack.fleet.enrollmentInstructions.platformSelectAriaLabel": "平台",
- "xpack.fleet.enrollmentInstructions.platformSelectLabel": "平台",
"xpack.fleet.enrollmentInstructions.troubleshootingLink": "故障排除指南",
"xpack.fleet.enrollmentInstructions.troubleshootingText": "如果有连接问题,请参阅我们的{link}。",
"xpack.fleet.enrollmentStepAgentPolicy.enrollmentTokenSelectLabel": "注册令牌",
@@ -11022,7 +11019,6 @@
"xpack.fleet.fleetServerSetup.generateServiceTokenDescription": "服务令牌授予 Fleet 服务器向 Elasticsearch 写入的权限。",
"xpack.fleet.fleetServerSetup.installAgentDescription": "从代理目录中,复制并运行适当的快速启动命令,以使用生成的令牌和自签名证书将 Elastic 代理启动为 Fleet 服务器。有关如何将自己的证书用于生产部署,请参阅 {userGuideLink}。所有命令都需要管理员权限。",
"xpack.fleet.fleetServerSetup.platformSelectAriaLabel": "平台",
- "xpack.fleet.fleetServerSetup.platformSelectLabel": "平台",
"xpack.fleet.fleetServerSetup.productionText": "生产",
"xpack.fleet.fleetServerSetup.quickStartText": "快速启动",
"xpack.fleet.fleetServerSetup.saveServiceTokenDescription": "保存服务令牌信息。其仅显示一次。",
@@ -25034,7 +25030,6 @@
"xpack.timelines.timeline.fieldTooltip": "字段",
"xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "移除列",
"xpack.timelines.timeline.fullScreenButton": "全屏",
- "xpack.timelines.timeline.hideColumnLabel": "隐藏列",
"xpack.timelines.timeline.openedAlertFailedToastMessage": "无法打开告警",
"xpack.timelines.timeline.openedAlertSuccessToastMessage": "已成功打开 {totalAlerts} 个{totalAlerts, plural, other {告警}}。",
"xpack.timelines.timeline.openSelectedTitle": "打开所选",
diff --git a/x-pack/plugins/triggers_actions_ui/server/index.ts b/x-pack/plugins/triggers_actions_ui/server/index.ts
index c7d363af45247..72ca584250d03 100644
--- a/x-pack/plugins/triggers_actions_ui/server/index.ts
+++ b/x-pack/plugins/triggers_actions_ui/server/index.ts
@@ -31,6 +31,7 @@ export const config: PluginConfigDescriptor = {
const triggersActionsUi = get(settings, fromPath);
if (triggersActionsUi?.enabled === false || triggersActionsUi?.enabled === true) {
addDeprecation({
+ configPath: 'xpack.trigger_actions_ui.enabled',
message: `"xpack.trigger_actions_ui.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`,
correctiveActions: {
manualSteps: [`Remove "xpack.trigger_actions_ui.enabled" from your kibana configs.`],
diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts
index 1975d0abaf11d..fdd8a1c993937 100644
--- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts
+++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts
@@ -35,7 +35,7 @@ describe('Default deprecation flyout', () => {
testBed.component.update();
});
- it('renders a flyout with deprecation details', async () => {
+ test('renders a flyout with deprecation details', async () => {
const multiFieldsDeprecation = esDeprecationsMockResponse.deprecations[2];
const { actions, find, exists } = testBed;
@@ -45,6 +45,9 @@ describe('Default deprecation flyout', () => {
expect(find('defaultDeprecationDetails.flyoutTitle').text()).toContain(
multiFieldsDeprecation.message
);
+ expect(find('defaultDeprecationDetails.documentationLink').props().href).toBe(
+ multiFieldsDeprecation.url
+ );
expect(find('defaultDeprecationDetails.flyoutDescription').text()).toContain(
multiFieldsDeprecation.index
);
diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts
index 145cea24dde8b..f62d24081ed56 100644
--- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts
+++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts
@@ -33,16 +33,21 @@ describe('Index settings deprecation flyout', () => {
testBed = await setupElasticsearchPage({ isReadOnlyMode: false });
});
- const { find, exists, actions, component } = testBed;
-
+ const { actions, component } = testBed;
component.update();
-
await actions.table.clickDeprecationRowAt('indexSetting', 0);
+ });
+
+ test('renders a flyout with deprecation details', async () => {
+ const { find, exists } = testBed;
expect(exists('indexSettingsDetails')).toBe(true);
expect(find('indexSettingsDetails.flyoutTitle').text()).toContain(
indexSettingDeprecation.message
);
+ expect(find('indexSettingsDetails.documentationLink').props().href).toBe(
+ indexSettingDeprecation.url
+ );
expect(exists('removeSettingsPrompt')).toBe(true);
});
diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts
index 6bcb3fa95985c..b24cd5a69a28e 100644
--- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts
+++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts
@@ -35,16 +35,19 @@ describe('Machine learning deprecation flyout', () => {
testBed = await setupElasticsearchPage({ isReadOnlyMode: false });
});
- const { find, exists, actions, component } = testBed;
-
+ const { actions, component } = testBed;
component.update();
-
await actions.table.clickDeprecationRowAt('mlSnapshot', 0);
+ });
+
+ test('renders a flyout with deprecation details', async () => {
+ const { find, exists } = testBed;
expect(exists('mlSnapshotDetails')).toBe(true);
expect(find('mlSnapshotDetails.flyoutTitle').text()).toContain(
'Upgrade or delete model snapshot'
);
+ expect(find('mlSnapshotDetails.documentationLink').props().href).toBe(mlDeprecation.url);
});
describe('upgrade snapshots', () => {
diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts
index 1767143fdf527..6a3d376acecab 100644
--- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts
+++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts
@@ -22,6 +22,7 @@ const kibanaDeprecations: DomainDeprecationDetails[] = [
title: 'Test deprecation title 1',
message: 'Test deprecation message 1',
deprecationType: 'config',
+ configPath: 'test',
},
{
correctiveActions: {
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx
index c436d585db9ae..6ec05b0c4fc99 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx
@@ -17,12 +17,11 @@ import {
EuiTitle,
EuiText,
EuiTextColor,
- EuiLink,
EuiSpacer,
} from '@elastic/eui';
import { EnrichedDeprecationInfo } from '../../../../../../common/types';
-import { DeprecationBadge } from '../../../shared';
+import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../../../shared';
export interface DefaultDeprecationFlyoutProps {
deprecation: EnrichedDeprecationInfo;
@@ -40,12 +39,6 @@ const i18nTexts = {
},
}
),
- learnMoreLinkLabel: i18n.translate(
- 'xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.learnMoreLinkLabel',
- {
- defaultMessage: 'Learn more about this deprecation',
- }
- ),
closeButtonLabel: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.closeButtonLabel',
{
@@ -80,9 +73,7 @@ export const DefaultDeprecationFlyout = ({
{details}
-
- {i18nTexts.learnMoreLinkLabel}
-
+
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx
index 3c19268a293f0..d0aac8ee922f7 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx
@@ -19,7 +19,6 @@ import {
EuiTitle,
EuiText,
EuiTextColor,
- EuiLink,
EuiSpacer,
EuiCallOut,
} from '@elastic/eui';
@@ -30,7 +29,7 @@ import {
ResponseError,
} from '../../../../../../common/types';
import type { Status } from '../../../types';
-import { DeprecationBadge } from '../../../shared';
+import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../../../shared';
export interface RemoveIndexSettingsFlyoutProps {
deprecation: EnrichedDeprecationInfo;
@@ -53,12 +52,6 @@ const i18nTexts = {
},
}
),
- learnMoreLinkLabel: i18n.translate(
- 'xpack.upgradeAssistant.esDeprecations.removeSettingsFlyout.learnMoreLinkLabel',
- {
- defaultMessage: 'Learn more about this deprecation',
- }
- ),
removeButtonLabel: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.removeSettingsFlyout.removeButtonLabel',
{
@@ -146,9 +139,7 @@ export const RemoveIndexSettingsFlyout = ({
{details}
-
- {i18nTexts.learnMoreLinkLabel}
-
+
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx
index 4e3d77ba72ae8..c4145bf3d4146 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx
@@ -25,9 +25,9 @@ import {
} from '@elastic/eui';
import { EnrichedDeprecationInfo } from '../../../../../../common/types';
-import { DeprecationBadge } from '../../../shared';
-import { MlSnapshotContext } from './context';
import { useAppContext } from '../../../../app_context';
+import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../../../shared';
+import { MlSnapshotContext } from './context';
import { SnapshotState } from './use_snapshot_state';
export interface FixSnapshotsFlyoutProps extends MlSnapshotContext {
@@ -93,12 +93,6 @@ const i18nTexts = {
defaultMessage: 'Error upgrading snapshot',
}
),
- learnMoreLinkLabel: i18n.translate(
- 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.learnMoreLinkLabel',
- {
- defaultMessage: 'Learn more about this deprecation',
- }
- ),
upgradeModeEnabledErrorTitle: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.mlSnapshots.upgradeModeEnabledErrorTitle',
{
@@ -229,9 +223,7 @@ export const FixSnapshotsFlyout = ({
{deprecation.details}
-
- {i18nTexts.learnMoreLinkLabel}
-
+
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx
index 577354b35fa4e..5d10350caad9e 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx
@@ -20,12 +20,11 @@ import {
EuiTitle,
EuiText,
EuiCallOut,
- EuiLink,
EuiSpacer,
} from '@elastic/eui';
+import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../shared';
import type { DeprecationResolutionState, KibanaDeprecationDetails } from './kibana_deprecations';
-import { DeprecationBadge } from '../shared';
import './_deprecation_details_flyout.scss';
@@ -37,12 +36,6 @@ export interface DeprecationDetailsFlyoutProps {
}
const i18nTexts = {
- learnMoreLinkLabel: i18n.translate(
- 'xpack.upgradeAssistant.kibanaDeprecations.flyout.learnMoreLinkLabel',
- {
- defaultMessage: 'Learn more about this deprecation',
- }
- ),
closeButtonLabel: i18n.translate(
'xpack.upgradeAssistant.kibanaDeprecations.flyout.closeButtonLabel',
{
@@ -162,12 +155,9 @@ export const DeprecationDetailsFlyout = ({
{message}
-
{documentationUrl && (
-
- {i18nTexts.learnMoreLinkLabel}
-
+
)}
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx
index 23697b00923c8..013f59a7dcf56 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx
@@ -75,10 +75,10 @@ export interface DeprecationResolutionState {
resolveDeprecationError?: string;
}
-export interface KibanaDeprecationDetails extends DomainDeprecationDetails {
+export type KibanaDeprecationDetails = DomainDeprecationDetails & {
id: string;
filterType: DomainDeprecationDetails['deprecationType'] | 'uncategorized';
-}
+};
const getDeprecationCountByLevel = (deprecations: KibanaDeprecationDetails[]) => {
const criticalDeprecations: KibanaDeprecationDetails[] = [];
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx
index b061ab5ea2d4d..590bfac96770d 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx
@@ -70,7 +70,7 @@ export const getFixIssuesStep = ({
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx
index 3312508a87073..32d214f0d80f2 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx
@@ -6,10 +6,11 @@
*/
import React, { FunctionComponent } from 'react';
-
import { EuiFlexGroup, EuiFlexItem, EuiHealth } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { LevelInfoTip } from './level_info_tip';
+
const i18nTexts = {
getCriticalStatusLabel: (count: number) =>
i18n.translate('xpack.upgradeAssistant.deprecationCount.criticalStatusLabel', {
@@ -39,14 +40,31 @@ export const DeprecationCount: FunctionComponent = ({
return (
-
- {i18nTexts.getCriticalStatusLabel(totalCriticalDeprecations)}
-
+
+
+
+ {i18nTexts.getCriticalStatusLabel(totalCriticalDeprecations)}
+
+
+
+
+
+
+
+
-
- {i18nTexts.getWarningStatusLabel(totalWarningDeprecations)}
-
+
+
+
+ {i18nTexts.getWarningStatusLabel(totalWarningDeprecations)}
+
+
+
+
+
+
+
);
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.tsx
new file mode 100644
index 0000000000000..da8c83597f7e2
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.tsx
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiLink } from '@elastic/eui';
+
+interface Props {
+ documentationUrl?: string;
+}
+
+export const DeprecationFlyoutLearnMoreLink = ({ documentationUrl }: Props) => {
+ return (
+
+ {i18n.translate('xpack.upgradeAssistant.deprecationFlyout.learnMoreLinkLabel', {
+ defaultMessage: 'Learn more',
+ })}
+
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts
index ef7916f6e8d17..34496e1e8eb55 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts
@@ -9,3 +9,5 @@ export { NoDeprecationsPrompt } from './no_deprecations';
export { DeprecationCount } from './deprecation_count';
export { DeprecationBadge } from './deprecation_badge';
export { DeprecationsPageLoadingError } from './deprecations_page_loading_error';
+export { DeprecationFlyoutLearnMoreLink } from './deprecation_flyout_learn_more_link';
+export { LevelInfoTip } from './level_info_tip';
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx
new file mode 100644
index 0000000000000..d3600a7290b4e
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiIconTip } from '@elastic/eui';
+
+const i18nTexts = {
+ critical: i18n.translate('xpack.upgradeAssistant.levelInfoTip.criticalLabel', {
+ defaultMessage: 'Critical issues must be resolved before you upgrade',
+ }),
+ warning: i18n.translate('xpack.upgradeAssistant.levelInfoTip.warningLabel', {
+ defaultMessage: 'Warning issues can be ignored at your discretion',
+ }),
+};
+
+interface Props {
+ level: 'critical' | 'warning';
+}
+
+export const LevelInfoTip: FunctionComponent = ({ level }) => {
+ return ;
+};
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/kibana_status.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/kibana_status.test.ts
index 63532543a418b..296a313abef38 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/kibana_status.test.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/kibana_status.test.ts
@@ -20,7 +20,7 @@ const mockKibanaDeprecations: DomainDeprecationDetails[] = [
'Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role.',
],
},
- deprecationType: 'config',
+ deprecationType: 'feature',
documentationUrl: 'testDocUrl',
level: 'critical',
message: 'testMessage',
diff --git a/x-pack/plugins/uptime/e2e/tasks/es_archiver.ts b/x-pack/plugins/uptime/e2e/tasks/es_archiver.ts
index ce82be18dff7f..dac5672bdf649 100644
--- a/x-pack/plugins/uptime/e2e/tasks/es_archiver.ts
+++ b/x-pack/plugins/uptime/e2e/tasks/es_archiver.ts
@@ -17,7 +17,7 @@ export const esArchiverLoad = (folder: string) => {
const path = Path.join(ES_ARCHIVE_DIR, folder);
execSync(
`node ../../../../scripts/es_archiver load "${path}" --config ../../../test/functional/config.js`,
- { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED } }
+ { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' }
);
};
@@ -25,13 +25,13 @@ export const esArchiverUnload = (folder: string) => {
const path = Path.join(ES_ARCHIVE_DIR, folder);
execSync(
`node ../../../../scripts/es_archiver unload "${path}" --config ../../../test/functional/config.js`,
- { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED } }
+ { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' }
);
};
export const esArchiverResetKibana = () => {
execSync(
`node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.js`,
- { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED } }
+ { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' }
);
};
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx
index f16e72837b343..26ee26cc8ed7f 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx
@@ -50,7 +50,8 @@ const defaultValidation = centralValidation[DataStream.HTTP];
const defaultHTTPConfig = defaultConfig[DataStream.HTTP];
const defaultTCPConfig = defaultConfig[DataStream.TCP];
-describe('', () => {
+// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699
+describe.skip('', () => {
const WrappedComponent = ({
validate = defaultValidation,
typeEditable = false,
diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.test.tsx
index d044ad4e6a3a2..240697af470b0 100644
--- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.test.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.test.tsx
@@ -5,7 +5,6 @@
* 2.0.
*/
-import 'jest';
import React from 'react';
import { MonitorListDrawerComponent } from './monitor_list_drawer';
import { MonitorDetails, MonitorSummary, makePing } from '../../../../../common/runtime_types';
diff --git a/x-pack/plugins/uptime/public/hooks/use_url_params.ts b/x-pack/plugins/uptime/public/hooks/use_url_params.ts
index 329e0ccef4d96..1318b635693c7 100644
--- a/x-pack/plugins/uptime/public/hooks/use_url_params.ts
+++ b/x-pack/plugins/uptime/public/hooks/use_url_params.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useCallback, useEffect, useMemo } from 'react';
+import { useCallback, useEffect } from 'react';
import { parse, stringify } from 'query-string';
import { useLocation, useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
@@ -28,7 +28,7 @@ const getParsedParams = (search: string) => {
export const useGetUrlParams: GetUrlParams = () => {
const { search } = useLocation();
- return useMemo(() => getSupportedUrlParams(getParsedParams(search)), [search]);
+ return getSupportedUrlParams(getParsedParams(search));
};
const getMapFromFilters = (value: any): Map | undefined => {
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts
index 7d69e80dae584..7e8272b0a8afa 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts
@@ -232,31 +232,8 @@ export default function jiraTest({ getService }: FtrProviderContext) {
expect(resp.body.connector_id).to.eql(simulatedActionId);
expect(resp.body.status).to.eql('error');
expect(resp.body.retry).to.eql(false);
- // Node.js 12 oddity:
- //
- // The first time after the server is booted, the error message will be:
- //
- // undefined is not iterable (cannot read property Symbol(Symbol.iterator))
- //
- // After this, the error will be:
- //
- // Cannot destructure property 'value' of 'undefined' as it is undefined.
- //
- // The error seems to come from the exact same place in the code based on the
- // exact same circomstances:
- //
- // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28
- //
- // What triggers the error is that the `handleError` function expects its 2nd
- // argument to be an object containing a `valids` property of type array.
- //
- // In this test the object does not contain a `valids` property, so hence the
- // error.
- //
- // Why the error message isn't the same in all scenarios is unknown to me and
- // could be a bug in V8.
- expect(resp.body.message).to.match(
- /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/
+ expect(resp.body.message).to.be(
+ `error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined.`
);
});
});
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts
index 00989b35fd4e2..4421c984b4aed 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts
@@ -234,31 +234,8 @@ export default function resilientTest({ getService }: FtrProviderContext) {
expect(resp.body.connector_id).to.eql(simulatedActionId);
expect(resp.body.status).to.eql('error');
expect(resp.body.retry).to.eql(false);
- // Node.js 12 oddity:
- //
- // The first time after the server is booted, the error message will be:
- //
- // undefined is not iterable (cannot read property Symbol(Symbol.iterator))
- //
- // After this, the error will be:
- //
- // Cannot destructure property 'value' of 'undefined' as it is undefined.
- //
- // The error seems to come from the exact same place in the code based on the
- // exact same circomstances:
- //
- // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28
- //
- // What triggers the error is that the `handleError` function expects its 2nd
- // argument to be an object containing a `valids` property of type array.
- //
- // In this test the object does not contain a `valids` property, so hence the
- // error.
- //
- // Why the error message isn't the same in all scenarios is unknown to me and
- // could be a bug in V8.
- expect(resp.body.message).to.match(
- /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/
+ expect(resp.body.message).to.be(
+ `error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined.`
);
});
});
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts
index fe1ebdf8d28a9..5ff1663975145 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts
@@ -242,31 +242,8 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) {
expect(resp.body.connector_id).to.eql(simulatedActionId);
expect(resp.body.status).to.eql('error');
expect(resp.body.retry).to.eql(false);
- // Node.js 12 oddity:
- //
- // The first time after the server is booted, the error message will be:
- //
- // undefined is not iterable (cannot read property Symbol(Symbol.iterator))
- //
- // After this, the error will be:
- //
- // Cannot destructure property 'value' of 'undefined' as it is undefined.
- //
- // The error seems to come from the exact same place in the code based on the
- // exact same circumstances:
- //
- // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28
- //
- // What triggers the error is that the `handleError` function expects its 2nd
- // argument to be an object containing a `valids` property of type array.
- //
- // In this test the object does not contain a `valids` property, so hence the
- // error.
- //
- // Why the error message isn't the same in all scenarios is unknown to me and
- // could be a bug in V8.
- expect(resp.body.message).to.match(
- /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/
+ expect(resp.body.message).to.be(
+ `error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined.`
);
});
});
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts
index eee3425b6a61f..bc4ec43fb4c7b 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts
@@ -246,31 +246,8 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) {
expect(resp.body.connector_id).to.eql(simulatedActionId);
expect(resp.body.status).to.eql('error');
expect(resp.body.retry).to.eql(false);
- // Node.js 12 oddity:
- //
- // The first time after the server is booted, the error message will be:
- //
- // undefined is not iterable (cannot read property Symbol(Symbol.iterator))
- //
- // After this, the error will be:
- //
- // Cannot destructure property 'value' of 'undefined' as it is undefined.
- //
- // The error seems to come from the exact same place in the code based on the
- // exact same circumstances:
- //
- // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28
- //
- // What triggers the error is that the `handleError` function expects its 2nd
- // argument to be an object containing a `valids` property of type array.
- //
- // In this test the object does not contain a `valids` property, so hence the
- // error.
- //
- // Why the error message isn't the same in all scenarios is unknown to me and
- // could be a bug in V8.
- expect(resp.body.message).to.match(
- /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/
+ expect(resp.body.message).to.be(
+ `error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined.`
);
});
});
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts
index eae630593b4df..93d3a6c9e003f 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts
@@ -323,31 +323,8 @@ export default function swimlaneTest({ getService }: FtrProviderContext) {
expect(resp.body.connector_id).to.eql(simulatedActionId);
expect(resp.body.status).to.eql('error');
expect(resp.body.retry).to.eql(false);
- // Node.js 12 oddity:
- //
- // The first time after the server is booted, the error message will be:
- //
- // undefined is not iterable (cannot read property Symbol(Symbol.iterator))
- //
- // After this, the error will be:
- //
- // Cannot destructure property 'value' of 'undefined' as it is undefined.
- //
- // The error seems to come from the exact same place in the code based on the
- // exact same circomstances:
- //
- // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28
- //
- // What triggers the error is that the `handleError` function expects its 2nd
- // argument to be an object containing a `valids` property of type array.
- //
- // In this test the object does not contain a `valids` property, so hence the
- // error.
- //
- // Why the error message isn't the same in all scenarios is unknown to me and
- // could be a bug in V8.
- expect(resp.body.message).to.match(
- /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/
+ expect(resp.body.message).to.be(
+ `error validating action params: undefined is not iterable (cannot read property Symbol(Symbol.iterator))`
);
});
});
diff --git a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts
index 00b820a025c8b..2742fbff294c0 100644
--- a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts
@@ -44,7 +44,7 @@ export default ({ getService }: FtrProviderContext) => {
user: USER.ML_POWERUSER,
expected: {
responseCode: 200,
- moduleIds: ['apm_jsbase', 'apm_nodejs'],
+ moduleIds: ['apm_jsbase', 'apm_transaction', 'apm_nodejs'],
},
},
{
diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
index 6ff6b8113cb1a..c4dd529ac14f5 100644
--- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
@@ -187,11 +187,9 @@ export default ({ getService }: FtrProviderContext) => {
dashboards: [] as string[],
},
},
- // Set startDatafeed and estimateModelMemory to false for the APM transaction test
- // until there is a new data set available with metric data.
{
testTitleSuffix:
- 'for apm_transaction with prefix, startDatafeed false and estimateModelMemory false',
+ 'for apm_transaction with prefix, startDatafeed true and estimateModelMemory true',
sourceDataArchive: 'x-pack/test/functional/es_archives/ml/module_apm',
indexPattern: { name: 'ft_module_apm', timeField: '@timestamp' },
module: 'apm_transaction',
@@ -199,14 +197,14 @@ export default ({ getService }: FtrProviderContext) => {
requestBody: {
prefix: 'pf5_',
indexPatternName: 'ft_module_apm',
- startDatafeed: false,
- estimateModelMemory: false,
+ startDatafeed: true,
+ end: Date.now(),
},
expected: {
responseCode: 200,
jobs: [
{
- jobId: 'pf5_apm_metrics',
+ jobId: 'pf5_high_mean_transaction_duration',
jobState: JOB_STATE.CLOSED,
datafeedState: DATAFEED_STATE.STOPPED,
},
diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts
index 8caae0afe746e..c15a7d39a6cf6 100644
--- a/x-pack/test/apm_api_integration/tests/index.ts
+++ b/x-pack/test/apm_api_integration/tests/index.ts
@@ -229,6 +229,10 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte
loadTestFile(require.resolve('./historical_data/has_data'));
});
+ describe('latency/service_apis', function () {
+ loadTestFile(require.resolve('./latency/service_apis'));
+ });
+
registry.run(providerContext);
});
}
diff --git a/x-pack/test/apm_api_integration/tests/latency/service_apis.ts b/x-pack/test/apm_api_integration/tests/latency/service_apis.ts
new file mode 100644
index 0000000000000..a09442cd73a2a
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/latency/service_apis.ts
@@ -0,0 +1,191 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { service, timerange } from '@elastic/apm-generator';
+import expect from '@kbn/expect';
+import { meanBy, sumBy } from 'lodash';
+import { LatencyAggregationType } from '../../../../plugins/apm/common/latency_aggregation_types';
+import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number';
+import { PromiseReturnType } from '../../../../plugins/observability/typings/common';
+import { FtrProviderContext } from '../../common/ftr_provider_context';
+import { registry } from '../../common/registry';
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const apmApiClient = getService('apmApiClient');
+ const traceData = getService('traceData');
+
+ const serviceName = 'synth-go';
+ const start = new Date('2021-01-01T00:00:00.000Z').getTime();
+ const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
+
+ async function getLatencyValues({
+ processorEvent,
+ latencyAggregationType = LatencyAggregationType.avg,
+ }: {
+ processorEvent: 'transaction' | 'metric';
+ latencyAggregationType?: LatencyAggregationType;
+ }) {
+ const commonQuery = {
+ start: new Date(start).toISOString(),
+ end: new Date(end).toISOString(),
+ environment: 'ENVIRONMENT_ALL',
+ };
+ const [
+ serviceInventoryAPIResponse,
+ serviceLantencyAPIResponse,
+ transactionsGroupDetailsAPIResponse,
+ serviceInstancesAPIResponse,
+ ] = await Promise.all([
+ apmApiClient.readUser({
+ endpoint: 'GET /internal/apm/services',
+ params: {
+ query: {
+ ...commonQuery,
+ kuery: `service.name : "${serviceName}" and processor.event : "${processorEvent}"`,
+ },
+ },
+ }),
+ apmApiClient.readUser({
+ endpoint: 'GET /internal/apm/services/{serviceName}/transactions/charts/latency',
+ params: {
+ path: { serviceName },
+ query: {
+ ...commonQuery,
+ kuery: `processor.event : "${processorEvent}"`,
+ latencyAggregationType,
+ transactionType: 'request',
+ },
+ },
+ }),
+ apmApiClient.readUser({
+ endpoint: `GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics`,
+ params: {
+ path: { serviceName },
+ query: {
+ ...commonQuery,
+ kuery: `processor.event : "${processorEvent}"`,
+ transactionType: 'request',
+ latencyAggregationType: 'avg' as LatencyAggregationType,
+ },
+ },
+ }),
+ apmApiClient.readUser({
+ endpoint: `GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`,
+ params: {
+ path: { serviceName },
+ query: {
+ ...commonQuery,
+ kuery: `processor.event : "${processorEvent}"`,
+ transactionType: 'request',
+ latencyAggregationType: 'avg' as LatencyAggregationType,
+ },
+ },
+ }),
+ ]);
+
+ const serviceInventoryLatency = serviceInventoryAPIResponse.body.items[0].latency;
+
+ const latencyChartApiMean = meanBy(
+ serviceLantencyAPIResponse.body.currentPeriod.latencyTimeseries.filter(
+ (item) => isFiniteNumber(item.y) && item.y > 0
+ ),
+ 'y'
+ );
+
+ const transactionsGroupLatencySum = sumBy(
+ transactionsGroupDetailsAPIResponse.body.transactionGroups,
+ 'latency'
+ );
+
+ const serviceInstancesLatencySum = sumBy(
+ serviceInstancesAPIResponse.body.currentPeriod,
+ 'latency'
+ );
+
+ return {
+ serviceInventoryLatency,
+ latencyChartApiMean,
+ transactionsGroupLatencySum,
+ serviceInstancesLatencySum,
+ };
+ }
+
+ let latencyMetricValues: PromiseReturnType;
+ let latencyTransactionValues: PromiseReturnType;
+
+ registry.when('Services APIs', { config: 'basic', archives: ['apm_8.0.0_empty'] }, () => {
+ describe('when data is loaded ', () => {
+ const GO_PROD_RATE = 80;
+ const GO_DEV_RATE = 20;
+ const GO_PROD_DURATION = 1000;
+ const GO_DEV_DURATION = 500;
+ before(async () => {
+ const serviceGoProdInstance = service(serviceName, 'production', 'go').instance(
+ 'instance-a'
+ );
+ const serviceGoDevInstance = service(serviceName, 'development', 'go').instance(
+ 'instance-b'
+ );
+ await traceData.index([
+ ...timerange(start, end)
+ .interval('1m')
+ .rate(GO_PROD_RATE)
+ .flatMap((timestamp) =>
+ serviceGoProdInstance
+ .transaction('GET /api/product/list')
+ .duration(GO_PROD_DURATION)
+ .timestamp(timestamp)
+ .serialize()
+ ),
+ ...timerange(start, end)
+ .interval('1m')
+ .rate(GO_DEV_RATE)
+ .flatMap((timestamp) =>
+ serviceGoDevInstance
+ .transaction('GET /api/product/:id')
+ .duration(GO_DEV_DURATION)
+ .timestamp(timestamp)
+ .serialize()
+ ),
+ ]);
+ });
+
+ after(() => traceData.clean());
+
+ describe('compare latency value between service inventory, latency chart, service inventory and transactions apis', () => {
+ before(async () => {
+ [latencyTransactionValues, latencyMetricValues] = await Promise.all([
+ getLatencyValues({ processorEvent: 'transaction' }),
+ getLatencyValues({ processorEvent: 'metric' }),
+ ]);
+ });
+
+ it('returns same avg latency value for Transaction-based and Metric-based data', () => {
+ const expectedLatencyAvgValueMs =
+ ((GO_PROD_RATE * GO_PROD_DURATION + GO_DEV_RATE * GO_DEV_DURATION) /
+ (GO_PROD_RATE + GO_DEV_RATE)) *
+ 1000;
+ [
+ latencyTransactionValues.latencyChartApiMean,
+ latencyTransactionValues.serviceInventoryLatency,
+ latencyMetricValues.latencyChartApiMean,
+ latencyMetricValues.serviceInventoryLatency,
+ ].forEach((value) => expect(value).to.be.equal(expectedLatencyAvgValueMs));
+ });
+
+ it('returns same sum latency value for Transaction-based and Metric-based data', () => {
+ const expectedLatencySumValueMs = (GO_PROD_DURATION + GO_DEV_DURATION) * 1000;
+ [
+ latencyTransactionValues.transactionsGroupLatencySum,
+ latencyTransactionValues.serviceInstancesLatencySum,
+ latencyMetricValues.transactionsGroupLatencySum,
+ latencyMetricValues.serviceInstancesLatencySum,
+ ].forEach((value) => expect(value).to.be.equal(expectedLatencySumValueMs));
+ });
+ });
+ });
+ });
+}
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts
index 9c169c1c34207..d5e9050ed9d41 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts
@@ -486,7 +486,8 @@ export default ({ getService }: FtrProviderContext) => {
expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]);
});
- it('will return 1 result if we have a list that includes all ips', async () => {
+ // FLAKY https://github.com/elastic/kibana/issues/89052
+ it.skip('will return 1 result if we have a list that includes all ips', async () => {
await importFile(
supertest,
'ip',
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts
index 2a2c8df30981f..e852558aaa6a8 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts
@@ -328,7 +328,8 @@ export default ({ getService }: FtrProviderContext) => {
});
describe('"exists" operator', () => {
- it('will return 1 results if matching against keyword for the empty array', async () => {
+ // FLAKY https://github.com/elastic/kibana/issues/115308
+ it.skip('will return 1 results if matching against keyword for the empty array', async () => {
const rule = getRuleForSignalTesting(['keyword_as_array']);
const { id } = await createRuleWithExceptionEntries(supertest, rule, [
[
@@ -496,7 +497,8 @@ export default ({ getService }: FtrProviderContext) => {
expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]);
});
- it('will return only the empty array for results if we have a list that includes all keyword', async () => {
+ // FLAKY https://github.com/elastic/kibana/issues/115304
+ it.skip('will return only the empty array for results if we have a list that includes all keyword', async () => {
await importFile(
supertest,
'keyword',
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts
index b152b44867a09..f0a5fe7c1ffb1 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts
@@ -326,7 +326,8 @@ export default ({ getService }: FtrProviderContext) => {
});
describe('"exists" operator', () => {
- it('will return 1 results if matching against text for the empty array', async () => {
+ // FLAKY https://github.com/elastic/kibana/issues/115313
+ it.skip('will return 1 results if matching against text for the empty array', async () => {
const rule = getRuleForSignalTesting(['text_as_array']);
const { id } = await createRuleWithExceptionEntries(supertest, rule, [
[
@@ -494,7 +495,8 @@ export default ({ getService }: FtrProviderContext) => {
expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]);
});
- it('will return only the empty array for results if we have a list that includes all text', async () => {
+ // FLAKY https://github.com/elastic/kibana/issues/113418
+ it.skip('will return only the empty array for results if we have a list that includes all text', async () => {
await importFile(
supertest,
'text',
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts
index d25fb5bfa5899..4c0f21df8c0ff 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts
@@ -65,6 +65,27 @@ export default ({ getService }: FtrProviderContext): void => {
undefined
);
});
+
+ it('migrates legacy siem-detection-engine-rule-actions and retains "ruleThrottle" and "alertThrottle" as the same attributes as before', async () => {
+ const response = await es.get<{
+ 'siem-detection-engine-rule-actions': {
+ ruleThrottle: string;
+ alertThrottle: string;
+ };
+ }>({
+ index: '.kibana',
+ id: 'siem-detection-engine-rule-actions:fce024a0-0452-11ec-9b15-d13d79d162f3',
+ });
+ expect(response.statusCode).to.eql(200);
+
+ // "alertThrottle" and "ruleThrottle" should still exist
+ expect(response.body._source?.['siem-detection-engine-rule-actions'].alertThrottle).to.eql(
+ '7d'
+ );
+ expect(response.body._source?.['siem-detection-engine-rule-actions'].ruleThrottle).to.eql(
+ '7d'
+ );
+ });
});
});
};
diff --git a/x-pack/test/fleet_api_integration/apis/epm/index.js b/x-pack/test/fleet_api_integration/apis/epm/index.js
index b6a1fd5d7346d..3428b4c1ded08 100644
--- a/x-pack/test/fleet_api_integration/apis/epm/index.js
+++ b/x-pack/test/fleet_api_integration/apis/epm/index.js
@@ -15,6 +15,7 @@ export default function loadTests({ loadTestFile }) {
loadTestFile(require.resolve('./template'));
loadTestFile(require.resolve('./ilm'));
loadTestFile(require.resolve('./install_by_upload'));
+ loadTestFile(require.resolve('./install_endpoint'));
loadTestFile(require.resolve('./install_overrides'));
loadTestFile(require.resolve('./install_prerelease'));
loadTestFile(require.resolve('./install_remove_assets'));
diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_endpoint.ts b/x-pack/test/fleet_api_integration/apis/epm/install_endpoint.ts
new file mode 100644
index 0000000000000..ba9264e1d1999
--- /dev/null
+++ b/x-pack/test/fleet_api_integration/apis/epm/install_endpoint.ts
@@ -0,0 +1,108 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
+import { skipIfNoDockerRegistry } from '../../helpers';
+import { setupFleetAndAgents } from '../agents/services';
+
+export default function (providerContext: FtrProviderContext) {
+ /**
+ * There are a few features that are only currently supported for the Endpoint
+ * package due to security concerns.
+ */
+ describe('Install endpoint package', () => {
+ const { getService } = providerContext;
+ skipIfNoDockerRegistry(providerContext);
+ setupFleetAndAgents(providerContext);
+
+ const supertest = getService('supertest');
+ const dockerServers = getService('dockerServers');
+ const server = dockerServers.get('registry');
+ const es = getService('es');
+ const pkgName = 'endpoint';
+ let pkgVersion: string;
+
+ const transforms = [
+ {
+ id: 'endpoint.metadata_current-default',
+ dest: 'metrics-endpoint.metadata_current_default',
+ },
+ {
+ id: 'endpoint.metadata_united-default',
+ dest: '.metrics-endpoint.metadata_united_default',
+ },
+ ];
+
+ before(async () => {
+ if (!server.enabled) return;
+ // The latest endpoint package is already installed by default in our FTR config,
+ // just get the most recent version number.
+ const getResp = await supertest.get(`/api/fleet/epm/packages/${pkgName}`).expect(200);
+ pkgVersion = getResp.body.response.version;
+ });
+
+ describe('install', () => {
+ transforms.forEach((transform) => {
+ it(`should have installed the [${transform.id}] transform`, async function () {
+ const res = await es.transport.request({
+ method: 'GET',
+ path: `/_transform/${transform.id}-${pkgVersion}`,
+ });
+ expect(res.statusCode).equal(200);
+ });
+ it(`should have created the destination index for the [${transform.id}] transform`, async function () {
+ // the index is defined in the transform file
+ const res = await es.transport.request({
+ method: 'GET',
+ path: `/${transform.dest}`,
+ });
+ expect(res.statusCode).equal(200);
+ });
+ });
+ });
+
+ const uninstallPackage = async (pkg: string) =>
+ supertest.delete(`/api/fleet/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx');
+
+ // Endpoint doesn't currently support uninstalls
+ describe.skip('uninstall', () => {
+ before(async () => {
+ await uninstallPackage(`${pkgName}-${pkgVersion}`);
+ });
+
+ transforms.forEach((transform) => {
+ it(`should have uninstalled the [${transform.id}] transforms`, async function () {
+ const res = await es.transport.request(
+ {
+ method: 'GET',
+ path: `/_transform/${transform.id}`,
+ },
+ {
+ ignore: [404],
+ }
+ );
+ expect(res.statusCode).equal(404);
+ });
+
+ it(`should have deleted the index for the [${transform.id}] transform`, async function () {
+ // the index is defined in the transform file
+ const res = await es.transport.request(
+ {
+ method: 'GET',
+ path: `/${transform.dest}`,
+ },
+ {
+ ignore: [404],
+ }
+ );
+ expect(res.statusCode).equal(404);
+ });
+ });
+ });
+ });
+}
diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts
index e57899531e939..7e48ed9d297c7 100644
--- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts
+++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts
@@ -155,31 +155,6 @@ export default function (providerContext: FtrProviderContext) {
);
expect(resPipeline2.statusCode).equal(404);
});
- it('should have uninstalled the transforms', async function () {
- const res = await es.transport.request(
- {
- method: 'GET',
- path: `/_transform/${pkgName}-test-default-${pkgVersion}`,
- },
- {
- ignore: [404],
- }
- );
- expect(res.statusCode).equal(404);
- });
- it('should have deleted the index for the transform', async function () {
- // the index is defined in the transform file
- const res = await es.transport.request(
- {
- method: 'GET',
- path: `/logs-all_assets.test_log_current_default`,
- },
- {
- ignore: [404],
- }
- );
- expect(res.statusCode).equal(404);
- });
it('should have uninstalled the kibana assets', async function () {
let resDashboard;
try {
@@ -380,21 +355,6 @@ const expectAssetsInstalled = ({
});
expect(resUserSettings.statusCode).equal(200);
});
- it('should have installed the transform components', async function () {
- const res = await es.transport.request({
- method: 'GET',
- path: `/_transform/${pkgName}.test-default-${pkgVersion}`,
- });
- expect(res.statusCode).equal(200);
- });
- it('should have created the index for the transform', async function () {
- // the index is defined in the transform file
- const res = await es.transport.request({
- method: 'GET',
- path: `/logs-all_assets.test_log_current_default`,
- });
- expect(res.statusCode).equal(200);
- });
it('should have installed the kibana assets', async function () {
// These are installed from Fleet along with every package
const resIndexPatternLogs = await kibanaServer.savedObjects.get({
@@ -575,10 +535,6 @@ const expectAssetsInstalled = ({
id: 'logs-all_assets.test_logs-0.1.0-pipeline2',
type: 'ingest_pipeline',
},
- {
- id: 'all_assets.test-default-0.1.0',
- type: 'transform',
- },
],
es_index_patterns: {
test_logs: 'logs-all_assets.test_logs-*',
@@ -597,7 +553,6 @@ const expectAssetsInstalled = ({
{ id: 'f839c76e-d194-555a-90a1-3265a45789e4', type: 'epm-packages-assets' },
{ id: '9af7bbb3-7d8a-50fa-acc9-9dde6f5efca2', type: 'epm-packages-assets' },
{ id: '1e97a20f-9d1c-529b-8ff2-da4e8ba8bb71', type: 'epm-packages-assets' },
- { id: '8cfe0a2b-7016-5522-87e4-6d352360d1fc', type: 'epm-packages-assets' },
{ id: 'bd5ff3c5-655e-5385-9918-b60ff3040aad', type: 'epm-packages-assets' },
{ id: '0954ce3b-3165-5c1f-a4c0-56eb5f2fa487', type: 'epm-packages-assets' },
{ id: '60d6d054-57e4-590f-a580-52bf3f5e7cca', type: 'epm-packages-assets' },
diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/elasticsearch/transform/test/default.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/elasticsearch/transform/test/default.json
deleted file mode 100644
index eddc6bc0c304a..0000000000000
--- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/elasticsearch/transform/test/default.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "source": {
- "index": "logs-all_assets.test_log-default*"
- },
- "dest": {
- "index": "logs-all_assets.test_log_current_default"
- },
- "pivot": {
- "group_by": {
- "agent.id": {
- "terms": {
- "field": "agent.id"
- }
- }
- },
- "aggregations": {
- "HostDetails": {
- "scripted_metric": {
- "init_script": "state.timestamp_latest = 0L; state.last_doc=''",
- "map_script": "def current_date = doc['@timestamp'].getValue().toInstant().toEpochMilli(); if (current_date \u003e state.timestamp_latest) {state.timestamp_latest = current_date;state.last_doc = new HashMap(params['_source']);}",
- "combine_script": "return state",
- "reduce_script": "def last_doc = '';def timestamp_latest = 0L; for (s in states) {if (s.timestamp_latest \u003e (timestamp_latest)) {timestamp_latest = s.timestamp_latest; last_doc = s.last_doc;}} return last_doc"
- }
- }
- }
- },
- "description": "collapse and update the latest document for each host",
- "frequency": "1m",
- "sync": {
- "time": {
- "field": "event.ingested",
- "delay": "60s"
- }
- }
-}
diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts
index a8d074ad0631b..29caf422b7acd 100644
--- a/x-pack/test/functional/apps/lens/formula.ts
+++ b/x-pack/test/functional/apps/lens/formula.ts
@@ -247,11 +247,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
field: 'bytes',
});
- await PageObjects.lens.createLayer('threshold');
+ await PageObjects.lens.createLayer('referenceLine');
await PageObjects.lens.configureDimension(
{
- dimension: 'lnsXY_yThresholdLeftPanel > lns-dimensionTrigger',
+ dimension: 'lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger',
operation: 'formula',
formula: `count()`,
keepOpen: true,
@@ -263,9 +263,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.lens.closeDimensionEditor();
await PageObjects.common.sleep(1000);
- expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yThresholdLeftPanel', 0)).to.eql(
- 'count()'
- );
+ expect(
+ await PageObjects.lens.getDimensionTriggerText('lnsXY_yReferenceLineLeftPanel', 0)
+ ).to.eql('count()');
});
it('should allow numeric only formulas', async () => {
diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts
index a6f579a9a4890..38cf7759a7782 100644
--- a/x-pack/test/functional/apps/lens/index.ts
+++ b/x-pack/test/functional/apps/lens/index.ts
@@ -43,7 +43,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./lens_tagging'));
loadTestFile(require.resolve('./formula'));
loadTestFile(require.resolve('./heatmap'));
- loadTestFile(require.resolve('./thresholds'));
+ loadTestFile(require.resolve('./reference_lines'));
loadTestFile(require.resolve('./inspector'));
// has to be last one in the suite because it overrides saved objects
diff --git a/x-pack/test/functional/apps/lens/thresholds.ts b/x-pack/test/functional/apps/lens/reference_lines.ts
similarity index 57%
rename from x-pack/test/functional/apps/lens/thresholds.ts
rename to x-pack/test/functional/apps/lens/reference_lines.ts
index 10e330114442b..8ea66cb29f5a8 100644
--- a/x-pack/test/functional/apps/lens/thresholds.ts
+++ b/x-pack/test/functional/apps/lens/reference_lines.ts
@@ -14,21 +14,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
- describe('lens thresholds tests', () => {
- it('should show a disabled threshold layer button if no data dimension is defined', async () => {
+ describe('lens reference lines tests', () => {
+ it('should show a disabled reference layer button if no data dimension is defined', async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
await testSubjects.click('lnsLayerAddButton');
await retry.waitFor('wait for layer popup to appear', async () =>
- testSubjects.exists(`lnsLayerAddButton-threshold`)
+ testSubjects.exists(`lnsLayerAddButton-referenceLine`)
);
expect(
- await (await testSubjects.find(`lnsLayerAddButton-threshold`)).getAttribute('disabled')
+ await (await testSubjects.find(`lnsLayerAddButton-referenceLine`)).getAttribute('disabled')
).to.be('true');
});
- it('should add a threshold layer with a static value in it', async () => {
+ it('should add a reference layer with a static value in it', async () => {
await PageObjects.lens.goToTimeRange();
await PageObjects.lens.configureDimension({
@@ -43,29 +43,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
field: 'bytes',
});
- await PageObjects.lens.createLayer('threshold');
+ await PageObjects.lens.createLayer('referenceLine');
expect((await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length).to.eql(2);
expect(
await (
- await testSubjects.find('lnsXY_yThresholdLeftPanel > lns-dimensionTrigger')
+ await testSubjects.find('lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger')
).getVisibleText()
).to.eql('Static value: 4992.44');
});
- it('should create a dynamic threshold when dragging a field to a threshold dimension group', async () => {
+ it('should create a dynamic referenceLine when dragging a field to a referenceLine dimension group', async () => {
await PageObjects.lens.dragFieldToDimensionTrigger(
'bytes',
- 'lnsXY_yThresholdLeftPanel > lns-empty-dimension'
+ 'lnsXY_yReferenceLineLeftPanel > lns-empty-dimension'
);
- expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yThresholdLeftPanel')).to.eql([
- 'Static value: 4992.44',
- 'Median of bytes',
- ]);
+ expect(
+ await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yReferenceLineLeftPanel')
+ ).to.eql(['Static value: 4992.44', 'Median of bytes']);
});
- it('should add a new group to the threshold layer when a right axis is enabled', async () => {
+ it('should add a new group to the reference layer when a right axis is enabled', async () => {
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'average',
@@ -77,42 +76,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.lens.closeDimensionEditor();
- await testSubjects.existOrFail('lnsXY_yThresholdRightPanel > lns-empty-dimension');
+ await testSubjects.existOrFail('lnsXY_yReferenceLineRightPanel > lns-empty-dimension');
});
- it('should carry the style when moving a threshold to another group', async () => {
+ it('should carry the style when moving a reference line to another group', async () => {
// style it enabling the fill
- await testSubjects.click('lnsXY_yThresholdLeftPanel > lns-dimensionTrigger');
- await testSubjects.click('lnsXY_fill_below');
+ await testSubjects.click('lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger');
+ await testSubjects.click('lnsXY_referenceLine_fill_below');
await PageObjects.lens.closeDimensionEditor();
// drag and drop it to the left axis
await PageObjects.lens.dragDimensionToDimension(
- 'lnsXY_yThresholdLeftPanel > lns-dimensionTrigger',
- 'lnsXY_yThresholdRightPanel > lns-empty-dimension'
+ 'lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger',
+ 'lnsXY_yReferenceLineRightPanel > lns-empty-dimension'
);
- await testSubjects.click('lnsXY_yThresholdRightPanel > lns-dimensionTrigger');
+ await testSubjects.click('lnsXY_yReferenceLineRightPanel > lns-dimensionTrigger');
expect(
- await find.existsByCssSelector('[data-test-subj="lnsXY_fill_below"][class$="isSelected"]')
+ await find.existsByCssSelector(
+ '[data-test-subj="lnsXY_referenceLine_fill_below"][class$="isSelected"]'
+ )
).to.be(true);
await PageObjects.lens.closeDimensionEditor();
});
- it('should duplicate also the original style when duplicating a threshold', async () => {
+ it('should duplicate also the original style when duplicating a reference line', async () => {
// drag and drop to the empty field to generate a duplicate
await PageObjects.lens.dragDimensionToDimension(
- 'lnsXY_yThresholdRightPanel > lns-dimensionTrigger',
- 'lnsXY_yThresholdRightPanel > lns-empty-dimension'
+ 'lnsXY_yReferenceLineRightPanel > lns-dimensionTrigger',
+ 'lnsXY_yReferenceLineRightPanel > lns-empty-dimension'
);
await (
await find.byCssSelector(
- '[data-test-subj="lnsXY_yThresholdRightPanel"]:nth-child(2) [data-test-subj="lns-dimensionTrigger"]'
+ '[data-test-subj="lnsXY_yReferenceLineRightPanel"]:nth-child(2) [data-test-subj="lns-dimensionTrigger"]'
)
).click();
expect(
- await find.existsByCssSelector('[data-test-subj="lnsXY_fill_below"][class$="isSelected"]')
+ await find.existsByCssSelector(
+ '[data-test-subj="lnsXY_referenceLine_fill_below"][class$="isSelected"]'
+ )
).to.be(true);
await PageObjects.lens.closeDimensionEditor();
});
diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js
index 6b1658dd9ed0e..07fda7a143a99 100644
--- a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js
+++ b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js
@@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }) {
const nodesList = getService('monitoringElasticsearchNodes');
const nodeDetail = getService('monitoringElasticsearchNodeDetail');
- describe('Elasticsearch node detail', () => {
+ // FLAKY https://github.com/elastic/kibana/issues/115130
+ describe.skip('Elasticsearch node detail', () => {
describe('Active Nodes', () => {
const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects);
diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail_mb.js b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail_mb.js
index 9130ce91e7b4d..70c9b42b37f42 100644
--- a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail_mb.js
+++ b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail_mb.js
@@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }) {
const nodesList = getService('monitoringElasticsearchNodes');
const nodeDetail = getService('monitoringElasticsearchNodeDetail');
- describe('Elasticsearch node detail mb', () => {
+ // Failing: See https://github.com/elastic/kibana/issues/115255
+ describe.skip('Elasticsearch node detail mb', () => {
describe('Active Nodes', () => {
const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects);
diff --git a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js
index 79bd479c45a17..cce6401453d21 100644
--- a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js
+++ b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js
@@ -11,7 +11,6 @@ export default function ({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['monitoring', 'common', 'header']);
const esSupertest = getService('esSupertest');
const noData = getService('monitoringNoData');
- const testSubjects = getService('testSubjects');
const clusterOverview = getService('monitoringClusterOverview');
const retry = getService('retry');
const esDeleteAllIndices = getService('esDeleteAllIndices');
@@ -53,8 +52,6 @@ export default function ({ getService, getPageObjects }) {
// Here we are checking that once Monitoring is enabled,
// it moves on to the cluster overview page.
await retry.tryForTime(20000, async () => {
- // Click the refresh button
- await testSubjects.click('querySubmitButton');
await clusterOverview.closeAlertsModal();
expect(await clusterOverview.isOnClusterOverview()).to.be(true);
});
diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts
index 988bbdc621f5f..bf83892ce1934 100644
--- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts
+++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts
@@ -13,6 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const security = getService('security');
const appsMenu = getService('appsMenu');
const PageObjects = getPageObjects(['common', 'security']);
+ const noData = getService('monitoringNoData');
describe('security', () => {
before(async () => {
@@ -103,5 +104,32 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(navLinks).to.contain('Stack Monitoring');
});
});
+
+ describe('monitoring_user and kibana_admin roles', function () {
+ this.tags(['skipCloud']);
+ before(async () => {
+ await security.user.create('monitoring_kibana_admin_user', {
+ password: 'monitoring_user-password',
+ roles: ['monitoring_user', 'kibana_admin'],
+ full_name: 'monitoring user',
+ });
+
+ await PageObjects.security.login(
+ 'monitoring_kibana_admin_user',
+ 'monitoring_user-password'
+ );
+ });
+
+ after(async () => {
+ await security.user.delete('monitoring_kibana_admin_user');
+ });
+
+ it('denies enabling monitoring without enough permissions', async () => {
+ await PageObjects.common.navigateToApp('monitoring');
+ await noData.isOnNoDataPage();
+ await noData.clickSetupWithSelfMonitoring();
+ expect(await noData.isOnNoDataPageMonitoringEnablementDenied()).to.be(true);
+ });
+ });
});
}
diff --git a/x-pack/test/functional/services/monitoring/no_data.js b/x-pack/test/functional/services/monitoring/no_data.js
index 7b4410425dcfe..bd34c45c2d293 100644
--- a/x-pack/test/functional/services/monitoring/no_data.js
+++ b/x-pack/test/functional/services/monitoring/no_data.js
@@ -30,5 +30,13 @@ export function MonitoringNoDataProvider({ getService }) {
const pageId = await retry.try(() => testSubjects.find('noDataContainer'));
return pageId !== null;
}
+
+ async isOnNoDataPageMonitoringEnablementDenied() {
+ return testSubjects.exists('weTriedContainer');
+ }
+
+ async clickSetupWithSelfMonitoring() {
+ await testSubjects.click('useInternalCollection');
+ }
})();
}
diff --git a/x-pack/test/functional/services/observability/alerts/add_to_case.ts b/x-pack/test/functional/services/observability/alerts/add_to_case.ts
new file mode 100644
index 0000000000000..1669fc69a683c
--- /dev/null
+++ b/x-pack/test/functional/services/observability/alerts/add_to_case.ts
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+const ADD_TO_EXISTING_CASE_SELECTOR = 'add-existing-case-menu-item';
+const ADD_TO_NEW_CASE_SELECTOR = 'add-new-case-item';
+const CREATE_CASE_FLYOUT = 'create-case-flyout';
+const SELECT_CASE_MODAL = 'all-cases-modal';
+
+export function ObservabilityAlertsAddToCaseProvider({ getService }: FtrProviderContext) {
+ const testSubjects = getService('testSubjects');
+
+ const getAddToExistingCaseSelector = async () => {
+ return await testSubjects.find(ADD_TO_EXISTING_CASE_SELECTOR);
+ };
+
+ const getAddToExistingCaseSelectorOrFail = async () => {
+ return await testSubjects.existOrFail(ADD_TO_EXISTING_CASE_SELECTOR);
+ };
+
+ const missingAddToExistingCaseSelectorOrFail = async () => {
+ return await testSubjects.missingOrFail(ADD_TO_EXISTING_CASE_SELECTOR);
+ };
+
+ const getAddToNewCaseSelector = async () => {
+ return await testSubjects.find(ADD_TO_NEW_CASE_SELECTOR);
+ };
+
+ const getAddToNewCaseSelectorOrFail = async () => {
+ return await testSubjects.existOrFail(ADD_TO_NEW_CASE_SELECTOR);
+ };
+
+ const missingAddToNewCaseSelectorOrFail = async () => {
+ return await testSubjects.missingOrFail(ADD_TO_NEW_CASE_SELECTOR);
+ };
+
+ const addToNewCaseButtonClick = async () => {
+ return await (await getAddToNewCaseSelector()).click();
+ };
+
+ const addToExistingCaseButtonClick = async () => {
+ return await (await getAddToExistingCaseSelector()).click();
+ };
+
+ const getCreateCaseFlyoutOrFail = async () => {
+ return await testSubjects.existOrFail(CREATE_CASE_FLYOUT);
+ };
+
+ const closeFlyout = async () => {
+ return await (await testSubjects.find('euiFlyoutCloseButton')).click();
+ };
+
+ const getAddtoExistingCaseModalOrFail = async () => {
+ return await testSubjects.existOrFail(SELECT_CASE_MODAL);
+ };
+
+ return {
+ getAddToExistingCaseSelector,
+ getAddToExistingCaseSelectorOrFail,
+ missingAddToExistingCaseSelectorOrFail,
+ getAddToNewCaseSelector,
+ getAddToNewCaseSelectorOrFail,
+ missingAddToNewCaseSelectorOrFail,
+ getCreateCaseFlyoutOrFail,
+ closeFlyout,
+ addToNewCaseButtonClick,
+ addToExistingCaseButtonClick,
+ getAddtoExistingCaseModalOrFail,
+ };
+}
diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts
index 7098fdec2a9d4..d5a2ce2a18c41 100644
--- a/x-pack/test/functional/services/observability/alerts/common.ts
+++ b/x-pack/test/functional/services/observability/alerts/common.ts
@@ -204,5 +204,6 @@ export function ObservabilityAlertsCommonProvider({
setWorkflowStatusFilter,
submitQuery,
typeInQueryBar,
+ openActionsMenuForRow,
};
}
diff --git a/x-pack/test/functional/services/observability/alerts/index.ts b/x-pack/test/functional/services/observability/alerts/index.ts
index f373b0d75c543..f2b5173dfe5b0 100644
--- a/x-pack/test/functional/services/observability/alerts/index.ts
+++ b/x-pack/test/functional/services/observability/alerts/index.ts
@@ -7,15 +7,17 @@
import { ObservabilityAlertsPaginationProvider } from './pagination';
import { ObservabilityAlertsCommonProvider } from './common';
+import { ObservabilityAlertsAddToCaseProvider } from './add_to_case';
import { FtrProviderContext } from '../../../ftr_provider_context';
export function ObservabilityAlertsProvider(context: FtrProviderContext) {
const common = ObservabilityAlertsCommonProvider(context);
const pagination = ObservabilityAlertsPaginationProvider(context);
-
+ const addToCase = ObservabilityAlertsAddToCaseProvider(context);
return {
common,
pagination,
+ addToCase,
};
}
diff --git a/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts b/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts
new file mode 100644
index 0000000000000..f29111f2cb66b
--- /dev/null
+++ b/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts
@@ -0,0 +1,92 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default ({ getService, getPageObjects }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const observability = getService('observability');
+ const retry = getService('retry');
+
+ describe('Observability alerts / Add to case', function () {
+ this.tags('includeFirefox');
+
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
+ });
+
+ describe('When user has all priviledges for cases', () => {
+ before(async () => {
+ await observability.users.setTestUserRole(
+ observability.users.defineBasicObservabilityRole({
+ observabilityCases: ['all'],
+ logs: ['all'],
+ })
+ );
+ await observability.alerts.common.navigateToTimeWithData();
+ });
+
+ after(async () => {
+ await observability.users.restoreDefaultTestUserRole();
+ });
+
+ it('renders case options in the overflow menu', async () => {
+ await observability.alerts.common.openActionsMenuForRow(0);
+ await retry.try(async () => {
+ await observability.alerts.addToCase.getAddToExistingCaseSelectorOrFail();
+ await observability.alerts.addToCase.getAddToNewCaseSelectorOrFail();
+ });
+ });
+
+ it('opens a flyout when Add to new case is clicked', async () => {
+ await observability.alerts.addToCase.addToNewCaseButtonClick();
+
+ await retry.try(async () => {
+ await observability.alerts.addToCase.getCreateCaseFlyoutOrFail();
+ await observability.alerts.addToCase.closeFlyout();
+ });
+ });
+
+ it('opens a modal when Add to existing case is clicked', async () => {
+ await observability.alerts.common.openActionsMenuForRow(0);
+
+ await retry.try(async () => {
+ await observability.alerts.addToCase.addToExistingCaseButtonClick();
+ await observability.alerts.addToCase.getAddtoExistingCaseModalOrFail();
+ });
+ });
+ });
+
+ describe('When user has read permissions for cases', () => {
+ before(async () => {
+ await observability.users.setTestUserRole(
+ observability.users.defineBasicObservabilityRole({
+ observabilityCases: ['read'],
+ logs: ['all'],
+ })
+ );
+ await observability.alerts.common.navigateToTimeWithData();
+ });
+
+ after(async () => {
+ await observability.users.restoreDefaultTestUserRole();
+ });
+
+ it('does not render case options in the overflow menu', async () => {
+ await observability.alerts.common.openActionsMenuForRow(0);
+ await retry.try(async () => {
+ await observability.alerts.addToCase.missingAddToExistingCaseSelectorOrFail();
+ await observability.alerts.addToCase.missingAddToNewCaseSelectorOrFail();
+ });
+ });
+ });
+ });
+};
diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts
index b163d4d6bb8d5..43e056bae65c0 100644
--- a/x-pack/test/observability_functional/apps/observability/index.ts
+++ b/x-pack/test/observability_functional/apps/observability/index.ts
@@ -15,5 +15,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./alerts'));
loadTestFile(require.resolve('./alerts/workflow_status'));
loadTestFile(require.resolve('./alerts/pagination'));
+ loadTestFile(require.resolve('./alerts/add_to_case'));
});
}
diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
index 6d78c69798e94..323b08dd88be1 100644
--- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
+++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
@@ -313,6 +313,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
advanced: { agent: { connection_delay: 'true' } },
malware: { mode: 'prevent' },
behavior_protection: { mode: 'prevent', supported: true },
+ memory_protection: { mode: 'prevent', supported: true },
popup: {
malware: {
enabled: true,
@@ -322,6 +323,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
enabled: true,
message: 'Elastic Security {action} {rule}',
},
+ memory_protection: {
+ enabled: true,
+ message: 'Elastic Security {action} {rule}',
+ },
},
},
mac: {
@@ -329,6 +334,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
logging: { file: 'info' },
malware: { mode: 'prevent' },
behavior_protection: { mode: 'prevent', supported: true },
+ memory_protection: { mode: 'prevent', supported: true },
popup: {
malware: {
enabled: true,
@@ -338,6 +344,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
enabled: true,
message: 'Elastic Security {action} {rule}',
},
+ memory_protection: {
+ enabled: true,
+ message: 'Elastic Security {action} {rule}',
+ },
},
},
windows: {
@@ -537,6 +547,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
advanced: { agent: { connection_delay: 'true' } },
malware: { mode: 'prevent' },
behavior_protection: { mode: 'prevent', supported: true },
+ memory_protection: { mode: 'prevent', supported: true },
popup: {
malware: {
enabled: true,
@@ -546,6 +557,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
enabled: true,
message: 'Elastic Security {action} {rule}',
},
+ memory_protection: {
+ enabled: true,
+ message: 'Elastic Security {action} {rule}',
+ },
},
},
mac: {
@@ -553,6 +568,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
logging: { file: 'info' },
malware: { mode: 'prevent' },
behavior_protection: { mode: 'prevent', supported: true },
+ memory_protection: { mode: 'prevent', supported: true },
popup: {
malware: {
enabled: true,
@@ -562,6 +578,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
enabled: true,
message: 'Elastic Security {action} {rule}',
},
+ memory_protection: {
+ enabled: true,
+ message: 'Elastic Security {action} {rule}',
+ },
},
},
windows: {
@@ -758,6 +778,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
logging: { file: 'info' },
malware: { mode: 'prevent' },
behavior_protection: { mode: 'prevent', supported: true },
+ memory_protection: { mode: 'prevent', supported: true },
popup: {
malware: {
enabled: true,
@@ -767,6 +788,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
enabled: true,
message: 'Elastic Security {action} {rule}',
},
+ memory_protection: {
+ enabled: true,
+ message: 'Elastic Security {action} {rule}',
+ },
},
},
mac: {
@@ -774,6 +799,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
logging: { file: 'info' },
malware: { mode: 'prevent' },
behavior_protection: { mode: 'prevent', supported: true },
+ memory_protection: { mode: 'prevent', supported: true },
popup: {
malware: {
enabled: true,
@@ -783,6 +809,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
enabled: true,
message: 'Elastic Security {action} {rule}',
},
+ memory_protection: {
+ enabled: true,
+ message: 'Elastic Security {action} {rule}',
+ },
},
},
windows: {
diff --git a/yarn.lock b/yarn.lock
index b2d03018a0e0c..94e95113f772a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6617,10 +6617,10 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44"
integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ==
-"@types/mock-fs@^4.10.0":
- version "4.10.0"
- resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.10.0.tgz#460061b186993d76856f669d5317cda8a007c24b"
- integrity sha512-FQ5alSzmHMmliqcL36JqIA4Yyn9jyJKvRSGV3mvPh108VFatX7naJDzSG4fnFQNZFq9dIx0Dzoe6ddflMB2Xkg==
+"@types/mock-fs@^4.13.1":
+ version "4.13.1"
+ resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.13.1.tgz#9201554ceb23671badbfa8ac3f1fa9e0706305be"
+ integrity sha512-m6nFAJ3lBSnqbvDZioawRvpLXSaPyn52Srf7OfzjubYbYX8MTUdIgDxQl0wEapm4m/pNYSd9TXocpQ0TvZFlYA==
dependencies:
"@types/node" "*"
@@ -6672,10 +6672,10 @@
dependencies:
"@types/node" "*"
-"@types/node@*", "@types/node@14.14.44", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=8.9.0", "@types/node@^10.1.0", "@types/node@^14.14.31":
- version "14.14.44"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.44.tgz#df7503e6002847b834371c004b372529f3f85215"
- integrity sha512-+gaugz6Oce6ZInfI/tK4Pq5wIIkJMEJUu92RB3Eu93mtj4wjjjz9EB5mLp5s1pSsLXdC/CPut/xF20ZzAQJbTA==
+"@types/node@*", "@types/node@16.10.2", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=8.9.0", "@types/node@^10.1.0", "@types/node@^14.14.31":
+ version "16.10.2"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.2.tgz#5764ca9aa94470adb4e1185fe2e9f19458992b2e"
+ integrity sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==
"@types/nodemailer@^6.4.0":
version "6.4.0"
@@ -7743,14 +7743,7 @@ agent-base@5:
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==
-agent-base@6:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.0.tgz#5d0101f19bbfaed39980b22ae866de153b93f09a"
- integrity sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==
- dependencies:
- debug "4"
-
-agent-base@^6.0.2:
+agent-base@6, agent-base@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
@@ -20750,10 +20743,10 @@ mochawesome@^6.2.1:
strip-ansi "^6.0.0"
uuid "^7.0.3"
-mock-fs@^4.12.0:
- version "4.12.0"
- resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.12.0.tgz#a5d50b12d2d75e5bec9dac3b67ffe3c41d31ade4"
- integrity sha512-/P/HtrlvBxY4o/PzXY9cCNBrdylDNxg7gnrv2sMNxj+UJ2m8jSpl0/A6fuJeNAWr99ZvGWH8XCbE0vmnM5KupQ==
+mock-fs@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.1.tgz#d4c95e916abf400664197079d7e399d133bb6048"
+ integrity sha512-p/8oZ3qvfKGPw+4wdVCyjDxa6wn2tP0TCf3WXC1UyUBAevezPn1TtOoxtMYVbZu/S/iExg+Ghed1busItj2CEw==
mock-http-server@1.3.0:
version "1.3.0"