From c4504928e8e4dfe9a29ffffb32fe92c44175f562 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Tue, 2 Mar 2021 14:13:01 +0100
Subject: [PATCH 01/63] [Lens] Fix Workspace hidden when using Safari (#92616)
(#93193)
Co-authored-by: Marta Bondyra
Co-authored-by: Marta Bondyra
---
.../__snapshots__/drag_drop.test.tsx.snap | 14 +++++---------
.../plugins/lens/public/drag_drop/drag_drop.scss | 7 -------
x-pack/plugins/lens/public/drag_drop/drag_drop.tsx | 4 ++--
.../workspace_panel/workspace_panel_wrapper.scss | 1 +
4 files changed, 8 insertions(+), 18 deletions(-)
diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
index e5594bb0bb7699..7aa838021f2a85 100644
--- a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
+++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
@@ -1,16 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DragDrop defined dropType is reflected in the className 1`] = `
-
-
- Hello!
-
-
+ Hello!
+
`;
exports[`DragDrop items that has dropType=undefined get special styling when another item is dragged 1`] = `
diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss
index 9e3f1e1c3cf264..961f7ee0ec4009 100644
--- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss
+++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss
@@ -81,13 +81,6 @@
}
}
-.lnsDragDrop__container {
- position: relative;
- overflow: visible !important; // sass-lint:disable-line no-important
- width: 100%;
- height: 100%;
-}
-
.lnsDragDrop__reorderableDrop {
position: absolute;
width: 100%;
diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
index 618a7accb9b2b1..76e44c29eaed5f 100644
--- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
+++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
@@ -456,7 +456,7 @@ const DropInner = memo(function DropInner(props: DropInnerProps) {
isActiveDropTarget && dropType !== 'reorder' && dragging?.ghost ? dragging.ghost : undefined;
return (
-
+ <>
{React.cloneElement(children, {
'data-test-subj': dataTestSubj || 'lnsDragDrop',
className: classNames(children.props.className, classes, className),
@@ -471,7 +471,7 @@ const DropInner = memo(function DropInner(props: DropInnerProps) {
style: ghost.style,
})
: null}
-
+ >
);
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss
index 3949c7deb53b4a..167c17ee6ae9c5 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss
@@ -11,6 +11,7 @@
min-height: $euiSizeXXL * 10;
overflow: visible;
border: none;
+ height: 100%;
.lnsWorkspacePanelWrapper__pageContentBody {
@include euiScrollBar;
From 30325b18ce2b62b5ef0b1b12ee6c89894ad04fb0 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 2 Mar 2021 09:50:41 -0500
Subject: [PATCH 02/63] [ML] Transforms: Fixes chart histograms for runtime
fields. (#93028) (#93202)
Fixes chart histograms for runtime fields. The runtime field configurations were not passed on to the endpoint to fetch the charts data, so charts ended up being empty with a 0 documents legend.
Co-authored-by: Walter Rafelsberger
---
x-pack/plugins/ml/common/index.ts | 1 +
.../ml/common/types/field_histograms.ts | 68 ++++++++++++++++++
.../components/data_grid/column_chart.tsx | 4 +-
.../application/components/data_grid/index.ts | 2 +-
.../application/components/data_grid/types.ts | 2 +-
.../data_grid/use_column_chart.test.tsx | 8 +--
.../components/data_grid/use_column_chart.tsx | 71 +++----------------
.../components/data_grid/use_data_grid.tsx | 2 +-
.../boolean_content_preview.tsx | 4 +-
.../field_data_row/top_values_preview.tsx | 5 +-
.../models/data_visualizer/data_visualizer.ts | 15 ++--
.../transform/common/api_schemas/common.ts | 24 +++++++
.../common/api_schemas/field_histograms.ts | 8 ++-
.../common/api_schemas/transforms.ts | 26 +------
.../transform/common/shared_imports.ts | 8 ++-
.../transform/public/app/hooks/use_api.ts | 7 +-
.../public/app/hooks/use_index_data.ts | 3 +-
.../advanced_runtime_mappings_editor.tsx | 5 +-
.../common/get_pivot_dropdown_options.ts | 4 +-
.../step_define/common/types.test.ts | 71 +++++++++++++++++++
.../components/step_define/common/types.ts | 26 ++++++-
.../server/routes/api/field_histograms.ts | 5 +-
22 files changed, 250 insertions(+), 119 deletions(-)
create mode 100644 x-pack/plugins/ml/common/types/field_histograms.ts
create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.test.ts
diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts
index c049b68990d2d9..ac21954118e502 100644
--- a/x-pack/plugins/ml/common/index.ts
+++ b/x-pack/plugins/ml/common/index.ts
@@ -6,6 +6,7 @@
*/
export { HitsTotalRelation, SearchResponse7, HITS_TOTAL_RELATION } from './types/es_client';
+export { ChartData } from './types/field_histograms';
export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD, SEVERITY_COLORS } from './constants/anomalies';
export { getSeverityColor, getSeverityType } from './util/anomaly_utils';
export { composeValidators, patternValidator } from './util/validators';
diff --git a/x-pack/plugins/ml/common/types/field_histograms.ts b/x-pack/plugins/ml/common/types/field_histograms.ts
new file mode 100644
index 00000000000000..22b0195a579acd
--- /dev/null
+++ b/x-pack/plugins/ml/common/types/field_histograms.ts
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+export interface NumericDataItem {
+ key: number;
+ key_as_string?: string | number;
+ doc_count: number;
+}
+
+export interface NumericChartData {
+ data: NumericDataItem[];
+ id: string;
+ interval: number;
+ stats: [number, number];
+ type: 'numeric';
+}
+
+export const isNumericChartData = (arg: any): arg is NumericChartData => {
+ return (
+ typeof arg === 'object' &&
+ arg.hasOwnProperty('data') &&
+ arg.hasOwnProperty('id') &&
+ arg.hasOwnProperty('interval') &&
+ arg.hasOwnProperty('stats') &&
+ arg.hasOwnProperty('type') &&
+ arg.type === 'numeric'
+ );
+};
+
+export interface OrdinalDataItem {
+ key: string;
+ key_as_string?: string;
+ doc_count: number;
+}
+
+export interface OrdinalChartData {
+ cardinality: number;
+ data: OrdinalDataItem[];
+ id: string;
+ type: 'ordinal' | 'boolean';
+}
+
+export const isOrdinalChartData = (arg: any): arg is OrdinalChartData => {
+ return (
+ typeof arg === 'object' &&
+ arg.hasOwnProperty('data') &&
+ arg.hasOwnProperty('cardinality') &&
+ arg.hasOwnProperty('id') &&
+ arg.hasOwnProperty('type') &&
+ (arg.type === 'ordinal' || arg.type === 'boolean')
+ );
+};
+
+export interface UnsupportedChartData {
+ id: string;
+ type: 'unsupported';
+}
+
+export const isUnsupportedChartData = (arg: any): arg is UnsupportedChartData => {
+ return typeof arg === 'object' && arg.hasOwnProperty('type') && arg.type === 'unsupported';
+};
+
+export type ChartDataItem = NumericDataItem | OrdinalDataItem;
+export type ChartData = NumericChartData | OrdinalChartData | UnsupportedChartData;
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx
index 102ccc560ba933..3800256927d542 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx
+++ b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx
@@ -13,7 +13,9 @@ import { EuiDataGridColumn } from '@elastic/eui';
import './column_chart.scss';
-import { isUnsupportedChartData, useColumnChart, ChartData } from './use_column_chart';
+import { isUnsupportedChartData, ChartData } from '../../../../common/types/field_histograms';
+
+import { useColumnChart } from './use_column_chart';
interface Props {
chartData: ChartData;
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts
index a3f1995736624e..be37e381d1bae8 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts
+++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts
@@ -16,7 +16,7 @@ export {
useRenderCellValue,
getProcessedFields,
} from './common';
-export { getFieldType, ChartData } from './use_column_chart';
+export { getFieldType } from './use_column_chart';
export { useDataGrid } from './use_data_grid';
export { DataGrid } from './data_grid';
export {
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts
index 77c7bdb3854698..2fb47a59284a38 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts
+++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts
@@ -11,10 +11,10 @@ import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } fro
import { Dictionary } from '../../../../common/types/common';
import { HitsTotalRelation } from '../../../../common/types/es_client';
+import { ChartData } from '../../../../common/types/field_histograms';
import { INDEX_STATUS } from '../../data_frame_analytics/common/analytics';
-import { ChartData } from './use_column_chart';
import { FeatureImportanceBaseline } from '../../../../common/types/feature_importance';
export type ColumnId = string;
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.test.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.test.tsx
index 0154d43a068652..631c214dd751cf 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.test.tsx
+++ b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.test.tsx
@@ -13,17 +13,15 @@ import '@testing-library/jest-dom/extend-expect';
import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public';
import {
- getFieldType,
- getLegendText,
- getXScaleType,
isNumericChartData,
isOrdinalChartData,
isUnsupportedChartData,
- useColumnChart,
NumericChartData,
OrdinalChartData,
UnsupportedChartData,
-} from './use_column_chart';
+} from '../../../../common/types/field_histograms';
+
+import { getFieldType, getLegendText, getXScaleType, useColumnChart } from './use_column_chart';
describe('getFieldType()', () => {
it('should return the Kibana field type for a given EUI data grid schema', () => {
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx
index fcb8a20f558fd0..4764a1674df2f6 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx
+++ b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx
@@ -17,6 +17,15 @@ import { i18n } from '@kbn/i18n';
import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public';
+import {
+ isNumericChartData,
+ isOrdinalChartData,
+ ChartData,
+ ChartDataItem,
+ NumericDataItem,
+ OrdinalDataItem,
+} from '../../../../common/types/field_histograms';
+
import { NON_AGGREGATABLE } from './common';
export const hoveredRow$ = new BehaviorSubject(null);
@@ -66,68 +75,6 @@ export const getFieldType = (schema: EuiDataGridColumn['schema']): KBN_FIELD_TYP
return fieldType;
};
-interface NumericDataItem {
- key: number;
- key_as_string?: string | number;
- doc_count: number;
-}
-
-export interface NumericChartData {
- data: NumericDataItem[];
- id: string;
- interval: number;
- stats: [number, number];
- type: 'numeric';
-}
-
-export const isNumericChartData = (arg: any): arg is NumericChartData => {
- return (
- typeof arg === 'object' &&
- arg.hasOwnProperty('data') &&
- arg.hasOwnProperty('id') &&
- arg.hasOwnProperty('interval') &&
- arg.hasOwnProperty('stats') &&
- arg.hasOwnProperty('type') &&
- arg.type === 'numeric'
- );
-};
-
-export interface OrdinalDataItem {
- key: string;
- key_as_string?: string;
- doc_count: number;
-}
-
-export interface OrdinalChartData {
- cardinality: number;
- data: OrdinalDataItem[];
- id: string;
- type: 'ordinal' | 'boolean';
-}
-
-export const isOrdinalChartData = (arg: any): arg is OrdinalChartData => {
- return (
- typeof arg === 'object' &&
- arg.hasOwnProperty('data') &&
- arg.hasOwnProperty('cardinality') &&
- arg.hasOwnProperty('id') &&
- arg.hasOwnProperty('type') &&
- (arg.type === 'ordinal' || arg.type === 'boolean')
- );
-};
-
-export interface UnsupportedChartData {
- id: string;
- type: 'unsupported';
-}
-
-export const isUnsupportedChartData = (arg: any): arg is UnsupportedChartData => {
- return typeof arg === 'object' && arg.hasOwnProperty('type') && arg.type === 'unsupported';
-};
-
-export type ChartDataItem = NumericDataItem | OrdinalDataItem;
-export type ChartData = NumericChartData | OrdinalChartData | UnsupportedChartData;
-
type LegendText = string | JSX.Element;
export const getLegendText = (
chartData: ChartData,
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx
index 4129f3a01bce98..228ee86a7f800a 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx
+++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx
@@ -10,6 +10,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui';
import { HITS_TOTAL_RELATION } from '../../../../common/types/es_client';
+import { ChartData } from '../../../../common/types/field_histograms';
import { INDEX_STATUS } from '../../data_frame_analytics/common';
@@ -26,7 +27,6 @@ import {
RowCountRelation,
UseDataGridReturnType,
} from './types';
-import { ChartData } from './use_column_chart';
export const useDataGrid = (
columns: EuiDataGridColumn[],
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/boolean_content_preview.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/boolean_content_preview.tsx
index 17b2e042560989..70adbbe85bc583 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/boolean_content_preview.tsx
+++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/boolean_content_preview.tsx
@@ -7,10 +7,10 @@
import React, { FC, useMemo } from 'react';
import { EuiDataGridColumn } from '@elastic/eui';
+import { OrdinalChartData } from '../../../../../../common/types/field_histograms';
+import { ColumnChart } from '../../../../components/data_grid/column_chart';
import { FieldDataRowProps } from '../../types';
import { getTFPercentage } from '../../utils';
-import { ColumnChart } from '../../../../components/data_grid/column_chart';
-import { OrdinalChartData } from '../../../../components/data_grid/use_column_chart';
export const BooleanContentPreview: FC = ({ config }) => {
const chartData = useMemo(() => {
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/top_values_preview.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/top_values_preview.tsx
index 122663b304024b..07a2eae95c8900 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/top_values_preview.tsx
+++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/top_values_preview.tsx
@@ -7,10 +7,9 @@
import React, { FC } from 'react';
import { EuiDataGridColumn } from '@elastic/eui';
-import type { FieldDataRowProps } from '../../types/field_data_row';
+import { ChartData, OrdinalDataItem } from '../../../../../../common/types/field_histograms';
import { ColumnChart } from '../../../../components/data_grid/column_chart';
-import { ChartData } from '../../../../components/data_grid';
-import { OrdinalDataItem } from '../../../../components/data_grid/use_column_chart';
+import type { FieldDataRowProps } from '../../types/field_data_row';
export const TopValuesPreview: FC = ({ config }) => {
const { stats } = config;
diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts
index 69ebfe5f0bc112..4db8295d939975 100644
--- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts
+++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts
@@ -16,7 +16,7 @@ import {
buildSamplerAggregation,
getSamplerAggregationsResponsePath,
} from '../../lib/query_utils';
-import { AggCardinality } from '../../../common/types/fields';
+import { AggCardinality, RuntimeMappings } from '../../../common/types/fields';
import { getDatafeedAggregations } from '../../../common/util/datafeed_utils';
import { Datafeed } from '../../../common/types/anomaly_detection_jobs';
import { isPopulatedObject } from '../../../common/util/object_utils';
@@ -183,7 +183,8 @@ const getAggIntervals = async (
indexPatternTitle: string,
query: any,
fields: HistogramField[],
- samplerShardSize: number
+ samplerShardSize: number,
+ runtimeMappings?: RuntimeMappings
): Promise => {
const numericColumns = fields.filter((field) => {
return field.type === KBN_FIELD_TYPES.NUMBER || field.type === KBN_FIELD_TYPES.DATE;
@@ -210,6 +211,7 @@ const getAggIntervals = async (
query,
aggs: buildSamplerAggregation(minMaxAggs, samplerShardSize),
size: 0,
+ ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}),
},
});
@@ -240,7 +242,8 @@ export const getHistogramsForFields = async (
indexPatternTitle: string,
query: any,
fields: HistogramField[],
- samplerShardSize: number
+ samplerShardSize: number,
+ runtimeMappings?: RuntimeMappings
) => {
const { asCurrentUser } = client;
const aggIntervals = await getAggIntervals(
@@ -248,7 +251,8 @@ export const getHistogramsForFields = async (
indexPatternTitle,
query,
fields,
- samplerShardSize
+ samplerShardSize,
+ runtimeMappings
);
const chartDataAggs = fields.reduce((aggs, field) => {
@@ -293,6 +297,7 @@ export const getHistogramsForFields = async (
query,
aggs: buildSamplerAggregation(chartDataAggs, samplerShardSize),
size: 0,
+ ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}),
},
});
@@ -607,7 +612,7 @@ export class DataVisualizer {
// Value count aggregation faster way of checking if field exists than using
// filter aggregation with exists query.
const aggs: Aggs = datafeedAggregations !== undefined ? { ...datafeedAggregations } : {};
- const runtimeMappings: any = {};
+ const runtimeMappings: { runtime_mappings?: RuntimeMappings } = {};
aggregatableFields.forEach((field, i) => {
const safeFieldName = getSafeAggregationName(field, i);
diff --git a/x-pack/plugins/transform/common/api_schemas/common.ts b/x-pack/plugins/transform/common/api_schemas/common.ts
index 1841724c16ef97..3651af69359a98 100644
--- a/x-pack/plugins/transform/common/api_schemas/common.ts
+++ b/x-pack/plugins/transform/common/api_schemas/common.ts
@@ -53,3 +53,27 @@ export interface ResponseStatus {
export interface CommonResponseStatusSchema {
[key: string]: ResponseStatus;
}
+
+export const runtimeMappingsSchema = schema.maybe(
+ schema.recordOf(
+ schema.string(),
+ schema.object({
+ type: schema.oneOf([
+ schema.literal('keyword'),
+ schema.literal('long'),
+ schema.literal('double'),
+ schema.literal('date'),
+ schema.literal('ip'),
+ schema.literal('boolean'),
+ ]),
+ script: schema.maybe(
+ schema.oneOf([
+ schema.string(),
+ schema.object({
+ source: schema.string(),
+ }),
+ ])
+ ),
+ })
+ )
+);
diff --git a/x-pack/plugins/transform/common/api_schemas/field_histograms.ts b/x-pack/plugins/transform/common/api_schemas/field_histograms.ts
index fc5a95590c7c6d..9f6f4c15d803ae 100644
--- a/x-pack/plugins/transform/common/api_schemas/field_histograms.ts
+++ b/x-pack/plugins/transform/common/api_schemas/field_histograms.ts
@@ -7,14 +7,20 @@
import { schema, TypeOf } from '@kbn/config-schema';
+import { ChartData } from '../shared_imports';
+
+import { runtimeMappingsSchema } from './common';
+
export const fieldHistogramsRequestSchema = schema.object({
/** Query to match documents in the index. */
query: schema.any(),
/** The fields to return histogram data. */
fields: schema.arrayOf(schema.any()),
+ /** Optional runtime mappings */
+ runtimeMappings: runtimeMappingsSchema,
/** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */
samplerShardSize: schema.number(),
});
export type FieldHistogramsRequestSchema = TypeOf;
-export type FieldHistogramsResponseSchema = any[];
+export type FieldHistogramsResponseSchema = ChartData[];
diff --git a/x-pack/plugins/transform/common/api_schemas/transforms.ts b/x-pack/plugins/transform/common/api_schemas/transforms.ts
index d86df3af3e1d0a..4d25bd74f4e74a 100644
--- a/x-pack/plugins/transform/common/api_schemas/transforms.ts
+++ b/x-pack/plugins/transform/common/api_schemas/transforms.ts
@@ -14,7 +14,7 @@ import type { PivotAggDict } from '../types/pivot_aggs';
import type { PivotGroupByDict } from '../types/pivot_group_by';
import type { TransformId, TransformPivotConfig } from '../types/transform';
-import { transformStateSchema } from './common';
+import { transformStateSchema, runtimeMappingsSchema } from './common';
// GET transforms
export const getTransformsRequestSchema = schema.arrayOf(
@@ -64,30 +64,6 @@ export const settingsSchema = schema.object({
docs_per_second: schema.maybe(schema.nullable(schema.number())),
});
-export const runtimeMappingsSchema = schema.maybe(
- schema.recordOf(
- schema.string(),
- schema.object({
- type: schema.oneOf([
- schema.literal('keyword'),
- schema.literal('long'),
- schema.literal('double'),
- schema.literal('date'),
- schema.literal('ip'),
- schema.literal('boolean'),
- ]),
- script: schema.maybe(
- schema.oneOf([
- schema.string(),
- schema.object({
- source: schema.string(),
- }),
- ])
- ),
- })
- )
-);
-
export const sourceSchema = schema.object({
runtime_mappings: runtimeMappingsSchema,
index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
diff --git a/x-pack/plugins/transform/common/shared_imports.ts b/x-pack/plugins/transform/common/shared_imports.ts
index 4506083a1876f2..3062c7ab8d23cb 100644
--- a/x-pack/plugins/transform/common/shared_imports.ts
+++ b/x-pack/plugins/transform/common/shared_imports.ts
@@ -6,5 +6,9 @@
*/
export type { HitsTotalRelation, SearchResponse7 } from '../../ml/common';
-export { HITS_TOTAL_RELATION } from '../../ml/common';
-export { composeValidators, patternValidator } from '../../ml/common';
+export {
+ composeValidators,
+ patternValidator,
+ ChartData,
+ HITS_TOTAL_RELATION,
+} from '../../ml/common';
diff --git a/x-pack/plugins/transform/public/app/hooks/use_api.ts b/x-pack/plugins/transform/public/app/hooks/use_api.ts
index 388bc8b432fc4e..7afbc5e403b78c 100644
--- a/x-pack/plugins/transform/public/app/hooks/use_api.ts
+++ b/x-pack/plugins/transform/public/app/hooks/use_api.ts
@@ -16,7 +16,10 @@ import type {
DeleteTransformsRequestSchema,
DeleteTransformsResponseSchema,
} from '../../../common/api_schemas/delete_transforms';
-import type { FieldHistogramsResponseSchema } from '../../../common/api_schemas/field_histograms';
+import type {
+ FieldHistogramsRequestSchema,
+ FieldHistogramsResponseSchema,
+} from '../../../common/api_schemas/field_histograms';
import type {
StartTransformsRequestSchema,
StartTransformsResponseSchema,
@@ -194,6 +197,7 @@ export const useApi = () => {
indexPatternTitle: string,
fields: FieldHistogramRequestConfig[],
query: string | SavedSearchQuery,
+ runtimeMappings?: FieldHistogramsRequestSchema['runtimeMappings'],
samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE
): Promise {
try {
@@ -202,6 +206,7 @@ export const useApi = () => {
query,
fields,
samplerShardSize,
+ ...(runtimeMappings !== undefined ? { runtimeMappings } : {}),
}),
});
} catch (e) {
diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts
index dde4c7eb0f3a01..e12aa78e33622d 100644
--- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts
+++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts
@@ -150,7 +150,8 @@ export const useIndexData = (
fieldName: cT.id,
type: getFieldType(cT.schema),
})),
- isDefaultQuery(query) ? matchAllQuery : query
+ isDefaultQuery(query) ? matchAllQuery : query,
+ combinedRuntimeMappings
);
if (!isFieldHistogramsResponseSchema(columnChartsData)) {
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx
index 087bae97e287ef..23c4cfcefe8ef0 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx
@@ -13,6 +13,7 @@ import { EuiCodeEditor } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { StepDefineFormHook } from '../step_define';
+import { isRuntimeMappings } from '../step_define/common/types';
export const AdvancedRuntimeMappingsEditor: FC = memo(
({
@@ -43,8 +44,8 @@ export const AdvancedRuntimeMappingsEditor: FC]/g;
@@ -77,7 +77,7 @@ export function getPivotDropdownOptions(
// Support for runtime_mappings that are defined by queries
let runtimeFields: Field[] = [];
- if (isPopulatedObject(runtimeMappings)) {
+ if (isRuntimeMappings(runtimeMappings)) {
runtimeFields = Object.keys(runtimeMappings).map((fieldName) => {
const field = runtimeMappings[fieldName];
return { name: fieldName, type: getKibanaFieldTypeFromEsType(field.type) };
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.test.ts
new file mode 100644
index 00000000000000..ec90d31a0d1698
--- /dev/null
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.test.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { isRuntimeField, isRuntimeMappings } from './types';
+
+describe('Transform: step_define type guards', () => {
+ it('isRuntimeField()', () => {
+ expect(isRuntimeField(1)).toBe(false);
+ expect(isRuntimeField(null)).toBe(false);
+ expect(isRuntimeField([])).toBe(false);
+ expect(isRuntimeField({})).toBe(false);
+ expect(isRuntimeField({ someAttribute: 'someValue' })).toBe(false);
+ expect(isRuntimeField({ type: 'wrong-type' })).toBe(false);
+ expect(isRuntimeField({ type: 'keyword', someAttribute: 'some value' })).toBe(false);
+
+ expect(isRuntimeField({ type: 'keyword' })).toBe(true);
+ expect(isRuntimeField({ type: 'keyword', script: 'some script' })).toBe(true);
+ });
+
+ it('isRuntimeMappings()', () => {
+ expect(isRuntimeMappings(1)).toBe(false);
+ expect(isRuntimeMappings(null)).toBe(false);
+ expect(isRuntimeMappings([])).toBe(false);
+ expect(isRuntimeMappings({})).toBe(false);
+ expect(isRuntimeMappings({ someAttribute: 'someValue' })).toBe(false);
+ expect(isRuntimeMappings({ fieldName1: { type: 'keyword' }, fieldName2: 'someValue' })).toBe(
+ false
+ );
+ expect(
+ isRuntimeMappings({
+ fieldName1: { type: 'keyword' },
+ fieldName2: { type: 'keyword', someAttribute: 'some value' },
+ })
+ ).toBe(false);
+ expect(
+ isRuntimeMappings({
+ fieldName: { type: 'long', script: 1234 },
+ })
+ ).toBe(false);
+ expect(
+ isRuntimeMappings({
+ fieldName: { type: 'long', script: { someAttribute: 'some value' } },
+ })
+ ).toBe(false);
+ expect(
+ isRuntimeMappings({
+ fieldName: { type: 'long', script: { source: 1234 } },
+ })
+ ).toBe(false);
+
+ expect(isRuntimeMappings({ fieldName: { type: 'keyword' } })).toBe(true);
+ expect(
+ isRuntimeMappings({ fieldName1: { type: 'keyword' }, fieldName2: { type: 'keyword' } })
+ ).toBe(true);
+ expect(
+ isRuntimeMappings({
+ fieldName1: { type: 'keyword' },
+ fieldName2: { type: 'keyword', script: 'some script as script' },
+ })
+ ).toBe(true);
+ expect(
+ isRuntimeMappings({
+ fieldName: { type: 'long', script: { source: 'some script as source' } },
+ })
+ ).toBe(true);
+ });
+});
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts
index 6c9293a6d13cf6..d0218996b2e4f4 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts
@@ -24,6 +24,8 @@ import {
} from '../../../../../../../common/types/transform';
import { LatestFunctionConfig } from '../../../../../../../common/api_schemas/transforms';
+import { isPopulatedObject } from '../../../../../common/utils/object_utils';
+
export interface ErrorMessage {
query: string;
message: string;
@@ -70,10 +72,30 @@ export interface StepDefineExposedState {
isRuntimeMappingsEditorEnabled: boolean;
}
+export function isRuntimeField(arg: any): arg is RuntimeField {
+ return (
+ isPopulatedObject(arg) &&
+ ((Object.keys(arg).length === 1 && arg.hasOwnProperty('type')) ||
+ (Object.keys(arg).length === 2 &&
+ arg.hasOwnProperty('type') &&
+ arg.hasOwnProperty('script') &&
+ (typeof arg.script === 'string' ||
+ (isPopulatedObject(arg.script) &&
+ Object.keys(arg.script).length === 1 &&
+ arg.script.hasOwnProperty('source') &&
+ typeof arg.script.source === 'string')))) &&
+ RUNTIME_FIELD_TYPES.includes(arg.type)
+ );
+}
+
+export function isRuntimeMappings(arg: any): arg is RuntimeMappings {
+ return isPopulatedObject(arg) && Object.values(arg).every((d) => isRuntimeField(d));
+}
+
export function isPivotPartialRequest(arg: any): arg is { pivot: PivotConfigDefinition } {
- return typeof arg === 'object' && arg.hasOwnProperty('pivot');
+ return isPopulatedObject(arg) && arg.hasOwnProperty('pivot');
}
export function isLatestPartialRequest(arg: any): arg is { latest: LatestFunctionConfig } {
- return typeof arg === 'object' && arg.hasOwnProperty('latest');
+ return isPopulatedObject(arg) && arg.hasOwnProperty('latest');
}
diff --git a/x-pack/plugins/transform/server/routes/api/field_histograms.ts b/x-pack/plugins/transform/server/routes/api/field_histograms.ts
index d3269bf14322ab..bfe2f470785690 100644
--- a/x-pack/plugins/transform/server/routes/api/field_histograms.ts
+++ b/x-pack/plugins/transform/server/routes/api/field_histograms.ts
@@ -32,7 +32,7 @@ export function registerFieldHistogramsRoutes({ router, license }: RouteDependen
license.guardApiRoute(
async (ctx, req, res) => {
const { indexPatternTitle } = req.params;
- const { query, fields, samplerShardSize } = req.body;
+ const { query, fields, runtimeMappings, samplerShardSize } = req.body;
try {
const resp = await getHistogramsForFields(
@@ -40,7 +40,8 @@ export function registerFieldHistogramsRoutes({ router, license }: RouteDependen
indexPatternTitle,
query,
fields,
- samplerShardSize
+ samplerShardSize,
+ runtimeMappings
);
return res.ok({ body: resp });
From 7b0ed48c01b84683fb4bd20fad114378219cc387 Mon Sep 17 00:00:00 2001
From: Steph Milovic
Date: Tue, 2 Mar 2021 08:45:36 -0700
Subject: [PATCH 03/63] [7.12] [Security Solution] [Timeline] Bugfix to include
unmapped fields in the timeline event details JSON (#92025) (#93172)
* fix merge
* fix lint
* fix func test
* fix func test
---
.../timeline/events/details/index.ts | 1 +
.../__snapshots__/json_view.test.tsx.snap | 2 +-
.../components/event_details/columns.tsx | 109 +++---
.../event_details/event_fields_browser.tsx | 1 -
.../components/event_details/helpers.tsx | 1 +
.../components/event_details/json_view.tsx | 20 +-
.../components/event_details/translations.ts | 7 +
.../public/common/mock/mock_detail_item.ts | 87 +++--
.../public/common/mock/timeline_results.ts | 2 +
.../components/alerts_table/helpers.test.ts | 11 +-
.../body/column_headers/header/index.tsx | 3 +-
.../__snapshots__/index.test.tsx.snap | 12 +-
.../get_column_renderer.test.tsx.snap | 2 +-
.../plain_column_renderer.test.tsx.snap | 2 +-
.../body/renderers/formatted_field.tsx | 17 +-
.../body/renderers/plain_column_renderer.tsx | 4 +-
.../public/timelines/store/timeline/model.ts | 3 +-
.../search_strategy/helpers/to_array.ts | 45 ++-
.../factory/hosts/all/helpers.ts | 8 +-
.../factory/hosts/authentications/helpers.ts | 8 +-
.../factory/hosts/details/helpers.ts | 8 +-
.../hosts/uncommon_processes/helpers.ts | 8 +-
.../factory/network/details/helpers.ts | 4 +-
.../search_strategy/timeline/eql/helpers.ts | 78 +++--
.../search_strategy/timeline/eql/index.ts | 2 +-
.../factory/events/all/helpers.test.ts | 73 ++--
.../timeline/factory/events/all/helpers.ts | 87 +++--
.../timeline/factory/events/all/index.ts | 6 +-
.../factory/events/details/helpers.test.ts | 300 +++++++++-------
.../factory/events/details/helpers.ts | 105 +++++-
.../timeline/factory/events/details/index.ts | 22 +-
.../details/query.events_details.dsl.test.ts | 4 +
.../details/query.events_details.dsl.ts | 2 +
.../timeline/factory/events/mocks.ts | 329 ++++++++++++++++++
.../security_solution/timeline_details.ts | 282 +++++++++++++--
35 files changed, 1253 insertions(+), 402 deletions(-)
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
index 5d6bc33ec49f8c..1f9820f8e5c2b0 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
@@ -16,6 +16,7 @@ export interface TimelineEventsDetailsItem {
values?: Maybe;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
originalValue?: Maybe;
+ isObjectArray: boolean;
}
export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse {
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap
index 2b681870e92fe4..0412b3074e3f17 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap
@@ -15,8 +15,8 @@ exports[`JSON View rendering should match snapshot 1`] = `
value="{
\\"_id\\": \\"pEMaMmkBUV60JmNWmWVi\\",
\\"_index\\": \\"filebeat-8.0.0-2019.02.19-000001\\",
- \\"_type\\": \\"_doc\\",
\\"_score\\": 1,
+ \\"_type\\": \\"_doc\\",
\\"@timestamp\\": \\"2019-02-28T16:50:54.621Z\\",
\\"agent\\": {
\\"ephemeral_id\\": \\"9d391ef2-a734-4787-8891-67031178c641\\",
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx
index 8fc6633df247f8..a62b652492c5f4 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx
@@ -85,24 +85,28 @@ export const getColumns = ({
sortable: false,
truncateText: false,
width: '30px',
- render: (field: string) => (
-
- c.id === field) !== -1}
- data-test-subj={`toggle-field-${field}`}
- data-colindex={1}
- id={field}
- onChange={() =>
- toggleColumn({
- columnHeaderType: defaultColumnHeaderType,
- id: field,
- width: DEFAULT_COLUMN_MIN_WIDTH,
- })
- }
- />
-
- ),
+ render: (field: string, data: EventFieldsData) => {
+ const label = data.isObjectArray ? i18n.NESTED_COLUMN(field) : i18n.VIEW_COLUMN(field);
+ return (
+
+ c.id === field) !== -1}
+ data-test-subj={`toggle-field-${field}`}
+ data-colindex={1}
+ id={field}
+ onChange={() =>
+ toggleColumn({
+ columnHeaderType: defaultColumnHeaderType,
+ id: field,
+ width: DEFAULT_COLUMN_MIN_WIDTH,
+ })
+ }
+ disabled={data.isObjectArray && data.type !== 'geo_point'}
+ />
+
+ );
+ },
},
{
field: 'field',
@@ -118,38 +122,42 @@ export const getColumns = ({
- (
-
-
-
-
-
- )}
- >
-
-
+ {data.isObjectArray && data.type !== 'geo_point' ? (
+ <>{field}>
+ ) : (
+ (
+
+
+
+
+
+ )}
+ >
+
+
+ )}
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx
index 497768785735b9..93d0e6ccfbe3c2 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx
@@ -108,7 +108,6 @@ export const EventFieldsBrowser = React.memo(
const columnHeaders = useDeepEqualSelector((state) => {
const { columns } = getTimeline(state, timelineId) ?? timelineDefaults;
-
return getColumnHeaders(columns, browserFields);
});
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx
index 7c7b8ba70f9bdd..00e2ee276f1816 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx
@@ -112,6 +112,7 @@ export const getIconFromType = (type: string | null) => {
case 'date':
return 'clock';
case 'ip':
+ case 'geo_point':
return 'globe';
case 'object':
return 'questionInCircle';
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx
index 449010781d448b..c9ca93582cd9a2 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx
@@ -54,12 +54,14 @@ export const JsonView = React.memo(({ data }) => {
JsonView.displayName = 'JsonView';
export const buildJsonView = (data: TimelineEventsDetailsItem[]) =>
- data.reduce(
- (accumulator, item) =>
- set(
- item.field,
- Array.isArray(item.originalValue) ? item.originalValue.join() : item.originalValue,
- accumulator
- ),
- {}
- );
+ data
+ .sort((a, b) => a.field.localeCompare(b.field))
+ .reduce(
+ (accumulator, item) =>
+ set(
+ item.field,
+ Array.isArray(item.originalValue) ? item.originalValue.join() : item.originalValue,
+ accumulator
+ ),
+ {}
+ );
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts
index c2b7bb4587dbd3..3a599b174251ac 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts
@@ -61,3 +61,10 @@ export const VIEW_COLUMN = (field: string) =>
values: { field },
defaultMessage: 'View {field} column',
});
+
+export const NESTED_COLUMN = (field: string) =>
+ i18n.translate('xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel', {
+ values: { field },
+ defaultMessage:
+ 'The {field} field is an object, and is broken down into nested fields which can be added as column',
+ });
diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts
index ca84ef539bec39..198ab084ae0b87 100644
--- a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts
@@ -14,105 +14,126 @@ export const mockDetailItemData: TimelineEventsDetailsItem[] = [
field: '_id',
originalValue: 'pEMaMmkBUV60JmNWmWVi',
values: ['pEMaMmkBUV60JmNWmWVi'],
+ isObjectArray: false,
},
{
field: '_index',
originalValue: 'filebeat-8.0.0-2019.02.19-000001',
values: ['filebeat-8.0.0-2019.02.19-000001'],
+ isObjectArray: false,
},
{
field: '_type',
originalValue: '_doc',
values: ['_doc'],
+ isObjectArray: false,
},
{
field: '_score',
originalValue: 1,
values: ['1'],
+ isObjectArray: false,
},
{
field: '@timestamp',
originalValue: '2019-02-28T16:50:54.621Z',
values: ['2019-02-28T16:50:54.621Z'],
+ isObjectArray: false,
},
{
field: 'agent.ephemeral_id',
originalValue: '9d391ef2-a734-4787-8891-67031178c641',
values: ['9d391ef2-a734-4787-8891-67031178c641'],
+ isObjectArray: false,
},
{
field: 'agent.hostname',
originalValue: 'siem-kibana',
values: ['siem-kibana'],
+ isObjectArray: false,
},
{
- field: 'agent.id',
- originalValue: '5de03d5f-52f3-482e-91d4-853c7de073c3',
- values: ['5de03d5f-52f3-482e-91d4-853c7de073c3'],
+ field: 'cloud.project.id',
+ originalValue: 'elastic-beats',
+ values: ['elastic-beats'],
+ isObjectArray: false,
},
{
- field: 'agent.type',
- originalValue: 'filebeat',
- values: ['filebeat'],
+ field: 'cloud.provider',
+ originalValue: 'gce',
+ values: ['gce'],
+ isObjectArray: false,
},
{
- field: 'agent.version',
- originalValue: '8.0.0',
- values: ['8.0.0'],
+ field: 'destination.bytes',
+ originalValue: 584,
+ values: ['584'],
+ isObjectArray: false,
},
{
- field: 'cloud.availability_zone',
- originalValue: 'projects/189716325846/zones/us-east1-b',
- values: ['projects/189716325846/zones/us-east1-b'],
+ field: 'destination.ip',
+ originalValue: '10.47.8.200',
+ values: ['10.47.8.200'],
+ isObjectArray: false,
},
{
- field: 'cloud.instance.id',
- originalValue: '5412578377715150143',
- values: ['5412578377715150143'],
+ field: 'agent.id',
+ originalValue: '5de03d5f-52f3-482e-91d4-853c7de073c3',
+ values: ['5de03d5f-52f3-482e-91d4-853c7de073c3'],
+ isObjectArray: false,
},
{
field: 'cloud.instance.name',
originalValue: 'siem-kibana',
values: ['siem-kibana'],
+ isObjectArray: false,
},
{
field: 'cloud.machine.type',
originalValue: 'projects/189716325846/machineTypes/n1-standard-1',
values: ['projects/189716325846/machineTypes/n1-standard-1'],
+ isObjectArray: false,
},
{
- field: 'cloud.project.id',
- originalValue: 'elastic-beats',
- values: ['elastic-beats'],
- },
- {
- field: 'cloud.provider',
- originalValue: 'gce',
- values: ['gce'],
- },
- {
- field: 'destination.bytes',
- originalValue: 584,
- values: ['584'],
- },
- {
- field: 'destination.ip',
- originalValue: '10.47.8.200',
- values: ['10.47.8.200'],
+ field: 'agent.type',
+ originalValue: 'filebeat',
+ values: ['filebeat'],
+ isObjectArray: false,
},
{
field: 'destination.packets',
originalValue: 4,
values: ['4'],
+ isObjectArray: false,
},
{
field: 'destination.port',
originalValue: 902,
values: ['902'],
+ isObjectArray: false,
},
{
field: 'event.kind',
originalValue: 'event',
values: ['event'],
+ isObjectArray: false,
+ },
+ {
+ field: 'agent.version',
+ originalValue: '8.0.0',
+ values: ['8.0.0'],
+ isObjectArray: false,
+ },
+ {
+ field: 'cloud.availability_zone',
+ originalValue: 'projects/189716325846/zones/us-east1-b',
+ values: ['projects/189716325846/zones/us-east1-b'],
+ isObjectArray: false,
+ },
+ {
+ field: 'cloud.instance.id',
+ originalValue: '5412578377715150143',
+ values: ['5412578377715150143'],
+ isObjectArray: false,
},
];
diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts
index 70ed497ce0caca..26b30e0d1f89ac 100644
--- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts
@@ -2287,11 +2287,13 @@ export const mockTimelineDetails: TimelineEventsDetailsItem[] = [
field: 'host.name',
values: ['apache'],
originalValue: 'apache',
+ isObjectArray: false,
},
{
field: 'user.id',
values: ['1'],
originalValue: 1,
+ isObjectArray: false,
},
];
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts
index 3eb00f8534979e..c296b75a0a253a 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts
@@ -29,6 +29,7 @@ describe('helpers', () => {
{
field: 'x',
values: ['The nickname of the developer we all :heart:'],
+ isObjectArray: false,
originalValue: 'The nickname of the developer we all :heart:',
},
]);
@@ -40,6 +41,7 @@ describe('helpers', () => {
{
field: 'x',
values: ['The nickname of the developer we all :heart:'],
+ isObjectArray: false,
originalValue: 'The nickname of the developer we all :heart:',
},
]);
@@ -51,6 +53,7 @@ describe('helpers', () => {
{
field: 'x',
values: ['The nickname of the developer we all :heart:', 'We are all made of stars'],
+ isObjectArray: false,
originalValue: 'The nickname of the developer we all :heart:',
},
]);
@@ -65,6 +68,7 @@ describe('helpers', () => {
{
field: 'x.y.z',
values: ['zed'],
+ isObjectArray: false,
originalValue: 'zed',
},
]);
@@ -76,6 +80,7 @@ describe('helpers', () => {
{
field: 'x.y.z',
values: ['zed'],
+ isObjectArray: false,
originalValue: 'zed',
},
]);
@@ -90,6 +95,7 @@ describe('helpers', () => {
{
field: 'a',
values: (5 as unknown) as string[],
+ isObjectArray: false,
originalValue: 'zed',
},
],
@@ -104,7 +110,7 @@ describe('helpers', () => {
'when trying to access field:',
'a',
'from data object of:',
- [{ field: 'a', originalValue: 'zed', values: 5 }]
+ [{ field: 'a', isObjectArray: false, originalValue: 'zed', values: 5 }]
);
});
@@ -116,6 +122,7 @@ describe('helpers', () => {
{
field: 'a',
values: (['hi', 5] as unknown) as string[],
+ isObjectArray: false,
originalValue: 'zed',
},
],
@@ -130,7 +137,7 @@ describe('helpers', () => {
'when trying to access field:',
'a',
'from data object of:',
- [{ field: 'a', originalValue: 'zed', values: ['hi', 5] }]
+ [{ field: 'a', isObjectArray: false, originalValue: 'zed', values: ['hi', 5] }]
);
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx
index bec241e10d6137..ece28faedb9511 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx
@@ -86,6 +86,7 @@ export const HeaderComponent: React.FC = ({
getManageTimelineById,
timelineId,
]);
+ const showSortingCapability = !isEqlOn && !(header.subType && header.subType.nested);
return (
<>
@@ -94,7 +95,7 @@ export const HeaderComponent: React.FC = ({
isLoading={isLoading}
isResizing={false}
onClick={onColumnSort}
- showSortingCapability={!isEqlOn}
+ showSortingCapability={showSortingCapability}
sort={sort}
>
@@ -99,7 +99,7 @@ exports[`Columns it renders the expected columns 1`] = `
fieldFormat=""
fieldName="event.action"
fieldType=""
- key="plain-column-renderer-formatted-field-value-test-event.action-1-event.action-Action"
+ key="plain-column-renderer-formatted-field-value-test-event.action-1-event.action-Action-0"
truncate={true}
value="Action"
/>
@@ -129,7 +129,7 @@ exports[`Columns it renders the expected columns 1`] = `
fieldFormat=""
fieldName="host.name"
fieldType=""
- key="plain-column-renderer-formatted-field-value-test-host.name-1-host.name-apache"
+ key="plain-column-renderer-formatted-field-value-test-host.name-1-host.name-apache-0"
truncate={true}
value="apache"
/>
@@ -159,7 +159,7 @@ exports[`Columns it renders the expected columns 1`] = `
fieldFormat=""
fieldName="source.ip"
fieldType=""
- key="plain-column-renderer-formatted-field-value-test-source.ip-1-source.ip-192.168.0.1"
+ key="plain-column-renderer-formatted-field-value-test-source.ip-1-source.ip-192.168.0.1-0"
truncate={true}
value="192.168.0.1"
/>
@@ -189,7 +189,7 @@ exports[`Columns it renders the expected columns 1`] = `
fieldFormat=""
fieldName="destination.ip"
fieldType=""
- key="plain-column-renderer-formatted-field-value-test-destination.ip-1-destination.ip-192.168.0.3"
+ key="plain-column-renderer-formatted-field-value-test-destination.ip-1-destination.ip-192.168.0.3-0"
truncate={true}
value="192.168.0.3"
/>
@@ -219,7 +219,7 @@ exports[`Columns it renders the expected columns 1`] = `
fieldFormat=""
fieldName="user.name"
fieldType=""
- key="plain-column-renderer-formatted-field-value-test-user.name-1-user.name-john.dee"
+ key="plain-column-renderer-formatted-field-value-test-user.name-1-user.name-john.dee-0"
truncate={true}
value="john.dee"
/>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap
index e2c46a07af8cce..4da4e12e0f7b3c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap
@@ -8,7 +8,7 @@ exports[`get_column_renderer renders correctly against snapshot 1`] = `
fieldFormat=""
fieldName="event.severity"
fieldType=""
- key="plain-column-renderer-formatted-field-value-test-event.severity-1-message-3"
+ key="plain-column-renderer-formatted-field-value-test-event.severity-1-message-3-0"
value="3"
/>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap
index 8ea7708bf5907e..13912e6ad3da92 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap
@@ -8,7 +8,7 @@ exports[`plain_column_renderer rendering renders correctly against snapshot 1`]
fieldFormat=""
fieldName="event.category"
fieldType="keyword"
- key="plain-column-renderer-formatted-field-value-test-event.category-1-event.category-Access"
+ key="plain-column-renderer-formatted-field-value-test-event.category-1-event.category-Access-0"
value="Access"
/>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx
index fa3612f08204d7..3032f556251f30 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx
@@ -41,14 +41,27 @@ const columnNamesNotDraggable = [MESSAGE_FIELD_NAME];
const FormattedFieldValueComponent: React.FC<{
contextId: string;
eventId: string;
+ isObjectArray?: boolean;
fieldFormat?: string;
fieldName: string;
fieldType: string;
truncate?: boolean;
value: string | number | undefined | null;
linkValue?: string | null | undefined;
-}> = ({ contextId, eventId, fieldFormat, fieldName, fieldType, truncate, value, linkValue }) => {
- if (fieldType === IP_FIELD_TYPE) {
+}> = ({
+ contextId,
+ eventId,
+ fieldFormat,
+ fieldName,
+ fieldType,
+ isObjectArray = false,
+ truncate,
+ value,
+ linkValue,
+}) => {
+ if (isObjectArray) {
+ return <>{value}>;
+ } else if (fieldType === IP_FIELD_TYPE) {
return (
values != null
- ? values.map((value) => (
+ ? values.map((value, i) => (
(value: T | T[] | null): T[] =>
Array.isArray(value) ? value : value == null ? [] : [value];
-
export const toStringArray = (value: T | T[] | null): string[] => {
if (Array.isArray(value)) {
return value.reduce((acc, v) => {
@@ -42,3 +41,47 @@ export const toStringArray = (value: T | T[] | null): string[] => {
return [`${value}`];
}
};
+export const toObjectArrayOfStrings = (
+ value: T | T[] | null
+): Array<{
+ str: string;
+ isObjectArray?: boolean;
+}> => {
+ if (Array.isArray(value)) {
+ return value.reduce<
+ Array<{
+ str: string;
+ isObjectArray?: boolean;
+ }>
+ >((acc, v) => {
+ if (v != null) {
+ switch (typeof v) {
+ case 'number':
+ case 'boolean':
+ return [...acc, { str: v.toString() }];
+ case 'object':
+ try {
+ return [...acc, { str: JSON.stringify(v), isObjectArray: true }]; // need to track when string is not a simple value
+ } catch {
+ return [...acc, { str: 'Invalid Object' }];
+ }
+ case 'string':
+ return [...acc, { str: v }];
+ default:
+ return [...acc, { str: `${v}` }];
+ }
+ }
+ return acc;
+ }, []);
+ } else if (value == null) {
+ return [];
+ } else if (!Array.isArray(value) && typeof value === 'object') {
+ try {
+ return [{ str: JSON.stringify(value), isObjectArray: true }];
+ } catch {
+ return [{ str: 'Invalid Object' }];
+ }
+ } else {
+ return [{ str: `${value}` }];
+ }
+};
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts
index 505f99dd284557..8b2397fd7fab07 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts
@@ -11,7 +11,7 @@ import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields';
import { HostsEdges } from '../../../../../../common/search_strategy/security_solution/hosts';
import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types';
-import { toStringArray } from '../../../../helpers/to_array';
+import { toObjectArrayOfStrings } from '../../../../helpers/to_array';
export const HOSTS_FIELDS: readonly string[] = [
'_id',
@@ -33,7 +33,11 @@ export const formatHostEdgesData = (
flattenedFields.cursor.value = hostId || '';
const fieldValue = getHostFieldValue(fieldName, bucket);
if (fieldValue != null) {
- return set(`node.${fieldName}`, toStringArray(fieldValue), flattenedFields);
+ return set(
+ `node.${fieldName}`,
+ toObjectArrayOfStrings(fieldValue).map(({ str }) => str),
+ flattenedFields
+ );
}
return flattenedFields;
},
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts
index 06d81140f475e5..aeaefe690cbde3 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts
@@ -8,7 +8,7 @@
import { get, getOr, isEmpty } from 'lodash/fp';
import { set } from '@elastic/safer-lodash-set/fp';
import { mergeFieldsWithHit } from '../../../../../utils/build_query';
-import { toStringArray } from '../../../../helpers/to_array';
+import { toObjectArrayOfStrings } from '../../../../helpers/to_array';
import {
AuthenticationsEdges,
AuthenticationHit,
@@ -55,7 +55,11 @@ export const formatAuthenticationData = (
const fieldPath = `node.${fieldName}`;
const fieldValue = get(fieldPath, mergedResult);
if (!isEmpty(fieldValue)) {
- return set(fieldPath, toStringArray(fieldValue), mergedResult);
+ return set(
+ fieldPath,
+ toObjectArrayOfStrings(fieldValue).map(({ str }) => str),
+ mergedResult
+ );
} else {
return mergedResult;
}
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts
index 9c522bd704ef06..2b35517d693d51 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts
@@ -9,7 +9,7 @@ import { set } from '@elastic/safer-lodash-set/fp';
import { get, has, head } from 'lodash/fp';
import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields';
import { HostItem } from '../../../../../../common/search_strategy/security_solution/hosts';
-import { toStringArray } from '../../../../helpers/to_array';
+import { toObjectArrayOfStrings } from '../../../../helpers/to_array';
import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types';
@@ -42,7 +42,11 @@ export const formatHostItem = (bucket: HostAggEsItem): HostItem =>
if (fieldName === '_id') {
return set('_id', fieldValue, flattenedFields);
}
- return set(fieldName, toStringArray(fieldValue), flattenedFields);
+ return set(
+ fieldName,
+ toObjectArrayOfStrings(fieldValue).map(({ str }) => str),
+ flattenedFields
+ );
}
return flattenedFields;
}, {});
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts
index 7b01f4e7dc816a..fe202b48540d7b 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts
@@ -14,7 +14,7 @@ import {
HostsUncommonProcessesEdges,
HostsUncommonProcessHit,
} from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes';
-import { toStringArray } from '../../../../helpers/to_array';
+import { toObjectArrayOfStrings } from '../../../../helpers/to_array';
import { HostHits } from '../../../../../../common/search_strategy';
export const uncommonProcessesFields = [
@@ -82,7 +82,11 @@ export const formatUncommonProcessesData = (
fieldPath = `node.hosts.0.name`;
fieldValue = get(fieldPath, mergedResult);
}
- return set(fieldPath, toStringArray(fieldValue), mergedResult);
+ return set(
+ fieldPath,
+ toObjectArrayOfStrings(fieldValue).map(({ str }) => str),
+ mergedResult
+ );
},
{
node: {
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts
index 3b387597618e8d..8fc7ae0304a352 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts
@@ -13,7 +13,7 @@ import {
NetworkDetailsHostHit,
NetworkHit,
} from '../../../../../../common/search_strategy/security_solution/network';
-import { toStringArray } from '../../../../helpers/to_array';
+import { toObjectArrayOfStrings } from '../../../../helpers/to_array';
export const getNetworkDetailsAgg = (type: string, networkHit: NetworkHit | {}) => {
const firstSeen = getOr(null, `firstSeen.value_as_string`, networkHit);
@@ -53,7 +53,7 @@ const formatHostEcs = (data: Record | null): HostEcs | null =>
}
return {
...acc,
- [key]: toStringArray(value),
+ [key]: toObjectArrayOfStrings(value).map(({ str }) => str),
};
}, {});
};
diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts
index bdd3195b3b756d..b007307412e955 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts
@@ -7,7 +7,7 @@
import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common';
import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants';
-import { EqlSearchResponse } from '../../../../common/detection_engine/types';
+import { EqlSearchResponse, EqlSequence } from '../../../../common/detection_engine/types';
import { EventHit, TimelineEdges } from '../../../../common/search_strategy';
import {
TimelineEqlRequestOptions,
@@ -56,51 +56,53 @@ export const buildEqlDsl = (options: TimelineEqlRequestOptions): Record>, fieldRequested: string[]) =>
+ sequences.reduce>(async (acc, sequence, sequenceIndex) => {
+ const sequenceParentId = sequence.events[0]?._id ?? null;
+ const data = await acc;
+ const allData = await Promise.all(
+ sequence.events.map(async (event, eventIndex) => {
+ const item = await formatTimelineData(
+ fieldRequested,
+ TIMELINE_EVENTS_FIELDS,
+ event as EventHit
+ );
+ return Promise.resolve({
+ ...item,
+ node: {
+ ...item.node,
+ ecs: {
+ ...item.node.ecs,
+ ...(sequenceParentId != null
+ ? {
+ eql: {
+ parentId: sequenceParentId,
+ sequenceNumber: `${sequenceIndex}-${eventIndex}`,
+ },
+ }
+ : {}),
+ },
+ },
+ });
+ })
+ );
+ return Promise.resolve([...data, ...allData]);
+ }, Promise.resolve([]));
-export const parseEqlResponse = (
+export const parseEqlResponse = async (
options: TimelineEqlRequestOptions,
response: EqlSearchStrategyResponse>
): Promise => {
const { activePage, querySize } = options.pagination;
- // const totalCount = response.rawResponse?.body?.hits?.total?.value ?? 0;
let edges: TimelineEdges[] = [];
+
if (response.rawResponse.body.hits.sequences !== undefined) {
- edges = response.rawResponse.body.hits.sequences.reduce(
- (data, sequence, sequenceIndex) => {
- const sequenceParentId = sequence.events[0]?._id ?? null;
- return [
- ...data,
- ...sequence.events.map((event, eventIndex) => {
- const item = formatTimelineData(
- options.fieldRequested,
- TIMELINE_EVENTS_FIELDS,
- event as EventHit
- );
- return {
- ...item,
- node: {
- ...item.node,
- ecs: {
- ...item.node.ecs,
- ...(sequenceParentId != null
- ? {
- eql: {
- parentId: sequenceParentId,
- sequenceNumber: `${sequenceIndex}-${eventIndex}`,
- },
- }
- : {}),
- },
- },
- };
- }),
- ];
- },
- []
- );
+ edges = await parseSequences(response.rawResponse.body.hits.sequences, options.fieldRequested);
} else if (response.rawResponse.body.hits.events !== undefined) {
- edges = response.rawResponse.body.hits.events.map((event) =>
- formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, event as EventHit)
+ edges = await Promise.all(
+ response.rawResponse.body.hits.events.map(async (event) =>
+ formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, event as EventHit)
+ )
);
}
diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts
index cf7877e987acea..249f5582d2a398 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts
@@ -38,7 +38,7 @@ export const securitySolutionTimelineEqlSearchStrategyProvider = (
},
};
}),
- mergeMap((esSearchRes) =>
+ mergeMap(async (esSearchRes) =>
parseEqlResponse(
request,
(esSearchRes as unknown) as EqlSearchStrategyResponse>
diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts
index 10bb606dc2387e..61af6a7664faa6 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts
@@ -8,54 +8,23 @@
import { EventHit } from '../../../../../../common/search_strategy';
import { TIMELINE_EVENTS_FIELDS } from './constants';
import { formatTimelineData } from './helpers';
+import { eventHit } from '../mocks';
describe('#formatTimelineData', () => {
- it('happy path', () => {
- const response: EventHit = {
- _index: 'auditbeat-7.8.0-2020.11.05-000003',
- _id: 'tkCt1nUBaEgqnrVSZ8R_',
- _score: 0,
- _type: '',
- fields: {
- 'event.category': ['process'],
- 'process.ppid': [3977],
- 'user.name': ['jenkins'],
- 'process.args': ['go', 'vet', './...'],
- message: ['Process go (PID: 4313) by user jenkins STARTED'],
- 'process.pid': [4313],
- 'process.working_directory': [
- '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat',
- ],
- 'process.entity_id': ['Z59cIkAAIw8ZoK0H'],
- 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'],
- 'process.name': ['go'],
- 'event.action': ['process_started'],
- 'agent.type': ['auditbeat'],
- '@timestamp': ['2020-11-17T14:48:08.922Z'],
- 'event.module': ['system'],
- 'event.type': ['start'],
- 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
- 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'],
- 'host.os.family': ['debian'],
- 'event.kind': ['event'],
- 'host.id': ['e59991e835905c65ed3e455b33e13bd6'],
- 'event.dataset': ['process'],
- 'process.executable': [
- '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go',
- ],
- },
- _source: {},
- sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'],
- aggregations: {},
- };
-
- expect(
- formatTimelineData(
- ['@timestamp', 'host.name', 'destination.ip', 'source.ip'],
- TIMELINE_EVENTS_FIELDS,
- response
- )
- ).toEqual({
+ it('happy path', async () => {
+ const res = await formatTimelineData(
+ [
+ '@timestamp',
+ 'host.name',
+ 'destination.ip',
+ 'source.ip',
+ 'source.geo.location',
+ 'threat.indicator.matched.field',
+ ],
+ TIMELINE_EVENTS_FIELDS,
+ eventHit
+ );
+ expect(res).toEqual({
cursor: {
tiebreaker: 'beats-ci-immutable-ubuntu-1804-1605624279743236239',
value: '1605624488922',
@@ -72,6 +41,14 @@ describe('#formatTimelineData', () => {
field: 'host.name',
value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
},
+ {
+ field: 'source.geo.location',
+ value: [`{"lon":118.7778,"lat":32.0617}`],
+ },
+ {
+ field: 'threat.indicator.matched.field',
+ value: ['matched_field', 'matched_field_2'],
+ },
],
ecs: {
'@timestamp': ['2020-11-17T14:48:08.922Z'],
@@ -122,7 +99,7 @@ describe('#formatTimelineData', () => {
});
});
- it('rule signal results', () => {
+ it('rule signal results', async () => {
const response: EventHit = {
_index: '.siem-signals-patrykkopycinski-default-000007',
_id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562',
@@ -290,7 +267,7 @@ describe('#formatTimelineData', () => {
};
expect(
- formatTimelineData(
+ await formatTimelineData(
['@timestamp', 'host.name', 'destination.ip', 'source.ip'],
TIMELINE_EVENTS_FIELDS,
response
diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts
index 1a83cbf40f1f4d..e5bb8cb7e14b73 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts
@@ -6,9 +6,13 @@
*/
import { get, has, merge, uniq } from 'lodash/fp';
-import { EventHit, TimelineEdges } from '../../../../../../common/search_strategy';
+import {
+ EventHit,
+ TimelineEdges,
+ TimelineNonEcsData,
+} from '../../../../../../common/search_strategy';
import { toStringArray } from '../../../../helpers/to_array';
-import { formatGeoLocation, isGeoField } from '../details/helpers';
+import { getDataSafety, getDataFromFieldsHits } from '../details/helpers';
const getTimestamp = (hit: EventHit): string => {
if (hit.fields && hit.fields['@timestamp']) {
@@ -19,13 +23,14 @@ const getTimestamp = (hit: EventHit): string => {
return '';
};
-export const formatTimelineData = (
+export const formatTimelineData = async (
dataFields: readonly string[],
ecsFields: readonly string[],
hit: EventHit
) =>
- uniq([...ecsFields, ...dataFields]).reduce(
- (flattenedFields, fieldName) => {
+ uniq([...ecsFields, ...dataFields]).reduce>(
+ async (acc, fieldName) => {
+ const flattenedFields: TimelineEdges = await acc;
flattenedFields.node._id = hit._id;
flattenedFields.node._index = hit._index;
flattenedFields.node.ecs._id = hit._id;
@@ -35,30 +40,81 @@ export const formatTimelineData = (
flattenedFields.cursor.value = hit.sort[0];
flattenedFields.cursor.tiebreaker = hit.sort[1];
}
- return mergeTimelineFieldsWithHit(fieldName, flattenedFields, hit, dataFields, ecsFields);
+ const waitForIt = await mergeTimelineFieldsWithHit(
+ fieldName,
+ flattenedFields,
+ hit,
+ dataFields,
+ ecsFields
+ );
+ return Promise.resolve(waitForIt);
},
- {
+ Promise.resolve({
node: { ecs: { _id: '' }, data: [], _id: '', _index: '' },
cursor: {
value: '',
tiebreaker: null,
},
- }
+ })
);
const specialFields = ['_id', '_index', '_type', '_score'];
-const mergeTimelineFieldsWithHit = (
+const getValuesFromFields = async (
+ fieldName: string,
+ hit: EventHit,
+ nestedParentFieldName?: string
+): Promise => {
+ if (specialFields.includes(fieldName)) {
+ return [{ field: fieldName, value: toStringArray(get(fieldName, hit)) }];
+ }
+
+ let fieldToEval;
+ if (has(fieldName, hit._source)) {
+ fieldToEval = {
+ [fieldName]: get(fieldName, hit._source),
+ };
+ } else {
+ if (nestedParentFieldName == null || nestedParentFieldName === fieldName) {
+ fieldToEval = {
+ [fieldName]: hit.fields[fieldName],
+ };
+ } else if (nestedParentFieldName != null) {
+ fieldToEval = {
+ [nestedParentFieldName]: hit.fields[nestedParentFieldName],
+ };
+ } else {
+ // fallback, should never hit
+ fieldToEval = {
+ [fieldName]: [],
+ };
+ }
+ }
+ const formattedData = await getDataSafety(getDataFromFieldsHits, fieldToEval);
+ return formattedData.reduce(
+ (acc: TimelineNonEcsData[], { field, values }) =>
+ // nested fields return all field values, pick only the one we asked for
+ field.includes(fieldName) ? [...acc, { field, value: values }] : acc,
+ []
+ );
+};
+
+const mergeTimelineFieldsWithHit = async (
fieldName: string,
flattenedFields: T,
- hit: { _source: {}; fields: Record },
+ hit: EventHit,
dataFields: readonly string[],
ecsFields: readonly string[]
) => {
if (fieldName != null || dataFields.includes(fieldName)) {
+ const fieldNameAsArray = fieldName.split('.');
+ const nestedParentFieldName = Object.keys(hit.fields ?? []).find((f) => {
+ return f === fieldNameAsArray.slice(0, f.split('.').length).join('.');
+ });
if (
has(fieldName, hit._source) ||
has(fieldName, hit.fields) ||
+ nestedParentFieldName != null ||
specialFields.includes(fieldName)
) {
const objectWithProperty = {
@@ -67,16 +123,7 @@ const mergeTimelineFieldsWithHit = (
data: dataFields.includes(fieldName)
? [
...get('node.data', flattenedFields),
- {
- field: fieldName,
- value: specialFields.includes(fieldName)
- ? toStringArray(get(fieldName, hit))
- : isGeoField(fieldName)
- ? formatGeoLocation(hit.fields[fieldName])
- : has(fieldName, hit._source)
- ? toStringArray(get(fieldName, hit._source))
- : toStringArray(hit.fields[fieldName]),
- },
+ ...(await getValuesFromFields(fieldName, hit, nestedParentFieldName)),
]
: get('node.data', flattenedFields),
ecs: ecsFields.includes(fieldName)
diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts
index 93985baed770e9..05058e3ee7a2dc 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts
@@ -40,8 +40,10 @@ export const timelineEventsAll: SecuritySolutionTimelineFactory
- formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit as EventHit)
+ const edges: TimelineEdges[] = await Promise.all(
+ hits.map((hit) =>
+ formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit as EventHit)
+ )
);
const inspect = {
dsl: [inspectStringifyObject(buildTimelineEventsAllQuery(queryOptions))],
diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts
index ca9a4b708161de..dc3efc6909c634 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts
@@ -5,150 +5,192 @@
* 2.0.
*/
-import { EventHit } from '../../../../../../common/search_strategy';
-import { getDataFromFieldsHits } from './helpers';
+import { EventHit, EventSource } from '../../../../../../common/search_strategy';
+import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './helpers';
+import { eventDetailsFormattedFields, eventHit } from '../mocks';
-describe('#getDataFromFieldsHits', () => {
- it('happy path', () => {
- const response: EventHit = {
- _index: 'auditbeat-7.8.0-2020.11.05-000003',
- _id: 'tkCt1nUBaEgqnrVSZ8R_',
- _score: 0,
- _type: '',
- fields: {
- 'event.category': ['process'],
- 'process.ppid': [3977],
- 'user.name': ['jenkins'],
- 'process.args': ['go', 'vet', './...'],
- message: ['Process go (PID: 4313) by user jenkins STARTED'],
- 'process.pid': [4313],
- 'process.working_directory': [
- '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat',
+describe('Events Details Helpers', () => {
+ const fields: EventHit['fields'] = eventHit.fields;
+ const resultFields = eventDetailsFormattedFields;
+ describe('#getDataFromFieldsHits', () => {
+ it('happy path', () => {
+ const result = getDataFromFieldsHits(fields);
+ expect(result).toEqual(resultFields);
+ });
+ it('lets get weird', () => {
+ const whackFields = {
+ 'crazy.pants': [
+ {
+ 'matched.field': ['matched_field'],
+ first_seen: ['2021-02-22T17:29:25.195Z'],
+ provider: ['yourself'],
+ type: ['custom'],
+ 'matched.atomic': ['matched_atomic'],
+ lazer: [
+ {
+ 'great.field': ['grrrrr'],
+ lazer: [
+ {
+ lazer: [
+ {
+ cool: true,
+ lazer: [
+ {
+ lazer: [
+ {
+ lazer: [
+ {
+ lazer: [
+ {
+ whoa: false,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ lazer: [
+ {
+ cool: false,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ 'great.field': ['grrrrr_2'],
+ },
+ ],
+ },
],
- 'process.entity_id': ['Z59cIkAAIw8ZoK0H'],
- 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'],
- 'process.name': ['go'],
- 'event.action': ['process_started'],
- 'agent.type': ['auditbeat'],
- '@timestamp': ['2020-11-17T14:48:08.922Z'],
- 'event.module': ['system'],
- 'event.type': ['start'],
- 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
- 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'],
- 'host.os.family': ['debian'],
- 'event.kind': ['event'],
- 'host.id': ['e59991e835905c65ed3e455b33e13bd6'],
- 'event.dataset': ['process'],
- 'process.executable': [
- '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go',
- ],
- },
- _source: {},
- sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'],
- aggregations: {},
+ };
+ const whackResultFields = [
+ {
+ category: 'crazy',
+ field: 'crazy.pants.matched.field',
+ values: ['matched_field'],
+ originalValue: ['matched_field'],
+ isObjectArray: false,
+ },
+ {
+ category: 'crazy',
+ field: 'crazy.pants.first_seen',
+ values: ['2021-02-22T17:29:25.195Z'],
+ originalValue: ['2021-02-22T17:29:25.195Z'],
+ isObjectArray: false,
+ },
+ {
+ category: 'crazy',
+ field: 'crazy.pants.provider',
+ values: ['yourself'],
+ originalValue: ['yourself'],
+ isObjectArray: false,
+ },
+ {
+ category: 'crazy',
+ field: 'crazy.pants.type',
+ values: ['custom'],
+ originalValue: ['custom'],
+ isObjectArray: false,
+ },
+ {
+ category: 'crazy',
+ field: 'crazy.pants.matched.atomic',
+ values: ['matched_atomic'],
+ originalValue: ['matched_atomic'],
+ isObjectArray: false,
+ },
+ {
+ category: 'crazy',
+ field: 'crazy.pants.lazer.great.field',
+ values: ['grrrrr', 'grrrrr_2'],
+ originalValue: ['grrrrr', 'grrrrr_2'],
+ isObjectArray: false,
+ },
+ {
+ category: 'crazy',
+ field: 'crazy.pants.lazer.lazer.lazer.cool',
+ values: ['true', 'false'],
+ originalValue: ['true', 'false'],
+ isObjectArray: false,
+ },
+ {
+ category: 'crazy',
+ field: 'crazy.pants.lazer.lazer.lazer.lazer.lazer.lazer.lazer.whoa',
+ values: ['false'],
+ originalValue: ['false'],
+ isObjectArray: false,
+ },
+ ];
+ const result = getDataFromFieldsHits(whackFields);
+ expect(result).toEqual(whackResultFields);
+ });
+ });
+ it('#getDataFromSourceHits', () => {
+ const _source: EventSource = {
+ '@timestamp': '2021-02-24T00:41:06.527Z',
+ 'signal.status': 'open',
+ 'signal.rule.name': 'Rawr',
+ 'threat.indicator': [
+ {
+ provider: 'yourself',
+ type: 'custom',
+ first_seen: ['2021-02-22T17:29:25.195Z'],
+ matched: { atomic: 'atom', field: 'field', type: 'type' },
+ },
+ {
+ provider: 'other_you',
+ type: 'custom',
+ first_seen: '2021-02-22T17:29:25.195Z',
+ matched: { atomic: 'atom', field: 'field', type: 'type' },
+ },
+ ],
};
-
- expect(getDataFromFieldsHits(response.fields)).toEqual([
- {
- category: 'event',
- field: 'event.category',
- originalValue: ['process'],
- values: ['process'],
- },
- { category: 'process', field: 'process.ppid', originalValue: ['3977'], values: ['3977'] },
- { category: 'user', field: 'user.name', originalValue: ['jenkins'], values: ['jenkins'] },
- {
- category: 'process',
- field: 'process.args',
- originalValue: ['go', 'vet', './...'],
- values: ['go', 'vet', './...'],
- },
- {
- category: 'base',
- field: 'message',
- originalValue: ['Process go (PID: 4313) by user jenkins STARTED'],
- values: ['Process go (PID: 4313) by user jenkins STARTED'],
- },
- { category: 'process', field: 'process.pid', originalValue: ['4313'], values: ['4313'] },
- {
- category: 'process',
- field: 'process.working_directory',
- originalValue: [
- '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat',
- ],
- values: [
- '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat',
- ],
- },
- {
- category: 'process',
- field: 'process.entity_id',
- originalValue: ['Z59cIkAAIw8ZoK0H'],
- values: ['Z59cIkAAIw8ZoK0H'],
- },
- {
- category: 'host',
- field: 'host.ip',
- originalValue: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'],
- values: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'],
- },
- { category: 'process', field: 'process.name', originalValue: ['go'], values: ['go'] },
- {
- category: 'event',
- field: 'event.action',
- originalValue: ['process_started'],
- values: ['process_started'],
- },
- {
- category: 'agent',
- field: 'agent.type',
- originalValue: ['auditbeat'],
- values: ['auditbeat'],
- },
+ expect(getDataFromSourceHits(_source)).toEqual([
{
category: 'base',
field: '@timestamp',
- originalValue: ['2020-11-17T14:48:08.922Z'],
- values: ['2020-11-17T14:48:08.922Z'],
+ values: ['2021-02-24T00:41:06.527Z'],
+ originalValue: ['2021-02-24T00:41:06.527Z'],
+ isObjectArray: false,
},
- { category: 'event', field: 'event.module', originalValue: ['system'], values: ['system'] },
- { category: 'event', field: 'event.type', originalValue: ['start'], values: ['start'] },
{
- category: 'host',
- field: 'host.name',
- originalValue: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
- values: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
+ category: 'signal',
+ field: 'signal.status',
+ values: ['open'],
+ originalValue: ['open'],
+ isObjectArray: false,
},
{
- category: 'process',
- field: 'process.hash.sha1',
- originalValue: ['1eac22336a41e0660fb302add9d97daa2bcc7040'],
- values: ['1eac22336a41e0660fb302add9d97daa2bcc7040'],
+ category: 'signal',
+ field: 'signal.rule.name',
+ values: ['Rawr'],
+ originalValue: ['Rawr'],
+ isObjectArray: false,
},
- { category: 'host', field: 'host.os.family', originalValue: ['debian'], values: ['debian'] },
- { category: 'event', field: 'event.kind', originalValue: ['event'], values: ['event'] },
{
- category: 'host',
- field: 'host.id',
- originalValue: ['e59991e835905c65ed3e455b33e13bd6'],
- values: ['e59991e835905c65ed3e455b33e13bd6'],
- },
- {
- category: 'event',
- field: 'event.dataset',
- originalValue: ['process'],
- values: ['process'],
- },
- {
- category: 'process',
- field: 'process.executable',
- originalValue: [
- '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go',
- ],
+ category: 'threat',
+ field: 'threat.indicator',
values: [
- '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go',
+ '{"provider":"yourself","type":"custom","first_seen":["2021-02-22T17:29:25.195Z"],"matched":{"atomic":"atom","field":"field","type":"type"}}',
+ '{"provider":"other_you","type":"custom","first_seen":"2021-02-22T17:29:25.195Z","matched":{"atomic":"atom","field":"field","type":"type"}}',
+ ],
+ originalValue: [
+ '{"provider":"yourself","type":"custom","first_seen":["2021-02-22T17:29:25.195Z"],"matched":{"atomic":"atom","field":"field","type":"type"}}',
+ '{"provider":"other_you","type":"custom","first_seen":"2021-02-22T17:29:25.195Z","matched":{"atomic":"atom","field":"field","type":"type"}}',
],
+ isObjectArray: true,
},
]);
});
+ it('#getDataSafety', async () => {
+ const result = await getDataSafety(getDataFromFieldsHits, fields);
+ expect(result).toEqual(resultFields);
+ });
});
diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts
index 779454e9474ee7..2fc729729e4351 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts
@@ -7,8 +7,12 @@
import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp';
-import { EventSource, TimelineEventsDetailsItem } from '../../../../../../common/search_strategy';
-import { toStringArray } from '../../../../helpers/to_array';
+import {
+ EventHit,
+ EventSource,
+ TimelineEventsDetailsItem,
+} from '../../../../../../common/search_strategy';
+import { toObjectArrayOfStrings, toStringArray } from '../../../../helpers/to_array';
export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags'];
@@ -24,7 +28,10 @@ export const formatGeoLocation = (item: unknown[]) => {
const itemGeo = item.length > 0 ? (item[0] as { coordinates: number[] }) : null;
if (itemGeo != null && !isEmpty(itemGeo.coordinates)) {
try {
- return toStringArray({ long: itemGeo.coordinates[0], lat: itemGeo.coordinates[1] });
+ return toStringArray({
+ lon: itemGeo.coordinates[0],
+ lat: itemGeo.coordinates[1],
+ });
} catch {
return toStringArray(item);
}
@@ -46,13 +53,18 @@ export const getDataFromSourceHits = (
const field = path ? `${path}.${source}` : source;
const fieldCategory = getFieldCategory(field);
+ const objArrStr = toObjectArrayOfStrings(item);
+ const strArr = objArrStr.map(({ str }) => str);
+ const isObjectArray = objArrStr.some((o) => o.isObjectArray);
+
return [
...accumulator,
{
category: fieldCategory,
field,
- values: toStringArray(item),
- originalValue: toStringArray(item),
+ values: strArr,
+ originalValue: strArr,
+ isObjectArray,
} as TimelineEventsDetailsItem,
];
} else if (isObject(item)) {
@@ -65,18 +77,81 @@ export const getDataFromSourceHits = (
}, []);
export const getDataFromFieldsHits = (
- fields: Record
+ fields: EventHit['fields'],
+ prependField?: string,
+ prependFieldCategory?: string
): TimelineEventsDetailsItem[] =>
Object.keys(fields).reduce((accumulator, field) => {
const item: unknown[] = fields[field];
- const fieldCategory = getFieldCategory(field);
- return [
+
+ const fieldCategory =
+ prependFieldCategory != null ? prependFieldCategory : getFieldCategory(field);
+ if (isGeoField(field)) {
+ return [
+ ...accumulator,
+ {
+ category: fieldCategory,
+ field,
+ values: formatGeoLocation(item),
+ originalValue: formatGeoLocation(item),
+ isObjectArray: true, // important for UI
+ },
+ ];
+ }
+ const objArrStr = toObjectArrayOfStrings(item);
+ const strArr = objArrStr.map(({ str }) => str);
+ const isObjectArray = objArrStr.some((o) => o.isObjectArray);
+ const dotField = prependField ? `${prependField}.${field}` : field;
+
+ // return simple field value (non-object, non-array)
+ if (!isObjectArray) {
+ return [
+ ...accumulator,
+ {
+ category: fieldCategory,
+ field: dotField,
+ values: strArr,
+ originalValue: strArr,
+ isObjectArray,
+ },
+ ];
+ }
+
+ // format nested fields
+ const nestedFields = Array.isArray(item)
+ ? item
+ .reduce((acc, i) => [...acc, getDataFromFieldsHits(i, dotField, fieldCategory)], [])
+ .flat()
+ : getDataFromFieldsHits(item, prependField, fieldCategory);
+
+ // combine duplicate fields
+ const flat: Record = [
...accumulator,
- {
- category: fieldCategory,
- field,
- values: isGeoField(field) ? formatGeoLocation(item) : toStringArray(item),
- originalValue: toStringArray(item),
- } as TimelineEventsDetailsItem,
- ];
+ ...nestedFields,
+ ].reduce(
+ (acc, f) => ({
+ ...acc,
+ // acc/flat is hashmap to determine if we already have the field or not without an array iteration
+ // its converted back to array in return with Object.values
+ ...(acc[f.field] != null
+ ? {
+ [f.field]: {
+ ...f,
+ originalValue: acc[f.field].originalValue.includes(f.originalValue[0])
+ ? acc[f.field].originalValue
+ : [...acc[f.field].originalValue, ...f.originalValue],
+ values: acc[f.field].values.includes(f.values[0])
+ ? acc[f.field].values
+ : [...acc[f.field].values, ...f.values],
+ },
+ }
+ : { [f.field]: f }),
+ }),
+ {}
+ );
+
+ return Object.values(flat);
}, []);
+
+export const getDataSafety = (fn: (args: A) => T, args: A): Promise =>
+ new Promise((resolve) => setTimeout(() => resolve(fn(args))));
diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts
index f5deb258fc1f48..7794de7f0f4119 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { cloneDeep, merge } from 'lodash/fp';
+import { cloneDeep, merge, unionBy } from 'lodash/fp';
import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common';
import {
@@ -13,11 +13,13 @@ import {
TimelineEventsQueries,
TimelineEventsDetailsStrategyResponse,
TimelineEventsDetailsRequestOptions,
+ TimelineEventsDetailsItem,
+ EventSource,
} from '../../../../../../common/search_strategy';
import { inspectStringifyObject } from '../../../../../utils/build_query';
import { SecuritySolutionTimelineFactory } from '../../types';
import { buildTimelineDetailsQuery } from './query.events_details.dsl';
-import { getDataFromSourceHits } from './helpers';
+import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './helpers';
export const timelineEventsDetails: SecuritySolutionTimelineFactory = {
buildDsl: (options: TimelineEventsDetailsRequestOptions) => {
@@ -29,11 +31,10 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory
): Promise => {
const { indexName, eventId, docValueFields = [] } = options;
- const { _source, ...hitsData } = cloneDeep(response.rawResponse.hits.hits[0] ?? {});
+ const { _source, fields, ...hitsData } = cloneDeep(response.rawResponse.hits.hits[0] ?? {});
const inspect = {
dsl: [inspectStringifyObject(buildTimelineDetailsQuery(indexName, eventId, docValueFields))],
};
-
if (response.isRunning) {
return {
...response,
@@ -41,12 +42,19 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory(
+ getDataFromSourceHits,
+ _source
+ );
+ const fieldsData = await getDataSafety(
+ getDataFromFieldsHits,
+ merge(fields, hitsData)
+ );
- const sourceData = getDataFromSourceHits(merge(_source, hitsData));
-
+ const data = unionBy('field', fieldsData, sourceData);
return {
...response,
- data: sourceData,
+ data,
inspect,
};
},
diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts
index 47716e21bca319..4545a3a3e136b3 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts
@@ -24,6 +24,7 @@ describe('buildTimelineDetailsQuery', () => {
Object {
"allowNoIndices": true,
"body": Object {
+ "_source": true,
"docvalue_fields": Array [
Object {
"field": "@timestamp",
@@ -38,6 +39,9 @@ describe('buildTimelineDetailsQuery', () => {
"field": "agent.name",
},
],
+ "fields": Array [
+ "*",
+ ],
"query": Object {
"terms": Object {
"_id": Array [
diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts
index e8890072c1aff6..c624eb14ae969f 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts
@@ -22,6 +22,8 @@ export const buildTimelineDetailsQuery = (
_id: [id],
},
},
+ fields: ['*'],
+ _source: true,
},
size: 1,
});
diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts
new file mode 100644
index 00000000000000..13b7fe70512460
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts
@@ -0,0 +1,329 @@
+/*
+ * 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.
+ */
+
+export const eventHit = {
+ _index: 'auditbeat-7.8.0-2020.11.05-000003',
+ _id: 'tkCt1nUBaEgqnrVSZ8R_',
+ _score: 0,
+ _type: '',
+ fields: {
+ 'event.category': ['process'],
+ 'process.ppid': [3977],
+ 'user.name': ['jenkins'],
+ 'process.args': ['go', 'vet', './...'],
+ message: ['Process go (PID: 4313) by user jenkins STARTED'],
+ 'process.pid': [4313],
+ 'process.working_directory': [
+ '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat',
+ ],
+ 'process.entity_id': ['Z59cIkAAIw8ZoK0H'],
+ 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'],
+ 'process.name': ['go'],
+ 'event.action': ['process_started'],
+ 'agent.type': ['auditbeat'],
+ '@timestamp': ['2020-11-17T14:48:08.922Z'],
+ 'event.module': ['system'],
+ 'event.type': ['start'],
+ 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
+ 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'],
+ 'host.os.family': ['debian'],
+ 'event.kind': ['event'],
+ 'host.id': ['e59991e835905c65ed3e455b33e13bd6'],
+ 'event.dataset': ['process'],
+ 'process.executable': [
+ '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go',
+ ],
+ 'source.geo.location': [{ coordinates: [118.7778, 32.0617], type: 'Point' }],
+ 'threat.indicator': [
+ {
+ 'matched.field': ['matched_field'],
+ first_seen: ['2021-02-22T17:29:25.195Z'],
+ provider: ['yourself'],
+ type: ['custom'],
+ 'matched.atomic': ['matched_atomic'],
+ lazer: [
+ {
+ 'great.field': ['grrrrr'],
+ },
+ {
+ 'great.field': ['grrrrr_2'],
+ },
+ ],
+ },
+ {
+ 'matched.field': ['matched_field_2'],
+ first_seen: ['2021-02-22T17:29:25.195Z'],
+ provider: ['other_you'],
+ type: ['custom'],
+ 'matched.atomic': ['matched_atomic_2'],
+ lazer: [
+ {
+ 'great.field': [
+ {
+ wowoe: [
+ {
+ fooooo: ['grrrrr'],
+ },
+ ],
+ astring: 'cool',
+ aNumber: 1,
+ anObject: {
+ neat: true,
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ _source: {},
+ sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'],
+ aggregations: {},
+};
+
+export const eventDetailsFormattedFields = [
+ {
+ category: 'event',
+ field: 'event.category',
+ isObjectArray: false,
+ originalValue: ['process'],
+ values: ['process'],
+ },
+ {
+ category: 'process',
+ field: 'process.ppid',
+ isObjectArray: false,
+ originalValue: ['3977'],
+ values: ['3977'],
+ },
+ {
+ category: 'user',
+ field: 'user.name',
+ isObjectArray: false,
+ originalValue: ['jenkins'],
+ values: ['jenkins'],
+ },
+ {
+ category: 'process',
+ field: 'process.args',
+ isObjectArray: false,
+ originalValue: ['go', 'vet', './...'],
+ values: ['go', 'vet', './...'],
+ },
+ {
+ category: 'base',
+ field: 'message',
+ isObjectArray: false,
+ originalValue: ['Process go (PID: 4313) by user jenkins STARTED'],
+ values: ['Process go (PID: 4313) by user jenkins STARTED'],
+ },
+ {
+ category: 'process',
+ field: 'process.pid',
+ isObjectArray: false,
+ originalValue: ['4313'],
+ values: ['4313'],
+ },
+ {
+ category: 'process',
+ field: 'process.working_directory',
+ isObjectArray: false,
+ originalValue: [
+ '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat',
+ ],
+ values: [
+ '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat',
+ ],
+ },
+ {
+ category: 'process',
+ field: 'process.entity_id',
+ isObjectArray: false,
+ originalValue: ['Z59cIkAAIw8ZoK0H'],
+ values: ['Z59cIkAAIw8ZoK0H'],
+ },
+ {
+ category: 'host',
+ field: 'host.ip',
+ isObjectArray: false,
+ originalValue: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'],
+ values: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'],
+ },
+ {
+ category: 'process',
+ field: 'process.name',
+ isObjectArray: false,
+ originalValue: ['go'],
+ values: ['go'],
+ },
+ {
+ category: 'event',
+ field: 'event.action',
+ isObjectArray: false,
+ originalValue: ['process_started'],
+ values: ['process_started'],
+ },
+ {
+ category: 'agent',
+ field: 'agent.type',
+ isObjectArray: false,
+ originalValue: ['auditbeat'],
+ values: ['auditbeat'],
+ },
+ {
+ category: 'base',
+ field: '@timestamp',
+ isObjectArray: false,
+ originalValue: ['2020-11-17T14:48:08.922Z'],
+ values: ['2020-11-17T14:48:08.922Z'],
+ },
+ {
+ category: 'event',
+ field: 'event.module',
+ isObjectArray: false,
+ originalValue: ['system'],
+ values: ['system'],
+ },
+ {
+ category: 'event',
+ field: 'event.type',
+ isObjectArray: false,
+ originalValue: ['start'],
+ values: ['start'],
+ },
+ {
+ category: 'host',
+ field: 'host.name',
+ isObjectArray: false,
+ originalValue: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
+ values: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
+ },
+ {
+ category: 'process',
+ field: 'process.hash.sha1',
+ isObjectArray: false,
+ originalValue: ['1eac22336a41e0660fb302add9d97daa2bcc7040'],
+ values: ['1eac22336a41e0660fb302add9d97daa2bcc7040'],
+ },
+ {
+ category: 'host',
+ field: 'host.os.family',
+ isObjectArray: false,
+ originalValue: ['debian'],
+ values: ['debian'],
+ },
+ {
+ category: 'event',
+ field: 'event.kind',
+ isObjectArray: false,
+ originalValue: ['event'],
+ values: ['event'],
+ },
+ {
+ category: 'host',
+ field: 'host.id',
+ isObjectArray: false,
+ originalValue: ['e59991e835905c65ed3e455b33e13bd6'],
+ values: ['e59991e835905c65ed3e455b33e13bd6'],
+ },
+ {
+ category: 'event',
+ field: 'event.dataset',
+ isObjectArray: false,
+ originalValue: ['process'],
+ values: ['process'],
+ },
+ {
+ category: 'process',
+ field: 'process.executable',
+ isObjectArray: false,
+ originalValue: [
+ '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go',
+ ],
+ values: [
+ '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go',
+ ],
+ },
+ {
+ category: 'source',
+ field: 'source.geo.location',
+ isObjectArray: true,
+ originalValue: [`{"lon":118.7778,"lat":32.0617}`],
+ values: [`{"lon":118.7778,"lat":32.0617}`],
+ },
+ {
+ category: 'threat',
+ field: 'threat.indicator.matched.field',
+ values: ['matched_field', 'matched_field_2'],
+ originalValue: ['matched_field', 'matched_field_2'],
+ isObjectArray: false,
+ },
+ {
+ category: 'threat',
+ field: 'threat.indicator.first_seen',
+ values: ['2021-02-22T17:29:25.195Z'],
+ originalValue: ['2021-02-22T17:29:25.195Z'],
+ isObjectArray: false,
+ },
+ {
+ category: 'threat',
+ field: 'threat.indicator.provider',
+ values: ['yourself', 'other_you'],
+ originalValue: ['yourself', 'other_you'],
+ isObjectArray: false,
+ },
+ {
+ category: 'threat',
+ field: 'threat.indicator.type',
+ values: ['custom'],
+ originalValue: ['custom'],
+ isObjectArray: false,
+ },
+ {
+ category: 'threat',
+ field: 'threat.indicator.matched.atomic',
+ values: ['matched_atomic', 'matched_atomic_2'],
+ originalValue: ['matched_atomic', 'matched_atomic_2'],
+ isObjectArray: false,
+ },
+ {
+ category: 'threat',
+ field: 'threat.indicator.lazer.great.field',
+ values: ['grrrrr', 'grrrrr_2'],
+ originalValue: ['grrrrr', 'grrrrr_2'],
+ isObjectArray: false,
+ },
+ {
+ category: 'threat',
+ field: 'threat.indicator.lazer.great.field.wowoe.fooooo',
+ values: ['grrrrr'],
+ originalValue: ['grrrrr'],
+ isObjectArray: false,
+ },
+ {
+ category: 'threat',
+ field: 'threat.indicator.lazer.great.field.astring',
+ values: ['cool'],
+ originalValue: ['cool'],
+ isObjectArray: false,
+ },
+ {
+ category: 'threat',
+ field: 'threat.indicator.lazer.great.field.aNumber',
+ values: ['1'],
+ originalValue: ['1'],
+ isObjectArray: false,
+ },
+ {
+ category: 'threat',
+ field: 'threat.indicator.lazer.great.field.neat',
+ values: ['true'],
+ originalValue: ['true'],
+ isObjectArray: false,
+ },
+];
diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts
index 2229f46a2e2735..8b60685b8b53d9 100644
--- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts
+++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts
@@ -20,96 +20,140 @@ const EXPECTED_DATA = [
field: '@timestamp',
values: ['2019-02-10T02:39:44.107Z'],
originalValue: ['2019-02-10T02:39:44.107Z'],
+ isObjectArray: false,
},
{
category: '@version',
field: '@version',
values: ['1'],
originalValue: ['1'],
+ isObjectArray: false,
+ },
+ {
+ category: '_id',
+ field: '_id',
+ values: ['QRhG1WgBqd-n62SwZYDT'],
+ originalValue: ['QRhG1WgBqd-n62SwZYDT'],
+ isObjectArray: false,
+ },
+ {
+ category: '_index',
+ field: '_index',
+ values: ['filebeat-7.0.0-iot-2019.06'],
+ originalValue: ['filebeat-7.0.0-iot-2019.06'],
+ isObjectArray: false,
+ },
+ {
+ category: '_score',
+ field: '_score',
+ values: ['1'],
+ originalValue: ['1'],
+ isObjectArray: false,
+ },
+ {
+ category: '_type',
+ field: '_type',
+ values: ['_doc'],
+ originalValue: ['_doc'],
+ isObjectArray: false,
},
{
category: 'agent',
field: 'agent.ephemeral_id',
values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'],
originalValue: ['909cd6a1-527d-41a5-9585-a7fb5386f851'],
+ isObjectArray: false,
},
{
category: 'agent',
field: 'agent.hostname',
values: ['raspberrypi'],
originalValue: ['raspberrypi'],
+ isObjectArray: false,
},
{
category: 'agent',
field: 'agent.id',
values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'],
originalValue: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'],
+ isObjectArray: false,
},
{
category: 'agent',
field: 'agent.type',
values: ['filebeat'],
originalValue: ['filebeat'],
+ isObjectArray: false,
},
{
category: 'agent',
field: 'agent.version',
values: ['7.0.0'],
originalValue: ['7.0.0'],
+ isObjectArray: false,
},
{
category: 'destination',
field: 'destination.domain',
values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'],
originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'],
+ isObjectArray: false,
},
{
category: 'destination',
field: 'destination.ip',
values: ['10.100.7.196'],
originalValue: ['10.100.7.196'],
+ isObjectArray: false,
},
{
category: 'destination',
field: 'destination.port',
values: ['40684'],
originalValue: ['40684'],
+ isObjectArray: false,
},
{
category: 'ecs',
field: 'ecs.version',
values: ['1.0.0-beta2'],
originalValue: ['1.0.0-beta2'],
+ isObjectArray: false,
},
{
category: 'event',
field: 'event.dataset',
values: ['suricata.eve'],
originalValue: ['suricata.eve'],
+ isObjectArray: false,
},
{
category: 'event',
field: 'event.end',
values: ['2019-02-10T02:39:44.107Z'],
originalValue: ['2019-02-10T02:39:44.107Z'],
+ isObjectArray: false,
},
{
category: 'event',
field: 'event.kind',
values: ['event'],
originalValue: ['event'],
+ isObjectArray: false,
},
{
category: 'event',
field: 'event.module',
values: ['suricata'],
originalValue: ['suricata'],
+ isObjectArray: false,
},
{
category: 'event',
field: 'event.type',
values: ['fileinfo'],
originalValue: ['fileinfo'],
+ isObjectArray: false,
},
{
category: 'file',
@@ -120,270 +164,484 @@ const EXPECTED_DATA = [
originalValue: [
'/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4',
],
+ isObjectArray: false,
},
{
category: 'file',
field: 'file.size',
values: ['48277'],
originalValue: ['48277'],
+ isObjectArray: false,
},
{
category: 'fileset',
field: 'fileset.name',
values: ['eve'],
originalValue: ['eve'],
+ isObjectArray: false,
},
{
category: 'flow',
field: 'flow.locality',
values: ['public'],
originalValue: ['public'],
+ isObjectArray: false,
},
{
category: 'host',
field: 'host.architecture',
values: ['armv7l'],
originalValue: ['armv7l'],
+ isObjectArray: false,
+ },
+ {
+ category: 'host',
+ field: 'host.containerized',
+ values: ['false'],
+ originalValue: ['false'],
+ isObjectArray: false,
},
{
category: 'host',
field: 'host.hostname',
values: ['raspberrypi'],
originalValue: ['raspberrypi'],
+ isObjectArray: false,
},
{
category: 'host',
field: 'host.id',
values: ['b19a781f683541a7a25ee345133aa399'],
originalValue: ['b19a781f683541a7a25ee345133aa399'],
+ isObjectArray: false,
},
{
category: 'host',
field: 'host.name',
values: ['raspberrypi'],
originalValue: ['raspberrypi'],
+ isObjectArray: false,
},
{
category: 'host',
field: 'host.os.codename',
values: ['stretch'],
originalValue: ['stretch'],
+ isObjectArray: false,
},
{
category: 'host',
field: 'host.os.family',
values: [''],
originalValue: [''],
+ isObjectArray: false,
},
{
category: 'host',
field: 'host.os.kernel',
values: ['4.14.50-v7+'],
originalValue: ['4.14.50-v7+'],
+ isObjectArray: false,
},
{
category: 'host',
field: 'host.os.name',
values: ['Raspbian GNU/Linux'],
originalValue: ['Raspbian GNU/Linux'],
+ isObjectArray: false,
},
{
category: 'host',
field: 'host.os.platform',
values: ['raspbian'],
originalValue: ['raspbian'],
+ isObjectArray: false,
},
{
category: 'host',
field: 'host.os.version',
values: ['9 (stretch)'],
originalValue: ['9 (stretch)'],
+ isObjectArray: false,
},
{
category: 'http',
field: 'http.request.method',
values: ['get'],
originalValue: ['get'],
+ isObjectArray: false,
},
{
category: 'http',
field: 'http.response.body.bytes',
values: ['48277'],
originalValue: ['48277'],
+ isObjectArray: false,
},
{
category: 'http',
field: 'http.response.status_code',
values: ['206'],
originalValue: ['206'],
+ isObjectArray: false,
},
{
category: 'input',
field: 'input.type',
values: ['log'],
originalValue: ['log'],
+ isObjectArray: false,
},
{
category: 'base',
field: 'labels.pipeline',
values: ['filebeat-7.0.0-suricata-eve-pipeline'],
originalValue: ['filebeat-7.0.0-suricata-eve-pipeline'],
+ isObjectArray: false,
},
{
category: 'log',
field: 'log.file.path',
values: ['/var/log/suricata/eve.json'],
originalValue: ['/var/log/suricata/eve.json'],
+ isObjectArray: false,
},
{
category: 'log',
field: 'log.offset',
values: ['1856288115'],
originalValue: ['1856288115'],
+ isObjectArray: false,
},
{
category: 'network',
field: 'network.name',
values: ['iot'],
originalValue: ['iot'],
+ isObjectArray: false,
},
{
category: 'network',
field: 'network.protocol',
values: ['http'],
originalValue: ['http'],
+ isObjectArray: false,
},
{
category: 'network',
field: 'network.transport',
values: ['tcp'],
originalValue: ['tcp'],
+ isObjectArray: false,
},
{
category: 'service',
field: 'service.type',
values: ['suricata'],
originalValue: ['suricata'],
+ isObjectArray: false,
},
{
category: 'source',
field: 'source.as.num',
values: ['16509'],
originalValue: ['16509'],
+ isObjectArray: false,
},
{
category: 'source',
field: 'source.as.org',
values: ['Amazon.com, Inc.'],
originalValue: ['Amazon.com, Inc.'],
+ isObjectArray: false,
},
{
category: 'source',
field: 'source.domain',
values: ['server-54-239-219-210.jfk51.r.cloudfront.net'],
originalValue: ['server-54-239-219-210.jfk51.r.cloudfront.net'],
+ isObjectArray: false,
},
{
category: 'source',
field: 'source.geo.city_name',
values: ['Seattle'],
originalValue: ['Seattle'],
+ isObjectArray: false,
},
{
category: 'source',
field: 'source.geo.continent_name',
values: ['North America'],
originalValue: ['North America'],
+ isObjectArray: false,
},
{
category: 'source',
field: 'source.geo.country_iso_code',
values: ['US'],
originalValue: ['US'],
+ isObjectArray: false,
+ },
+ {
+ category: 'source',
+ field: 'source.geo.location',
+ values: ['{"lon":-122.3341,"lat":47.6103}'],
+ originalValue: ['{"lon":-122.3341,"lat":47.6103}'],
+ isObjectArray: true,
},
{
category: 'source',
field: 'source.geo.location.lat',
values: ['47.6103'],
originalValue: ['47.6103'],
+ isObjectArray: false,
},
{
category: 'source',
field: 'source.geo.location.lon',
values: ['-122.3341'],
originalValue: ['-122.3341'],
+ isObjectArray: false,
},
{
category: 'source',
field: 'source.geo.region_iso_code',
values: ['US-WA'],
originalValue: ['US-WA'],
+ isObjectArray: false,
},
{
category: 'source',
field: 'source.geo.region_name',
values: ['Washington'],
originalValue: ['Washington'],
+ isObjectArray: false,
},
{
category: 'source',
field: 'source.ip',
values: ['54.239.219.210'],
originalValue: ['54.239.219.210'],
+ isObjectArray: false,
},
{
category: 'source',
field: 'source.port',
values: ['80'],
originalValue: ['80'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.app_proto',
+ values: ['http'],
+ originalValue: ['http'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.dest_ip',
+ values: ['10.100.7.196'],
+ originalValue: ['10.100.7.196'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.dest_port',
+ values: ['40684'],
+ originalValue: ['40684'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.fileinfo.filename',
+ values: [
+ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4',
+ ],
+ originalValue: [
+ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4',
+ ],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.fileinfo.size',
+ values: ['48277'],
+ originalValue: ['48277'],
+ isObjectArray: false,
},
{
category: 'suricata',
field: 'suricata.eve.fileinfo.state',
values: ['CLOSED'],
originalValue: ['CLOSED'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.fileinfo.stored',
+ values: ['false'],
+ originalValue: ['false'],
+ isObjectArray: false,
},
{
category: 'suricata',
field: 'suricata.eve.fileinfo.tx_id',
values: ['301'],
originalValue: ['301'],
+ isObjectArray: false,
},
{
category: 'suricata',
field: 'suricata.eve.flow_id',
values: ['196625917175466'],
originalValue: ['196625917175466'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.http.hostname',
+ values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'],
+ originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'],
+ isObjectArray: false,
},
{
category: 'suricata',
field: 'suricata.eve.http.http_content_type',
values: ['video/mp4'],
originalValue: ['video/mp4'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.http.http_method',
+ values: ['get'],
+ originalValue: ['get'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.http.length',
+ values: ['48277'],
+ originalValue: ['48277'],
+ isObjectArray: false,
},
{
category: 'suricata',
field: 'suricata.eve.http.protocol',
values: ['HTTP/1.1'],
originalValue: ['HTTP/1.1'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.http.status',
+ values: ['206'],
+ originalValue: ['206'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.http.url',
+ values: [
+ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4',
+ ],
+ originalValue: [
+ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4',
+ ],
+ isObjectArray: false,
},
{
category: 'suricata',
field: 'suricata.eve.in_iface',
values: ['eth0'],
originalValue: ['eth0'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.proto',
+ values: ['tcp'],
+ originalValue: ['tcp'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.src_ip',
+ values: ['54.239.219.210'],
+ originalValue: ['54.239.219.210'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.src_port',
+ values: ['80'],
+ originalValue: ['80'],
+ isObjectArray: false,
+ },
+ {
+ category: 'suricata',
+ field: 'suricata.eve.timestamp',
+ values: ['2019-02-10T02:39:44.107Z'],
+ originalValue: ['2019-02-10T02:39:44.107Z'],
+ isObjectArray: false,
},
{
category: 'base',
field: 'tags',
values: ['suricata'],
originalValue: ['suricata'],
+ isObjectArray: false,
+ },
+ {
+ category: 'traefik',
+ field: 'traefik.access.geoip.city_name',
+ values: ['Seattle'],
+ originalValue: ['Seattle'],
+ isObjectArray: false,
+ },
+ {
+ category: 'traefik',
+ field: 'traefik.access.geoip.continent_name',
+ values: ['North America'],
+ originalValue: ['North America'],
+ isObjectArray: false,
+ },
+ {
+ category: 'traefik',
+ field: 'traefik.access.geoip.country_iso_code',
+ values: ['US'],
+ originalValue: ['US'],
+ isObjectArray: false,
+ },
+ {
+ category: 'traefik',
+ field: 'traefik.access.geoip.location',
+ values: ['{"lon":-122.3341,"lat":47.6103}'],
+ originalValue: ['{"lon":-122.3341,"lat":47.6103}'],
+ isObjectArray: true,
+ },
+ {
+ category: 'traefik',
+ field: 'traefik.access.geoip.region_iso_code',
+ values: ['US-WA'],
+ originalValue: ['US-WA'],
+ isObjectArray: false,
+ },
+ {
+ category: 'traefik',
+ field: 'traefik.access.geoip.region_name',
+ values: ['Washington'],
+ originalValue: ['Washington'],
+ isObjectArray: false,
},
{
category: 'url',
field: 'url.domain',
values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'],
originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'],
+ isObjectArray: false,
},
{
category: 'url',
@@ -394,6 +652,7 @@ const EXPECTED_DATA = [
originalValue: [
'/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4',
],
+ isObjectArray: false,
},
{
category: 'url',
@@ -404,28 +663,9 @@ const EXPECTED_DATA = [
originalValue: [
'/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4',
],
- },
- {
- category: '_index',
- field: '_index',
- values: ['filebeat-7.0.0-iot-2019.06'],
- originalValue: ['filebeat-7.0.0-iot-2019.06'],
- },
- { category: '_type', field: '_type', values: ['_doc'], originalValue: ['_doc'] },
- {
- category: '_id',
- field: '_id',
- values: ['QRhG1WgBqd-n62SwZYDT'],
- originalValue: ['QRhG1WgBqd-n62SwZYDT'],
- },
- {
- category: '_score',
- field: '_score',
- values: ['1'],
- originalValue: ['1'],
+ isObjectArray: false,
},
];
-
const EXPECTED_KPI_COUNTS = {
destinationIpCount: 154,
hostCount: 1,
@@ -457,7 +697,7 @@ export default function ({ getService }: FtrProviderContext) {
wait_for_completion_timeout: '10s',
})
.expect(200);
- expect(sortBy(detailsData, 'name')).to.eql(sortBy(EXPECTED_DATA, 'name'));
+ expect(sortBy(detailsData, 'field')).to.eql(sortBy(EXPECTED_DATA, 'field'));
});
it('Make sure that we get kpi data', async () => {
From d3b4f8e93bc5951da05001022c313c48809ece6b Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Tue, 2 Mar 2021 17:11:31 +0100
Subject: [PATCH 04/63] [Lens] Fix runtime validation error message (#93195)
(#93217)
---
.../editor_frame/workspace_panel/workspace_panel.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
index af5411dd4d3b05..486c6f120d4a80 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
@@ -515,7 +515,7 @@ export const InnerVisualizationWrapper = ({
{localState.expandError ? (
- visibleErrorMessage
+ {visibleErrorMessage}
) : null}
>
}
From 1af47c71c5133071def7b832cc2551dbef93a0a9 Mon Sep 17 00:00:00 2001
From: Oliver Gupte
Date: Tue, 2 Mar 2021 11:19:47 -0500
Subject: [PATCH 05/63] [APM] Fix hidden search bar in error pages while
loading (#84476) (#93139) (#93165)
---
.../app/ErrorGroupDetails/index.tsx | 68 +++++++++++--------
.../app/error_group_overview/index.tsx | 2 +-
2 files changed, 42 insertions(+), 28 deletions(-)
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx
index 4cd2db43621a8f..2ef64b9945e286 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx
@@ -60,6 +60,45 @@ function getShortGroupId(errorGroupId?: string) {
return errorGroupId.slice(0, 5);
}
+function ErrorGroupHeader({
+ groupId,
+ isUnhandled,
+}: {
+ groupId: string;
+ isUnhandled?: boolean;
+}) {
+ return (
+ <>
+
+
+
+
+
+ {i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', {
+ defaultMessage: 'Error group {errorGroupId}',
+ values: {
+ errorGroupId: getShortGroupId(groupId),
+ },
+ })}
+
+
+
+ {isUnhandled && (
+
+
+ {i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', {
+ defaultMessage: 'Unhandled',
+ })}
+
+
+ )}
+
+
+
+ >
+ );
+}
+
type ErrorGroupDetailsProps = RouteComponentProps<{
groupId: string;
serviceName: string;
@@ -102,7 +141,7 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) {
useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 });
if (!errorGroupData || !errorDistributionData) {
- return null;
+ return ;
}
// If there are 0 occurrences, show only distribution chart w. empty message
@@ -115,32 +154,7 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) {
return (
<>
-
-
-
-
-
- {i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', {
- defaultMessage: 'Error group {errorGroupId}',
- values: {
- errorGroupId: getShortGroupId(groupId),
- },
- })}
-
-
-
- {isUnhandled && (
-
-
- {i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', {
- defaultMessage: 'Unhandled',
- })}
-
-
- )}
-
-
-
+
diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx
index bde23eddaa44fa..d2aac0d4523e50 100644
--- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx
@@ -67,7 +67,7 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 });
if (!errorDistributionData || !errorGroupListData) {
- return null;
+ return ;
}
return (
From 17d97e9ce6f85fc0751edc9ffec4df0b8342ce91 Mon Sep 17 00:00:00 2001
From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com>
Date: Tue, 2 Mar 2021 11:25:25 -0500
Subject: [PATCH 06/63] [7.12] [Security Solution][Case][Bug] Prevent closing
collection when pushing (#93095) (#93148)
* [Security Solution][Case][Bug] Prevent closing collection when pushing (#93095)
* Prevent closing collection when pushing
* Fixing translations
# Conflicts:
# x-pack/plugins/translations/translations/ja-JP.json
# x-pack/plugins/translations/translations/zh-CN.json
* Fixing translations
* Removing translation changes
* Removing translation changes take 2
---
.../plugins/case/server/client/cases/push.ts | 28 +++++++--
.../configure_cases/closure_options.tsx | 7 ++-
.../configure_cases/translations.ts | 7 +++
.../basic/tests/cases/push_case.ts | 61 ++++++++++++++++++-
4 files changed, 97 insertions(+), 6 deletions(-)
diff --git a/x-pack/plugins/case/server/client/cases/push.ts b/x-pack/plugins/case/server/client/cases/push.ts
index 352328ed1dd40d..80dcc7a0e018c3 100644
--- a/x-pack/plugins/case/server/client/cases/push.ts
+++ b/x-pack/plugins/case/server/client/cases/push.ts
@@ -11,6 +11,8 @@ import {
SavedObjectsClientContract,
SavedObjectsUpdateResponse,
Logger,
+ SavedObjectsFindResponse,
+ SavedObject,
} from 'kibana/server';
import { ActionResult, ActionsClient } from '../../../../actions/server';
import { flattenCaseSavedObject, getAlertIndicesAndIDs } from '../../routes/api/utils';
@@ -25,6 +27,8 @@ import {
CommentAttributes,
CaseUserActionsResponse,
User,
+ ESCasesConfigureAttributes,
+ CaseType,
} from '../../../common/api';
import { buildCaseUserActionItem } from '../../services/user_actions/helpers';
@@ -37,6 +41,22 @@ import {
import { CaseClientHandler } from '../client';
import { createCaseError } from '../../common/error';
+/**
+ * Returns true if the case should be closed based on the configuration settings and whether the case
+ * is a collection. Collections are not closable because we aren't allowing their status to be changed.
+ * In the future we could allow push to close all the sub cases of a collection but that's not currently supported.
+ */
+function shouldCloseByPush(
+ configureSettings: SavedObjectsFindResponse,
+ caseInfo: SavedObject
+): boolean {
+ return (
+ configureSettings.total > 0 &&
+ configureSettings.saved_objects[0].attributes.closure_type === 'close-by-pushing' &&
+ caseInfo.attributes.type !== CaseType.collection
+ );
+}
+
interface PushParams {
savedObjectsClient: SavedObjectsClientContract;
caseService: CaseServiceSetup;
@@ -190,14 +210,15 @@ export const push = async ({
let updatedCase: SavedObjectsUpdateResponse;
let updatedComments: SavedObjectsBulkUpdateResponse;
+ const shouldMarkAsClosed = shouldCloseByPush(myCaseConfigure, myCase);
+
try {
[updatedCase, updatedComments] = await Promise.all([
caseService.patchCase({
client: savedObjectsClient,
caseId,
updatedAttributes: {
- ...(myCaseConfigure.total > 0 &&
- myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing'
+ ...(shouldMarkAsClosed
? {
status: CaseStatuses.closed,
closed_at: pushedDate,
@@ -228,8 +249,7 @@ export const push = async ({
userActionService.postUserActions({
client: savedObjectsClient,
actions: [
- ...(myCaseConfigure.total > 0 &&
- myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing'
+ ...(shouldMarkAsClosed
? [
buildCaseUserActionItem({
action: 'update',
diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options.tsx
index 9417877e58f759..ba892116320ce2 100644
--- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options.tsx
@@ -27,7 +27,12 @@ const ClosureOptionsComponent: React.FC = ({
{i18n.CASE_CLOSURE_OPTIONS_TITLE}}
- description={i18n.CASE_CLOSURE_OPTIONS_DESC}
+ description={
+ <>
+ {i18n.CASE_CLOSURE_OPTIONS_DESC}
+ {i18n.CASE_COLSURE_OPTIONS_SUB_CASES}
+ >
+ }
data-test-subj="case-closure-options-form-group"
>
{
@@ -228,6 +234,59 @@ export default ({ getService }: FtrProviderContext): void => {
expect(body.status).to.eql('closed');
});
+ it('should push a collection case but not close it when closure_type: close-by-pushing', async () => {
+ const { body: connector } = await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'true')
+ .send({
+ ...getServiceNowConnector(),
+ config: { apiUrl: servicenowSimulatorURL },
+ })
+ .expect(200);
+
+ actionsRemover.add('default', connector.id, 'action', 'actions');
+ await supertest
+ .post(CASE_CONFIGURE_URL)
+ .set('kbn-xsrf', 'true')
+ .send({
+ ...getConfiguration({
+ id: connector.id,
+ name: connector.name,
+ type: connector.actionTypeId,
+ }),
+ closure_type: 'close-by-pushing',
+ })
+ .expect(200);
+
+ const { body: postedCase } = await supertest
+ .post(CASES_URL)
+ .set('kbn-xsrf', 'true')
+ .send({
+ ...postCollectionReq,
+ connector: getConfiguration({
+ id: connector.id,
+ name: connector.name,
+ type: connector.actionTypeId,
+ fields: {
+ urgency: '2',
+ impact: '2',
+ severity: '2',
+ category: 'software',
+ subcategory: 'os',
+ },
+ }).connector,
+ })
+ .expect(200);
+
+ const { body } = await supertest
+ .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`)
+ .set('kbn-xsrf', 'true')
+ .send({})
+ .expect(200);
+
+ expect(body.status).to.eql(CaseStatuses.open);
+ });
+
it('unhappy path - 404s when case does not exist', async () => {
await supertest
.post(`${CASES_URL}/fake-id/connector/fake-connector/_push`)
From b359f135cb88185957fbdd209f07ae403de74756 Mon Sep 17 00:00:00 2001
From: Sonja Krause-Harder
Date: Tue, 2 Mar 2021 17:26:32 +0100
Subject: [PATCH 07/63] Collect agent telemetry even when fleet server is
disabled. (#93198) (#93222)
---
x-pack/plugins/fleet/server/collectors/agent_collectors.ts | 5 ++++-
x-pack/plugins/fleet/server/collectors/register.ts | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts
index 154e78feae2832..ce4fb3e3df7d4c 100644
--- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts
+++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts
@@ -6,6 +6,7 @@
*/
import { ElasticsearchClient, SavedObjectsClient } from 'kibana/server';
+import { FleetConfigType } from '../../common/types';
import * as AgentService from '../services/agents';
import { isFleetServerSetup } from '../services/fleet_server';
@@ -17,11 +18,13 @@ export interface AgentUsage {
}
export const getAgentUsage = async (
+ config: FleetConfigType,
soClient?: SavedObjectsClient,
esClient?: ElasticsearchClient
): Promise => {
// TODO: unsure if this case is possible at all.
- if (!soClient || !esClient || !(await isFleetServerSetup())) {
+ const fleetServerMissing = config.agents.fleetServerEnabled && !(await isFleetServerSetup());
+ if (!soClient || !esClient || fleetServerMissing) {
return {
total: 0,
online: 0,
diff --git a/x-pack/plugins/fleet/server/collectors/register.ts b/x-pack/plugins/fleet/server/collectors/register.ts
index 3e4ed80a3a83fe..c2e043145cd972 100644
--- a/x-pack/plugins/fleet/server/collectors/register.ts
+++ b/x-pack/plugins/fleet/server/collectors/register.ts
@@ -38,7 +38,7 @@ export function registerFleetUsageCollector(
const [soClient, esClient] = await getInternalClients(core);
return {
agents_enabled: getIsAgentsEnabled(config),
- agents: await getAgentUsage(soClient, esClient),
+ agents: await getAgentUsage(config, soClient, esClient),
packages: await getPackageUsage(soClient),
};
},
From f77dfbe15c4ad09d5482cc8e426db200a8647ed8 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 2 Mar 2021 11:40:48 -0500
Subject: [PATCH 08/63] [Security Solution] [Detections] Updates warning
message when no indices match provided index patterns (#93094) (#93221)
* updates warning messages and modifies warning message when endpoint security rule is missing index pattern
* fix integration test text
Co-authored-by: Devin W. Hurley
---
.../signals/signal_rule_alert_type.ts | 1 +
.../detection_engine/signals/utils.test.ts | 56 +++++++++++++++++++
.../lib/detection_engine/signals/utils.ts | 13 +++--
.../security_and_spaces/tests/create_rules.ts | 2 +-
4 files changed, 67 insertions(+), 5 deletions(-)
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 94415f9b59e004..1fd0552569b2d8 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
@@ -217,6 +217,7 @@ export const signalRulesAlertType = ({
hasTimestampFields(
wroteStatus,
hasTimestampOverride ? (timestampOverride as string) : '@timestamp',
+ name,
timestampFieldCaps,
inputIndices,
ruleStatusService,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
index 7888bb6deaab79..a68fac7d920f26 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
@@ -843,6 +843,7 @@ describe('utils', () => {
const res = await hasTimestampFields(
false,
timestampField,
+ 'myfakerulename',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
timestampFieldCapsResponse as ApiResponse>,
['myfa*'],
@@ -883,6 +884,7 @@ describe('utils', () => {
const res = await hasTimestampFields(
false,
timestampField,
+ 'myfakerulename',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
timestampFieldCapsResponse as ApiResponse>,
['myfa*'],
@@ -895,6 +897,60 @@ describe('utils', () => {
);
expect(res).toBeTruthy();
});
+
+ test('returns true when missing logs-endpoint.alerts-* index and rule name is Endpoint Security', async () => {
+ const timestampField = '@timestamp';
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const timestampFieldCapsResponse: Partial, Context>> = {
+ body: {
+ indices: [],
+ fields: {},
+ },
+ };
+ mockLogger.error.mockClear();
+ const res = await hasTimestampFields(
+ false,
+ timestampField,
+ 'Endpoint Security',
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ timestampFieldCapsResponse as ApiResponse>,
+ ['logs-endpoint.alerts-*'],
+ ruleStatusServiceMock,
+ mockLogger,
+ buildRuleMessage
+ );
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"'
+ );
+ expect(res).toBeTruthy();
+ });
+
+ test('returns true when missing logs-endpoint.alerts-* index and rule name is NOT Endpoint Security', async () => {
+ const timestampField = '@timestamp';
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const timestampFieldCapsResponse: Partial, Context>> = {
+ body: {
+ indices: [],
+ fields: {},
+ },
+ };
+ mockLogger.error.mockClear();
+ const res = await hasTimestampFields(
+ false,
+ timestampField,
+ 'NOT Endpoint Security',
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ timestampFieldCapsResponse as ApiResponse>,
+ ['logs-endpoint.alerts-*'],
+ ruleStatusServiceMock,
+ mockLogger,
+ buildRuleMessage
+ );
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"'
+ );
+ expect(res).toBeTruthy();
+ });
});
describe('wrapBuildingBlocks', () => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
index 58bf22be97bf87..30471ae2a35484 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
@@ -105,6 +105,7 @@ export const hasReadIndexPrivileges = async (
export const hasTimestampFields = async (
wroteStatus: boolean,
timestampField: string,
+ ruleName: string,
// any is derived from here
// node_modules/@elastic/elasticsearch/api/kibana.d.ts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -115,11 +116,15 @@ export const hasTimestampFields = async (
buildRuleMessage: BuildRuleMessage
): Promise => {
if (!wroteStatus && isEmpty(timestampFieldCapsResponse.body.indices)) {
- const errorString = `The following index patterns did not match any indices: ${JSON.stringify(
+ const errorString = `This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ${JSON.stringify(
inputIndices
- )}`;
- logger.error(buildRuleMessage(errorString));
- await ruleStatusService.warning(errorString);
+ )} was found. This warning will continue to appear until a matching index is created or this rule is de-activated. ${
+ ruleName === 'Endpoint Security'
+ ? 'If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent.'
+ : ''
+ }`;
+ logger.error(buildRuleMessage(errorString.trimEnd()));
+ await ruleStatusService.warning(errorString.trimEnd());
return true;
} else if (
!wroteStatus &&
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts
index 8d494724d9f766..6bdf881ba8ca29 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts
@@ -138,7 +138,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(statusBody[body.id].current_status.status).to.eql('warning');
expect(statusBody[body.id].current_status.last_success_message).to.eql(
- 'The following index patterns did not match any indices: ["does-not-exist-*"]'
+ 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["does-not-exist-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated.'
);
});
From fc19967e6a33a60ee2875cc1e9b5f484bd9f38e6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Yulia=20=C4=8Cech?=
<6585477+yuliacech@users.noreply.github.com>
Date: Tue, 2 Mar 2021 17:45:23 +0100
Subject: [PATCH 09/63] [ILM] Refactor edit_policy client integration tests
into separate feature files (#92826) (#93229)
* Refactor edit_policy client integration tests into separate feature files
* Fixed merge conflicts with master
* Updated rollover tests to match master branch code
* Split reactive_form into smaller files
* Renamed flyout tests file
---
.../edit_policy/constants.ts | 21 +-
.../edit_policy/edit_policy.helpers.tsx | 9 -
.../edit_policy/edit_policy.test.ts | 1136 -----------------
.../edit_policy/features/cold_phase.test.ts | 52 +
.../edit_policy/features/delete_phase.test.ts | 169 +++
.../node_allocation.test.ts | 138 +-
.../features/request_flyout.test.ts | 66 +
.../edit_policy/features/rollover.test.ts | 108 ++
.../features/searchable_snapshots.test.ts | 163 +++
.../edit_policy/features/timeline.test.ts | 64 +
.../edit_policy/features/warm_phase.test.ts | 52 +
.../form_validation/error_indicators.test.ts | 159 +++
.../reactive_form/reactive_form.test.ts | 143 ---
.../policy_serialization.test.ts | 426 +++++++
14 files changed, 1373 insertions(+), 1333 deletions(-)
delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/cold_phase.test.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts
rename x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/{reactive_form => features}/node_allocation.test.ts (78%)
create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts
delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts
index 2c8fbfc749a82d..e47036b82e5941 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts
@@ -13,26 +13,6 @@ export const POLICY_NAME = 'my_policy';
export const SNAPSHOT_POLICY_NAME = 'my_snapshot_policy';
export const NEW_SNAPSHOT_POLICY_NAME = 'my_new_snapshot_policy';
-export const DEFAULT_POLICY: PolicyFromES = {
- version: 1,
- modified_date: Date.now().toString(),
- policy: {
- name: 'my_policy',
- phases: {
- hot: {
- min_age: '0ms',
- actions: {
- rollover: {
- max_age: '30d',
- max_size: '50gb',
- },
- },
- },
- },
- },
- name: 'my_policy',
-};
-
export const POLICY_WITH_MIGRATE_OFF: PolicyFromES = {
version: 1,
modified_date: Date.now().toString(),
@@ -191,6 +171,7 @@ export const POLICY_WITH_NODE_ROLE_ALLOCATION: PolicyFromES = {
},
},
warm: {
+ min_age: '0ms',
actions: {},
},
},
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
index c61b431eed46dc..b692d7fe69cd48 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
@@ -183,13 +183,6 @@ export const setup = async (arg?: {
const enable = (phase: Phases) => createFormToggleAction(`enablePhaseSwitch-${phase}`);
- const showDataAllocationOptions = (phase: Phases) => () => {
- act(() => {
- find(`${phase}-dataTierAllocationControls.dataTierSelect`).simulate('click');
- });
- component.update();
- };
-
const createMinAgeActions = (phase: Phases) => {
return {
hasMinAgeInput: () => exists(`${phase}-selectedMinimumAge`),
@@ -384,7 +377,6 @@ export const setup = async (arg?: {
},
warm: {
enable: enable('warm'),
- showDataAllocationOptions: showDataAllocationOptions('warm'),
...createMinAgeActions('warm'),
setReplicas: setReplicas('warm'),
hasErrorIndicator: () => exists('phaseErrorIndicator-warm'),
@@ -396,7 +388,6 @@ export const setup = async (arg?: {
},
cold: {
enable: enable('cold'),
- showDataAllocationOptions: showDataAllocationOptions('cold'),
...createMinAgeActions('cold'),
setReplicas: setReplicas('cold'),
setFreeze,
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
deleted file mode 100644
index 740aeebb852f18..00000000000000
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
+++ /dev/null
@@ -1,1136 +0,0 @@
-/*
- * 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 { act } from 'react-dom/test-utils';
-
-import { licensingMock } from '../../../../licensing/public/mocks';
-import { API_BASE_PATH } from '../../../common/constants';
-import { setupEnvironment } from '../helpers/setup_environment';
-import { EditPolicyTestBed, setup } from './edit_policy.helpers';
-
-import {
- DELETE_PHASE_POLICY,
- NEW_SNAPSHOT_POLICY_NAME,
- SNAPSHOT_POLICY_NAME,
- DEFAULT_POLICY,
- POLICY_WITH_MIGRATE_OFF,
- POLICY_WITH_INCLUDE_EXCLUDE,
- POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION,
- POLICY_WITH_NODE_ROLE_ALLOCATION,
- POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS,
- getDefaultHotPhasePolicy,
-} from './constants';
-
-describe(' ', () => {
- let testBed: EditPolicyTestBed;
- const { server, httpRequestsMockHelpers } = setupEnvironment();
-
- afterAll(() => {
- server.restore();
- });
-
- describe('serialization', () => {
- /**
- * We assume that policies that populate this form are loaded directly from ES and so
- * are valid according to ES. There may be settings in the policy created through the ILM
- * API that the UI does not cater for, like the unfollow action. We do not want to overwrite
- * the configuration for these actions in the UI.
- */
- it('preserves policy settings it did not configure', async () => {
- httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS]);
- httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
- httpRequestsMockHelpers.setListNodes({
- nodesByRoles: {},
- nodesByAttributes: { test: ['123'] },
- isUsingDeprecatedDataRoleConfig: false,
- });
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component, actions } = testBed;
- component.update();
-
- // Set max docs to test whether we keep the unknown fields in that object after serializing
- await actions.hot.setMaxDocs('1000');
- // Remove the delete phase to ensure that we also correctly remove data
- await actions.delete.disablePhase();
- await actions.savePolicy();
-
- const latestRequest = server.requests[server.requests.length - 1];
- const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
-
- expect(entirePolicy).toEqual({
- foo: 'bar', // Made up value
- name: 'my_policy',
- phases: {
- hot: {
- actions: {
- rollover: {
- max_docs: 1000,
- max_size: '50gb',
- unknown_setting: 123, // Made up setting that should stay preserved
- },
- },
- min_age: '0ms',
- },
- warm: {
- actions: {
- my_unfollow_action: {}, // Made up action
- set_priority: {
- priority: 22,
- unknown_setting: true,
- },
- },
- min_age: '0d',
- },
- },
- });
- });
- });
-
- describe('hot phase', () => {
- describe('serialization', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
- httpRequestsMockHelpers.setListNodes({
- nodesByRoles: {},
- nodesByAttributes: { test: ['123'] },
- isUsingDeprecatedDataRoleConfig: false,
- });
- httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component } = testBed;
- component.update();
- });
-
- test('setting all values', async () => {
- const { actions } = testBed;
-
- await actions.hot.toggleDefaultRollover(false);
- await actions.hot.setMaxSize('123', 'mb');
- await actions.hot.setMaxDocs('123');
- await actions.hot.setMaxAge('123', 'h');
- await actions.hot.toggleForceMerge(true);
- await actions.hot.setForcemergeSegmentsCount('123');
- await actions.hot.setBestCompression(true);
- await actions.hot.toggleShrink(true);
- await actions.hot.setShrink('2');
- await actions.hot.toggleReadonly(true);
- await actions.hot.toggleIndexPriority(true);
- await actions.hot.setIndexPriority('123');
-
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
- expect(entirePolicy).toMatchInlineSnapshot(`
- Object {
- "name": "my_policy",
- "phases": Object {
- "hot": Object {
- "actions": Object {
- "forcemerge": Object {
- "index_codec": "best_compression",
- "max_num_segments": 123,
- },
- "readonly": Object {},
- "rollover": Object {
- "max_age": "123h",
- "max_docs": 123,
- "max_size": "123mb",
- },
- "set_priority": Object {
- "priority": 123,
- },
- "shrink": Object {
- "number_of_shards": 2,
- },
- },
- "min_age": "0ms",
- },
- },
- }
- `);
- });
-
- test('setting searchable snapshot', async () => {
- const { actions } = testBed;
-
- await actions.hot.setSearchableSnapshot('my-repo');
-
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
- expect(entirePolicy.phases.hot.actions.searchable_snapshot.snapshot_repository).toBe(
- 'my-repo'
- );
- });
-
- test('disabling rollover', async () => {
- const { actions } = testBed;
- await actions.hot.toggleDefaultRollover(false);
- await actions.hot.toggleRollover(false);
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const policy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
- const hotActions = policy.phases.hot.actions;
- const rolloverAction = hotActions.rollover;
- expect(rolloverAction).toBe(undefined);
- expect(hotActions).toMatchInlineSnapshot(`Object {}`);
- });
-
- test('enabling searchable snapshot should hide force merge, freeze and shrink in subsequent phases', async () => {
- const { actions } = testBed;
-
- await actions.warm.enable(true);
- await actions.cold.enable(true);
-
- expect(actions.warm.forceMergeFieldExists()).toBeTruthy();
- expect(actions.warm.shrinkExists()).toBeTruthy();
- expect(actions.cold.searchableSnapshotsExists()).toBeTruthy();
- expect(actions.cold.freezeExists()).toBeTruthy();
-
- await actions.hot.setSearchableSnapshot('my-repo');
-
- expect(actions.warm.forceMergeFieldExists()).toBeFalsy();
- expect(actions.warm.shrinkExists()).toBeFalsy();
- // searchable snapshot in cold is still visible
- expect(actions.cold.searchableSnapshotsExists()).toBeTruthy();
- expect(actions.cold.freezeExists()).toBeFalsy();
- });
-
- test('disabling rollover toggle, but enabling default rollover', async () => {
- const { actions } = testBed;
- await actions.hot.toggleDefaultRollover(false);
- await actions.hot.toggleRollover(false);
- await actions.hot.toggleDefaultRollover(true);
-
- expect(actions.hot.forceMergeFieldExists()).toBeTruthy();
- expect(actions.hot.shrinkExists()).toBeTruthy();
- expect(actions.hot.searchableSnapshotsExists()).toBeTruthy();
- });
- });
- });
-
- describe('warm phase', () => {
- describe('serialization', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]);
- httpRequestsMockHelpers.setListNodes({
- nodesByRoles: {},
- nodesByAttributes: { test: ['123'] },
- isUsingDeprecatedDataRoleConfig: false,
- });
- httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component } = testBed;
- component.update();
- });
-
- test('default values', async () => {
- const { actions } = testBed;
- await actions.warm.enable(true);
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm;
- expect(warmPhase).toMatchInlineSnapshot(`
- Object {
- "actions": Object {
- "set_priority": Object {
- "priority": 50,
- },
- },
- "min_age": "0d",
- }
- `);
- });
-
- test('setting all values', async () => {
- const { actions } = testBed;
- await actions.warm.enable(true);
- await actions.warm.setDataAllocation('node_attrs');
- await actions.warm.setSelectedNodeAttribute('test:123');
- await actions.warm.setReplicas('123');
- await actions.warm.toggleShrink(true);
- await actions.warm.setShrink('123');
- await actions.warm.toggleForceMerge(true);
- await actions.warm.setForcemergeSegmentsCount('123');
- await actions.warm.setBestCompression(true);
- await actions.warm.toggleReadonly(true);
- await actions.warm.setIndexPriority('123');
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
- // Check shape of entire policy
- expect(entirePolicy).toMatchInlineSnapshot(`
- Object {
- "name": "my_policy",
- "phases": Object {
- "hot": Object {
- "actions": Object {
- "rollover": Object {
- "max_age": "30d",
- "max_size": "50gb",
- },
- },
- "min_age": "0ms",
- },
- "warm": Object {
- "actions": Object {
- "allocate": Object {
- "number_of_replicas": 123,
- "require": Object {
- "test": "123",
- },
- },
- "forcemerge": Object {
- "index_codec": "best_compression",
- "max_num_segments": 123,
- },
- "readonly": Object {},
- "set_priority": Object {
- "priority": 123,
- },
- "shrink": Object {
- "number_of_shards": 123,
- },
- },
- "min_age": "0d",
- },
- },
- }
- `);
- });
- });
-
- describe('policy with include and exclude', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_INCLUDE_EXCLUDE]);
- httpRequestsMockHelpers.setListNodes({
- nodesByRoles: {},
- nodesByAttributes: { test: ['123'] },
- isUsingDeprecatedDataRoleConfig: false,
- });
- httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component } = testBed;
- component.update();
- });
-
- test('preserves include, exclude allocation settings', async () => {
- const { actions } = testBed;
- await actions.warm.setDataAllocation('node_attrs');
- await actions.warm.setSelectedNodeAttribute('test:123');
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const warmPhaseAllocate = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm
- .actions.allocate;
- expect(warmPhaseAllocate).toMatchInlineSnapshot(`
- Object {
- "exclude": Object {
- "def": "456",
- },
- "include": Object {
- "abc": "123",
- },
- "require": Object {
- "test": "123",
- },
- }
- `);
- });
- });
- });
-
- describe('cold phase', () => {
- describe('serialization', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]);
- httpRequestsMockHelpers.setListNodes({
- nodesByRoles: {},
- nodesByAttributes: { test: ['123'] },
- isUsingDeprecatedDataRoleConfig: false,
- });
- httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component } = testBed;
- component.update();
- });
-
- test('default values', async () => {
- const { actions } = testBed;
-
- await actions.cold.enable(true);
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
- expect(entirePolicy.phases.cold).toMatchInlineSnapshot(`
- Object {
- "actions": Object {
- "set_priority": Object {
- "priority": 0,
- },
- },
- "min_age": "0d",
- }
- `);
- });
-
- test('setting all values, excluding searchable snapshot', async () => {
- const { actions } = testBed;
-
- await actions.cold.enable(true);
- await actions.cold.setMinAgeValue('123');
- await actions.cold.setMinAgeUnits('s');
- await actions.cold.setDataAllocation('node_attrs');
- await actions.cold.setSelectedNodeAttribute('test:123');
- await actions.cold.setSearchableSnapshot('my-repo');
- await actions.cold.setReplicas('123');
- await actions.cold.setFreeze(true);
- await actions.cold.setIndexPriority('123');
-
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
-
- expect(entirePolicy).toMatchInlineSnapshot(`
- Object {
- "name": "my_policy",
- "phases": Object {
- "cold": Object {
- "actions": Object {
- "allocate": Object {
- "number_of_replicas": 123,
- "require": Object {
- "test": "123",
- },
- },
- "freeze": Object {},
- "searchable_snapshot": Object {
- "snapshot_repository": "my-repo",
- },
- "set_priority": Object {
- "priority": 123,
- },
- },
- "min_age": "123s",
- },
- "hot": Object {
- "actions": Object {
- "rollover": Object {
- "max_age": "30d",
- "max_size": "50gb",
- },
- },
- "min_age": "0ms",
- },
- },
- }
- `);
- });
- });
- });
-
- describe('delete phase', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([DELETE_PHASE_POLICY]);
- httpRequestsMockHelpers.setLoadSnapshotPolicies([
- SNAPSHOT_POLICY_NAME,
- NEW_SNAPSHOT_POLICY_NAME,
- ]);
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component } = testBed;
- component.update();
- });
-
- test('serialization', async () => {
- httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]);
- await act(async () => {
- testBed = await setup();
- });
- const { component, actions } = testBed;
- component.update();
- await actions.delete.enablePhase();
- await actions.setWaitForSnapshotPolicy('test');
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
- expect(entirePolicy.phases.delete).toEqual({
- min_age: '365d',
- actions: {
- delete: {},
- wait_for_snapshot: {
- policy: 'test',
- },
- },
- });
- });
-
- test('wait for snapshot policy field should correctly display snapshot policy name', () => {
- expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([
- {
- label: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy,
- },
- ]);
- });
-
- test('wait for snapshot field should correctly update snapshot policy name', async () => {
- const { actions } = testBed;
-
- await actions.setWaitForSnapshotPolicy(NEW_SNAPSHOT_POLICY_NAME);
- await actions.savePolicy();
-
- const expected = {
- name: DELETE_PHASE_POLICY.name,
- phases: {
- ...DELETE_PHASE_POLICY.policy.phases,
- delete: {
- ...DELETE_PHASE_POLICY.policy.phases.delete,
- actions: {
- ...DELETE_PHASE_POLICY.policy.phases.delete?.actions,
- wait_for_snapshot: {
- policy: NEW_SNAPSHOT_POLICY_NAME,
- },
- },
- },
- },
- };
-
- const latestRequest = server.requests[server.requests.length - 1];
- expect(latestRequest.url).toBe(`${API_BASE_PATH}/policies`);
- expect(latestRequest.method).toBe('POST');
- expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
- });
-
- test('wait for snapshot field should display a callout when the input is not an existing policy', async () => {
- const { actions } = testBed;
-
- await actions.setWaitForSnapshotPolicy('my_custom_policy');
- expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy();
- expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy();
- expect(testBed.find('customPolicyCallout').exists()).toBeTruthy();
- });
-
- test('wait for snapshot field should delete action if field is empty', async () => {
- const { actions } = testBed;
-
- await actions.setWaitForSnapshotPolicy('');
- await actions.savePolicy();
-
- const expected = {
- name: DELETE_PHASE_POLICY.name,
- phases: {
- ...DELETE_PHASE_POLICY.policy.phases,
- delete: {
- ...DELETE_PHASE_POLICY.policy.phases.delete,
- actions: {
- ...DELETE_PHASE_POLICY.policy.phases.delete?.actions,
- },
- },
- },
- };
-
- delete expected.phases.delete.actions.wait_for_snapshot;
-
- const latestRequest = server.requests[server.requests.length - 1];
- expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
- });
-
- test('wait for snapshot field should display a callout when there are no snapshot policies', async () => {
- // need to call setup on testBed again for it to use a newly defined snapshot policies response
- httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
- await act(async () => {
- testBed = await setup();
- });
-
- testBed.component.update();
- expect(testBed.find('customPolicyCallout').exists()).toBeFalsy();
- expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy();
- expect(testBed.find('noPoliciesCallout').exists()).toBeTruthy();
- });
-
- test('wait for snapshot field should display a callout when there is an error loading snapshot policies', async () => {
- // need to call setup on testBed again for it to use a newly defined snapshot policies response
- httpRequestsMockHelpers.setLoadSnapshotPolicies([], { status: 500, body: 'error' });
- await act(async () => {
- testBed = await setup();
- });
-
- testBed.component.update();
- expect(testBed.find('customPolicyCallout').exists()).toBeFalsy();
- expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy();
- expect(testBed.find('policiesErrorCallout').exists()).toBeTruthy();
- });
- });
-
- describe('data allocation', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_MIGRATE_OFF]);
- httpRequestsMockHelpers.setListNodes({
- nodesByRoles: {},
- nodesByAttributes: { test: ['123'] },
- isUsingDeprecatedDataRoleConfig: false,
- });
- httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component } = testBed;
- component.update();
- });
-
- test('setting node_attr based allocation, but not selecting node attribute', async () => {
- const { actions } = testBed;
- await actions.warm.setDataAllocation('node_attrs');
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm;
-
- expect(warmPhase.actions.migrate).toEqual({ enabled: false });
- });
-
- describe('node roles', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ROLE_ALLOCATION]);
- httpRequestsMockHelpers.setListNodes({
- isUsingDeprecatedDataRoleConfig: false,
- nodesByAttributes: { test: ['123'] },
- nodesByRoles: { data: ['123'] },
- });
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component } = testBed;
- component.update();
- });
-
- test('detecting use of the recommended allocation type', () => {
- const { find } = testBed;
- const selectedDataAllocation = find(
- 'warm-dataTierAllocationControls.dataTierSelect'
- ).text();
- expect(selectedDataAllocation).toBe('Use warm nodes (recommended)');
- });
-
- test('setting replicas serialization', async () => {
- const { actions } = testBed;
- await actions.warm.setReplicas('123');
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const warmPhaseActions = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm
- .actions;
- expect(warmPhaseActions).toMatchInlineSnapshot(`
- Object {
- "allocate": Object {
- "number_of_replicas": 123,
- },
- }
- `);
- });
- });
-
- describe('node attr and none', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION]);
- httpRequestsMockHelpers.setListNodes({
- isUsingDeprecatedDataRoleConfig: false,
- nodesByAttributes: { test: ['123'] },
- nodesByRoles: { data: ['123'] },
- });
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component } = testBed;
- component.update();
- });
-
- test('detecting use of the custom allocation type', () => {
- const { find } = testBed;
- expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toBe('Custom');
- });
- test('detecting use of the "off" allocation type', () => {
- const { find } = testBed;
- expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off');
- });
- });
- describe('on cloud', () => {
- describe('using legacy data role config', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
- httpRequestsMockHelpers.setListNodes({
- nodesByAttributes: { test: ['123'] },
- // On cloud, even if there are data_* roles set, the default, recommended allocation option should not
- // be available.
- nodesByRoles: { data_hot: ['123'] },
- isUsingDeprecatedDataRoleConfig: true,
- });
- httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] });
-
- await act(async () => {
- testBed = await setup({
- appServicesContext: {
- cloud: {
- isCloudEnabled: true,
- },
- license: licensingMock.createLicense({ license: { type: 'basic' } }),
- },
- });
- });
-
- const { component } = testBed;
- component.update();
- });
- test('removes default, recommended option', async () => {
- const { actions, find } = testBed;
- await actions.warm.enable(true);
- actions.warm.showDataAllocationOptions();
-
- expect(find('defaultDataAllocationOption').exists()).toBeFalsy();
- expect(find('customDataAllocationOption').exists()).toBeTruthy();
- expect(find('noneDataAllocationOption').exists()).toBeTruthy();
- // Show the call-to-action for users to migrate their cluster to use node roles
- expect(find('cloudDataTierCallout').exists()).toBeTruthy();
- });
- });
- describe('using node roles', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
- httpRequestsMockHelpers.setListNodes({
- nodesByAttributes: { test: ['123'] },
- nodesByRoles: { data_hot: ['123'] },
- isUsingDeprecatedDataRoleConfig: false,
- });
- httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] });
-
- await act(async () => {
- testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
- });
-
- const { component } = testBed;
- component.update();
- });
-
- test('should show recommended, custom and "off" options on cloud with data roles', async () => {
- const { actions, find } = testBed;
-
- await actions.warm.enable(true);
- actions.warm.showDataAllocationOptions();
- expect(find('defaultDataAllocationOption').exists()).toBeTruthy();
- expect(find('customDataAllocationOption').exists()).toBeTruthy();
- expect(find('noneDataAllocationOption').exists()).toBeTruthy();
- // We should not be showing the call-to-action for users to activate the cold tier on cloud
- expect(find('cloudMissingColdTierCallout').exists()).toBeFalsy();
- // Do not show the call-to-action for users to migrate their cluster to use node roles
- expect(find('cloudDataTierCallout').exists()).toBeFalsy();
- });
-
- test('should show cloud notice when cold tier nodes do not exist', async () => {
- const { actions, find } = testBed;
- await actions.cold.enable(true);
- expect(find('cloudMissingColdTierCallout').exists()).toBeTruthy();
- // Assert that other notices are not showing
- expect(find('defaultAllocationNotice').exists()).toBeFalsy();
- expect(find('noNodeAttributesWarning').exists()).toBeFalsy();
- });
- });
- });
- });
-
- describe('searchable snapshot', () => {
- describe('on non-enterprise license', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
- httpRequestsMockHelpers.setListNodes({
- isUsingDeprecatedDataRoleConfig: false,
- nodesByAttributes: { test: ['123'] },
- nodesByRoles: { data: ['123'] },
- });
- httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] });
-
- await act(async () => {
- testBed = await setup({
- appServicesContext: {
- license: licensingMock.createLicense({ license: { type: 'basic' } }),
- },
- });
- });
-
- const { component } = testBed;
- component.update();
- });
- test('disable setting searchable snapshots', async () => {
- const { actions } = testBed;
-
- expect(actions.cold.searchableSnapshotsExists()).toBeFalsy();
- expect(actions.hot.searchableSnapshotsExists()).toBeFalsy();
-
- await actions.cold.enable(true);
-
- // Still hidden in hot
- expect(actions.hot.searchableSnapshotsExists()).toBeFalsy();
-
- expect(actions.cold.searchableSnapshotsExists()).toBeTruthy();
- expect(actions.cold.searchableSnapshotDisabledDueToLicense()).toBeTruthy();
- });
- });
-
- describe('on cloud', () => {
- describe('new policy', () => {
- beforeEach(async () => {
- // simulate creating a new policy
- httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('')]);
- httpRequestsMockHelpers.setListNodes({
- nodesByAttributes: { test: ['123'] },
- nodesByRoles: { data: ['123'] },
- isUsingDeprecatedDataRoleConfig: false,
- });
- httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] });
-
- await act(async () => {
- testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
- });
-
- const { component } = testBed;
- component.update();
- });
- test('defaults searchable snapshot to true on cloud', async () => {
- const { find, actions } = testBed;
- await actions.cold.enable(true);
- expect(
- find('searchableSnapshotField-cold.searchableSnapshotToggle').props()['aria-checked']
- ).toBe(true);
- });
- });
- describe('existing policy', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
- httpRequestsMockHelpers.setListNodes({
- isUsingDeprecatedDataRoleConfig: false,
- nodesByAttributes: { test: ['123'] },
- nodesByRoles: { data_hot: ['123'] },
- });
- httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] });
-
- await act(async () => {
- testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
- });
-
- const { component } = testBed;
- component.update();
- });
- test('correctly sets snapshot repository default to "found-snapshots"', async () => {
- const { actions } = testBed;
- await actions.cold.enable(true);
- await actions.cold.toggleSearchableSnapshot(true);
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const request = JSON.parse(JSON.parse(latestRequest.requestBody).body);
- expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual(
- 'found-snapshots'
- );
- });
- });
- });
- });
- describe('with rollover', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
- httpRequestsMockHelpers.setListNodes({
- isUsingDeprecatedDataRoleConfig: false,
- nodesByAttributes: { test: ['123'] },
- nodesByRoles: { data: ['123'] },
- });
- httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['abc'] });
- httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component } = testBed;
- component.update();
- });
-
- test('shows rollover tip on minimum age', async () => {
- const { actions } = testBed;
-
- await actions.warm.enable(true);
- await actions.cold.enable(true);
- await actions.delete.enablePhase();
-
- expect(actions.warm.hasRolloverTipOnMinAge()).toBeTruthy();
- expect(actions.cold.hasRolloverTipOnMinAge()).toBeTruthy();
- expect(actions.delete.hasRolloverTipOnMinAge()).toBeTruthy();
- });
- });
-
- describe('without rollover', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
- httpRequestsMockHelpers.setListNodes({
- isUsingDeprecatedDataRoleConfig: false,
- nodesByAttributes: { test: ['123'] },
- nodesByRoles: { data: ['123'] },
- });
- httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] });
- httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
-
- await act(async () => {
- testBed = await setup({
- appServicesContext: {
- license: licensingMock.createLicense({ license: { type: 'enterprise' } }),
- },
- });
- });
-
- const { component } = testBed;
- component.update();
- });
- test('hides fields in hot phase', async () => {
- const { actions } = testBed;
- await actions.hot.toggleDefaultRollover(false);
- await actions.hot.toggleRollover(false);
-
- expect(actions.hot.forceMergeFieldExists()).toBeFalsy();
- expect(actions.hot.shrinkExists()).toBeFalsy();
- expect(actions.hot.searchableSnapshotsExists()).toBeFalsy();
- expect(actions.hot.readonlyExists()).toBeFalsy();
- });
-
- test('hiding rollover tip on minimum age', async () => {
- const { actions } = testBed;
- await actions.hot.toggleDefaultRollover(false);
- await actions.hot.toggleRollover(false);
-
- await actions.warm.enable(true);
- await actions.cold.enable(true);
- await actions.delete.enablePhase();
-
- expect(actions.warm.hasRolloverTipOnMinAge()).toBeFalsy();
- expect(actions.cold.hasRolloverTipOnMinAge()).toBeFalsy();
- expect(actions.delete.hasRolloverTipOnMinAge()).toBeFalsy();
- });
- });
-
- describe('policy timeline', () => {
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
- httpRequestsMockHelpers.setListNodes({
- nodesByRoles: {},
- nodesByAttributes: { test: ['123'] },
- isUsingDeprecatedDataRoleConfig: false,
- });
- httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component } = testBed;
- component.update();
- });
-
- test('showing all phases on the timeline', async () => {
- const { actions } = testBed;
- // This is how the default policy should look
- expect(actions.timeline.hasHotPhase()).toBe(true);
- expect(actions.timeline.hasWarmPhase()).toBe(false);
- expect(actions.timeline.hasColdPhase()).toBe(false);
- expect(actions.timeline.hasDeletePhase()).toBe(false);
-
- await actions.warm.enable(true);
- expect(actions.timeline.hasHotPhase()).toBe(true);
- expect(actions.timeline.hasWarmPhase()).toBe(true);
- expect(actions.timeline.hasColdPhase()).toBe(false);
- expect(actions.timeline.hasDeletePhase()).toBe(false);
-
- await actions.cold.enable(true);
- expect(actions.timeline.hasHotPhase()).toBe(true);
- expect(actions.timeline.hasWarmPhase()).toBe(true);
- expect(actions.timeline.hasColdPhase()).toBe(true);
- expect(actions.timeline.hasDeletePhase()).toBe(false);
-
- await actions.delete.enablePhase();
- expect(actions.timeline.hasHotPhase()).toBe(true);
- expect(actions.timeline.hasWarmPhase()).toBe(true);
- expect(actions.timeline.hasColdPhase()).toBe(true);
- expect(actions.timeline.hasDeletePhase()).toBe(true);
- });
- });
-
- describe('policy error notifications', () => {
- let runTimers: () => void;
- beforeAll(() => {
- jest.useFakeTimers();
- });
-
- afterAll(() => {
- jest.useRealTimers();
- });
-
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
- httpRequestsMockHelpers.setListNodes({
- nodesByRoles: {},
- nodesByAttributes: { test: ['123'] },
- isUsingDeprecatedDataRoleConfig: false,
- });
- httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component } = testBed;
- component.update();
-
- ({ runTimers } = testBed);
- });
-
- test('shows phase error indicators correctly', async () => {
- // This test simulates a user configuring a policy phase by phase. The flow is the following:
- // 0. Start with policy with no validation issues present
- // 1. Configure hot, introducing a validation error
- // 2. Configure warm, introducing a validation error
- // 3. Configure cold, introducing a validation error
- // 4. Fix validation error in hot
- // 5. Fix validation error in warm
- // 6. Fix validation error in cold
- // We assert against each of these progressive states.
-
- const { actions } = testBed;
-
- // 0. No validation issues
- expect(actions.hasGlobalErrorCallout()).toBe(false);
- expect(actions.hot.hasErrorIndicator()).toBe(false);
- expect(actions.warm.hasErrorIndicator()).toBe(false);
- expect(actions.cold.hasErrorIndicator()).toBe(false);
-
- // 1. Hot phase validation issue
- await actions.hot.toggleForceMerge(true);
- await actions.hot.setForcemergeSegmentsCount('-22');
- runTimers();
- expect(actions.hasGlobalErrorCallout()).toBe(true);
- expect(actions.hot.hasErrorIndicator()).toBe(true);
- expect(actions.warm.hasErrorIndicator()).toBe(false);
- expect(actions.cold.hasErrorIndicator()).toBe(false);
-
- // 2. Warm phase validation issue
- await actions.warm.enable(true);
- await actions.warm.toggleForceMerge(true);
- await actions.warm.setForcemergeSegmentsCount('-22');
- runTimers();
- expect(actions.hasGlobalErrorCallout()).toBe(true);
- expect(actions.hot.hasErrorIndicator()).toBe(true);
- expect(actions.warm.hasErrorIndicator()).toBe(true);
- expect(actions.cold.hasErrorIndicator()).toBe(false);
-
- // 3. Cold phase validation issue
- await actions.cold.enable(true);
- await actions.cold.setReplicas('-33');
- runTimers();
- expect(actions.hasGlobalErrorCallout()).toBe(true);
- expect(actions.hot.hasErrorIndicator()).toBe(true);
- expect(actions.warm.hasErrorIndicator()).toBe(true);
- expect(actions.cold.hasErrorIndicator()).toBe(true);
-
- // 4. Fix validation issue in hot
- await actions.hot.setForcemergeSegmentsCount('1');
- runTimers();
- expect(actions.hasGlobalErrorCallout()).toBe(true);
- expect(actions.hot.hasErrorIndicator()).toBe(false);
- expect(actions.warm.hasErrorIndicator()).toBe(true);
- expect(actions.cold.hasErrorIndicator()).toBe(true);
-
- // 5. Fix validation issue in warm
- await actions.warm.setForcemergeSegmentsCount('1');
- runTimers();
- expect(actions.hasGlobalErrorCallout()).toBe(true);
- expect(actions.hot.hasErrorIndicator()).toBe(false);
- expect(actions.warm.hasErrorIndicator()).toBe(false);
- expect(actions.cold.hasErrorIndicator()).toBe(true);
-
- // 6. Fix validation issue in cold
- await actions.cold.setReplicas('1');
- runTimers();
- expect(actions.hasGlobalErrorCallout()).toBe(false);
- expect(actions.hot.hasErrorIndicator()).toBe(false);
- expect(actions.warm.hasErrorIndicator()).toBe(false);
- expect(actions.cold.hasErrorIndicator()).toBe(false);
- });
-
- test('global error callout should show if there are any form errors', async () => {
- const { actions } = testBed;
-
- expect(actions.hasGlobalErrorCallout()).toBe(false);
- expect(actions.hot.hasErrorIndicator()).toBe(false);
- expect(actions.warm.hasErrorIndicator()).toBe(false);
- expect(actions.cold.hasErrorIndicator()).toBe(false);
-
- await actions.saveAsNewPolicy(true);
- await actions.setPolicyName('');
- runTimers();
-
- expect(actions.hasGlobalErrorCallout()).toBe(true);
- expect(actions.hot.hasErrorIndicator()).toBe(false);
- expect(actions.warm.hasErrorIndicator()).toBe(false);
- expect(actions.cold.hasErrorIndicator()).toBe(false);
- });
-
- test('clears all error indicators if last erroring field is unmounted', async () => {
- const { actions } = testBed;
-
- await actions.cold.enable(true);
- // introduce validation error
- await actions.cold.setSearchableSnapshot('');
- runTimers();
-
- await actions.savePolicy();
- runTimers();
-
- expect(actions.hasGlobalErrorCallout()).toBe(true);
- expect(actions.hot.hasErrorIndicator()).toBe(false);
- expect(actions.warm.hasErrorIndicator()).toBe(false);
- expect(actions.cold.hasErrorIndicator()).toBe(true);
-
- // unmount the field
- await actions.cold.toggleSearchableSnapshot(false);
-
- expect(actions.hasGlobalErrorCallout()).toBe(false);
- expect(actions.hot.hasErrorIndicator()).toBe(false);
- expect(actions.warm.hasErrorIndicator()).toBe(false);
- expect(actions.cold.hasErrorIndicator()).toBe(false);
- });
- });
-});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/cold_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/cold_phase.test.ts
new file mode 100644
index 00000000000000..dfb7411eb941f4
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/cold_phase.test.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { act } from 'react-dom/test-utils';
+import { setupEnvironment } from '../../helpers/setup_environment';
+import { EditPolicyTestBed, setup } from '../edit_policy.helpers';
+import { getDefaultHotPhasePolicy } from '../constants';
+
+describe(' cold phase', () => {
+ let testBed: EditPolicyTestBed;
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ server.restore();
+ });
+
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: { data: ['node1'] },
+ nodesByAttributes: { 'attribute:true': ['node1'] },
+ isUsingDeprecatedDataRoleConfig: true,
+ });
+ httpRequestsMockHelpers.setNodesDetails('attribute:true', [
+ { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } },
+ ]);
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('shows timing only when enabled', async () => {
+ const { actions } = testBed;
+ expect(actions.cold.hasMinAgeInput()).toBeFalsy();
+ await actions.cold.enable(true);
+ expect(actions.cold.hasMinAgeInput()).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts
new file mode 100644
index 00000000000000..0fb4951e4a4a68
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts
@@ -0,0 +1,169 @@
+/*
+ * 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 {
+ DELETE_PHASE_POLICY,
+ getDefaultHotPhasePolicy,
+ NEW_SNAPSHOT_POLICY_NAME,
+ SNAPSHOT_POLICY_NAME,
+} from '../constants';
+import { act } from 'react-dom/test-utils';
+import { EditPolicyTestBed, setup } from '../edit_policy.helpers';
+import { API_BASE_PATH } from '../../../../common/constants';
+import { setupEnvironment } from '../../helpers/setup_environment';
+
+describe(' delete phase', () => {
+ let testBed: EditPolicyTestBed;
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([DELETE_PHASE_POLICY]);
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([
+ SNAPSHOT_POLICY_NAME,
+ NEW_SNAPSHOT_POLICY_NAME,
+ ]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('is hidden when disabled', async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component, actions } = testBed;
+ component.update();
+
+ expect(actions.delete.isShown()).toBeFalsy();
+ await actions.delete.enablePhase();
+ expect(actions.delete.isShown()).toBeTruthy();
+ });
+
+ test('shows timing after it was enabled', async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component, actions } = testBed;
+ component.update();
+
+ expect(actions.delete.hasMinAgeInput()).toBeFalsy();
+ await actions.delete.enablePhase();
+ expect(actions.delete.hasMinAgeInput()).toBeTruthy();
+ });
+
+ describe('wait for snapshot', () => {
+ test('shows snapshot policy name', () => {
+ expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([
+ {
+ label: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy,
+ },
+ ]);
+ });
+
+ test('updates snapshot policy name', async () => {
+ const { actions } = testBed;
+
+ await actions.setWaitForSnapshotPolicy(NEW_SNAPSHOT_POLICY_NAME);
+ await actions.savePolicy();
+
+ const expected = {
+ name: DELETE_PHASE_POLICY.name,
+ phases: {
+ ...DELETE_PHASE_POLICY.policy.phases,
+ delete: {
+ ...DELETE_PHASE_POLICY.policy.phases.delete,
+ actions: {
+ ...DELETE_PHASE_POLICY.policy.phases.delete?.actions,
+ wait_for_snapshot: {
+ policy: NEW_SNAPSHOT_POLICY_NAME,
+ },
+ },
+ },
+ },
+ };
+
+ const latestRequest = server.requests[server.requests.length - 1];
+ expect(latestRequest.url).toBe(`${API_BASE_PATH}/policies`);
+ expect(latestRequest.method).toBe('POST');
+ expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
+ });
+
+ test('shows a callout when the input is not an existing policy', async () => {
+ const { actions } = testBed;
+
+ await actions.setWaitForSnapshotPolicy('my_custom_policy');
+ expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy();
+ expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy();
+ expect(testBed.find('customPolicyCallout').exists()).toBeTruthy();
+ });
+
+ test('removes the action if field is empty', async () => {
+ const { actions } = testBed;
+
+ await actions.setWaitForSnapshotPolicy('');
+ await actions.savePolicy();
+
+ const expected = {
+ name: DELETE_PHASE_POLICY.name,
+ phases: {
+ ...DELETE_PHASE_POLICY.policy.phases,
+ delete: {
+ ...DELETE_PHASE_POLICY.policy.phases.delete,
+ actions: {
+ ...DELETE_PHASE_POLICY.policy.phases.delete?.actions,
+ },
+ },
+ },
+ };
+
+ delete expected.phases.delete.actions.wait_for_snapshot;
+
+ const latestRequest = server.requests[server.requests.length - 1];
+ expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
+ });
+
+ test('shows a callout when there are no snapshot policies', async () => {
+ // need to call setup on testBed again for it to use a newly defined snapshot policies response
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ testBed.component.update();
+ expect(testBed.find('customPolicyCallout').exists()).toBeFalsy();
+ expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy();
+ expect(testBed.find('noPoliciesCallout').exists()).toBeTruthy();
+ });
+
+ test('shows a callout when there is an error loading snapshot policies', async () => {
+ // need to call setup on testBed again for it to use a newly defined snapshot policies response
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([], { status: 500, body: 'error' });
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ testBed.component.update();
+ expect(testBed.find('customPolicyCallout').exists()).toBeFalsy();
+ expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy();
+ expect(testBed.find('policiesErrorCallout').exists()).toBeTruthy();
+ });
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts
similarity index 78%
rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts
rename to x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts
index b02d190d108997..13e55a1f39e2c7 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts
@@ -8,6 +8,11 @@
import { act } from 'react-dom/test-utils';
import { setupEnvironment } from '../../helpers/setup_environment';
import { EditPolicyTestBed, setup } from '../edit_policy.helpers';
+import {
+ POLICY_WITH_MIGRATE_OFF,
+ POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION,
+ POLICY_WITH_NODE_ROLE_ALLOCATION,
+} from '../constants';
describe(' node allocation', () => {
let testBed: EditPolicyTestBed;
@@ -308,7 +313,7 @@ describe(' node allocation', () => {
});
describe('on cloud', () => {
- describe('with deprecated data role config', () => {
+ describe('using legacy data role config', () => {
test('should hide data tier option on cloud', async () => {
httpRequestsMockHelpers.setListNodes({
nodesByAttributes: { test: ['123'] },
@@ -319,7 +324,7 @@ describe(' node allocation', () => {
await act(async () => {
testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
});
- const { actions, component, exists } = testBed;
+ const { actions, component, exists, find } = testBed;
component.update();
await actions.warm.enable(true);
@@ -330,30 +335,13 @@ describe(' node allocation', () => {
expect(exists('defaultDataAllocationOption')).toBeFalsy();
expect(exists('customDataAllocationOption')).toBeTruthy();
expect(exists('noneDataAllocationOption')).toBeTruthy();
- });
-
- test('should ask users to migrate to node roles when on cloud using legacy data role', async () => {
- httpRequestsMockHelpers.setListNodes({
- nodesByAttributes: { test: ['123'] },
- // On cloud, if using legacy config there will not be any "data_*" roles set.
- nodesByRoles: { data: ['test'] },
- isUsingDeprecatedDataRoleConfig: true,
- });
- await act(async () => {
- testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
- });
- const { actions, component, exists } = testBed;
-
- component.update();
- await actions.warm.enable(true);
- expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy();
-
- expect(exists('cloudDataTierCallout')).toBeTruthy();
+ // Show the call-to-action for users to migrate their cluster to use node roles
+ expect(find('cloudDataTierCallout').exists()).toBeTruthy();
});
});
- describe('with node role config', () => {
- test('shows data role, custom and "off" options on cloud with data roles', async () => {
+ describe('using node role config', () => {
+ test('shows recommended, custom and "off" options on cloud with data roles', async () => {
httpRequestsMockHelpers.setListNodes({
nodesByAttributes: { test: ['123'] },
nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] },
@@ -362,7 +350,7 @@ describe(' node allocation', () => {
await act(async () => {
testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
});
- const { actions, component, exists } = testBed;
+ const { actions, component, exists, find } = testBed;
component.update();
await actions.warm.enable(true);
@@ -372,8 +360,10 @@ describe(' node allocation', () => {
expect(exists('defaultDataAllocationOption')).toBeTruthy();
expect(exists('customDataAllocationOption')).toBeTruthy();
expect(exists('noneDataAllocationOption')).toBeTruthy();
- // We should not be showing the call-to-action for users to activate data tiers in cloud
+ // We should not be showing the call-to-action for users to activate data tier in cloud
expect(exists('cloudDataTierCallout')).toBeFalsy();
+ // Do not show the call-to-action for users to migrate their cluster to use node roles
+ expect(find('cloudDataTierCallout').exists()).toBeFalsy();
});
test('shows cloud notice when cold tier nodes do not exist', async () => {
@@ -398,4 +388,102 @@ describe(' node allocation', () => {
});
});
});
+
+ describe('data allocation', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_MIGRATE_OFF]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: {},
+ nodesByAttributes: { test: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
+ });
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('setting node_attr based allocation, but not selecting node attribute', async () => {
+ const { actions } = testBed;
+ await actions.warm.setDataAllocation('node_attrs');
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm;
+
+ expect(warmPhase.actions.migrate).toEqual({ enabled: false });
+ });
+
+ describe('node roles', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ROLE_ALLOCATION]);
+ httpRequestsMockHelpers.setListNodes({
+ isUsingDeprecatedDataRoleConfig: false,
+ nodesByAttributes: { test: ['123'] },
+ nodesByRoles: { data: ['123'] },
+ });
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('detecting use of the recommended allocation type', () => {
+ const { find } = testBed;
+ const selectedDataAllocation = find(
+ 'warm-dataTierAllocationControls.dataTierSelect'
+ ).text();
+ expect(selectedDataAllocation).toBe('Use warm nodes (recommended)');
+ });
+
+ test('setting replicas serialization', async () => {
+ const { actions } = testBed;
+ await actions.warm.setReplicas('123');
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const warmPhaseActions = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm
+ .actions;
+ expect(warmPhaseActions).toMatchInlineSnapshot(`
+ Object {
+ "allocate": Object {
+ "number_of_replicas": 123,
+ },
+ }
+ `);
+ });
+ });
+
+ describe('node attr and none', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION]);
+ httpRequestsMockHelpers.setListNodes({
+ isUsingDeprecatedDataRoleConfig: false,
+ nodesByAttributes: { test: ['123'] },
+ nodesByRoles: { data: ['123'] },
+ });
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('detecting use of the custom allocation type', () => {
+ const { find } = testBed;
+ expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toBe('Custom');
+ });
+ test('detecting use of the "off" allocation type', () => {
+ const { find } = testBed;
+ expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off');
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts
new file mode 100644
index 00000000000000..6584c19c85be3a
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts
@@ -0,0 +1,66 @@
+/*
+ * 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 { act } from 'react-dom/test-utils';
+import { EditPolicyTestBed, setup } from '../edit_policy.helpers';
+import { setupEnvironment } from '../../helpers/setup_environment';
+import { getDefaultHotPhasePolicy } from '../constants';
+
+describe(' request flyout', () => {
+ let testBed: EditPolicyTestBed;
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ server.restore();
+ });
+
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('renders a json in flyout for a default policy', async () => {
+ const { find, component } = testBed;
+ await act(async () => {
+ find('requestButton').simulate('click');
+ });
+ component.update();
+
+ const json = component.find(`code`).text();
+ const expected = `PUT _ilm/policy/my_policy\n${JSON.stringify(
+ {
+ policy: {
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {
+ rollover: {
+ max_age: '30d',
+ max_size: '50gb',
+ },
+ },
+ },
+ },
+ },
+ },
+ null,
+ 2
+ )}`;
+ expect(json).toBe(expected);
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts
new file mode 100644
index 00000000000000..e2b67efbf588db
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.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 { EditPolicyTestBed, setup } from '../edit_policy.helpers';
+import { setupEnvironment } from '../../helpers/setup_environment';
+import { getDefaultHotPhasePolicy } from '../constants';
+import { act } from 'react-dom/test-utils';
+import { licensingMock } from '../../../../../licensing/public/mocks';
+
+describe(' timeline', () => {
+ let testBed: EditPolicyTestBed;
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+ httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['abc'] });
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: {},
+ nodesByAttributes: { test: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
+ });
+
+ await act(async () => {
+ testBed = await setup({
+ appServicesContext: {
+ license: licensingMock.createLicense({ license: { type: 'enterprise' } }),
+ },
+ });
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('shows forcemerge when rollover enabled', async () => {
+ const { actions } = testBed;
+ expect(actions.hot.forceMergeFieldExists()).toBeTruthy();
+ });
+ test('hides forcemerge when rollover is disabled', async () => {
+ const { actions } = testBed;
+ await actions.hot.toggleDefaultRollover(false);
+ await actions.hot.toggleRollover(false);
+ expect(actions.hot.forceMergeFieldExists()).toBeFalsy();
+ });
+
+ test('shows shrink input when rollover enabled', async () => {
+ const { actions } = testBed;
+ expect(actions.hot.shrinkExists()).toBeTruthy();
+ });
+ test('hides shrink input when rollover is disabled', async () => {
+ const { actions } = testBed;
+ await actions.hot.toggleDefaultRollover(false);
+ await actions.hot.toggleRollover(false);
+ expect(actions.hot.shrinkExists()).toBeFalsy();
+ });
+ test('shows readonly input when rollover enabled', async () => {
+ const { actions } = testBed;
+ expect(actions.hot.readonlyExists()).toBeTruthy();
+ });
+ test('hides readonly input when rollover is disabled', async () => {
+ const { actions } = testBed;
+ await actions.hot.toggleDefaultRollover(false);
+ await actions.hot.toggleRollover(false);
+ expect(actions.hot.readonlyExists()).toBeFalsy();
+ });
+ test('hides and disables searchable snapshot field', async () => {
+ const { actions } = testBed;
+ await actions.hot.toggleDefaultRollover(false);
+ await actions.hot.toggleRollover(false);
+ await actions.cold.enable(true);
+
+ expect(actions.hot.searchableSnapshotsExists()).toBeFalsy();
+ });
+
+ test('shows rollover tip on minimum age', async () => {
+ const { actions } = testBed;
+
+ await actions.warm.enable(true);
+ await actions.cold.enable(true);
+ await actions.delete.enablePhase();
+
+ expect(actions.warm.hasRolloverTipOnMinAge()).toBeTruthy();
+ expect(actions.cold.hasRolloverTipOnMinAge()).toBeTruthy();
+ expect(actions.delete.hasRolloverTipOnMinAge()).toBeTruthy();
+ });
+ test('hiding rollover tip on minimum age', async () => {
+ const { actions } = testBed;
+ await actions.hot.toggleDefaultRollover(false);
+ await actions.hot.toggleRollover(false);
+
+ await actions.warm.enable(true);
+ await actions.cold.enable(true);
+ await actions.delete.enablePhase();
+
+ expect(actions.warm.hasRolloverTipOnMinAge()).toBeFalsy();
+ expect(actions.cold.hasRolloverTipOnMinAge()).toBeFalsy();
+ expect(actions.delete.hasRolloverTipOnMinAge()).toBeFalsy();
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts
new file mode 100644
index 00000000000000..ed678a6b217ae9
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts
@@ -0,0 +1,163 @@
+/*
+ * 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 { act } from 'react-dom/test-utils';
+import { licensingMock } from '../../../../../licensing/public/mocks';
+import { setupEnvironment } from '../../helpers/setup_environment';
+import { getDefaultHotPhasePolicy } from '../constants';
+import { EditPolicyTestBed, setup } from '../edit_policy.helpers';
+
+describe(' searchable snapshots', () => {
+ let testBed: EditPolicyTestBed;
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: {},
+ nodesByAttributes: { test: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
+ });
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('enabling searchable snapshot should hide force merge, freeze and shrink in subsequent phases', async () => {
+ const { actions } = testBed;
+
+ await actions.warm.enable(true);
+ await actions.cold.enable(true);
+
+ expect(actions.warm.forceMergeFieldExists()).toBeTruthy();
+ expect(actions.warm.shrinkExists()).toBeTruthy();
+ expect(actions.cold.searchableSnapshotsExists()).toBeTruthy();
+ expect(actions.cold.freezeExists()).toBeTruthy();
+
+ await actions.hot.setSearchableSnapshot('my-repo');
+
+ expect(actions.warm.forceMergeFieldExists()).toBeFalsy();
+ expect(actions.warm.shrinkExists()).toBeFalsy();
+ // searchable snapshot in cold is still visible
+ expect(actions.cold.searchableSnapshotsExists()).toBeTruthy();
+ expect(actions.cold.freezeExists()).toBeFalsy();
+ });
+
+ test('disabling rollover toggle, but enabling default rollover', async () => {
+ const { actions } = testBed;
+ await actions.hot.toggleDefaultRollover(false);
+ await actions.hot.toggleRollover(false);
+ await actions.hot.toggleDefaultRollover(true);
+
+ expect(actions.hot.forceMergeFieldExists()).toBeTruthy();
+ expect(actions.hot.shrinkExists()).toBeTruthy();
+ expect(actions.hot.searchableSnapshotsExists()).toBeTruthy();
+ });
+
+ describe('on cloud', () => {
+ describe('new policy', () => {
+ beforeEach(async () => {
+ // simulate creating a new policy
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('')]);
+ httpRequestsMockHelpers.setListNodes({
+ isUsingDeprecatedDataRoleConfig: false,
+ nodesByAttributes: { test: ['123'] },
+ nodesByRoles: { data: ['123'] },
+ });
+ httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] });
+
+ await act(async () => {
+ testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+ test('defaults searchable snapshot to true on cloud', async () => {
+ const { find, actions } = testBed;
+ await actions.cold.enable(true);
+ expect(
+ find('searchableSnapshotField-cold.searchableSnapshotToggle').props()['aria-checked']
+ ).toBe(true);
+ });
+ });
+ describe('existing policy', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setListNodes({
+ isUsingDeprecatedDataRoleConfig: false,
+ nodesByAttributes: { test: ['123'] },
+ nodesByRoles: { data: ['123'] },
+ });
+ httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] });
+
+ await act(async () => {
+ testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+ test('correctly sets snapshot repository default to "found-snapshots"', async () => {
+ const { actions } = testBed;
+ await actions.cold.enable(true);
+ await actions.cold.toggleSearchableSnapshot(true);
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const request = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+ expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual(
+ 'found-snapshots'
+ );
+ });
+ });
+ });
+ describe('on non-enterprise license', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setListNodes({
+ isUsingDeprecatedDataRoleConfig: false,
+ nodesByAttributes: { test: ['123'] },
+ nodesByRoles: { data: ['123'] },
+ });
+ httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['my-repo'] });
+
+ await act(async () => {
+ testBed = await setup({
+ appServicesContext: {
+ license: licensingMock.createLicense({ license: { type: 'basic' } }),
+ },
+ });
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+ test('disable setting searchable snapshots', async () => {
+ const { actions } = testBed;
+
+ expect(actions.cold.searchableSnapshotsExists()).toBeFalsy();
+ expect(actions.hot.searchableSnapshotsExists()).toBeFalsy();
+
+ await actions.cold.enable(true);
+
+ // Still hidden in hot
+ expect(actions.hot.searchableSnapshotsExists()).toBeFalsy();
+
+ expect(actions.cold.searchableSnapshotsExists()).toBeTruthy();
+ expect(actions.cold.searchableSnapshotDisabledDueToLicense()).toBeTruthy();
+ });
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts
new file mode 100644
index 00000000000000..3618bad45e4f14
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { act } from 'react-dom/test-utils';
+import { setupEnvironment } from '../../helpers/setup_environment';
+import { getDefaultHotPhasePolicy } from '../constants';
+import { EditPolicyTestBed, setup } from '../edit_policy.helpers';
+
+describe(' timeline', () => {
+ let testBed: EditPolicyTestBed;
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: {},
+ nodesByAttributes: { test: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
+ });
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('showing all phases on the timeline', async () => {
+ const { actions } = testBed;
+ // This is how the default policy should look
+ expect(actions.timeline.hasHotPhase()).toBe(true);
+ expect(actions.timeline.hasWarmPhase()).toBe(false);
+ expect(actions.timeline.hasColdPhase()).toBe(false);
+ expect(actions.timeline.hasDeletePhase()).toBe(false);
+
+ await actions.warm.enable(true);
+ expect(actions.timeline.hasHotPhase()).toBe(true);
+ expect(actions.timeline.hasWarmPhase()).toBe(true);
+ expect(actions.timeline.hasColdPhase()).toBe(false);
+ expect(actions.timeline.hasDeletePhase()).toBe(false);
+
+ await actions.cold.enable(true);
+ expect(actions.timeline.hasHotPhase()).toBe(true);
+ expect(actions.timeline.hasWarmPhase()).toBe(true);
+ expect(actions.timeline.hasColdPhase()).toBe(true);
+ expect(actions.timeline.hasDeletePhase()).toBe(false);
+
+ await actions.delete.enablePhase();
+ expect(actions.timeline.hasHotPhase()).toBe(true);
+ expect(actions.timeline.hasWarmPhase()).toBe(true);
+ expect(actions.timeline.hasColdPhase()).toBe(true);
+ expect(actions.timeline.hasDeletePhase()).toBe(true);
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts
new file mode 100644
index 00000000000000..2252f8d1f5fa8b
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { act } from 'react-dom/test-utils';
+import { setupEnvironment } from '../../helpers/setup_environment';
+import { EditPolicyTestBed, setup } from '../edit_policy.helpers';
+import { getDefaultHotPhasePolicy } from '../constants';
+
+describe(' warm phase', () => {
+ let testBed: EditPolicyTestBed;
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ server.restore();
+ });
+
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: { data: ['node1'] },
+ nodesByAttributes: { 'attribute:true': ['node1'] },
+ isUsingDeprecatedDataRoleConfig: true,
+ });
+ httpRequestsMockHelpers.setNodesDetails('attribute:true', [
+ { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } },
+ ]);
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('shows timing only when enabled', async () => {
+ const { actions } = testBed;
+ expect(actions.warm.hasMinAgeInput()).toBeFalsy();
+ await actions.warm.enable(true);
+ expect(actions.warm.hasMinAgeInput()).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts
new file mode 100644
index 00000000000000..e2d937cf9c8dbb
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts
@@ -0,0 +1,159 @@
+/*
+ * 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 { act } from 'react-dom/test-utils';
+import { setupEnvironment } from '../../helpers/setup_environment';
+import { getDefaultHotPhasePolicy } from '../constants';
+import { EditPolicyTestBed, setup } from '../edit_policy.helpers';
+
+describe(' error indicators', () => {
+ let testBed: EditPolicyTestBed;
+ let runTimers: () => void;
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ server.restore();
+ });
+
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: {},
+ nodesByAttributes: { test: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
+ });
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+
+ ({ runTimers } = testBed);
+ });
+ test('shows phase error indicators correctly', async () => {
+ // This test simulates a user configuring a policy phase by phase. The flow is the following:
+ // 0. Start with policy with no validation issues present
+ // 1. Configure hot, introducing a validation error
+ // 2. Configure warm, introducing a validation error
+ // 3. Configure cold, introducing a validation error
+ // 4. Fix validation error in hot
+ // 5. Fix validation error in warm
+ // 6. Fix validation error in cold
+ // We assert against each of these progressive states.
+
+ const { actions } = testBed;
+
+ // 0. No validation issues
+ expect(actions.hasGlobalErrorCallout()).toBe(false);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+
+ // 1. Hot phase validation issue
+ await actions.hot.toggleForceMerge(true);
+ await actions.hot.setForcemergeSegmentsCount('-22');
+ runTimers();
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(true);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+
+ // 2. Warm phase validation issue
+ await actions.warm.enable(true);
+ await actions.warm.toggleForceMerge(true);
+ await actions.warm.setForcemergeSegmentsCount('-22');
+ runTimers();
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(true);
+ expect(actions.warm.hasErrorIndicator()).toBe(true);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+
+ // 3. Cold phase validation issue
+ await actions.cold.enable(true);
+ await actions.cold.setReplicas('-33');
+ runTimers();
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(true);
+ expect(actions.warm.hasErrorIndicator()).toBe(true);
+ expect(actions.cold.hasErrorIndicator()).toBe(true);
+
+ // 4. Fix validation issue in hot
+ await actions.hot.setForcemergeSegmentsCount('1');
+ runTimers();
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(true);
+ expect(actions.cold.hasErrorIndicator()).toBe(true);
+
+ // 5. Fix validation issue in warm
+ await actions.warm.setForcemergeSegmentsCount('1');
+ runTimers();
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(true);
+
+ // 6. Fix validation issue in cold
+ await actions.cold.setReplicas('1');
+ runTimers();
+ expect(actions.hasGlobalErrorCallout()).toBe(false);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+ });
+
+ test('global error callout should show if there are any form errors', async () => {
+ const { actions } = testBed;
+
+ expect(actions.hasGlobalErrorCallout()).toBe(false);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+
+ await actions.saveAsNewPolicy(true);
+ await actions.setPolicyName('');
+ runTimers();
+
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+ });
+
+ test('clears all error indicators if last erroring field is unmounted', async () => {
+ const { actions } = testBed;
+
+ await actions.cold.enable(true);
+ // introduce validation error
+ await actions.cold.setSearchableSnapshot('');
+ runTimers();
+
+ await actions.savePolicy();
+ runTimers();
+
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(true);
+
+ // unmount the field
+ await actions.cold.toggleSearchableSnapshot(false);
+
+ expect(actions.hasGlobalErrorCallout()).toBe(false);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts
deleted file mode 100644
index 9c23780f1d0213..00000000000000
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * 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 { act } from 'react-dom/test-utils';
-import { setupEnvironment } from '../../helpers/setup_environment';
-import { EditPolicyTestBed, setup } from '../edit_policy.helpers';
-import { DEFAULT_POLICY } from '../constants';
-
-describe(' reactive form', () => {
- let testBed: EditPolicyTestBed;
- const { server, httpRequestsMockHelpers } = setupEnvironment();
-
- beforeAll(() => {
- jest.useFakeTimers();
- });
-
- afterAll(() => {
- jest.useRealTimers();
- server.restore();
- });
-
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]);
- httpRequestsMockHelpers.setListNodes({
- nodesByRoles: { data: ['node1'] },
- nodesByAttributes: { 'attribute:true': ['node1'] },
- isUsingDeprecatedDataRoleConfig: true,
- });
- httpRequestsMockHelpers.setNodesDetails('attribute:true', [
- { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } },
- ]);
- httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
-
- await act(async () => {
- testBed = await setup();
- });
-
- const { component } = testBed;
- component.update();
- });
-
- describe('rollover', () => {
- test('shows forcemerge when rollover enabled', async () => {
- const { actions } = testBed;
- expect(actions.hot.forceMergeFieldExists()).toBeTruthy();
- });
- test('hides forcemerge when rollover is disabled', async () => {
- const { actions } = testBed;
- await actions.hot.toggleDefaultRollover(false);
- await actions.hot.toggleRollover(false);
- expect(actions.hot.forceMergeFieldExists()).toBeFalsy();
- });
-
- test('shows shrink input when rollover enabled', async () => {
- const { actions } = testBed;
- expect(actions.hot.shrinkExists()).toBeTruthy();
- });
- test('hides shrink input when rollover is disabled', async () => {
- const { actions } = testBed;
- await actions.hot.toggleDefaultRollover(false);
- await actions.hot.toggleRollover(false);
- expect(actions.hot.shrinkExists()).toBeFalsy();
- });
- test('shows readonly input when rollover enabled', async () => {
- const { actions } = testBed;
- expect(actions.hot.readonlyExists()).toBeTruthy();
- });
- test('hides readonly input when rollover is disabled', async () => {
- const { actions } = testBed;
- await actions.hot.toggleDefaultRollover(false);
- await actions.hot.toggleRollover(false);
- expect(actions.hot.readonlyExists()).toBeFalsy();
- });
- });
-
- describe('timing', () => {
- test('warm phase shows timing only when enabled', async () => {
- const { actions } = testBed;
- expect(actions.warm.hasMinAgeInput()).toBeFalsy();
- await actions.warm.enable(true);
- expect(actions.warm.hasMinAgeInput()).toBeTruthy();
- });
-
- test('cold phase shows timing only when enabled', async () => {
- const { actions } = testBed;
- expect(actions.cold.hasMinAgeInput()).toBeFalsy();
- await actions.cold.enable(true);
- expect(actions.cold.hasMinAgeInput()).toBeTruthy();
- });
-
- test('delete phase shows timing after it was enabled', async () => {
- const { actions } = testBed;
- expect(actions.delete.hasMinAgeInput()).toBeFalsy();
- await actions.delete.enablePhase();
- expect(actions.delete.hasMinAgeInput()).toBeTruthy();
- });
- });
-
- describe('delete phase', () => {
- test('is hidden when disabled', async () => {
- const { actions } = testBed;
- expect(actions.delete.isShown()).toBeFalsy();
- await actions.delete.enablePhase();
- expect(actions.delete.isShown()).toBeTruthy();
- });
- });
-
- describe('json in flyout', () => {
- test('renders a json in flyout for a default policy', async () => {
- const { find, component } = testBed;
- await act(async () => {
- find('requestButton').simulate('click');
- });
- component.update();
-
- const json = component.find(`code`).text();
- const expected = `PUT _ilm/policy/my_policy\n${JSON.stringify(
- {
- policy: {
- phases: {
- hot: {
- min_age: '0ms',
- actions: {
- rollover: {
- max_age: '30d',
- max_size: '50gb',
- },
- },
- },
- },
- },
- },
- null,
- 2
- )}`;
- expect(json).toBe(expected);
- });
- });
-});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts
new file mode 100644
index 00000000000000..61ceab1990c724
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts
@@ -0,0 +1,426 @@
+/*
+ * 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 { act } from 'react-dom/test-utils';
+import { setupEnvironment } from '../../helpers/setup_environment';
+import {
+ getDefaultHotPhasePolicy,
+ POLICY_WITH_INCLUDE_EXCLUDE,
+ POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS,
+} from '../constants';
+import { EditPolicyTestBed, setup } from '../edit_policy.helpers';
+
+describe(' serialization', () => {
+ let testBed: EditPolicyTestBed;
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: {},
+ nodesByAttributes: { test: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
+ });
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ describe('top level form', () => {
+ /**
+ * We assume that policies that populate this form are loaded directly from ES and so
+ * are valid according to ES. There may be settings in the policy created through the ILM
+ * API that the UI does not cater for, like the unfollow action. We do not want to overwrite
+ * the configuration for these actions in the UI.
+ */
+ it('preserves policy settings it did not configure', async () => {
+ httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS]);
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component, actions } = testBed;
+ component.update();
+
+ // Set max docs to test whether we keep the unknown fields in that object after serializing
+ await actions.hot.setMaxDocs('1000');
+ // Remove the delete phase to ensure that we also correctly remove data
+ await actions.delete.disablePhase();
+ await actions.savePolicy();
+
+ const latestRequest = server.requests[server.requests.length - 1];
+ const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+
+ expect(entirePolicy).toEqual({
+ foo: 'bar', // Made up value
+ name: 'my_policy',
+ phases: {
+ hot: {
+ actions: {
+ rollover: {
+ max_docs: 1000,
+ max_size: '50gb',
+ unknown_setting: 123, // Made up setting that should stay preserved
+ },
+ },
+ min_age: '0ms',
+ },
+ warm: {
+ actions: {
+ my_unfollow_action: {}, // Made up action
+ set_priority: {
+ priority: 22,
+ unknown_setting: true,
+ },
+ },
+ min_age: '0d',
+ },
+ },
+ });
+ });
+ });
+
+ describe('hot phase', () => {
+ test('setting all values', async () => {
+ const { actions } = testBed;
+
+ await actions.hot.toggleDefaultRollover(false);
+ await actions.hot.setMaxSize('123', 'mb');
+ await actions.hot.setMaxDocs('123');
+ await actions.hot.setMaxAge('123', 'h');
+ await actions.hot.toggleForceMerge(true);
+ await actions.hot.setForcemergeSegmentsCount('123');
+ await actions.hot.setBestCompression(true);
+ await actions.hot.toggleShrink(true);
+ await actions.hot.setShrink('2');
+ await actions.hot.toggleReadonly(true);
+ await actions.hot.toggleIndexPriority(true);
+ await actions.hot.setIndexPriority('123');
+
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+ expect(entirePolicy).toMatchInlineSnapshot(`
+ Object {
+ "name": "my_policy",
+ "phases": Object {
+ "hot": Object {
+ "actions": Object {
+ "forcemerge": Object {
+ "index_codec": "best_compression",
+ "max_num_segments": 123,
+ },
+ "readonly": Object {},
+ "rollover": Object {
+ "max_age": "123h",
+ "max_docs": 123,
+ "max_size": "123mb",
+ },
+ "set_priority": Object {
+ "priority": 123,
+ },
+ "shrink": Object {
+ "number_of_shards": 2,
+ },
+ },
+ "min_age": "0ms",
+ },
+ },
+ }
+ `);
+ });
+
+ test('setting searchable snapshot', async () => {
+ const { actions } = testBed;
+
+ await actions.hot.setSearchableSnapshot('my-repo');
+
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+ expect(entirePolicy.phases.hot.actions.searchable_snapshot.snapshot_repository).toBe(
+ 'my-repo'
+ );
+ });
+
+ test('disabling rollover', async () => {
+ const { actions } = testBed;
+ await actions.hot.toggleDefaultRollover(false);
+ await actions.hot.toggleRollover(false);
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const policy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+ const hotActions = policy.phases.hot.actions;
+ const rolloverAction = hotActions.rollover;
+ expect(rolloverAction).toBe(undefined);
+ expect(hotActions).toMatchInlineSnapshot(`Object {}`);
+ });
+ });
+
+ describe('warm phase', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: {},
+ nodesByAttributes: { test: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
+ });
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('default values', async () => {
+ const { actions } = testBed;
+ await actions.warm.enable(true);
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm;
+ expect(warmPhase).toMatchInlineSnapshot(`
+ Object {
+ "actions": Object {
+ "set_priority": Object {
+ "priority": 50,
+ },
+ },
+ "min_age": "0d",
+ }
+ `);
+ });
+
+ test('setting all values', async () => {
+ const { actions } = testBed;
+ await actions.warm.enable(true);
+ await actions.warm.setDataAllocation('node_attrs');
+ await actions.warm.setSelectedNodeAttribute('test:123');
+ await actions.warm.setReplicas('123');
+ await actions.warm.toggleShrink(true);
+ await actions.warm.setShrink('123');
+ await actions.warm.toggleForceMerge(true);
+ await actions.warm.setForcemergeSegmentsCount('123');
+ await actions.warm.setBestCompression(true);
+ await actions.warm.toggleReadonly(true);
+ await actions.warm.setIndexPriority('123');
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+ // Check shape of entire policy
+ expect(entirePolicy).toMatchInlineSnapshot(`
+ Object {
+ "name": "my_policy",
+ "phases": Object {
+ "hot": Object {
+ "actions": Object {
+ "rollover": Object {
+ "max_age": "30d",
+ "max_size": "50gb",
+ },
+ },
+ "min_age": "0ms",
+ },
+ "warm": Object {
+ "actions": Object {
+ "allocate": Object {
+ "number_of_replicas": 123,
+ "require": Object {
+ "test": "123",
+ },
+ },
+ "forcemerge": Object {
+ "index_codec": "best_compression",
+ "max_num_segments": 123,
+ },
+ "readonly": Object {},
+ "set_priority": Object {
+ "priority": 123,
+ },
+ "shrink": Object {
+ "number_of_shards": 123,
+ },
+ },
+ "min_age": "0d",
+ },
+ },
+ }
+ `);
+ });
+
+ describe('policy with include and exclude', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_INCLUDE_EXCLUDE]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: {},
+ nodesByAttributes: { test: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
+ });
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('preserves include, exclude allocation settings', async () => {
+ const { actions } = testBed;
+ await actions.warm.setDataAllocation('node_attrs');
+ await actions.warm.setSelectedNodeAttribute('test:123');
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const warmPhaseAllocate = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm
+ .actions.allocate;
+ expect(warmPhaseAllocate).toMatchInlineSnapshot(`
+ Object {
+ "exclude": Object {
+ "def": "456",
+ },
+ "include": Object {
+ "abc": "123",
+ },
+ "require": Object {
+ "test": "123",
+ },
+ }
+ `);
+ });
+ });
+ });
+
+ describe('cold phase', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: {},
+ nodesByAttributes: { test: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
+ });
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('default values', async () => {
+ const { actions } = testBed;
+
+ await actions.cold.enable(true);
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+ expect(entirePolicy.phases.cold).toMatchInlineSnapshot(`
+ Object {
+ "actions": Object {
+ "set_priority": Object {
+ "priority": 0,
+ },
+ },
+ "min_age": "0d",
+ }
+ `);
+ });
+
+ test('setting all values, excluding searchable snapshot', async () => {
+ const { actions } = testBed;
+
+ await actions.cold.enable(true);
+ await actions.cold.setMinAgeValue('123');
+ await actions.cold.setMinAgeUnits('s');
+ await actions.cold.setDataAllocation('node_attrs');
+ await actions.cold.setSelectedNodeAttribute('test:123');
+ await actions.cold.setReplicas('123');
+ await actions.cold.setFreeze(true);
+ await actions.cold.setIndexPriority('123');
+
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+
+ expect(entirePolicy).toMatchInlineSnapshot(`
+ Object {
+ "name": "my_policy",
+ "phases": Object {
+ "cold": Object {
+ "actions": Object {
+ "allocate": Object {
+ "number_of_replicas": 123,
+ "require": Object {
+ "test": "123",
+ },
+ },
+ "freeze": Object {},
+ "set_priority": Object {
+ "priority": 123,
+ },
+ },
+ "min_age": "123s",
+ },
+ "hot": Object {
+ "actions": Object {
+ "rollover": Object {
+ "max_age": "30d",
+ "max_size": "50gb",
+ },
+ },
+ "min_age": "0ms",
+ },
+ },
+ }
+ `);
+ });
+
+ // Setting searchable snapshot field disables setting replicas so we test this separately
+ test('setting searchable snapshot', async () => {
+ const { actions } = testBed;
+ await actions.cold.enable(true);
+ await actions.cold.setSearchableSnapshot('my-repo');
+ await actions.savePolicy();
+ const latestRequest2 = server.requests[server.requests.length - 1];
+ const entirePolicy2 = JSON.parse(JSON.parse(latestRequest2.requestBody).body);
+ expect(entirePolicy2.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual(
+ 'my-repo'
+ );
+ });
+ });
+
+ test('delete phase', async () => {
+ const { actions } = testBed;
+ await actions.delete.enablePhase();
+ await actions.setWaitForSnapshotPolicy('test');
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+ expect(entirePolicy.phases.delete).toEqual({
+ min_age: '365d',
+ actions: {
+ delete: {},
+ wait_for_snapshot: {
+ policy: 'test',
+ },
+ },
+ });
+ });
+});
From 395f0f50009ce087f310307dbf80e9e5cd3d4bef Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 2 Mar 2021 12:08:53 -0500
Subject: [PATCH 10/63] Improve consistency for display of management items
(#92694) (#93238)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Larry Gregory
---
.../beats_management/public/bootstrap.tsx | 11 +++++++----
.../cross_cluster_replication/public/plugin.ts | 16 ++++++++++------
x-pack/plugins/watcher/public/plugin.ts | 16 ++++++++++++----
3 files changed, 29 insertions(+), 14 deletions(-)
diff --git a/x-pack/plugins/beats_management/public/bootstrap.tsx b/x-pack/plugins/beats_management/public/bootstrap.tsx
index 4a4d3a893286ba..b5bdd39fa0817e 100644
--- a/x-pack/plugins/beats_management/public/bootstrap.tsx
+++ b/x-pack/plugins/beats_management/public/bootstrap.tsx
@@ -18,10 +18,13 @@ import { BeatsManagementConfigType } from '../common';
import { MANAGEMENT_SECTION } from '../common/constants';
async function startApp(libs: FrontendLibs, core: CoreSetup) {
- const [startServices] = await Promise.all([
- core.getStartServices(),
- libs.framework.waitUntilFrameworkReady(),
- ]);
+ const startServices = await core.getStartServices();
+
+ if (startServices[0].http.anonymousPaths.isAnonymous(window.location.pathname)) {
+ return;
+ }
+ // Can't run until the `start` lifecycle, so we wait for start services to resolve above before calling this.
+ await libs.framework.waitUntilFrameworkReady();
const capabilities = startServices[0].application.capabilities;
const hasBeatsCapability = capabilities.management.ingest?.[MANAGEMENT_SECTION] ?? false;
diff --git a/x-pack/plugins/cross_cluster_replication/public/plugin.ts b/x-pack/plugins/cross_cluster_replication/public/plugin.ts
index 24c9d8dae379d1..7998cdbdf750b9 100644
--- a/x-pack/plugins/cross_cluster_replication/public/plugin.ts
+++ b/x-pack/plugins/cross_cluster_replication/public/plugin.ts
@@ -73,18 +73,21 @@ export class CrossClusterReplicationPlugin implements Plugin {
// NOTE: We enable the plugin by default instead of disabling it by default because this
// creates a race condition that causes functional tests to fail on CI (see #66781).
- licensing.license$
- .pipe(first())
- .toPromise()
- .then((license) => {
+ Promise.all([licensing.license$.pipe(first()).toPromise(), getStartServices()]).then(
+ ([license, startServices]) => {
const licenseStatus = license.check(PLUGIN.ID, PLUGIN.minimumLicenseType);
const isLicenseOk = licenseStatus.state === 'valid';
const config = this.initializerContext.config.get();
+ const capabilities = startServices[0].application.capabilities;
+
// remoteClusters.isUiEnabled is driven by the xpack.remote_clusters.ui.enabled setting.
// The CCR UI depends upon the Remote Clusters UI (e.g. by cross-linking to it), so if
// the Remote Clusters UI is disabled we can't show the CCR UI.
- const isCcrUiEnabled = config.ui.enabled && remoteClusters.isUiEnabled;
+ const isCcrUiEnabled =
+ capabilities.management.data?.[MANAGEMENT_ID] &&
+ config.ui.enabled &&
+ remoteClusters.isUiEnabled;
if (isLicenseOk && isCcrUiEnabled) {
if (indexManagement) {
@@ -106,7 +109,8 @@ export class CrossClusterReplicationPlugin implements Plugin {
} else {
ccrApp.disable();
}
- });
+ }
+ );
}
public start() {}
diff --git a/x-pack/plugins/watcher/public/plugin.ts b/x-pack/plugins/watcher/public/plugin.ts
index 9cc0b1bbe99a80..6c6d6f1169658e 100644
--- a/x-pack/plugins/watcher/public/plugin.ts
+++ b/x-pack/plugins/watcher/public/plugin.ts
@@ -6,9 +6,10 @@
*/
import { i18n } from '@kbn/i18n';
-import { CoreSetup, Plugin, CoreStart } from 'kibana/public';
+import { CoreSetup, Plugin, CoreStart, Capabilities } from 'kibana/public';
import { first, map, skip } from 'rxjs/operators';
+import { Subject, combineLatest } from 'rxjs';
import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public';
import { LicenseStatus } from '../common/types/license_status';
@@ -26,6 +27,8 @@ const licenseToLicenseStatus = (license: ILicense): LicenseStatus => {
};
export class WatcherUIPlugin implements Plugin {
+ private capabilities$: Subject = new Subject();
+
setup(
{ notifications, http, uiSettings, getStartServices }: CoreSetup,
{ licensing, management, data, home, charts }: Dependencies
@@ -99,13 +102,16 @@ export class WatcherUIPlugin implements Plugin {
home.featureCatalogue.register(watcherHome);
- licensing.license$.pipe(first(), map(licenseToLicenseStatus)).subscribe(({ valid }) => {
+ combineLatest([
+ licensing.license$.pipe(first(), map(licenseToLicenseStatus)),
+ this.capabilities$,
+ ]).subscribe(([{ valid }, capabilities]) => {
// NOTE: We enable the plugin by default instead of disabling it by default because this
// creates a race condition that can cause the app nav item to not render in the side nav.
// The race condition still exists, but it will result in the item rendering when it shouldn't
// (e.g. on a license it's not available for), instead of *not* rendering when it *should*,
// which is a less frustrating UX.
- if (valid) {
+ if (valid && capabilities.management.insightsAndAlerting?.watcher === true) {
watcherESApp.enable();
} else {
watcherESApp.disable();
@@ -113,7 +119,9 @@ export class WatcherUIPlugin implements Plugin {
});
}
- start(core: CoreStart) {}
+ start(core: CoreStart) {
+ this.capabilities$.next(core.application.capabilities);
+ }
stop() {}
}
From dff54d7ed9670ca2796f53c58cc4261d42329992 Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Tue, 2 Mar 2021 18:16:37 +0100
Subject: [PATCH 11/63] [ILM] Allow multiple searchable snapshot actions
(#92789) (#93242)
* remove logic that disables SS action in cold if no rollover and always show replicas field
* update test coverage to be consistent with new form behaviour and expand hot phase without rollover test
* only licensing can disable searchable snapshot field
* clean up i18n
* remove ss field callout
* update error reporting logic to include causes chain, also update UI to show causes
* updated searchable snapshot field in hot phase callout
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../errors/es_error_parser.ts | 6 ++---
.../errors/handle_es_error.ts | 11 +++++++-
.../searchable_snapshot_field.tsx | 27 ++++---------------
.../public/application/services/api_errors.ts | 5 +++-
.../translations/translations/ja-JP.json | 2 --
.../translations/translations/zh-CN.json | 2 --
6 files changed, 22 insertions(+), 31 deletions(-)
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.ts
index 61a8882317f9f5..fc400e4a87b3af 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.ts
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.ts
@@ -11,14 +11,14 @@ interface ParsedError {
cause: string[];
}
-const getCause = (obj: any = {}, causes: string[] = []): string[] => {
+export const getEsCause = (obj: any = {}, causes: string[] = []): string[] => {
const updated = [...causes];
if (obj.caused_by) {
updated.push(obj.caused_by.reason);
// Recursively find all the "caused by" reasons
- return getCause(obj.caused_by, updated);
+ return getEsCause(obj.caused_by, updated);
}
return updated.filter(Boolean);
@@ -27,7 +27,7 @@ const getCause = (obj: any = {}, causes: string[] = []): string[] => {
export const parseEsError = (err: string): ParsedError => {
try {
const { error } = JSON.parse(err);
- const cause = getCause(error);
+ const cause = getEsCause(error);
return {
message: error.reason,
cause,
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts
index b645b62c863d5d..123db891bf41de 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts
@@ -9,6 +9,7 @@
import { ApiError } from '@elastic/elasticsearch';
import { ResponseError } from '@elastic/elasticsearch/lib/errors';
import { IKibanaResponse, KibanaResponseFactory } from 'kibana/server';
+import { getEsCause } from './es_error_parser';
interface EsErrorHandlerParams {
error: ApiError;
@@ -34,7 +35,15 @@ export const handleEsError = ({
const { statusCode, body } = error as ResponseError;
return response.customError({
statusCode,
- body: { message: body.error?.reason },
+ body: {
+ message: body.error?.reason,
+ attributes: {
+ // The full original ES error object
+ error: body.error,
+ // We assume that this is an ES error object with a nested caused by chain if we can see the "caused_by" field at the top-level
+ causes: body.error?.caused_by ? getEsCause(body.error) : undefined,
+ },
+ },
});
}
// Case: default
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx
index 1a78149521e63c..3fc7064575555e 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx
@@ -61,9 +61,6 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) =>
const isColdPhase = phase === 'cold';
const isDisabledDueToLicense = !license.canUseSearchableSnapshot();
- const isDisabledInColdDueToHotPhase = isColdPhase && isUsingSearchableSnapshotInHotPhase;
-
- const isDisabled = isDisabledDueToLicense || isDisabledInColdDueToHotPhase;
const [isFieldToggleChecked, setIsFieldToggleChecked] = useState(() =>
Boolean(
@@ -74,10 +71,10 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) =>
);
useEffect(() => {
- if (isDisabled) {
+ if (isDisabledDueToLicense) {
setIsFieldToggleChecked(false);
}
- }, [isDisabled]);
+ }, [isDisabledDueToLicense]);
const renderField = () => (
@@ -254,7 +251,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) =>
'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody',
{
defaultMessage:
- 'Force merge, shrink, freeze and cold phase searchable snapshots are not allowed when searchable snapshots are enabled in the hot phase.',
+ 'Force merge, shrink and freeze actions are not allowed when searchable snapshots are enabled in this phase.',
}
)}
data-test-subj="searchableSnapshotFieldsDisabledCallout"
@@ -278,20 +275,6 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) =>
)}
);
- } else if (isDisabledInColdDueToHotPhase) {
- infoCallout = (
-
- );
}
return infoCallout ? (
@@ -308,7 +291,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) =>
data-test-subj={`searchableSnapshotField-${phase}`}
switchProps={{
checked: isFieldToggleChecked,
- disabled: isDisabled,
+ disabled: isDisabledDueToLicense,
onChange: setIsFieldToggleChecked,
'data-test-subj': 'searchableSnapshotToggle',
label: i18n.translate(
@@ -339,7 +322,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) =>
fieldNotices={renderInfoCallout()}
fullWidth
>
- {isDisabled ?
: renderField}
+ {isDisabledDueToLicense ?
: renderField}
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api_errors.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api_errors.ts
index 54ef91457b1f9a..fc37b62e30eb25 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/api_errors.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api_errors.ts
@@ -11,7 +11,10 @@ import { fatalErrors, toasts } from './notification';
function createToastConfig(error: IHttpFetchError, errorTitle: string) {
if (error && error.body) {
// Error body shape is defined by the API.
- const { error: errorString, statusCode, message } = error.body;
+ const { error: errorString, statusCode, message: errorMessage, attributes } = error.body;
+ const message = attributes?.causes?.length
+ ? attributes.causes[attributes.causes.length - 1]
+ : errorMessage;
return {
title: errorTitle,
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 7698d11b10c223..472ecc3f801644 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -9505,8 +9505,6 @@
"xpack.indexLifecycleMgmt.editPolicy.saveAsNewPolicyMessage": "新規ポリシーとして保存します",
"xpack.indexLifecycleMgmt.editPolicy.saveButton": "ポリシーを保存",
"xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage": "ライフサイクルポリシー {lifecycleName} の保存中にエラーが発生しました",
- "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "検索可能なスナップショットがホットフェーズで有効な場合には、強制、マージ、縮小、凍結、コールドフェーズの検索可能なスナップショットは許可されません。",
- "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotDisabledCalloutBody": "ホットフェーズで構成されているときには、コールドフェーズで検索可能なスナップショットを作成できません。",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldDescription": "選択したリポジトリで管理されたインデックスのスナップショットを作成し、検索可能なスナップショットとしてマウントします。{learnMoreLink}。",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel": "検索可能なスナップショットリポジドリ",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "検索可能スナップショット",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 208c0a2d4e7e56..a72c83b736a6d0 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -9529,8 +9529,6 @@
"xpack.indexLifecycleMgmt.editPolicy.saveAsNewPolicyMessage": "另存为新策略",
"xpack.indexLifecycleMgmt.editPolicy.saveButton": "保存策略",
"xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage": "保存生命周期策略 {lifecycleName} 时出错",
- "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "在热阶段启用可搜索快照时,不允许强制合并、缩小、冻结可搜索快照以及将其置入冷阶段。",
- "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotDisabledCalloutBody": "无法在冷阶段创建在热阶段配置的可搜索快照。",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldDescription": "在所选存储库中拍取受管索引的快照,并将其安装为可搜索快照。{learnMoreLink}。",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel": "可搜索快照存储库",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "可搜索快照",
From 18c807b55d58dee9ab5e4286214e82a5ca1371ff Mon Sep 17 00:00:00 2001
From: Devon Thomson
Date: Tue, 2 Mar 2021 13:11:20 -0500
Subject: [PATCH 12/63] [Time to Visualize] Disable Visualize URL Tracker When
Linked to OriginatingApp (#92917) (#93252)
* changed url tracker for visualize to not save when linked to originating app
---
src/plugins/visualize/public/plugin.ts | 5 +----
.../dashboard/create_and_add_embeddables.ts | 4 ++--
.../apps/dashboard/edit_visualizations.js | 18 ++++++++++++++++++
3 files changed, 21 insertions(+), 6 deletions(-)
diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts
index 300afd69c84ccd..4b369e8be86eee 100644
--- a/src/plugins/visualize/public/plugin.ts
+++ b/src/plugins/visualize/public/plugin.ts
@@ -116,10 +116,7 @@ export class VisualizePlugin
],
getHistory: () => this.currentHistory!,
onBeforeNavLinkSaved: (urlToSave: string) => {
- if (
- !urlToSave.includes(`${VisualizeConstants.EDIT_PATH}/`) &&
- this.isLinkedToOriginatingApp?.()
- ) {
+ if (this.isLinkedToOriginatingApp?.()) {
return core.http.basePath.prepend(VisualizeConstants.VISUALIZE_BASE_PATH);
}
return urlToSave;
diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.ts b/test/functional/apps/dashboard/create_and_add_embeddables.ts
index c26b375a37b8f5..f4ee4e99047686 100644
--- a/test/functional/apps/dashboard/create_and_add_embeddables.ts
+++ b/test/functional/apps/dashboard/create_and_add_embeddables.ts
@@ -69,10 +69,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.dashboard.waitForRenderComplete();
});
- it('saves the saved visualization url to the app link', async () => {
+ it('saves the listing page instead of the visualization to the app link', async () => {
await PageObjects.header.clickVisualize(true);
const currentUrl = await browser.getCurrentUrl();
- expect(currentUrl).to.contain(VisualizeConstants.EDIT_PATH);
+ expect(currentUrl).not.to.contain(VisualizeConstants.EDIT_PATH);
});
after(async () => {
diff --git a/test/functional/apps/dashboard/edit_visualizations.js b/test/functional/apps/dashboard/edit_visualizations.js
index a918c017bd88f8..d5df97881a1d3b 100644
--- a/test/functional/apps/dashboard/edit_visualizations.js
+++ b/test/functional/apps/dashboard/edit_visualizations.js
@@ -109,6 +109,24 @@ export default function ({ getService, getPageObjects }) {
expect(await testSubjects.exists('visualizationLandingPage')).to.be(true);
});
+ it('visualize app menu navigates to the visualize listing page if the last opened visualization was linked to dashboard', async () => {
+ await PageObjects.common.navigateToApp('dashboard');
+ await PageObjects.dashboard.gotoDashboardLandingPage();
+ await PageObjects.dashboard.clickNewDashboard();
+
+ // Create markdown by reference.
+ await createMarkdownVis('by reference');
+
+ // Edit then save and return
+ await editMarkdownVis();
+ await PageObjects.visualize.saveVisualizationAndReturn();
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await appsMenu.clickLink('Visualize Library');
+ await PageObjects.common.clickConfirmOnModal();
+ expect(await testSubjects.exists('visualizationLandingPage')).to.be(true);
+ });
+
describe('by value', () => {
it('save and return button returns to dashboard after editing visualization with changes saved', async () => {
await PageObjects.common.navigateToApp('dashboard');
From cfbd64ff4f9edb34c81d11114a17b85ecbf1b115 Mon Sep 17 00:00:00 2001
From: Michael Olorunnisola
Date: Tue, 2 Mar 2021 13:14:22 -0500
Subject: [PATCH 13/63] [Security Solution] - Bug fixes (#92294) (#93209)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../overview_description_list/index.tsx | 4 +-
.../network/components/details/index.tsx | 6 +-
.../host_overview/endpoint_overview/index.tsx | 134 ++--
.../components/host_overview/index.tsx | 12 +-
.../view/resolver_no_process_events.tsx | 2 +-
.../__snapshots__/index.test.tsx.snap | 36 +-
.../event_details/expandable_event.tsx | 2 +-
.../expandable_host.test.tsx.snap | 669 ++++++++++++++++++
.../host_details/expandable_host.test.tsx | 78 ++
.../host_details/expandable_host.tsx | 25 +-
.../side_panel/host_details/index.tsx | 25 +-
.../timelines/components/side_panel/index.tsx | 11 +-
.../side_panel/network_details/index.tsx | 22 +-
13 files changed, 888 insertions(+), 138 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/__snapshots__/expandable_host.test.tsx.snap
create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx
diff --git a/x-pack/plugins/security_solution/public/common/components/overview_description_list/index.tsx b/x-pack/plugins/security_solution/public/common/components/overview_description_list/index.tsx
index 570ac4e9577b7e..606991bc089104 100644
--- a/x-pack/plugins/security_solution/public/common/components/overview_description_list/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/overview_description_list/index.tsx
@@ -14,13 +14,11 @@ import { DescriptionListStyled } from '../../../common/components/page';
export const OverviewDescriptionList = ({
dataTestSubj,
descriptionList,
- isInDetailsSidePanel = false,
}: {
dataTestSubj?: string;
descriptionList: DescriptionList[];
- isInDetailsSidePanel: boolean;
}) => (
-
+
);
diff --git a/x-pack/plugins/security_solution/public/network/components/details/index.tsx b/x-pack/plugins/security_solution/public/network/components/details/index.tsx
index e263d49e22fc01..851197a78520bc 100644
--- a/x-pack/plugins/security_solution/public/network/components/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/details/index.tsx
@@ -144,11 +144,7 @@ export const IpOverview = React.memo(
)}
{descriptionLists.map((descriptionList, index) => (
-
+
))}
{loading && (
diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx
index 0d8e763b649bf6..4caf854278cc2e 100644
--- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx
@@ -20,79 +20,73 @@ import * as i18n from './translations';
interface Props {
contextID?: string;
data: EndpointFields | null;
- isInDetailsSidePanel?: boolean;
}
-export const EndpointOverview = React.memo(
- ({ contextID, data, isInDetailsSidePanel = false }) => {
- const getDefaultRenderer = useCallback(
- (fieldName: string, fieldData: EndpointFields, attrName: string) => (
-
- ),
- [contextID]
- );
- const descriptionLists: Readonly = useMemo(
- () => [
- [
- {
- title: i18n.ENDPOINT_POLICY,
- description:
- data != null && data.endpointPolicy != null
- ? data.endpointPolicy
- : getEmptyTagValue(),
- },
- ],
- [
- {
- title: i18n.POLICY_STATUS,
- description:
- data != null && data.policyStatus != null ? (
-
- {data.policyStatus}
-
- ) : (
- getEmptyTagValue()
- ),
- },
- ],
- [
- {
- title: i18n.SENSORVERSION,
- description:
- data != null && data.sensorVersion != null
- ? getDefaultRenderer('sensorVersion', data, 'agent.version')
- : getEmptyTagValue(),
- },
- ],
- [], // needs 4 columns for design
+export const EndpointOverview = React.memo(({ contextID, data }) => {
+ const getDefaultRenderer = useCallback(
+ (fieldName: string, fieldData: EndpointFields, attrName: string) => (
+
+ ),
+ [contextID]
+ );
+ const descriptionLists: Readonly = useMemo(
+ () => [
+ [
+ {
+ title: i18n.ENDPOINT_POLICY,
+ description:
+ data != null && data.endpointPolicy != null ? data.endpointPolicy : getEmptyTagValue(),
+ },
+ ],
+ [
+ {
+ title: i18n.POLICY_STATUS,
+ description:
+ data != null && data.policyStatus != null ? (
+
+ {data.policyStatus}
+
+ ) : (
+ getEmptyTagValue()
+ ),
+ },
],
- [data, getDefaultRenderer]
- );
+ [
+ {
+ title: i18n.SENSORVERSION,
+ description:
+ data != null && data.sensorVersion != null
+ ? getDefaultRenderer('sensorVersion', data, 'agent.version')
+ : getEmptyTagValue(),
+ },
+ ],
+ [], // needs 4 columns for design
+ ],
+ [data, getDefaultRenderer]
+ );
- return (
- <>
- {descriptionLists.map((descriptionList, index) => (
-
- ))}
- >
- );
- }
-);
+ return (
+ <>
+ {descriptionLists.map((descriptionList, index) => (
+
+ ))}
+ >
+ );
+});
EndpointOverview.displayName = 'EndpointOverview';
diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx
index de0d782b3ceb72..c5d51a9466235f 100644
--- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx
@@ -207,11 +207,7 @@ export const HostOverview = React.memo(
)}
{descriptionLists.map((descriptionList, index) => (
-
+
))}
{loading && (
@@ -229,11 +225,7 @@ export const HostOverview = React.memo(
<>
-
+
{loading && (
(
- {"event.category: 'process'"}
+ {'event.category: "process"'}
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap
index aece377ee4f2dc..a55710f3461413 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap
@@ -24,9 +24,9 @@ exports[`Details Panel Component DetailsPanel: rendering it should not render th
exports[`Details Panel Component DetailsPanel:EventDetails: rendering it should render the Details Panel when the panelView is set and the associated params are set 1`] = `
.c0 {
- -webkit-flex: 0;
- -ms-flex: 0;
- flex: 0;
+ -webkit-flex: 0 1 auto;
+ -ms-flex: 0 1 auto;
+ flex: 0 1 auto;
}
,
.c1 {
- -webkit-flex: 0;
- -ms-flex: 0;
- flex: 0;
+ -webkit-flex: 0 1 auto;
+ -ms-flex: 0 1 auto;
+ flex: 0 1 auto;
}
.c2 .euiFlyoutBody__overflow {
@@ -541,7 +541,7 @@ Array [
className="c0"
data-test-subj="timeline:details-panel:flyout"
onClose={[Function]}
- size="s"
+ size="m"
>
,
.c1 {
- -webkit-flex: 0;
- -ms-flex: 0;
- flex: 0;
+ -webkit-flex: 0 1 auto;
+ -ms-flex: 0 1 auto;
+ flex: 0 1 auto;
}
.c2 .euiFlyoutBody__overflow {
@@ -804,7 +804,7 @@ Array [
}
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx
new file mode 100644
index 00000000000000..2ce7090a5b83af
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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 { mount } from 'enzyme';
+import React from 'react';
+
+import '../../../../common/mock/match_media';
+import {
+ apolloClientObservable,
+ mockGlobalState,
+ TestProviders,
+ SUB_PLUGINS_REDUCER,
+ kibanaObservable,
+ createSecuritySolutionStorageMock,
+} from '../../../../common/mock';
+import { createStore, State } from '../../../../common/store';
+import { ExpandableHostDetails } from './expandable_host';
+
+jest.mock('react-apollo', () => {
+ const original = jest.requireActual('react-apollo');
+ return {
+ ...original,
+ // eslint-disable-next-line react/display-name
+ Query: () => <>>,
+ };
+});
+
+describe('Expandable Host Component', () => {
+ const state: State = {
+ ...mockGlobalState,
+ sourcerer: {
+ ...mockGlobalState.sourcerer,
+ configIndexPatterns: ['IShouldBeUsed'],
+ },
+ };
+
+ const { storage } = createSecuritySolutionStorageMock();
+ const store = createStore(
+ state,
+ SUB_PLUGINS_REDUCER,
+ apolloClientObservable,
+ kibanaObservable,
+ storage
+ );
+
+ const mockProps = {
+ contextID: 'text-context',
+ hostName: 'testHostName',
+ };
+
+ describe('ExpandableHostDetails: rendering', () => {
+ test('it should render the HostOverview of the ExpandableHostDetails', () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find('ExpandableHostDetails')).toMatchSnapshot();
+ });
+
+ test('it should render the HostOverview of the ExpandableHostDetails with the correct indices', () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find('HostOverviewByNameComponentQuery').prop('indexNames')).toStrictEqual([
+ 'IShouldBeUsed',
+ ]);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx
index 8fce9a186bbd46..78367d17d7b629 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx
@@ -5,10 +5,11 @@
* 2.0.
*/
-import React from 'react';
+import React, { useMemo } from 'react';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { EuiTitle } from '@elastic/eui';
+import { sourcererSelectors } from '../../../../common/store/sourcerer';
import { HostDetailsLink } from '../../../../common/components/links';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
@@ -19,6 +20,7 @@ import { AnomalyTableProvider } from '../../../../common/components/ml/anomaly/a
import { hostToCriteria } from '../../../../common/components/ml/criteria/host_to_criteria';
import { scoreIntervalToDateTime } from '../../../../common/components/ml/score/score_interval_to_datetime';
import { HostOverviewByNameQuery } from '../../../../hosts/containers/hosts/details';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
interface ExpandableHostProps {
hostName: string;
@@ -54,10 +56,25 @@ export const ExpandableHostDetails = ({
hostName,
}: ExpandableHostProps & { contextID: string }) => {
const { to, from, isInitializing } = useGlobalTime();
- const { docValueFields, selectedPatterns } = useSourcererScope();
+ const { docValueFields } = useSourcererScope();
+ /*
+ Normally `selectedPatterns` from useSourcerScope would be where we obtain the indices,
+ but those indices are only loaded when viewing the pages where the sourcerer is initialized (i.e. Hosts and Overview)
+ When a user goes directly to the detections page, the patterns have not been loaded yet
+ as that information isn't used for the detections page. With this details component being accessible
+ from the detections page, the decision was made to get all existing index names to account for this.
+ Otherwise, an empty array is defaulted for the `indexNames` in the query which leads to inconsistencies in the data returned
+ (i.e. extraneous endpoint data is retrieved from the backend leading to endpoint data not being returned)
+ */
+ const allExistingIndexNamesSelector = useMemo(
+ () => sourcererSelectors.getAllExistingIndexNamesSelector(),
+ []
+ );
+ const allPatterns = useDeepEqualSelector
(allExistingIndexNamesSelector);
+
return (
`${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 64px`};
+ margin-bottom: ${({ theme }) => `${theme.eui.paddingSizes.l}`};
+ padding: ${({ theme }) => `${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 0px`};
}
}
`;
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
- flex: 0;
-`;
-
-const StyledEuiFlexItem = styled(EuiFlexItem)`
- &.euiFlexItem {
- flex: 1 0 0;
- overflow-y: scroll;
- overflow-x: hidden;
- }
+ flex: 1 0 auto;
`;
const StyledEuiFlexButtonWrapper = styled(EuiFlexItem)`
align-self: flex-start;
+ flex: 1 0 auto;
+`;
+
+const StyledPanelContent = styled.div`
+ display: block;
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
`;
interface HostDetailsProps {
@@ -107,9 +108,9 @@ export const HostDetailsPanel: React.FC = React.memo(
-
+
-
+
>
);
}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx
index 0482491562f57b..177cd2e5ded41b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx
@@ -7,7 +7,7 @@
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
-import { EuiFlyout } from '@elastic/eui';
+import { EuiFlyout, EuiFlyoutProps } from '@elastic/eui';
import styled from 'styled-components';
import { timelineActions, timelineSelectors } from '../../store/timeline';
import { timelineDefaults } from '../../store/timeline/defaults';
@@ -69,9 +69,10 @@ export const DetailsPanel = React.memo(
if (!currentTabDetail?.panelView) return null;
let visiblePanel = null; // store in variable to make return statement more readable
+ let panelSize: EuiFlyoutProps['size'] = 's';
const contextID = `${timelineId}-${activeTab}`;
-
if (currentTabDetail?.panelView === 'eventDetail' && currentTabDetail?.params?.eventId) {
+ panelSize = 'm';
visiblePanel = (
+
{visiblePanel}
) : (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/index.tsx
index e05c9435fc456f..ea857da926f84e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/index.tsx
@@ -42,19 +42,19 @@ const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
`;
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
- flex: 0;
-`;
-
-const StyledEuiFlexItem = styled(EuiFlexItem)`
- &.euiFlexItem {
- flex: 1 0 0;
- overflow-y: scroll;
- overflow-x: hidden;
- }
+ flex: 1 0 auto;
`;
const StyledEuiFlexButtonWrapper = styled(EuiFlexItem)`
align-self: flex-start;
+ flex: 1 0 auto;
+`;
+
+const StyledPanelContent = styled.div`
+ display: block;
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
`;
interface NetworkDetailsProps {
@@ -104,9 +104,9 @@ export const NetworkDetailsPanel = React.memo(
-
+
-
+
>
);
}
From 495c259d19568e46a8895a3221a02ab3e85214c2 Mon Sep 17 00:00:00 2001
From: Nathan L Smith
Date: Tue, 2 Mar 2021 12:18:39 -0600
Subject: [PATCH 14/63] Pass service node name in query for instance table
links (#91796) (#93240)
For a non-Java service, the previous link was like:
```
http://localhost:5601/kbn/app/apm/services/opbeans-python/metrics?rangeFrom=now-15m&rangeTo=now
```
which did not filter by the `service.node.name`.
It now is:
```
http://localhost:5601/kbn/app/apm/services/opbeans-python/metrics?kuery=service.node.name:%226a7f116fe344aee7e92fceeb426cbfdf6a534a8e3ba6345c16a47793eba6daf5%22&rangeFrom=now-15m&rangeTo=now
````
Which links to the metrics page with the filter applied.
The component is using a `MetricOverviewLink` which was using a `EuiLink` and passing throught the props, including `mergeQuery`, which includes the `kuery` parameter.
Replace the `EuiLink` with an `APMLink` which does use the `mergeQuery` prop and does pass the parameters through correctly.
Looks like this was changed to an `EuiLink` by a refactor in #86986.
Since we'll be making some further changes to how `kuery` is handled in #84526, I'm just making the minimal change to fix this bug at this time.
---
.../components/shared/Links/apm/MetricOverviewLink.tsx | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx
index 3bfdb5df61c2ee..c3d418b63426b5 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx
@@ -5,10 +5,9 @@
* 2.0.
*/
-import { EuiLink } from '@elastic/eui';
import React from 'react';
import { APMQueryParams } from '../url_helpers';
-import { APMLinkExtendProps, useAPMHref } from './APMLink';
+import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink';
const persistedFilters: Array = [
'host',
@@ -29,6 +28,5 @@ interface Props extends APMLinkExtendProps {
}
export function MetricOverviewLink({ serviceName, ...rest }: Props) {
- const href = useMetricOverviewHref(serviceName);
- return ;
+ return ;
}
From e709686224432337a772739a86b797e06d41a1be Mon Sep 17 00:00:00 2001
From: Matthias Wilhelm
Date: Tue, 2 Mar 2021 20:33:50 +0100
Subject: [PATCH 15/63] [Discover] Change icon of saved search in open search
panel and embeddable selection (#93001) (#93271)
---
.../public/application/components/top_nav/open_search_panel.tsx | 2 +-
.../public/application/embeddable/search_embeddable_factory.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx b/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx
index 8c5f44b97f4b30..f6881d1856566a 100644
--- a/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx
+++ b/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx
@@ -59,7 +59,7 @@ export function OpenSearchPanel(props: OpenSearchPanelProps) {
savedObjectMetaData={[
{
type: SEARCH_OBJECT_TYPE,
- getIconForSavedObject: () => 'search',
+ getIconForSavedObject: () => 'discoverApp',
name: i18n.translate('discover.savedSearch.savedObjectName', {
defaultMessage: 'Saved search',
}),
diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts
index 043869f99bb350..77da138d118dd0 100644
--- a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts
+++ b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts
@@ -36,7 +36,7 @@ export class SearchEmbeddableFactory
defaultMessage: 'Saved search',
}),
type: 'search',
- getIconForSavedObject: () => 'search',
+ getIconForSavedObject: () => 'discoverApp',
};
constructor(
From 08417cbd6c15e4c866651a7dcdfeded58845206d Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 2 Mar 2021 14:39:39 -0500
Subject: [PATCH 16/63] [Bazel] More resilient Workspace Status (#93244)
(#93273)
Signed-off-by: Tyler Smalley
Co-authored-by: Tyler Smalley
---
src/dev/bazel_workspace_status.js | 37 ++++++++++++++-----------------
1 file changed, 17 insertions(+), 20 deletions(-)
diff --git a/src/dev/bazel_workspace_status.js b/src/dev/bazel_workspace_status.js
index fe60f9176d243e..3c3ef1574cd8e8 100644
--- a/src/dev/bazel_workspace_status.js
+++ b/src/dev/bazel_workspace_status.js
@@ -43,25 +43,23 @@
// Commit SHA
const commitSHACmdResult = await runCmd('git', ['rev-parse', 'HEAD']);
- if (commitSHACmdResult.exitCode !== 0) {
- process.exit(1);
- }
- console.log(`COMMIT_SHA ${commitSHACmdResult.stdout}`);
+ if (commitSHACmdResult.exitCode === 0) {
+ console.log(`COMMIT_SHA ${commitSHACmdResult.stdout}`);
- // Git branch
- const gitBranchCmdResult = await runCmd('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
- if (gitBranchCmdResult.exitCode !== 0) {
- process.exit(1);
- }
- console.log(`GIT_BRANCH ${gitBranchCmdResult.stdout}`);
+ // Branch
+ const gitBranchCmdResult = await runCmd('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
+ if (gitBranchCmdResult.exitCode === 0) {
+ console.log(`GIT_BRANCH ${gitBranchCmdResult.stdout}`);
+ }
- // Tree status
- const treeStatusCmdResult = await runCmd('git', ['diff-index', '--quiet', 'HEAD', '--']);
- const treeStatusVarStr = 'GIT_TREE_STATUS';
- if (treeStatusCmdResult.exitCode === 0) {
- console.log(`${treeStatusVarStr} Clean`);
- } else {
- console.log(`${treeStatusVarStr} Modified`);
+ // Tree status
+ const treeStatusCmdResult = await runCmd('git', ['diff-index', '--quiet', 'HEAD', '--']);
+ const treeStatusVarStr = 'GIT_TREE_STATUS';
+ if (treeStatusCmdResult.exitCode === 0) {
+ console.log(`${treeStatusVarStr} Clean`);
+ } else {
+ console.log(`${treeStatusVarStr} Modified`);
+ }
}
// Host
@@ -72,9 +70,8 @@
return !cpu.model.includes('Intel') || index % 2 === 1;
}).length;
- if (hostCmdResult.exitCode !== 0) {
- process.exit(1);
+ if (hostCmdResult.exitCode === 0) {
+ console.log(`HOST ${hostStr}-${coresStr}`);
}
- console.log(`HOST ${hostStr}-${coresStr}`);
}
})();
From 3cd4cdf26fbeca92f3ae0b67fadc08fc71a2983a Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 2 Mar 2021 15:20:17 -0500
Subject: [PATCH 17/63] [Actions][Docs] Moving subaction and subaction params
back to README (#92878) (#93281)
* Moving subaction and subaction params back to README
* Apply suggestions from code review
Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
* PR fixes
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
Co-authored-by: ymao1
Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
---
docs/user/alerting/action-types/jira.asciidoc | 45 +----
.../alerting/action-types/resilient.asciidoc | 29 +--
.../alerting/action-types/servicenow.asciidoc | 33 +--
x-pack/plugins/actions/README.md | 190 +++++++++++++++---
4 files changed, 175 insertions(+), 122 deletions(-)
diff --git a/docs/user/alerting/action-types/jira.asciidoc b/docs/user/alerting/action-types/jira.asciidoc
index 0740cf7838b15a..d37f565c1739ba 100644
--- a/docs/user/alerting/action-types/jira.asciidoc
+++ b/docs/user/alerting/action-types/jira.asciidoc
@@ -65,44 +65,13 @@ API token (or password):: Jira API authentication token (or password) for HTTP
Jira actions have the following configuration properties:
-Subaction:: The subaction to perform: `pushToService`, `getIncident`, `issueTypes`, `fieldsByIssueType`, `issues`, `issue`, or `getFields`.
-Subaction params:: The parameters of the subaction.
-
-==== `pushToService` subaction configuration
-
-Incident:: A Jira incident has the following properties:
-* `summary` - The title of the issue.
-* `description` - A description of the issue.
-* `externalId` - The ID of the issue in Jira. If present, the issue is updated. Otherwise, a new issue is created.
-* `issueType` - The ID of the issue type in Jira.
-* `priority` - The priority level in Jira. Example: `Medium`.
-* `labels` - An array of labels. Labels cannot contain spaces.
-* `parent` - The parent issue ID or key. Only for subtask issue types.
-Comments:: A comment in the form of `{ commentId: string, version: string, comment: string }`.
-
-==== `getIncident` subaction configuration
-
-External ID:: The ID of the issue in Jira.
-
-==== `issueTypes` subaction configuration
-
-The `issueTypes` subaction has no parameters. Provide an empty object `{}`.
-
-==== `fieldsByIssueType` subaction configuration
-
-ID:: The ID of the issue in Jira.
-
-==== `issues` subaction configuration
-
-Title:: The title to search for.
-
-==== `issue` subaction configuration
-
-ID:: The ID of the issue in Jira.
-
-==== `getFields` subaction configuration
-
-The `getFields` subaction has no parameters. Provide an empty object `{}`.
+Issue type:: The type of the issue.
+Priority:: The priority of the incident.
+Labels:: The labels for the incident.
+Title:: A title for the issue, used for searching the contents of the knowledge base.
+Description:: The details about the incident.
+Parent:: The ID or key of the parent issue. Only for `Subtask` issue types.
+Additional comments:: Additional information for the client, such as how to troubleshoot the issue.
[[configuring-jira]]
==== Configuring and testing Jira
diff --git a/docs/user/alerting/action-types/resilient.asciidoc b/docs/user/alerting/action-types/resilient.asciidoc
index dfa95e2deec008..feca42a542a2f2 100644
--- a/docs/user/alerting/action-types/resilient.asciidoc
+++ b/docs/user/alerting/action-types/resilient.asciidoc
@@ -65,30 +65,11 @@ API key secret:: The authentication key secret for HTTP Basic authentication.
IBM Resilient actions have the following configuration properties:
-Subaction:: The subaction to perform: `pushToService`, `getFields`, `incidentTypes`, or `severity`.
-Subaction params:: The parameters of the subaction.
-
-==== `pushToService` subaction configuration
-
-Incident:: The IBM resilient incident has the following properties:
-* `name` - A name for the issue, used for searching the contents of the knowledge base.
-* `description` - The details about the incident.
-* `externalId` - The ID of the incident in IBM Resilient. If present, the incident is updated. Otherwise, a new incident is created.
-* `incidentTypes` - An array with the IDs of IBM Resilient incident types.
-* `severityCode` - The IBM Resilient ID of the severity code.
-Comments:: A comment in the form of `{ commentId: string, version: string, comment: string }`.
-
-===== `getFields` subaction configuration
-
-The `getFields` subaction has not parameters. Provide an empty object `{}`.
-
-===== `incidentTypes` subaction configuration
-
-The `incidentTypes` subaction has no parameters. Provide an empty object `{}`.
-
-===== `severity` subaction configuration
-
-The `severity` subaction has no parameters. Provide an empty object `{}`.
+Incident types:: The type of the incident.
+Severity code:: The severity of the incident.
+Name:: A name for the issue, used for searching the contents of the knowledge base.
+Description:: The details about the incident.
+Additional comments:: Additional information for the client, such as how to troubleshoot the issue.
[[configuring-resilient]]
==== Configuring and testing IBM Resilient
diff --git a/docs/user/alerting/action-types/servicenow.asciidoc b/docs/user/alerting/action-types/servicenow.asciidoc
index d1ee1b9357737d..4a11a2e28712a0 100644
--- a/docs/user/alerting/action-types/servicenow.asciidoc
+++ b/docs/user/alerting/action-types/servicenow.asciidoc
@@ -60,33 +60,12 @@ Password:: Password for HTTP Basic authentication.
ServiceNow actions have the following configuration properties:
-Subaction:: The subaction to perform: `pushToService`, `getFields`, `getIncident`, or `getChoices`.
-Subaction params:: The parameters of the subaction.
-
-==== `pushToService` subaction configuration
-
-Incident:: The ServiceNow incident has the following properties:
-* `short_description` - A short description for the incident, used for searching the contents of the knowledge base.
-* `description` - The details about the incident.
-* `externalId` - The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created.
-* `severity` - The severity of the incident.
-* `urgency` - The extent to which the incident resolution can delay.
-* `impact` - The effect an incident has on business. Can be measured by the number of affected users or by how critical it is to the business in question.
-* `category` - The name of the category in ServiceNow.
-* `subcategory` - The name of the subcategory in ServiceNow.
-Comments:: A comment in the form of `{ commentId: string, version: string, comment: string }`.
-
-===== `getFields` subaction configuration
-
-The `getFields` subaction has no parameters. Provide an empty object `{}`.
-
-===== `getIncident` subaction configuration
-
-External ID:: The ID of the incident in ServiceNow.
-
-===== `getChoices` subaction configuration
-
-Fields:: An array of fields. Example: `[priority, category, impact]`.
+Urgency:: The extent to which the incident resolution can delay.
+Severity:: The severity of the incident.
+Impact:: The effect an incident has on business. Can be measured by the number of affected users or by how critical it is to the business in question.
+Short description:: A short description for the incident, used for searching the contents of the knowledge base.
+Description:: The details about the incident.
+Additional comments:: Additional information for the client, such as how to troubleshoot the issue.
[[configuring-servicenow]]
==== Configuring and testing ServiceNow
diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md
index 78094f4c0eb0b1..f1be1ec12b79c5 100644
--- a/x-pack/plugins/actions/README.md
+++ b/x-pack/plugins/actions/README.md
@@ -33,42 +33,14 @@ Table of Contents
- [actionsClient.execute(options)](#actionsclientexecuteoptions)
- [Example](#example-2)
- [Built-in Action Types](#built-in-action-types)
- - [Server log](#server-log)
- - [`config`](#config)
- - [`secrets`](#secrets)
- - [`params`](#params)
- - [Email](#email)
- - [`config`](#config-1)
- - [`secrets`](#secrets-1)
- - [`params`](#params-1)
- - [Slack](#slack)
- - [`config`](#config-2)
- - [`secrets`](#secrets-2)
- - [`params`](#params-2)
- - [Index](#index)
- - [`config`](#config-3)
- - [`secrets`](#secrets-3)
- - [`params`](#params-3)
- - [Webhook](#webhook)
- - [`config`](#config-4)
- - [`secrets`](#secrets-4)
- - [`params`](#params-4)
- - [PagerDuty](#pagerduty)
- - [`config`](#config-5)
- - [`secrets`](#secrets-5)
- - [`params`](#params-5)
- [ServiceNow](#servicenow)
- - [`config`](#config-6)
- - [`secrets`](#secrets-6)
- - [`params`](#params-6)
+ - [`params`](#params)
- [`subActionParams (pushToService)`](#subactionparams-pushtoservice)
- [`subActionParams (getFields)`](#subactionparams-getfields)
- [`subActionParams (getIncident)`](#subactionparams-getincident)
- [`subActionParams (getChoices)`](#subactionparams-getchoices)
- [Jira](#jira)
- - [`config`](#config-7)
- - [`secrets`](#secrets-7)
- - [`params`](#params-7)
+ - [`params`](#params-1)
- [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1)
- [`subActionParams (getIncident)`](#subactionparams-getincident-1)
- [`subActionParams (issueTypes)`](#subactionparams-issuetypes)
@@ -77,9 +49,7 @@ Table of Contents
- [`subActionParams (issue)`](#subactionparams-issue)
- [`subActionParams (getFields)`](#subactionparams-getfields-1)
- [IBM Resilient](#ibm-resilient)
- - [`config`](#config-8)
- - [`secrets`](#secrets-8)
- - [`params`](#params-8)
+ - [`params`](#params-2)
- [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2)
- [`subActionParams (getFields)`](#subactionparams-getfields-2)
- [`subActionParams (incidentTypes)`](#subactionparams-incidenttypes)
@@ -272,6 +242,160 @@ const result = await actionsClient.execute({
Kibana ships with a set of built-in action types. See [Actions and connector types Documentation](https://www.elastic.co/guide/en/kibana/master/action-types.html).
+In addition to the documented configurations, several built in action type offer additional `params` configurations.
+
+## ServiceNow
+
+The [ServiceNow user documentation `params`](https://www.elastic.co/guide/en/kibana/master/servicenow-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available.
+### `params`
+
+| Property | Description | Type |
+| --------------- | -------------------------------------------------------------------------------------------------- | ------ |
+| subAction | The subaction to perform. It can be `pushToService`, `getFields`, `getIncident`, and `getChoices`. | string |
+| subActionParams | The parameters of the subaction. | object |
+
+#### `subActionParams (pushToService)`
+
+| Property | Description | Type |
+| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- |
+| incident | The ServiceNow incident. | object |
+| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ |
+
+The following table describes the properties of the `incident` object.
+
+| Property | Description | Type |
+| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------- |
+| short_description | The title of the incident. | string |
+| description | The description of the incident. | string _(optional)_ |
+| externalId | The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ |
+| severity | The severity in ServiceNow. | string _(optional)_ |
+| urgency | The urgency in ServiceNow. | string _(optional)_ |
+| impact | The impact in ServiceNow. | string _(optional)_ |
+| category | The category in ServiceNow. | string _(optional)_ |
+| subcategory | The subcategory in ServiceNow. | string _(optional)_ |
+
+#### `subActionParams (getFields)`
+
+No parameters for the `getFields` subaction. Provide an empty object `{}`.
+
+#### `subActionParams (getIncident)`
+
+| Property | Description | Type |
+| ---------- | ------------------------------------- | ------ |
+| externalId | The ID of the incident in ServiceNow. | string |
+
+
+#### `subActionParams (getChoices)`
+
+| Property | Description | Type |
+| -------- | ------------------------------------------------------------ | -------- |
+| fields | An array of fields. Example: `[priority, category, impact]`. | string[] |
+
+---
+
+## Jira
+
+The [Jira user documentation `params`](https://www.elastic.co/guide/en/kibana/master/jira-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available.
+### `params`
+
+| Property | Description | Type |
+| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------ |
+| subAction | The subaction to perform. It can be `pushToService`, `getIncident`, `issueTypes`, `fieldsByIssueType`, `issues`, `issue`, and `getFields`. | string |
+| subActionParams | The parameters of the subaction. | object |
+
+#### `subActionParams (pushToService)`
+
+| Property | Description | Type |
+| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- |
+| incident | The Jira incident. | object |
+| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ |
+
+The following table describes the properties of the `incident` object.
+
+| Property | Description | Type |
+| ----------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- |
+| summary | The title of the issue. | string |
+| description | The description of the issue. | string _(optional)_ |
+| externalId | The ID of the issue in Jira. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ |
+| issueType | The ID of the issue type in Jira. | string _(optional)_ |
+| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ |
+| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ |
+| parent | The ID or key of the parent issue. Only for `Sub-task` issue types. | string _(optional)_ |
+
+#### `subActionParams (getIncident)`
+
+| Property | Description | Type |
+| ---------- | --------------------------- | ------ |
+| externalId | The ID of the issue in Jira. | string |
+
+#### `subActionParams (issueTypes)`
+
+No parameters for the `issueTypes` subaction. Provide an empty object `{}`.
+
+#### `subActionParams (fieldsByIssueType)`
+
+| Property | Description | Type |
+| -------- | -------------------------------- | ------ |
+| id | The ID of the issue type in Jira. | string |
+
+#### `subActionParams (issues)`
+
+| Property | Description | Type |
+| -------- | ----------------------- | ------ |
+| title | The title to search for. | string |
+
+#### `subActionParams (issue)`
+
+| Property | Description | Type |
+| -------- | --------------------------- | ------ |
+| id | The ID of the issue in Jira. | string |
+
+#### `subActionParams (getFields)`
+
+No parameters for the `getFields` subaction. Provide an empty object `{}`.
+
+---
+## IBM Resilient
+
+The [IBM Resilient user documentation `params`](https://www.elastic.co/guide/en/kibana/master/resilient-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available.
+
+### `params`
+
+| Property | Description | Type |
+| --------------- | -------------------------------------------------------------------------------------------------- | ------ |
+| subAction | The subaction to perform. It can be `pushToService`, `getFields`, `incidentTypes`, and `severity. | string |
+| subActionParams | The parameters of the subaction. | object |
+
+#### `subActionParams (pushToService)`
+
+| Property | Description | Type |
+| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- |
+| incident | The IBM Resilient incident. | object |
+| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ |
+
+The following table describes the properties of the `incident` object.
+
+| Property | Description | Type |
+| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- |
+| name | The title of the incident. | string _(optional)_ |
+| description | The description of the incident. | string _(optional)_ |
+| externalId | The ID of the incident in IBM Resilient. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ |
+| incidentTypes | An array with the IDs of IBM Resilient incident types. | number[] _(optional)_ |
+| severityCode | IBM Resilient ID of the severity code. | number _(optional)_ |
+
+#### `subActionParams (getFields)`
+
+No parameters for the `getFields` subaction. Provide an empty object `{}`.
+
+#### `subActionParams (incidentTypes)`
+
+No parameters for the `incidentTypes` subaction. Provide an empty object `{}`.
+
+#### `subActionParams (severity)`
+
+No parameters for the `severity` subaction. Provide an empty object `{}`.
+
+---
# Command Line Utility
The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command:
From 26fa8d4a5a370dc0dcd187f990ad47273212a411 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Tue, 2 Mar 2021 15:34:19 -0500
Subject: [PATCH 18/63] Different screenshot (#92970) (#93309)
Co-authored-by: ymao1
---
.../alert-flyout-alert-type-selection.png | Bin 255161 -> 166951 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/docs/user/alerting/images/alert-flyout-alert-type-selection.png b/docs/user/alerting/images/alert-flyout-alert-type-selection.png
index 4e5c49975160f3e810c22b0140512b04880aff18..6ebbe4213ba7d30b06bd3f36c372d88129272f6b 100644
GIT binary patch
literal 166951
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