@@ -104,8 +100,6 @@ const PreviewRenderer = ({
);
};
-const DebouncedPreviewRenderer = debouncedComponent(PreviewRenderer, 2000);
-
const SuggestionPreview = ({
preview,
ExpressionRenderer: ExpressionRendererComponent,
@@ -126,7 +120,7 @@ const SuggestionPreview = ({
return (
-
{preview.expression ? (
- {preview.title}
)}
-
+
);
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
index 3e05d4ddfbc20..9dc59eacd40d3 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
@@ -172,6 +172,7 @@ describe('embeddable', () => {
timeRange,
query,
filters,
+ searchSessionId: 'searchSessionId',
});
expect(expressionRenderer).toHaveBeenCalledTimes(2);
@@ -182,7 +183,13 @@ describe('embeddable', () => {
const query: Query = { language: 'kquery', query: '' };
const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }];
- const input = { savedObjectId: '123', timeRange, query, filters } as LensEmbeddableInput;
+ const input = {
+ savedObjectId: '123',
+ timeRange,
+ query,
+ filters,
+ searchSessionId: 'searchSessionId',
+ } as LensEmbeddableInput;
const embeddable = new Embeddable(
{
@@ -214,6 +221,8 @@ describe('embeddable', () => {
filters,
})
);
+
+ expect(expressionRenderer.mock.calls[0][0].searchSessionId).toBe(input.searchSessionId);
});
it('should merge external context with query and filters of the saved object', async () => {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx
index d245b7f2fcde4..10c243a272138 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx
@@ -177,6 +177,7 @@ export class Embeddable
ExpressionRenderer={this.expressionRenderer}
expression={this.expression || null}
searchContext={this.getMergedSearchContext()}
+ searchSessionId={this.input.searchSessionId}
handleEvent={this.handleEvent}
/>,
domNode
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx
index 4fb0630a305e7..13376e56e2144 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx
@@ -19,6 +19,7 @@ export interface ExpressionWrapperProps {
ExpressionRenderer: ReactExpressionRendererType;
expression: string | null;
searchContext: ExecutionContextSearch;
+ searchSessionId?: string;
handleEvent: (event: ExpressionRendererEvent) => void;
}
@@ -27,6 +28,7 @@ export function ExpressionWrapper({
expression,
searchContext,
handleEvent,
+ searchSessionId,
}: ExpressionWrapperProps) {
return (
@@ -51,6 +53,7 @@ export function ExpressionWrapper({
padding="m"
expression={expression}
searchContext={searchContext}
+ searchSessionId={searchSessionId}
renderError={(errorMessage, error) => (
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
index 900cd02622aaf..77dc6f97fb236 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
@@ -319,10 +319,10 @@ describe('IndexPattern Data Source', () => {
"1",
],
"metricsAtAllLevels": Array [
- true,
+ false,
],
"partialRows": Array [
- true,
+ false,
],
"timeFields": Array [
"timestamp",
@@ -334,7 +334,7 @@ describe('IndexPattern Data Source', () => {
Object {
"arguments": Object {
"idMap": Array [
- "{\\"col--1-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-2-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}",
+ "{\\"col-0-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}",
],
},
"function": "lens_rename_columns",
@@ -392,6 +392,58 @@ describe('IndexPattern Data Source', () => {
expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']);
});
+ it('should rename the output from esaggs when using flat query', () => {
+ const queryBaseState: IndexPatternBaseState = {
+ currentIndexPatternId: '1',
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: ['bucket1', 'bucket2', 'metric'],
+ columns: {
+ metric: {
+ label: 'Count of records',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'Records',
+ operationType: 'count',
+ },
+ bucket1: {
+ label: 'Date',
+ dataType: 'date',
+ isBucketed: true,
+ operationType: 'date_histogram',
+ sourceField: 'timestamp',
+ params: {
+ interval: '1d',
+ },
+ },
+ bucket2: {
+ label: 'Terms',
+ dataType: 'string',
+ isBucketed: true,
+ operationType: 'terms',
+ sourceField: 'geo.src',
+ params: {
+ orderBy: { type: 'alphabetical' },
+ orderDirection: 'asc',
+ size: 10,
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const state = enrichBaseState(queryBaseState);
+ const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
+ expect(ast.chain[0].arguments.metricsAtAllLevels).toEqual([false]);
+ expect(JSON.parse(ast.chain[1].arguments.idMap[0] as string)).toEqual({
+ 'col-0-bucket1': expect.any(Object),
+ 'col-1-bucket2': expect.any(Object),
+ 'col-2-metric': expect.any(Object),
+ });
+ });
+
it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts
index e2c4323b56c2a..ea7aa62054e5c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts
@@ -29,34 +29,16 @@ function getExpressionForLayer(
}
const columnEntries = columnOrder.map((colId) => [colId, columns[colId]] as const);
- const bucketsCount = columnEntries.filter(([, entry]) => entry.isBucketed).length;
- const metricsCount = columnEntries.length - bucketsCount;
if (columnEntries.length) {
const aggs = columnEntries.map(([colId, col]) => {
return getEsAggsConfig(col, colId);
});
- /**
- * Because we are turning on metrics at all levels, the sequence generation
- * logic here is more complicated. Examples follow:
- *
- * Example 1: [Count]
- * Output: [`col-0-count`]
- *
- * Example 2: [Terms, Terms, Count]
- * Output: [`col-0-terms0`, `col-2-terms1`, `col-3-count`]
- *
- * Example 3: [Terms, Terms, Count, Max]
- * Output: [`col-0-terms0`, `col-3-terms1`, `col-4-count`, `col-5-max`]
- */
const idMap = columnEntries.reduce((currentIdMap, [colId, column], index) => {
- const newIndex = column.isBucketed
- ? index * (metricsCount + 1) // Buckets are spaced apart by N + 1
- : (index ? index + 1 : 0) - bucketsCount + (bucketsCount - 1) * (metricsCount + 1);
return {
...currentIdMap,
- [`col-${columnEntries.length === 1 ? 0 : newIndex}-${colId}`]: {
+ [`col-${columnEntries.length === 1 ? 0 : index}-${colId}`]: {
...column,
id: colId,
},
@@ -122,8 +104,8 @@ function getExpressionForLayer(
function: 'esaggs',
arguments: {
index: [indexPattern.id],
- metricsAtAllLevels: [true],
- partialRows: [true],
+ metricsAtAllLevels: [false],
+ partialRows: [false],
includeFormatHints: [true],
timeFields: allDateHistogramFields,
aggConfigs: [JSON.stringify(aggs)],
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
index cb2458a76967c..d4c85ce9b8843 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
@@ -27,8 +27,8 @@ import {
import { FormatFactory, LensFilterEvent } from '../types';
import { VisualizationContainer } from '../visualization_container';
import { CHART_NAMES, DEFAULT_PERCENT_DECIMALS } from './constants';
-import { ColumnGroups, PieExpressionProps } from './types';
-import { getSliceValueWithFallback, getFilterContext } from './render_helpers';
+import { PieExpressionProps } from './types';
+import { getSliceValue, getFilterContext } from './render_helpers';
import { EmptyPlaceholder } from '../shared_components';
import './visualization.scss';
import { desanitizeFilterContext } from '../utils';
@@ -72,21 +72,6 @@ export function PieComponent(
});
}
- // The datatable for pie charts should include subtotals, like this:
- // [bucket, subtotal, bucket, count]
- // But the user only configured [bucket, bucket, count]
- const columnGroups: ColumnGroups = [];
- firstTable.columns.forEach((col) => {
- if (groups.includes(col.id)) {
- columnGroups.push({
- col,
- metrics: [],
- });
- } else if (columnGroups.length > 0) {
- columnGroups[columnGroups.length - 1].metrics.push(col);
- }
- });
-
const fillLabel: Partial = {
textInvertible: false,
valueFont: {
@@ -100,7 +85,9 @@ export function PieComponent(
fillLabel.valueFormatter = () => '';
}
- const layers: PartitionLayer[] = columnGroups.map(({ col }, layerIndex) => {
+ const bucketColumns = firstTable.columns.filter((col) => groups.includes(col.id));
+
+ const layers: PartitionLayer[] = bucketColumns.map((col, layerIndex) => {
return {
groupByRollup: (d: Datum) => d[col.id] ?? EMPTY_SLICE,
showAccessor: (d: Datum) => d !== EMPTY_SLICE,
@@ -116,7 +103,7 @@ export function PieComponent(
fillLabel:
isDarkMode &&
shape === 'treemap' &&
- layerIndex < columnGroups.length - 1 &&
+ layerIndex < bucketColumns.length - 1 &&
categoryDisplay !== 'hide'
? { ...fillLabel, textColor: euiDarkVars.euiTextColor }
: fillLabel,
@@ -136,10 +123,10 @@ export function PieComponent(
if (shape === 'treemap') {
// Only highlight the innermost color of the treemap, as it accurately represents area
- return layerIndex < columnGroups.length - 1 ? 'rgba(0,0,0,0)' : outputColor;
+ return layerIndex < bucketColumns.length - 1 ? 'rgba(0,0,0,0)' : outputColor;
}
- const lighten = (d.depth - 1) / (columnGroups.length * 2);
+ const lighten = (d.depth - 1) / (bucketColumns.length * 2);
return color(outputColor, 'hsl').lighten(lighten).hex();
},
},
@@ -198,8 +185,6 @@ export function PieComponent(
setState({ isReady: true });
}, []);
- const reverseGroups = [...columnGroups].reverse();
-
const hasNegative = firstTable.rows.some((row) => {
const value = row[metricColumn.id];
return typeof value === 'number' && value < 0;
@@ -243,16 +228,12 @@ export function PieComponent(
showLegend={
!hideLabels &&
(legendDisplay === 'show' ||
- (legendDisplay === 'default' && columnGroups.length > 1 && shape !== 'treemap'))
+ (legendDisplay === 'default' && bucketColumns.length > 1 && shape !== 'treemap'))
}
legendPosition={legendPosition || Position.Right}
legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */}
onElementClick={(args) => {
- const context = getFilterContext(
- args[0][0] as LayerValue[],
- columnGroups.map(({ col }) => col.id),
- firstTable
- );
+ const context = getFilterContext(args[0][0] as LayerValue[], groups, firstTable);
onClickValue(desanitizeFilterContext(context));
}}
@@ -262,7 +243,7 @@ export function PieComponent(
getSliceValueWithFallback(d, reverseGroups, metricColumn)}
+ valueAccessor={(d: Datum) => getSliceValue(d, metricColumn)}
percentFormatter={(d: number) => percentFormatter.convert(d / 100)}
valueGetter={hideLabels || numberDisplay === 'value' ? undefined : 'percent'}
valueFormatter={(d: number) => (hideLabels ? '' : formatters[metricColumn.id].convert(d))}
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts
index d9ccda2a99ab2..22c63cd67281b 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts
+++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts
@@ -5,86 +5,47 @@
*/
import { Datatable } from 'src/plugins/expressions/public';
-import { getSliceValueWithFallback, getFilterContext } from './render_helpers';
-import { ColumnGroups } from './types';
+import { getSliceValue, getFilterContext } from './render_helpers';
describe('render helpers', () => {
- describe('#getSliceValueWithFallback', () => {
- describe('without fallback', () => {
- const columnGroups: ColumnGroups = [
- { col: { id: 'a', name: 'A', meta: { type: 'string' } }, metrics: [] },
- { col: { id: 'b', name: 'C', meta: { type: 'string' } }, metrics: [] },
- ];
-
- it('returns the metric when positive number', () => {
- expect(
- getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: 5 }, columnGroups, {
+ describe('#getSliceValue', () => {
+ it('returns the metric when positive number', () => {
+ expect(
+ getSliceValue(
+ { a: 'Cat', b: 'Home', c: 5 },
+ {
id: 'c',
name: 'C',
meta: { type: 'number' },
- })
- ).toEqual(5);
- });
+ }
+ )
+ ).toEqual(5);
+ });
- it('returns the metric when negative number', () => {
- expect(
- getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: -100 }, columnGroups, {
+ it('returns the metric when negative number', () => {
+ expect(
+ getSliceValue(
+ { a: 'Cat', b: 'Home', c: -100 },
+ {
id: 'c',
name: 'C',
meta: { type: 'number' },
- })
- ).toEqual(-100);
- });
+ }
+ )
+ ).toEqual(-100);
+ });
- it('returns epsilon when metric is 0 without fallback', () => {
- expect(
- getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: 0 }, columnGroups, {
+ it('returns epsilon when metric is 0 without fallback', () => {
+ expect(
+ getSliceValue(
+ { a: 'Cat', b: 'Home', c: 0 },
+ {
id: 'c',
name: 'C',
meta: { type: 'number' },
- })
- ).toEqual(Number.EPSILON);
- });
- });
-
- describe('fallback behavior', () => {
- const columnGroups: ColumnGroups = [
- {
- col: { id: 'a', name: 'A', meta: { type: 'string' } },
- metrics: [{ id: 'a_subtotal', name: '', meta: { type: 'number' } }],
- },
- { col: { id: 'b', name: 'C', meta: { type: 'string' } }, metrics: [] },
- ];
-
- it('falls back to metric from previous column if available', () => {
- expect(
- getSliceValueWithFallback(
- { a: 'Cat', a_subtotal: 5, b: 'Home', c: undefined },
- columnGroups,
- { id: 'c', name: 'C', meta: { type: 'number' } }
- )
- ).toEqual(5);
- });
-
- it('uses epsilon if fallback is 0', () => {
- expect(
- getSliceValueWithFallback(
- { a: 'Cat', a_subtotal: 0, b: 'Home', c: undefined },
- columnGroups,
- { id: 'c', name: 'C', meta: { type: 'number' } }
- )
- ).toEqual(Number.EPSILON);
- });
-
- it('uses epsilon if fallback is missing', () => {
- expect(
- getSliceValueWithFallback(
- { a: 'Cat', a_subtotal: undefined, b: 'Home', c: undefined },
- columnGroups,
- { id: 'c', name: 'C', meta: { type: 'number' } }
- )
- ).toEqual(Number.EPSILON);
- });
+ }
+ )
+ ).toEqual(Number.EPSILON);
});
});
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts
index 26b4f9ccda853..978afcca6a550 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts
+++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts
@@ -6,22 +6,13 @@
import { Datum, LayerValue } from '@elastic/charts';
import { Datatable, DatatableColumn } from 'src/plugins/expressions/public';
-import { ColumnGroups } from './types';
import { LensFilterEvent } from '../types';
-export function getSliceValueWithFallback(
- d: Datum,
- reverseGroups: ColumnGroups,
- metricColumn: DatatableColumn
-) {
+export function getSliceValue(d: Datum, metricColumn: DatatableColumn) {
if (typeof d[metricColumn.id] === 'number' && d[metricColumn.id] !== 0) {
return d[metricColumn.id];
}
- // Sometimes there is missing data for outer groups
- // When there is missing data, we fall back to the next groups
- // This creates a sunburst effect
- const hasMetric = reverseGroups.find((group) => group.metrics.length && d[group.metrics[0].id]);
- return hasMetric ? d[hasMetric.metrics[0].id] || Number.EPSILON : Number.EPSILON;
+ return Number.EPSILON;
}
export function getFilterContext(
diff --git a/x-pack/plugins/lens/public/pie_visualization/types.ts b/x-pack/plugins/lens/public/pie_visualization/types.ts
index 0596e54870a94..54bececa13c2a 100644
--- a/x-pack/plugins/lens/public/pie_visualization/types.ts
+++ b/x-pack/plugins/lens/public/pie_visualization/types.ts
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { DatatableColumn } from 'src/plugins/expressions/public';
import { LensMultiTable } from '../types';
export interface SharedLayerState {
@@ -38,8 +37,3 @@ export interface PieExpressionProps {
data: LensMultiTable;
args: PieExpressionArgs;
}
-
-export type ColumnGroups = Array<{
- col: DatatableColumn;
- metrics: DatatableColumn[];
-}>;
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts
index 8076733be2d7d..0b708133d947b 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts
@@ -411,7 +411,12 @@ describe('Detections Rules API', () => {
describe('createPrepackagedRules', () => {
beforeEach(() => {
fetchMock.mockClear();
- fetchMock.mockResolvedValue('unknown');
+ fetchMock.mockResolvedValue({
+ rules_installed: 0,
+ rules_updated: 0,
+ timelines_installed: 0,
+ timelines_updated: 0,
+ });
});
test('check parameter url when creating pre-packaged rules', async () => {
@@ -423,7 +428,12 @@ describe('Detections Rules API', () => {
});
test('happy path', async () => {
const resp = await createPrepackagedRules({ signal: abortCtrl.signal });
- expect(resp).toEqual(true);
+ expect(resp).toEqual({
+ rules_installed: 0,
+ rules_updated: 0,
+ timelines_installed: 0,
+ timelines_updated: 0,
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts
index 23adfe0228333..ce1fdd18dbdef 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts
@@ -245,13 +245,25 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => {
- await KibanaServices.get().http.fetch(DETECTION_ENGINE_PREPACKAGED_URL, {
+export const createPrepackagedRules = async ({
+ signal,
+}: BasicFetchProps): Promise<{
+ rules_installed: number;
+ rules_updated: number;
+ timelines_installed: number;
+ timelines_updated: number;
+}> => {
+ const result = await KibanaServices.get().http.fetch<{
+ rules_installed: number;
+ rules_updated: number;
+ timelines_installed: number;
+ timelines_updated: number;
+ }>(DETECTION_ENGINE_PREPACKAGED_URL, {
method: 'PUT',
signal,
});
- return true;
+ return result;
};
/**
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts
index 721790a36b27f..6e2aee9658658 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts
@@ -30,7 +30,21 @@ export const RULE_AND_TIMELINE_PREPACKAGED_FAILURE = i18n.translate(
export const RULE_AND_TIMELINE_PREPACKAGED_SUCCESS = i18n.translate(
'xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleAndTimelineSuccesDescription',
{
- defaultMessage: 'Installed pre-packaged rules and timelines from elastic',
+ defaultMessage: 'Installed pre-packaged rules and timeline templates from elastic',
+ }
+);
+
+export const RULE_PREPACKAGED_SUCCESS = i18n.translate(
+ 'xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleSuccesDescription',
+ {
+ defaultMessage: 'Installed pre-packaged rules from elastic',
+ }
+);
+
+export const TIMELINE_PREPACKAGED_SUCCESS = i18n.translate(
+ 'xpack.securitySolution.containers.detectionEngine.createPrePackagedTimelineSuccesDescription',
+ {
+ defaultMessage: 'Installed pre-packaged timeline templates from elastic',
}
);
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx
index 7f74e92584494..f6bd8c4359d6e 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx
@@ -3,12 +3,17 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
+import { ReactElement } from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { ReturnPrePackagedRulesAndTimelines, usePrePackagedRules } from './use_pre_packaged_rules';
import * as api from './api';
+import { shallow } from 'enzyme';
+import * as i18n from './translations';
-jest.mock('./api');
+jest.mock('./api', () => ({
+ getPrePackagedRulesStatus: jest.fn(),
+ createPrepackagedRules: jest.fn(),
+}));
describe('usePrePackagedRules', () => {
beforeEach(() => {
@@ -52,6 +57,21 @@ describe('usePrePackagedRules', () => {
});
test('fetch getPrePackagedRulesStatus', async () => {
+ (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_custom_installed: 33,
+ rules_installed: 12,
+ rules_not_installed: 0,
+ rules_not_updated: 0,
+ timelines_installed: 0,
+ timelines_not_installed: 0,
+ timelines_not_updated: 0,
+ });
+ (api.createPrepackagedRules as jest.Mock).mockResolvedValue({
+ rules_installed: 0,
+ rules_updated: 0,
+ timelines_installed: 0,
+ timelines_updated: 0,
+ });
await act(async () => {
const { result, waitForNextUpdate } = renderHook(
() =>
@@ -87,7 +107,6 @@ describe('usePrePackagedRules', () => {
});
test('happy path to createPrePackagedRules', async () => {
- const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules');
await act(async () => {
const { result, waitForNextUpdate } = renderHook(
() =>
@@ -106,7 +125,7 @@ describe('usePrePackagedRules', () => {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(true);
- expect(spyOnCreatePrepackagedRules).toHaveBeenCalled();
+ expect(api.createPrepackagedRules).toHaveBeenCalled();
expect(result.current).toEqual({
getLoadPrebuiltRulesAndTemplatesButton:
result.current.getLoadPrebuiltRulesAndTemplatesButton,
@@ -127,6 +146,253 @@ describe('usePrePackagedRules', () => {
});
});
+ test('getLoadPrebuiltRulesAndTemplatesButton - LOAD_PREPACKAGED_RULES', async () => {
+ (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_custom_installed: 0,
+ rules_installed: 0,
+ rules_not_installed: 1,
+ rules_not_updated: 0,
+ timelines_installed: 0,
+ timelines_not_installed: 0,
+ timelines_not_updated: 0,
+ });
+ (api.createPrepackagedRules as jest.Mock).mockResolvedValue({
+ rules_installed: 0,
+ rules_updated: 0,
+ timelines_installed: 0,
+ timelines_updated: 0,
+ });
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ usePrePackagedRules({
+ canUserCRUD: true,
+ hasIndexWrite: true,
+ isAuthenticated: true,
+ hasEncryptionKey: true,
+ isSignalIndexExists: true,
+ })
+ );
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ const button = result.current.getLoadPrebuiltRulesAndTemplatesButton({
+ isDisabled: false,
+ onClick: jest.fn(),
+ 'data-test-subj': 'button',
+ });
+ const wrapper = shallow(button as ReactElement);
+ expect(wrapper.find('[data-test-subj="button"]').text()).toEqual(i18n.LOAD_PREPACKAGED_RULES);
+ });
+ });
+
+ test('getLoadPrebuiltRulesAndTemplatesButton - LOAD_PREPACKAGED_TIMELINE_TEMPLATES', async () => {
+ (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_custom_installed: 0,
+ rules_installed: 0,
+ rules_not_installed: 0,
+ rules_not_updated: 0,
+ timelines_installed: 0,
+ timelines_not_installed: 1,
+ timelines_not_updated: 0,
+ });
+ (api.createPrepackagedRules as jest.Mock).mockResolvedValue({
+ rules_installed: 0,
+ rules_updated: 0,
+ timelines_installed: 0,
+ timelines_updated: 0,
+ });
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ usePrePackagedRules({
+ canUserCRUD: true,
+ hasIndexWrite: true,
+ isAuthenticated: true,
+ hasEncryptionKey: true,
+ isSignalIndexExists: true,
+ })
+ );
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ const button = result.current.getLoadPrebuiltRulesAndTemplatesButton({
+ isDisabled: false,
+ onClick: jest.fn(),
+ 'data-test-subj': 'button',
+ });
+ const wrapper = shallow(button as ReactElement);
+ expect(wrapper.find('[data-test-subj="button"]').text()).toEqual(
+ i18n.LOAD_PREPACKAGED_TIMELINE_TEMPLATES
+ );
+ });
+ });
+
+ test('getLoadPrebuiltRulesAndTemplatesButton - LOAD_PREPACKAGED_RULES_AND_TEMPLATES', async () => {
+ (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_custom_installed: 0,
+ rules_installed: 0,
+ rules_not_installed: 1,
+ rules_not_updated: 0,
+ timelines_installed: 0,
+ timelines_not_installed: 1,
+ timelines_not_updated: 0,
+ });
+ (api.createPrepackagedRules as jest.Mock).mockResolvedValue({
+ rules_installed: 0,
+ rules_updated: 0,
+ timelines_installed: 0,
+ timelines_updated: 0,
+ });
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ usePrePackagedRules({
+ canUserCRUD: true,
+ hasIndexWrite: true,
+ isAuthenticated: true,
+ hasEncryptionKey: true,
+ isSignalIndexExists: true,
+ })
+ );
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ const button = result.current.getLoadPrebuiltRulesAndTemplatesButton({
+ isDisabled: false,
+ onClick: jest.fn(),
+ 'data-test-subj': 'button',
+ });
+ const wrapper = shallow(button as ReactElement);
+ expect(wrapper.find('[data-test-subj="button"]').text()).toEqual(
+ i18n.LOAD_PREPACKAGED_RULES_AND_TEMPLATES
+ );
+ });
+ });
+
+ test('getReloadPrebuiltRulesAndTemplatesButton - missing rules and templates', async () => {
+ (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_custom_installed: 0,
+ rules_installed: 1,
+ rules_not_installed: 1,
+ rules_not_updated: 0,
+ timelines_installed: 0,
+ timelines_not_installed: 1,
+ timelines_not_updated: 0,
+ });
+ (api.createPrepackagedRules as jest.Mock).mockResolvedValue({
+ rules_installed: 0,
+ rules_updated: 0,
+ timelines_installed: 0,
+ timelines_updated: 0,
+ });
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ usePrePackagedRules({
+ canUserCRUD: true,
+ hasIndexWrite: true,
+ isAuthenticated: true,
+ hasEncryptionKey: true,
+ isSignalIndexExists: true,
+ })
+ );
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ const button = result.current.getReloadPrebuiltRulesAndTemplatesButton({
+ isDisabled: false,
+ onClick: jest.fn(),
+ });
+ const wrapper = shallow(button as ReactElement);
+ expect(wrapper.find('[data-test-subj="reloadPrebuiltRulesBtn"]').text()).toEqual(
+ 'Install 1 Elastic prebuilt rule and 1 Elastic prebuilt timeline '
+ );
+ });
+ });
+
+ test('getReloadPrebuiltRulesAndTemplatesButton - missing rules', async () => {
+ (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_custom_installed: 0,
+ rules_installed: 1,
+ rules_not_installed: 1,
+ rules_not_updated: 0,
+ timelines_installed: 0,
+ timelines_not_installed: 0,
+ timelines_not_updated: 0,
+ });
+ (api.createPrepackagedRules as jest.Mock).mockResolvedValue({
+ rules_installed: 0,
+ rules_updated: 0,
+ timelines_installed: 0,
+ timelines_updated: 0,
+ });
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ usePrePackagedRules({
+ canUserCRUD: true,
+ hasIndexWrite: true,
+ isAuthenticated: true,
+ hasEncryptionKey: true,
+ isSignalIndexExists: true,
+ })
+ );
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ const button = result.current.getReloadPrebuiltRulesAndTemplatesButton({
+ isDisabled: false,
+ onClick: jest.fn(),
+ });
+ const wrapper = shallow(button as ReactElement);
+ expect(wrapper.find('[data-test-subj="reloadPrebuiltRulesBtn"]').text()).toEqual(
+ 'Install 1 Elastic prebuilt rule '
+ );
+ });
+ });
+
+ test('getReloadPrebuiltRulesAndTemplatesButton - missing templates', async () => {
+ (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_custom_installed: 0,
+ rules_installed: 1,
+ rules_not_installed: 0,
+ rules_not_updated: 0,
+ timelines_installed: 1,
+ timelines_not_installed: 1,
+ timelines_not_updated: 0,
+ });
+ (api.createPrepackagedRules as jest.Mock).mockResolvedValue({
+ rules_installed: 0,
+ rules_updated: 0,
+ timelines_installed: 0,
+ timelines_updated: 0,
+ });
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ usePrePackagedRules({
+ canUserCRUD: true,
+ hasIndexWrite: true,
+ isAuthenticated: true,
+ hasEncryptionKey: true,
+ isSignalIndexExists: true,
+ })
+ );
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ const button = result.current.getReloadPrebuiltRulesAndTemplatesButton({
+ isDisabled: false,
+ onClick: jest.fn(),
+ });
+ const wrapper = shallow(button as ReactElement);
+ expect(wrapper.find('[data-test-subj="reloadPrebuiltRulesBtn"]').text()).toEqual(
+ 'Install 1 Elastic prebuilt timeline '
+ );
+ });
+ });
+
test('unhappy path to createPrePackagedRules', async () => {
const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules');
spyOnCreatePrepackagedRules.mockImplementation(() => {
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx
index 4d19f44bcfc84..48530ddeb181e 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx
@@ -114,6 +114,27 @@ export const usePrePackagedRules = ({
const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false);
const [loading, setLoading] = useState(true);
const [, dispatchToaster] = useStateToaster();
+ const getSuccessToastMessage = (result: {
+ rules_installed: number;
+ rules_updated: number;
+ timelines_installed: number;
+ timelines_updated: number;
+ }) => {
+ const {
+ rules_installed: rulesInstalled,
+ rules_updated: rulesUpdated,
+ timelines_installed: timelinesInstalled,
+ timelines_updated: timelinesUpdated,
+ } = result;
+ if (rulesInstalled === 0 && (timelinesInstalled > 0 || timelinesUpdated > 0)) {
+ return i18n.TIMELINE_PREPACKAGED_SUCCESS;
+ } else if ((rulesInstalled > 0 || rulesUpdated > 0) && timelinesInstalled === 0) {
+ return i18n.RULE_PREPACKAGED_SUCCESS;
+ } else {
+ return i18n.RULE_AND_TIMELINE_PREPACKAGED_SUCCESS;
+ }
+ };
+
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
@@ -170,7 +191,7 @@ export const usePrePackagedRules = ({
isSignalIndexExists
) {
setLoadingCreatePrePackagedRules(true);
- await createPrepackagedRules({
+ const result = await createPrepackagedRules({
signal: abortCtrl.signal,
});
@@ -209,11 +230,7 @@ export const usePrePackagedRules = ({
timelinesNotInstalled: prePackagedRuleStatusResponse.timelines_not_installed,
timelinesNotUpdated: prePackagedRuleStatusResponse.timelines_not_updated,
});
-
- displaySuccessToast(
- i18n.RULE_AND_TIMELINE_PREPACKAGED_SUCCESS,
- dispatchToaster
- );
+ displaySuccessToast(getSuccessToastMessage(result), dispatchToaster);
stopTimeOut();
resolve(true);
} else {
@@ -277,8 +294,9 @@ export const usePrePackagedRules = ({
);
const getLoadPrebuiltRulesAndTemplatesButton = useCallback(
({ isDisabled, onClick, fill, 'data-test-subj': dataTestSubj = 'loadPrebuiltRulesBtn' }) => {
- return prePackagedRuleStatus === 'ruleNotInstalled' ||
- prePackagedTimelineStatus === 'timelinesNotInstalled' ? (
+ return (prePackagedRuleStatus === 'ruleNotInstalled' ||
+ prePackagedTimelineStatus === 'timelinesNotInstalled') &&
+ prePackagedRuleStatus !== 'someRuleUninstall' ? (
{
}
}
/>
+
`);
});
diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx
index 2ffb3f0feb4dd..9a3e045017f9a 100644
--- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx
@@ -79,6 +79,7 @@ export const ExecutedJourney: FC
= ({ journey }) => (
{journey.steps.filter(isStepEnd).map((step, index) => (
))}
+
);
diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx
index 3c26ba12eea65..5966851973af2 100644
--- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx
@@ -41,7 +41,7 @@ export const ExecutedStep: FC = ({ step, index }) => (
-
+
@@ -87,6 +87,5 @@ export const ExecutedStep: FC = ({ step, index }) => (
-