new AttributeService(type, core.savedObjects.client),
filtersFromContext,
filtersAndTimeRangeFromContext,
getStateTransfer: (history?: ScopedHistory) => {
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap
index 6a2fd1000e6b4..0c89fb494b6ac 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap
@@ -49,7 +49,7 @@ exports[`Header should render a different name, prompt, and beta tag if provided
>
multiple
- data souces,
+ data sources,
multiple
- data souces,
+ data sources,
multiple
- data souces,
+ data sources,
multiple,
single: filebeat-4-3-22 ,
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js b/src/plugins/vis_type_vislib/public/vislib/lib/chart_title.test.js
similarity index 73%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js
rename to src/plugins/vis_type_vislib/public/vislib/lib/chart_title.test.js
index 6790c49691dfd..d8d5087f8c380 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js
+++ b/src/plugins/vis_type_vislib/public/vislib/lib/chart_title.test.js
@@ -19,11 +19,15 @@
import d3 from 'd3';
import _ from 'lodash';
-import expect from '@kbn/expect';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../test_utils/public';
-import { ChartTitle } from '../../../../../../../plugins/vis_type_vislib/public/vislib/lib/chart_title';
-import { VisConfig } from '../../../../../../../plugins/vis_type_vislib/public/vislib/lib/vis_config';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
+import { ChartTitle } from './chart_title';
+import { VisConfig } from './vis_config';
+import { getMockUiState } from '../../fixtures/mocks';
describe('Vislib ChartTitle Class Test Suite', function () {
let mockUiState;
@@ -88,6 +92,16 @@ describe('Vislib ChartTitle Class Test Suite', function () {
yAxisLabel: 'Count',
};
+ let mockedHTMLElementClientSizes;
+ let mockedSVGElementGetBBox;
+ let mockedSVGElementGetComputedTextLength;
+
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
mockUiState = getMockUiState();
el = d3.select('body').append('div').attr('class', 'visWrapper').datum(data);
@@ -113,23 +127,29 @@ describe('Vislib ChartTitle Class Test Suite', function () {
el.remove();
});
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('render Method', function () {
beforeEach(function () {
chartTitle.render();
});
- it('should append an svg to div', function () {
- expect(el.select('.chart-title').selectAll('svg').length).to.be(1);
+ test('should append an svg to div', function () {
+ expect(el.select('.chart-title').selectAll('svg').length).toBe(1);
});
- it('should append text', function () {
- expect(!!el.select('.chart-title').selectAll('svg').selectAll('text')).to.be(true);
+ test('should append text', function () {
+ expect(!!el.select('.chart-title').selectAll('svg').selectAll('text')).toBe(true);
});
});
describe('draw Method', function () {
- it('should be a function', function () {
- expect(_.isFunction(chartTitle.draw())).to.be(true);
+ test('should be a function', function () {
+ expect(_.isFunction(chartTitle.draw())).toBe(true);
});
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.test.js
similarity index 67%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js
rename to src/plugins/vis_type_vislib/public/vislib/lib/dispatch.test.js
index 20281d8479ab4..9c714af4d8434 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js
+++ b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.test.js
@@ -19,13 +19,21 @@
import _ from 'lodash';
import d3 from 'd3';
-import expect from '@kbn/expect';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../test_utils/public';
// Data
-import data from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
+import data from '../../fixtures/mock_data/date_histogram/_series';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { getVis } from '../_vis_fixture';
+import { getMockUiState } from '../../fixtures/mocks';
+import { getVis } from '../visualizations/_vis_fixture';
+
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
describe('Vislib Dispatch Class Test Suite', function () {
function destroyVis(vis) {
@@ -36,6 +44,18 @@ describe('Vislib Dispatch Class Test Suite', function () {
return d3.select(element).data(new Array(n)).enter().append(type);
}
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('', function () {
let vis;
let mockUiState;
@@ -50,13 +70,13 @@ describe('Vislib Dispatch Class Test Suite', function () {
destroyVis(vis);
});
- it('implements on, off, emit methods', function () {
+ test('implements on, off, emit methods', function () {
const events = _.map(vis.handler.charts, 'events');
- expect(events.length).to.be.above(0);
+ expect(events.length).toBeGreaterThan(0);
events.forEach(function (dispatch) {
- expect(dispatch).to.have.property('on');
- expect(dispatch).to.have.property('off');
- expect(dispatch).to.have.property('emit');
+ expect(dispatch).toHaveProperty('on');
+ expect(dispatch).toHaveProperty('off');
+ expect(dispatch).toHaveProperty('emit');
});
});
});
@@ -77,15 +97,15 @@ describe('Vislib Dispatch Class Test Suite', function () {
});
describe('addEvent method', function () {
- it('returns a function that binds the passed event to a selection', function () {
+ test('returns a function that binds the passed event to a selection', function () {
const chart = _.first(vis.handler.charts);
const apply = chart.events.addEvent('event', _.noop);
- expect(apply).to.be.a('function');
+ expect(apply).toBeInstanceOf(Function);
const els = getEls(vis.element, 3, 'div');
apply(els);
els.each(function () {
- expect(d3.select(this).on('event')).to.be(_.noop);
+ expect(d3.select(this).on('event')).toBe(_.noop);
});
});
});
@@ -94,21 +114,21 @@ describe('Vislib Dispatch Class Test Suite', function () {
// checking that they return function which bind the events expected
function checkBoundAddMethod(name, event) {
describe(name + ' method', function () {
- it('should be a function', function () {
+ test('should be a function', function () {
vis.handler.charts.forEach(function (chart) {
- expect(chart.events[name]).to.be.a('function');
+ expect(chart.events[name]).toBeInstanceOf(Function);
});
});
- it('returns a function that binds ' + event + ' events to a selection', function () {
+ test('returns a function that binds ' + event + ' events to a selection', function () {
const chart = _.first(vis.handler.charts);
const apply = chart.events[name](chart.series[0].chartEl);
- expect(apply).to.be.a('function');
+ expect(apply).toBeInstanceOf(Function);
const els = getEls(vis.element, 3, 'div');
apply(els);
els.each(function () {
- expect(d3.select(this).on(event)).to.be.a('function');
+ expect(d3.select(this).on(event)).toBeInstanceOf(Function);
});
});
});
@@ -119,26 +139,26 @@ describe('Vislib Dispatch Class Test Suite', function () {
checkBoundAddMethod('addClickEvent', 'click');
describe('addMousePointer method', function () {
- it('should be a function', function () {
+ test('should be a function', function () {
vis.handler.charts.forEach(function (chart) {
const pointer = chart.events.addMousePointer;
- expect(_.isFunction(pointer)).to.be(true);
+ expect(_.isFunction(pointer)).toBe(true);
});
});
});
describe('clickEvent handler', () => {
describe('for pie chart', () => {
- it('prepares data points', () => {
+ test('prepares data points', () => {
const expectedResponse = [{ column: 0, row: 0, table: {}, value: 0 }];
const d = { rawData: { column: 0, row: 0, table: {}, value: 0 } };
const chart = _.first(vis.handler.charts);
const response = chart.events.clickEventResponse(d, { isSlices: true });
- expect(response.data).to.eql(expectedResponse);
+ expect(response.data).toEqual(expectedResponse);
});
- it('remove invalid points', () => {
+ test('remove invalid points', () => {
const expectedResponse = [{ column: 0, row: 0, table: {}, value: 0 }];
const d = {
rawData: { column: 0, row: 0, table: {}, value: 0 },
@@ -146,20 +166,20 @@ describe('Vislib Dispatch Class Test Suite', function () {
};
const chart = _.first(vis.handler.charts);
const response = chart.events.clickEventResponse(d, { isSlices: true });
- expect(response.data).to.eql(expectedResponse);
+ expect(response.data).toEqual(expectedResponse);
});
});
describe('for xy charts', () => {
- it('prepares data points', () => {
+ test('prepares data points', () => {
const expectedResponse = [{ column: 0, row: 0, table: {}, value: 0 }];
const d = { xRaw: { column: 0, row: 0, table: {}, value: 0 } };
const chart = _.first(vis.handler.charts);
const response = chart.events.clickEventResponse(d, { isSlices: false });
- expect(response.data).to.eql(expectedResponse);
+ expect(response.data).toEqual(expectedResponse);
});
- it('remove invalid points', () => {
+ test('remove invalid points', () => {
const expectedResponse = [{ column: 0, row: 0, table: {}, value: 0 }];
const d = {
xRaw: { column: 0, row: 0, table: {}, value: 0 },
@@ -167,35 +187,35 @@ describe('Vislib Dispatch Class Test Suite', function () {
};
const chart = _.first(vis.handler.charts);
const response = chart.events.clickEventResponse(d, { isSlices: false });
- expect(response.data).to.eql(expectedResponse);
+ expect(response.data).toEqual(expectedResponse);
});
});
});
});
describe('Custom event handlers', function () {
- it('should attach whatever gets passed on vis.on() to chart.events', function (done) {
+ test('should attach whatever gets passed on vis.on() to chart.events', function (done) {
const vis = getVis();
const mockUiState = getMockUiState();
vis.on('someEvent', _.noop);
vis.render(data, mockUiState);
vis.handler.charts.forEach(function (chart) {
- expect(chart.events.listenerCount('someEvent')).to.be(1);
+ expect(chart.events.listenerCount('someEvent')).toBe(1);
});
destroyVis(vis);
done();
});
- it('can be added after rendering', function () {
+ test('can be added after rendering', function () {
const vis = getVis();
const mockUiState = getMockUiState();
vis.render(data, mockUiState);
vis.on('someEvent', _.noop);
vis.handler.charts.forEach(function (chart) {
- expect(chart.events.listenerCount('someEvent')).to.be(1);
+ expect(chart.events.listenerCount('someEvent')).toBe(1);
});
destroyVis(vis);
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js b/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js
similarity index 58%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js
rename to src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js
index e4f75c47e621c..d50c70de1bb48 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js
+++ b/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js
@@ -17,25 +17,38 @@
* under the License.
*/
-import expect from '@kbn/expect';
import $ from 'jquery';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../test_utils/public';
// Data
-import series from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
-import columns from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns';
-import rows from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows';
-import stackedSeries from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series';
-import { getMockUiState } from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { getVis } from '../../_vis_fixture';
+import series from '../../fixtures/mock_data/date_histogram/_series';
+import columns from '../../fixtures/mock_data/date_histogram/_columns';
+import rows from '../../fixtures/mock_data/date_histogram/_rows';
+import stackedSeries from '../../fixtures/mock_data/date_histogram/_stacked_series';
+import { getMockUiState } from '../../fixtures/mocks';
+import { getVis } from '../visualizations/_vis_fixture';
const dateHistogramArray = [series, columns, rows, stackedSeries];
const names = ['series', 'columns', 'rows', 'stackedSeries'];
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
dateHistogramArray.forEach(function (data, i) {
describe('Vislib Handler Test Suite for ' + names[i] + ' Data', function () {
const events = ['click', 'brush'];
let vis;
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
vis = getVis();
vis.render(data, getMockUiState());
@@ -45,11 +58,17 @@ dateHistogramArray.forEach(function (data, i) {
vis.destroy();
});
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('render Method', function () {
- it('should render charts', function () {
- expect(vis.handler.charts.length).to.be.greaterThan(0);
+ test('should render charts', function () {
+ expect(vis.handler.charts.length).toBeGreaterThan(0);
vis.handler.charts.forEach(function (chart) {
- expect($(chart.chartEl).find('svg').length).to.be(1);
+ expect($(chart.chartEl).find('svg').length).toBe(1);
});
});
});
@@ -67,10 +86,10 @@ dateHistogramArray.forEach(function (data, i) {
});
});
- it('should add events to chart and emit to the Events class', function () {
+ test('should add events to chart and emit to the Events class', function () {
charts.forEach(function (chart) {
events.forEach(function (event) {
- expect(chart.events.listenerCount(event)).to.be.above(0);
+ expect(chart.events.listenerCount(event)).toBeGreaterThan(0);
});
});
});
@@ -89,10 +108,10 @@ dateHistogramArray.forEach(function (data, i) {
});
});
- it('should remove events from the chart', function () {
+ test('should remove events from the chart', function () {
charts.forEach(function (chart) {
events.forEach(function (event) {
- expect(chart.events.listenerCount(event)).to.be(0);
+ expect(chart.events.listenerCount(event)).toBe(0);
});
});
});
@@ -103,8 +122,8 @@ dateHistogramArray.forEach(function (data, i) {
vis.handler.removeAll(vis.element);
});
- it('should remove all DOM elements from the el', function () {
- expect($(vis.element).children().length).to.be(0);
+ test('should remove all DOM elements from the el', function () {
+ expect($(vis.element).children().length).toBe(0);
});
});
@@ -113,9 +132,9 @@ dateHistogramArray.forEach(function (data, i) {
vis.handler.error('This is an error!');
});
- it('should return an error classed DOM element with a text message', function () {
- expect($(vis.element).find('.error').length).to.be(1);
- expect($('.error h4').html()).to.be('This is an error!');
+ test('should return an error classed DOM element with a text message', function () {
+ expect($(vis.element).find('.error').length).toBe(1);
+ expect($('.error h4').html()).toBe('This is an error!');
});
});
@@ -124,21 +143,21 @@ dateHistogramArray.forEach(function (data, i) {
vis.handler.destroy();
});
- it('should destroy all the charts in the visualization', function () {
- expect(vis.handler.charts.length).to.be(0);
+ test('should destroy all the charts in the visualization', function () {
+ expect(vis.handler.charts.length).toBe(0);
});
});
describe('event proxying', function () {
- it('should only pass the original event object to downstream handlers', function (done) {
+ test('should only pass the original event object to downstream handlers', function (done) {
const event = {};
const chart = vis.handler.charts[0];
const mockEmitter = function () {
const args = Array.from(arguments);
- expect(args.length).to.be(2);
- expect(args[0]).to.be('click');
- expect(args[1]).to.be(event);
+ expect(args.length).toBe(2);
+ expect(args[0]).toBe('click');
+ expect(args[1]).toBe(event);
done();
};
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.test.js
similarity index 55%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js
rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.test.js
index 7ad962fefc341..824d7073d6db5 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js
+++ b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.test.js
@@ -18,22 +18,30 @@
*/
import d3 from 'd3';
-import expect from '@kbn/expect';
import $ from 'jquery';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../../test_utils/public';
// Data
-import series from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
-import columns from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns';
-import rows from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows';
-import stackedSeries from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series';
-import { getMockUiState } from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { Layout } from '../../../../../../../../plugins/vis_type_vislib/public/vislib/lib/layout/layout';
-import { VisConfig } from '../../../../../../../../plugins/vis_type_vislib/public/vislib/lib/vis_config';
-import { getVis } from '../../_vis_fixture';
+import series from '../../../fixtures/mock_data/date_histogram/_series';
+import columns from '../../../fixtures/mock_data/date_histogram/_columns';
+import rows from '../../../fixtures/mock_data/date_histogram/_rows';
+import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series';
+import { getMockUiState } from '../../../fixtures/mocks';
+import { Layout } from './layout';
+import { VisConfig } from '../vis_config';
+import { getVis } from '../../visualizations/_vis_fixture';
const dateHistogramArray = [series, columns, rows, stackedSeries];
const names = ['series', 'columns', 'rows', 'stackedSeries'];
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+
dateHistogramArray.forEach(function (data, i) {
describe('Vislib Layout Class Test Suite for ' + names[i] + ' Data', function () {
let vis;
@@ -41,6 +49,12 @@ dateHistogramArray.forEach(function (data, i) {
let numberOfCharts;
let testLayout;
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
vis = getVis();
mockUiState = getMockUiState();
@@ -52,19 +66,25 @@ dateHistogramArray.forEach(function (data, i) {
vis.destroy();
});
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('createLayout Method', function () {
- it('should append all the divs', function () {
- expect($(vis.element).find('.visWrapper').length).to.be(1);
- expect($(vis.element).find('.visAxis--y').length).to.be(2);
- expect($(vis.element).find('.visWrapper__column').length).to.be(1);
- expect($(vis.element).find('.visAxis__column--y').length).to.be(2);
- expect($(vis.element).find('.y-axis-title').length).to.be.above(0);
- expect($(vis.element).find('.visAxis__splitAxes--y').length).to.be(2);
- expect($(vis.element).find('.visAxis__spacer--y').length).to.be(4);
- expect($(vis.element).find('.visWrapper__chart').length).to.be(numberOfCharts);
- expect($(vis.element).find('.visAxis--x').length).to.be(2);
- expect($(vis.element).find('.visAxis__splitAxes--x').length).to.be(2);
- expect($(vis.element).find('.x-axis-title').length).to.be.above(0);
+ test('should append all the divs', function () {
+ expect($(vis.element).find('.visWrapper').length).toBe(1);
+ expect($(vis.element).find('.visAxis--y').length).toBe(2);
+ expect($(vis.element).find('.visWrapper__column').length).toBe(1);
+ expect($(vis.element).find('.visAxis__column--y').length).toBe(2);
+ expect($(vis.element).find('.y-axis-title').length).toBeGreaterThan(0);
+ expect($(vis.element).find('.visAxis__splitAxes--y').length).toBe(2);
+ expect($(vis.element).find('.visAxis__spacer--y').length).toBe(4);
+ expect($(vis.element).find('.visWrapper__chart').length).toBe(numberOfCharts);
+ expect($(vis.element).find('.visAxis--x').length).toBe(2);
+ expect($(vis.element).find('.visAxis__splitAxes--x').length).toBe(2);
+ expect($(vis.element).find('.x-axis-title').length).toBeGreaterThan(0);
});
});
@@ -82,44 +102,44 @@ dateHistogramArray.forEach(function (data, i) {
testLayout = new Layout(visConfig);
});
- it('should append a div with the correct class name', function () {
- expect($(vis.element).find('.chart').length).to.be(numberOfCharts);
+ test('should append a div with the correct class name', function () {
+ expect($(vis.element).find('.chart').length).toBe(numberOfCharts);
});
- it('should bind data to the DOM element', function () {
- expect(!!$(vis.element).find('.chart').data()).to.be(true);
+ test('should bind data to the DOM element', function () {
+ expect(!!$(vis.element).find('.chart').data()).toBe(true);
});
- it('should create children', function () {
- expect(typeof $(vis.element).find('.x-axis-div')).to.be('object');
+ test('should create children', function () {
+ expect(typeof $(vis.element).find('.x-axis-div')).toBe('object');
});
- it('should call split function when provided', function () {
- expect(typeof $(vis.element).find('.x-axis-div')).to.be('object');
+ test('should call split function when provided', function () {
+ expect(typeof $(vis.element).find('.x-axis-div')).toBe('object');
});
- it('should throw errors when incorrect arguments provided', function () {
+ test('should throw errors when incorrect arguments provided', function () {
expect(function () {
testLayout.layout({
parent: vis.element,
type: undefined,
class: 'chart',
});
- }).to.throwError();
+ }).toThrowError();
expect(function () {
testLayout.layout({
type: 'div',
class: 'chart',
});
- }).to.throwError();
+ }).toThrowError();
expect(function () {
testLayout.layout({
parent: 'histogram',
type: 'div',
});
- }).to.throwError();
+ }).toThrowError();
expect(function () {
testLayout.layout({
@@ -129,7 +149,7 @@ dateHistogramArray.forEach(function (data, i) {
},
class: 'chart',
});
- }).to.throwError();
+ }).toThrowError();
});
});
@@ -139,9 +159,9 @@ dateHistogramArray.forEach(function (data, i) {
vis.handler.layout.appendElem('.visChart', 'div', 'test');
});
- it('should append DOM element to el with a class name', function () {
- expect(typeof $(vis.element).find('.column')).to.be('object');
- expect(typeof $(vis.element).find('.test')).to.be('object');
+ test('should append DOM element to el with a class name', function () {
+ expect(typeof $(vis.element).find('.column')).toBe('object');
+ expect(typeof $(vis.element).find('.test')).toBe('object');
});
});
@@ -151,8 +171,8 @@ dateHistogramArray.forEach(function (data, i) {
vis.handler.layout.removeAll(vis.element);
});
- it('should remove all DOM elements from the el', function () {
- expect($(vis.element).children().length).to.be(0);
+ test('should remove all DOM elements from the el', function () {
+ expect($(vis.element).children().length).toBe(0);
});
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js b/src/plugins/vis_type_vislib/public/vislib/vis.test.js
similarity index 56%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js
rename to src/plugins/vis_type_vislib/public/vislib/vis.test.js
index 36decdc415ed8..0c4fac97ccb9c 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js
+++ b/src/plugins/vis_type_vislib/public/vislib/vis.test.js
@@ -19,18 +19,25 @@
import _ from 'lodash';
import $ from 'jquery';
-import expect from '@kbn/expect';
-
-import series from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
-import columns from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns';
-import rows from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows';
-import stackedSeries from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series';
-import { getMockUiState } from '../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { getVis } from './_vis_fixture';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../test_utils/public';
+import series from '../fixtures/mock_data/date_histogram/_series';
+import columns from '../fixtures/mock_data/date_histogram/_columns';
+import rows from '../fixtures/mock_data/date_histogram/_rows';
+import stackedSeries from '../fixtures/mock_data/date_histogram/_stacked_series';
+import { getMockUiState } from '../fixtures/mocks';
+import { getVis } from './visualizations/_vis_fixture';
const dataArray = [series, columns, rows, stackedSeries];
const names = ['series', 'columns', 'rows', 'stackedSeries'];
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+
dataArray.forEach(function (data, i) {
describe('Vislib Vis Test Suite for ' + names[i] + ' Data', function () {
const beforeEvent = 'click';
@@ -40,6 +47,12 @@ dataArray.forEach(function (data, i) {
let secondVis;
let numberOfCharts;
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
vis = getVis();
secondVis = getVis();
@@ -51,34 +64,40 @@ dataArray.forEach(function (data, i) {
secondVis.destroy();
});
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('render Method', function () {
beforeEach(function () {
vis.render(data, mockUiState);
numberOfCharts = vis.handler.charts.length;
});
- it('should bind data to this object', function () {
- expect(_.isObject(vis.data)).to.be(true);
+ test('should bind data to this object', function () {
+ expect(_.isObject(vis.data)).toBe(true);
});
- it('should instantiate a handler object', function () {
- expect(_.isObject(vis.handler)).to.be(true);
+ test('should instantiate a handler object', function () {
+ expect(_.isObject(vis.handler)).toBe(true);
});
- it('should append a chart', function () {
- expect($('.chart').length).to.be(numberOfCharts);
+ test('should append a chart', function () {
+ expect($('.chart').length).toBe(numberOfCharts);
});
- it('should throw an error if no data is provided', function () {
+ test('should throw an error if no data is provided', function () {
expect(function () {
vis.render(null, mockUiState);
- }).to.throwError();
+ }).toThrowError();
});
});
describe('getLegendColors method', () => {
- it('should return null if no colors are defined', () => {
- expect(vis.getLegendColors()).to.equal(null);
+ test('should return null if no colors are defined', () => {
+ expect(vis.getLegendColors()).toEqual(null);
});
});
@@ -89,12 +108,12 @@ dataArray.forEach(function (data, i) {
secondVis.destroy();
});
- it('should remove all DOM elements from el', function () {
- expect($(secondVis.el).find('.visWrapper').length).to.be(0);
+ test('should remove all DOM elements from el', function () {
+ expect($(secondVis.el).find('.visWrapper').length).toBe(0);
});
- it('should not remove visualizations that have not been destroyed', function () {
- expect($(vis.element).find('.visWrapper').length).to.be(1);
+ test('should not remove visualizations that have not been destroyed', function () {
+ expect($(vis.element).find('.visWrapper').length).toBe(1);
});
});
@@ -105,9 +124,9 @@ dataArray.forEach(function (data, i) {
vis.set('offset', 'wiggle');
});
- it('should set an attribute', function () {
- expect(vis.get('addLegend')).to.be(false);
- expect(vis.get('offset')).to.be('wiggle');
+ test('should set an attribute', function () {
+ expect(vis.get('addLegend')).toBe(false);
+ expect(vis.get('offset')).toBe('wiggle');
});
});
@@ -116,10 +135,10 @@ dataArray.forEach(function (data, i) {
vis.render(data, mockUiState);
});
- it('should get attribute values', function () {
- expect(vis.get('addLegend')).to.be(true);
- expect(vis.get('addTooltip')).to.be(true);
- expect(vis.get('type')).to.be('point_series');
+ test('should get attribute values', function () {
+ expect(vis.get('addLegend')).toBe(true);
+ expect(vis.get('addTooltip')).toBe(true);
+ expect(vis.get('type')).toBe('point_series');
});
});
@@ -148,22 +167,22 @@ dataArray.forEach(function (data, i) {
vis.removeAllListeners(afterEvent);
});
- it('should add an event and its listeners', function () {
+ test('should add an event and its listeners', function () {
listeners.forEach(function (listener) {
- expect(vis.listeners(beforeEvent)).to.contain(listener);
+ expect(vis.listeners(beforeEvent)).toContain(listener);
});
listeners.forEach(function (listener) {
- expect(vis.listeners(afterEvent)).to.contain(listener);
+ expect(vis.listeners(afterEvent)).toContain(listener);
});
});
- it('should cause a listener for each event to be attached to each chart', function () {
+ test('should cause a listener for each event to be attached to each chart', function () {
const charts = vis.handler.charts;
charts.forEach(function (chart) {
- expect(chart.events.listenerCount(beforeEvent)).to.be.above(0);
- expect(chart.events.listenerCount(afterEvent)).to.be.above(0);
+ expect(chart.events.listenerCount(beforeEvent)).toBeGreaterThan(0);
+ expect(chart.events.listenerCount(afterEvent)).toBeGreaterThan(0);
});
});
});
@@ -205,45 +224,45 @@ dataArray.forEach(function (data, i) {
vis.removeAllListeners(afterEvent);
});
- it('should remove a listener', function () {
+ test('should remove a listener', function () {
const charts = vis.handler.charts;
- expect(vis.listeners(beforeEvent)).to.not.contain(listener1);
- expect(vis.listeners(beforeEvent)).to.contain(listener2);
+ expect(vis.listeners(beforeEvent)).not.toContain(listener1);
+ expect(vis.listeners(beforeEvent)).toContain(listener2);
- expect(vis.listeners(afterEvent)).to.not.contain(listener1);
- expect(vis.listeners(afterEvent)).to.contain(listener2);
+ expect(vis.listeners(afterEvent)).not.toContain(listener1);
+ expect(vis.listeners(afterEvent)).toContain(listener2);
// Events should still be attached to charts
charts.forEach(function (chart) {
- expect(chart.events.listenerCount(beforeEvent)).to.be.above(0);
- expect(chart.events.listenerCount(afterEvent)).to.be.above(0);
+ expect(chart.events.listenerCount(beforeEvent)).toBeGreaterThan(0);
+ expect(chart.events.listenerCount(afterEvent)).toBeGreaterThan(0);
});
});
- it('should remove the event and all listeners when only event passed an argument', function () {
+ test('should remove the event and all listeners when only event passed an argument', function () {
const charts = vis.handler.charts;
vis.removeAllListeners(afterEvent);
// should remove 'brush' event
- expect(vis.listeners(beforeEvent)).to.contain(listener2);
- expect(vis.listeners(afterEvent)).to.not.contain(listener2);
+ expect(vis.listeners(beforeEvent)).toContain(listener2);
+ expect(vis.listeners(afterEvent)).not.toContain(listener2);
// should remove the event from the charts
charts.forEach(function (chart) {
- expect(chart.events.listenerCount(beforeEvent)).to.be.above(0);
- expect(chart.events.listenerCount(afterEvent)).to.be(0);
+ expect(chart.events.listenerCount(beforeEvent)).toBeGreaterThan(0);
+ expect(chart.events.listenerCount(afterEvent)).toBe(0);
});
});
- it('should remove the event from the chart when the last listener is removed', function () {
+ test('should remove the event from the chart when the last listener is removed', function () {
const charts = vis.handler.charts;
vis.off(afterEvent, listener2);
- expect(vis.listenerCount(afterEvent)).to.be(0);
+ expect(vis.listenerCount(afterEvent)).toBe(0);
charts.forEach(function (chart) {
- expect(chart.events.listenerCount(afterEvent)).to.be(0);
+ expect(chart.events.listenerCount(afterEvent)).toBe(0);
});
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js
similarity index 83%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js
index 7a68e847f13b1..0ffa53fc7ca9c 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js
@@ -19,11 +19,10 @@
import _ from 'lodash';
import $ from 'jquery';
+import { coreMock } from '../../../../../core/public/mocks';
+import { chartPluginMock } from '../../../../charts/public/mocks';
-import { Vis } from '../../../../../../plugins/vis_type_vislib/public/vislib/vis';
-
-// TODO: Remove when converted to jest mocks
-import { ColorsService } from '../../../../../../plugins/charts/public/services';
+import { Vis } from '../vis';
const $visCanvas = $('')
.attr('id', 'vislib-vis-fixtures')
@@ -55,15 +54,12 @@ afterEach(function () {
});
const getDeps = () => {
- const uiSettings = new Map();
- const colors = new ColorsService();
- colors.init(uiSettings);
+ const mockUiSettings = coreMock.createSetup().uiSettings;
+ const charts = chartPluginMock.createStartContract();
return {
- uiSettings,
- charts: {
- colors,
- },
+ uiSettings: mockUiSettings,
+ charts: charts,
};
};
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/chart.test.js
similarity index 77%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/chart.test.js
index 2b41ce5d1a5c6..94c9693819b69 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/chart.test.js
@@ -18,11 +18,10 @@
*/
import d3 from 'd3';
-import expect from '@kbn/expect';
-
-import { Chart } from '../../../../../../../plugins/vis_type_vislib/public/vislib/visualizations/_chart';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { getVis } from '../_vis_fixture';
+import { setHTMLElementClientSizes, setSVGElementGetBBox } from '../../../../../test_utils/public';
+import { Chart } from './_chart';
+import { getMockUiState } from '../../fixtures/mocks';
+import { getVis } from './_vis_fixture';
describe('Vislib _chart Test Suite', function () {
let vis;
@@ -106,6 +105,14 @@ describe('Vislib _chart Test Suite', function () {
yAxisLabel: 'Count',
};
+ let mockedHTMLElementClientSizes;
+ let mockedSVGElementGetBBox;
+
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ });
+
beforeEach(() => {
el = d3.select('body').append('div').attr('class', 'column-chart');
@@ -127,11 +134,16 @@ describe('Vislib _chart Test Suite', function () {
vis.destroy();
});
- it('should be a constructor for visualization modules', function () {
- expect(myChart instanceof Chart).to.be(true);
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ });
+
+ test('should be a constructor for visualization modules', function () {
+ expect(myChart instanceof Chart).toBe(true);
});
- it('should have a render method', function () {
- expect(typeof myChart.render === 'function').to.be(true);
+ test('should have a render method', function () {
+ expect(typeof myChart.render === 'function').toBe(true);
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js
similarity index 65%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js
index 7c588800ae659..6fdc2a134b820 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js
@@ -19,11 +19,11 @@
import $ from 'jquery';
import _ from 'lodash';
-import expect from '@kbn/expect';
+import { setHTMLElementClientSizes, setSVGElementGetBBox } from '../../../../../test_utils/public';
-import data from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series_multiple';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { getVis } from '../_vis_fixture';
+import data from '../../fixtures/mock_data/terms/_series_multiple';
+import { getMockUiState } from '../../fixtures/mocks';
+import { getVis } from './_vis_fixture';
describe('Vislib Gauge Chart Test Suite', function () {
let vis;
@@ -82,6 +82,14 @@ describe('Vislib Gauge Chart Test Suite', function () {
chartEl = vis.handler.charts[0].chartEl;
}
+ let mockedHTMLElementClientSizes;
+ let mockedSVGElementGetBBox;
+
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ });
+
beforeEach(() => {
generateVis();
});
@@ -91,55 +99,60 @@ describe('Vislib Gauge Chart Test Suite', function () {
$('.visChart').remove();
});
- it('creates meter gauge', function () {
- expect($(chartEl).find('svg').length).to.equal(5);
- expect($(chartEl).find('svg > g > g > text').text()).to.equal('2820231918357341352');
+ afterAll(function () {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ });
+
+ test('creates meter gauge', function () {
+ expect($(chartEl).find('svg').length).toEqual(5);
+ expect($(chartEl).find('svg > g > g > text').text()).toEqual('2820231918357341352');
});
- it('creates circle gauge', function () {
+ test('creates circle gauge', function () {
generateVis({
gauge: {
minAngle: 0,
maxAngle: 2 * Math.PI,
},
});
- expect($(chartEl).find('svg').length).to.equal(5);
+ expect($(chartEl).find('svg').length).toEqual(5);
});
- it('creates gauge with automatic mode', function () {
+ test('creates gauge with automatic mode', function () {
generateVis({
gauge: {
alignment: 'automatic',
},
});
- expect($(chartEl).find('svg').width()).to.equal(197);
+ expect($(chartEl).find('svg')[0].getAttribute('width')).toEqual('97');
});
- it('creates gauge with vertical mode', function () {
+ test('creates gauge with vertical mode', function () {
generateVis({
gauge: {
alignment: 'vertical',
},
});
- expect($(chartEl).find('svg').width()).to.equal($(chartEl).width());
+ expect($(chartEl).find('svg').width()).toEqual($(chartEl).width());
});
- it('applies range settings correctly', function () {
+ test('applies range settings correctly', function () {
const paths = $(chartEl).find('svg > g > g:nth-child(1) > path:nth-child(2)');
const fills = [];
paths.each(function () {
fills.push(this.style.fill);
});
- expect(fills).to.eql([
- 'rgb(165, 0, 38)',
- 'rgb(255, 255, 190)',
- 'rgb(255, 255, 190)',
- 'rgb(0, 104, 55)',
- 'rgb(0, 104, 55)',
+ expect(fills).toEqual([
+ 'rgb(165,0,38)',
+ 'rgb(255,255,190)',
+ 'rgb(255,255,190)',
+ 'rgb(0,104,55)',
+ 'rgb(0,104,55)',
]);
});
- it('applies color schema correctly', function () {
+ test('applies color schema correctly', function () {
generateVis({
gauge: {
colorSchema: 'Blues',
@@ -150,12 +163,12 @@ describe('Vislib Gauge Chart Test Suite', function () {
paths.each(function () {
fills.push(this.style.fill);
});
- expect(fills).to.eql([
- 'rgb(8, 48, 107)',
- 'rgb(107, 174, 214)',
- 'rgb(107, 174, 214)',
- 'rgb(247, 251, 255)',
- 'rgb(247, 251, 255)',
+ expect(fills).toEqual([
+ 'rgb(8,48,107)',
+ 'rgb(107,174,214)',
+ 'rgb(107,174,214)',
+ 'rgb(247,251,255)',
+ 'rgb(247,251,255)',
]);
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js
similarity index 65%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js
index d245905729c7e..e2da33d0808ba 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js
@@ -20,16 +20,25 @@
import d3 from 'd3';
import _ from 'lodash';
import $ from 'jquery';
-import expect from '@kbn/expect';
-
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { getVis } from '../_vis_fixture';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../test_utils/public';
+import { getMockUiState } from '../../fixtures/mocks';
+import { getVis } from './_vis_fixture';
import { pieChartMockData } from './pie_chart_mock_data';
const names = ['rows', 'columns', 'slices'];
const sizes = [0, 5, 15, 30, 60, 120];
+let mockedHTMLElementClientSizes = {};
+let mockWidth;
+let mockHeight;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+
describe('No global chart settings', function () {
const visLibParams1 = {
el: '
',
@@ -40,6 +49,14 @@ describe('No global chart settings', function () {
let chart1;
let mockUiState;
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(120);
+ mockHeight = jest.spyOn($.prototype, 'height').mockReturnValue(120);
+ });
+
beforeEach(() => {
chart1 = getVis(visLibParams1);
mockUiState = getMockUiState();
@@ -53,8 +70,16 @@ describe('No global chart settings', function () {
chart1.destroy();
});
- it('should render chart titles for all charts', function () {
- expect($(chart1.element).find('.visAxis__splitTitles--y').length).to.be(1);
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ mockWidth.mockRestore();
+ mockHeight.mockRestore();
+ });
+
+ test('should render chart titles for all charts', function () {
+ expect($(chart1.element).find('.visAxis__splitTitles--y').length).toBe(1);
});
describe('_validatePieData method', function () {
@@ -76,24 +101,54 @@ describe('No global chart settings', function () {
{ slices: { children: [{}] } },
];
- it('should throw an error when all charts contain zeros', function () {
+ test('should throw an error when all charts contain zeros', function () {
expect(function () {
chart1.handler.ChartClass.prototype._validatePieData(allZeros);
- }).to.throwError();
+ }).toThrowError();
});
- it('should not throw an error when only some or no charts contain zeros', function () {
+ test('should not throw an error when only some or no charts contain zeros', function () {
expect(function () {
chart1.handler.ChartClass.prototype._validatePieData(someZeros);
- }).to.not.throwError();
+ }).not.toThrowError();
expect(function () {
chart1.handler.ChartClass.prototype._validatePieData(noZeros);
- }).to.not.throwError();
+ }).not.toThrowError();
});
});
});
describe('Vislib PieChart Class Test Suite', function () {
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ let width = 120;
+ let height = 120;
+ const mockWidth = jest.spyOn($.prototype, 'width');
+ mockWidth.mockImplementation((size) => {
+ if (size === undefined) {
+ return width;
+ }
+ width = size;
+ });
+ const mockHeight = jest.spyOn($.prototype, 'height');
+ mockHeight.mockImplementation((size) => {
+ if (size === undefined) {
+ return height;
+ }
+ height = size;
+ });
+ });
+
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ mockWidth.mockRestore();
+ mockHeight.mockRestore();
+ });
+
['rowData', 'columnData', 'sliceData'].forEach(function (aggItem, i) {
describe('Vislib PieChart Class Test Suite for ' + names[i] + ' data', function () {
const mockPieData = pieChartMockData[aggItem];
@@ -132,15 +187,15 @@ describe('Vislib PieChart Class Test Suite', function () {
});
});
- it('should attach a click event', function () {
+ test('should attach a click event', function () {
vis.handler.charts.forEach(function () {
- expect(onClick).to.be(true);
+ expect(onClick).toBe(true);
});
});
- it('should attach a hover event', function () {
+ test('should attach a hover event', function () {
vis.handler.charts.forEach(function () {
- expect(onMouseOver).to.be(true);
+ expect(onMouseOver).toBe(true);
});
});
});
@@ -151,25 +206,25 @@ describe('Vislib PieChart Class Test Suite', function () {
let svg;
let slices;
- it('should return an SVG object', function () {
+ test('should return an SVG object', function () {
vis.handler.charts.forEach(function (chart) {
$(chart.chartEl).find('svg').empty();
width = $(chart.chartEl).width();
height = $(chart.chartEl).height();
svg = d3.select($(chart.chartEl).find('svg')[0]);
slices = chart.chartData.slices;
- expect(_.isObject(chart.addPath(width, height, svg, slices))).to.be(true);
+ expect(_.isObject(chart.addPath(width, height, svg, slices))).toBe(true);
});
});
- it('should draw path elements', function () {
+ test('should draw path elements', function () {
vis.handler.charts.forEach(function (chart) {
// test whether path elements are drawn
- expect($(chart.chartEl).find('path').length).to.be.greaterThan(0);
+ expect($(chart.chartEl).find('path').length).toBeGreaterThan(0);
});
});
- it('should draw labels', function () {
+ test('should draw labels', function () {
vis.handler.charts.forEach(function (chart) {
$(chart.chartEl).find('svg').empty();
width = $(chart.chartEl).width();
@@ -178,22 +233,22 @@ describe('Vislib PieChart Class Test Suite', function () {
slices = chart.chartData.slices;
chart._attr.labels.show = true;
chart.addPath(width, height, svg, slices);
- expect($(chart.chartEl).find('text.label-text').length).to.be.greaterThan(0);
+ expect($(chart.chartEl).find('text.label-text').length).toBeGreaterThan(0);
});
});
});
describe('draw method', function () {
- it('should return a function', function () {
+ test('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
- expect(_.isFunction(chart.draw())).to.be(true);
+ expect(_.isFunction(chart.draw())).toBe(true);
});
});
});
sizes.forEach(function (size) {
describe('containerTooSmall error', function () {
- it('should throw an error', function () {
+ test('should throw an error', function () {
// 20px is the minimum height and width
vis.handler.charts.forEach(function (chart) {
$(chart.chartEl).height(size);
@@ -202,12 +257,12 @@ describe('Vislib PieChart Class Test Suite', function () {
if (size < 20) {
expect(function () {
chart.render();
- }).to.throwError();
+ }).toThrowError();
}
});
});
- it('should not throw an error', function () {
+ test('should not throw an error', function () {
vis.handler.charts.forEach(function (chart) {
$(chart.chartEl).height(size);
$(chart.chartEl).width(size);
@@ -215,7 +270,7 @@ describe('Vislib PieChart Class Test Suite', function () {
if (size > 20) {
expect(function () {
chart.render();
- }).to.not.throwError();
+ }).not.toThrowError();
}
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart_mock_data.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart_mock_data.js
similarity index 100%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart_mock_data.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart_mock_data.js
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js
similarity index 64%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js
index fd2240c0c64c5..3cd58060978ee 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js
@@ -20,18 +20,22 @@
import d3 from 'd3';
import _ from 'lodash';
import $ from 'jquery';
-import expect from '@kbn/expect';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../../test_utils/public';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
+import { getMockUiState } from '../../../fixtures/mocks';
import { getVis } from '../_vis_fixture';
const dataTypesArray = {
- 'series pos': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'),
- 'series pos neg': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'),
- 'series neg': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'),
- 'term columns': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns'),
- 'range rows': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows'),
- stackedSeries: require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'),
+ 'series pos': import('../../../fixtures/mock_data/date_histogram/_series'),
+ 'series pos neg': import('../../../fixtures/mock_data/date_histogram/_series_pos_neg'),
+ 'series neg': import('../../../fixtures/mock_data/date_histogram/_series_neg'),
+ 'term columns': import('../../../fixtures/mock_data/terms/_columns'),
+ 'range rows': import('../../../fixtures/mock_data/range/_rows'),
+ stackedSeries: import('../../../fixtures/mock_data/date_histogram/_stacked_series'),
};
const visLibParams = {
@@ -41,22 +45,38 @@ const visLibParams = {
mode: 'stacked',
};
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+
_.forOwn(dataTypesArray, function (dataType, dataTypeName) {
describe('Vislib Area Chart Test Suite for ' + dataTypeName + ' Data', function () {
let vis;
let mockUiState;
- beforeEach(() => {
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
+ beforeEach(async () => {
vis = getVis(visLibParams);
mockUiState = getMockUiState();
vis.on('brush', _.noop);
- vis.render(dataType, mockUiState);
+ vis.render(await dataType, mockUiState);
});
afterEach(function () {
vis.destroy();
});
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('stackData method', function () {
let stackedData;
let isStacked;
@@ -73,15 +93,15 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) {
});
});
- it('should append a d.y0 key to the data object', function () {
- expect(isStacked).to.be(true);
+ test('should append a d.y0 key to the data object', function () {
+ expect(isStacked).toBe(true);
});
});
describe('addPath method', function () {
- it('should append a area paths', function () {
+ test('should append a area paths', function () {
vis.handler.charts.forEach(function (chart) {
- expect($(chart.chartEl).find('path').length).to.be.greaterThan(0);
+ expect($(chart.chartEl).find('path').length).toBeGreaterThan(0);
});
});
});
@@ -101,9 +121,9 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) {
});
});
- it('should attach a hover event', function () {
+ test('should attach a hover event', function () {
vis.handler.charts.forEach(function () {
- expect(onMouseOver).to.be(true);
+ expect(onMouseOver).toBe(true);
});
});
});
@@ -134,33 +154,33 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) {
// listeners, however, I was not able to test for the listener
// function being present. I will need to update this test
// in the future.
- it('should attach a brush g element', function () {
+ test('should attach a brush g element', function () {
vis.handler.charts.forEach(function () {
- expect(onBrush).to.be(true);
+ expect(onBrush).toBe(true);
});
});
- it('should attach a click event', function () {
+ test('should attach a click event', function () {
vis.handler.charts.forEach(function () {
- expect(onClick).to.be(true);
+ expect(onClick).toBe(true);
});
});
- it('should attach a hover event', function () {
+ test('should attach a hover event', function () {
vis.handler.charts.forEach(function () {
- expect(onMouseOver).to.be(true);
+ expect(onMouseOver).toBe(true);
});
});
});
describe('addCircles method', function () {
- it('should append circles', function () {
+ test('should append circles', function () {
vis.handler.charts.forEach(function (chart) {
- expect($(chart.chartEl).find('circle').length).to.be.greaterThan(0);
+ expect($(chart.chartEl).find('circle').length).toBeGreaterThan(0);
});
});
- it('should not draw circles where d.y === 0', function () {
+ test('should not draw circles where d.y === 0', function () {
vis.handler.charts.forEach(function (chart) {
const series = chart.chartData.series;
const isZero = series.some(function (d) {
@@ -172,80 +192,80 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) {
});
if (isZero) {
- expect(isNotDrawn).to.be(false);
+ expect(isNotDrawn).toBe(false);
}
});
});
});
describe('draw method', function () {
- it('should return a function', function () {
+ test('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
- expect(_.isFunction(chart.draw())).to.be(true);
+ expect(_.isFunction(chart.draw())).toBe(true);
});
});
- it('should return a yMin and yMax', function () {
+ test('should return a yMin and yMax', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.not.be(undefined);
- expect(domain[1]).to.not.be(undefined);
+ expect(domain[0]).not.toBe(undefined);
+ expect(domain[1]).not.toBe(undefined);
});
});
- it('should render a zero axis line', function () {
+ test('should render a zero axis line', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
if (yAxis.yMin < 0 && yAxis.yMax > 0) {
- expect($(chart.chartEl).find('line.zero-line').length).to.be(1);
+ expect($(chart.chartEl).find('line.zero-line').length).toBe(1);
}
});
});
});
describe('defaultYExtents is true', function () {
- beforeEach(function () {
+ beforeEach(async function () {
vis.visConfigArgs.defaultYExtents = true;
- vis.render(dataType, mockUiState);
+ vis.render(await dataType, mockUiState);
});
- it('should return yAxis extents equal to data extents', function () {
+ test('should return yAxis extents equal to data extents', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const min = vis.handler.valueAxes[0].axisScale.getYMin();
const max = vis.handler.valueAxes[0].axisScale.getYMax();
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1]).toEqual(max);
});
});
});
[0, 2, 4, 8].forEach(function (boundsMarginValue) {
describe('defaultYExtents is true and boundsMargin is defined', function () {
- beforeEach(function () {
+ beforeEach(async function () {
vis.visConfigArgs.defaultYExtents = true;
vis.visConfigArgs.boundsMargin = boundsMarginValue;
- vis.render(dataType, mockUiState);
+ vis.render(await dataType, mockUiState);
});
- it('should return yAxis extents equal to data extents with boundsMargin', function () {
+ test('should return yAxis extents equal to data extents with boundsMargin', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const min = vis.handler.valueAxes[0].axisScale.getYMin();
const max = vis.handler.valueAxes[0].axisScale.getYMax();
const domain = yAxis.getScale().domain();
if (min < 0 && max < 0) {
- expect(domain[0]).to.equal(min);
- expect(domain[1] - boundsMarginValue).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1] - boundsMarginValue).toEqual(max);
} else if (min > 0 && max > 0) {
- expect(domain[0] + boundsMarginValue).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0] + boundsMarginValue).toEqual(min);
+ expect(domain[1]).toEqual(max);
} else {
- expect(domain[0]).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1]).toEqual(max);
}
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js
similarity index 59%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js
index 6b7ccaed25d49..f3d8d66df2d85 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js
@@ -20,20 +20,24 @@
import _ from 'lodash';
import d3 from 'd3';
import $ from 'jquery';
-import expect from '@kbn/expect';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../../test_utils/public';
// Data
-import series from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
-import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg';
-import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg';
-import termsColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns';
-import histogramRows from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_rows';
-import stackedSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series';
-
-import { seriesMonthlyInterval } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_monthly_interval';
-import { rowsSeriesWithHoles } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows_series_with_holes';
-import rowsWithZeros from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
+import series from '../../../fixtures/mock_data/date_histogram/_series';
+import seriesPosNeg from '../../../fixtures/mock_data/date_histogram/_series_pos_neg';
+import seriesNeg from '../../../fixtures/mock_data/date_histogram/_series_neg';
+import termsColumns from '../../../fixtures/mock_data/terms/_columns';
+import histogramRows from '../../../fixtures/mock_data/histogram/_rows';
+import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series';
+
+import { seriesMonthlyInterval } from '../../../fixtures/mock_data/date_histogram/_series_monthly_interval';
+import { rowsSeriesWithHoles } from '../../../fixtures/mock_data/date_histogram/_rows_series_with_holes';
+import rowsWithZeros from '../../../fixtures/mock_data/date_histogram/_rows';
+import { getMockUiState } from '../../../fixtures/mocks';
import { getVis } from '../_vis_fixture';
// tuple, with the format [description, mode, data]
@@ -46,6 +50,10 @@ const dataTypesArray = [
['stackedSeries', 'stacked', stackedSeries],
];
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+
dataTypesArray.forEach(function (dataType) {
const name = dataType[0];
const mode = dataType[1];
@@ -66,6 +74,12 @@ dataTypesArray.forEach(function (dataType) {
},
};
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
vis = getVis(visLibParams);
mockUiState = getMockUiState();
@@ -77,6 +91,12 @@ dataTypesArray.forEach(function (dataType) {
vis.destroy();
});
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('stackData method', function () {
let stackedData;
let isStacked;
@@ -93,21 +113,21 @@ dataTypesArray.forEach(function (dataType) {
});
});
- it('should stack values when mode is stacked', function () {
+ test('should stack values when mode is stacked', function () {
if (mode === 'stacked') {
- expect(isStacked).to.be(true);
+ expect(isStacked).toBe(true);
}
});
- it('should stack values when mode is percentage', function () {
+ test('should stack values when mode is percentage', function () {
if (mode === 'percentage') {
- expect(isStacked).to.be(true);
+ expect(isStacked).toBe(true);
}
});
});
describe('addBars method', function () {
- it('should append rects', function () {
+ test('should append rects', function () {
let numOfSeries;
let numOfValues;
let product;
@@ -116,7 +136,7 @@ dataTypesArray.forEach(function (dataType) {
numOfSeries = chart.chartData.series.length;
numOfValues = chart.chartData.series[0].values.length;
product = numOfSeries * numOfValues;
- expect($(chart.chartEl).find('.series rect')).to.have.length(product);
+ expect($(chart.chartEl).find('.series rect')).toHaveLength(product);
});
});
});
@@ -138,53 +158,53 @@ dataTypesArray.forEach(function (dataType) {
};
}
- it('should attach the brush if data is a set is ordered', function () {
+ test('should attach the brush if data is a set is ordered', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
const ordered = vis.handler.data.get('ordered');
const allowBrushing = Boolean(ordered);
- expect(has.brush).to.be(allowBrushing);
+ expect(has.brush).toBe(allowBrushing);
});
});
- it('should attach a click event', function () {
+ test('should attach a click event', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
- expect(has.click).to.be(true);
+ expect(has.click).toBe(true);
});
});
- it('should attach a hover event', function () {
+ test('should attach a hover event', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
- expect(has.mouseOver).to.be(true);
+ expect(has.mouseOver).toBe(true);
});
});
});
describe('draw method', function () {
- it('should return a function', function () {
+ test('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
- expect(_.isFunction(chart.draw())).to.be(true);
+ expect(_.isFunction(chart.draw())).toBe(true);
});
});
- it('should return a yMin and yMax', function () {
+ test('should return a yMin and yMax', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.not.be(undefined);
- expect(domain[1]).to.not.be(undefined);
+ expect(domain[0]).not.toBe(undefined);
+ expect(domain[1]).not.toBe(undefined);
});
});
- it('should render a zero axis line', function () {
+ test('should render a zero axis line', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
if (yAxis.yMin < 0 && yAxis.yMax > 0) {
- expect($(chart.chartEl).find('line.zero-line').length).to.be(1);
+ expect($(chart.chartEl).find('line.zero-line').length).toBe(1);
}
});
});
@@ -196,14 +216,14 @@ dataTypesArray.forEach(function (dataType) {
vis.render(data, mockUiState);
});
- it('should return yAxis extents equal to data extents', function () {
+ test('should return yAxis extents equal to data extents', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const min = vis.handler.valueAxes[0].axisScale.getYMin();
const max = vis.handler.valueAxes[0].axisScale.getYMax();
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1]).toEqual(max);
});
});
});
@@ -215,21 +235,21 @@ dataTypesArray.forEach(function (dataType) {
vis.render(data, mockUiState);
});
- it('should return yAxis extents equal to data extents with boundsMargin', function () {
+ test('should return yAxis extents equal to data extents with boundsMargin', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const min = vis.handler.valueAxes[0].axisScale.getYMin();
const max = vis.handler.valueAxes[0].axisScale.getYMax();
const domain = yAxis.getScale().domain();
if (min < 0 && max < 0) {
- expect(domain[0]).to.equal(min);
- expect(domain[1] - boundsMarginValue).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1] - boundsMarginValue).toEqual(max);
} else if (min > 0 && max > 0) {
- expect(domain[0] + boundsMarginValue).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0] + boundsMarginValue).toEqual(min);
+ expect(domain[1]).toEqual(max);
} else {
- expect(domain[0]).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1]).toEqual(max);
}
});
});
@@ -249,6 +269,12 @@ describe('stackData method - data set with zeros in percentage mode', function (
zeroFill: true,
};
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
vis = getVis(visLibParams);
mockUiState = getMockUiState();
@@ -259,29 +285,35 @@ describe('stackData method - data set with zeros in percentage mode', function (
vis.destroy();
});
- it('should not mutate the injected zeros', function () {
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
+ test('should not mutate the injected zeros', function () {
vis.render(seriesMonthlyInterval, mockUiState);
- expect(vis.handler.charts).to.have.length(1);
+ expect(vis.handler.charts).toHaveLength(1);
const chart = vis.handler.charts[0];
- expect(chart.chartData.series).to.have.length(1);
+ expect(chart.chartData.series).toHaveLength(1);
const series = chart.chartData.series[0].values;
// with the interval set in seriesMonthlyInterval data, the point at x=1454309600000 does not exist
const point = _.find(series, ['x', 1454309600000]);
- expect(point).to.not.be(undefined);
- expect(point.y).to.be(0);
+ expect(point).not.toBe(undefined);
+ expect(point.y).toBe(0);
});
- it('should not mutate zeros that exist in the data', function () {
+ test('should not mutate zeros that exist in the data', function () {
vis.render(rowsWithZeros, mockUiState);
- expect(vis.handler.charts).to.have.length(2);
+ expect(vis.handler.charts).toHaveLength(2);
const chart = vis.handler.charts[0];
- expect(chart.chartData.series).to.have.length(5);
+ expect(chart.chartData.series).toHaveLength(5);
const series = chart.chartData.series[0].values;
const point = _.find(series, ['x', 1415826240000]);
- expect(point).to.not.be(undefined);
- expect(point.y).to.be(0);
+ expect(point).not.toBe(undefined);
+ expect(point.y).toBe(0);
});
});
@@ -296,6 +328,12 @@ describe('datumWidth - split chart data set with holes', function () {
zeroFill: true,
};
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
vis = getVis(visLibParams);
mockUiState = getMockUiState();
@@ -307,14 +345,20 @@ describe('datumWidth - split chart data set with holes', function () {
vis.destroy();
});
- it('should not have bar widths that span multiple time bins', function () {
- expect(vis.handler.charts.length).to.equal(1);
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
+ test('should not have bar widths that span multiple time bins', function () {
+ expect(vis.handler.charts.length).toEqual(1);
const chart = vis.handler.charts[0];
const rects = $(chart.chartEl).find('.series rect');
const MAX_WIDTH_IN_PIXELS = 27;
rects.each(function () {
- const width = $(this).attr('width');
- expect(width).to.be.lessThan(MAX_WIDTH_IN_PIXELS);
+ const width = parseInt($(this).attr('width'), 10);
+ expect(width).toBeLessThan(MAX_WIDTH_IN_PIXELS);
});
});
});
@@ -330,6 +374,15 @@ describe('datumWidth - monthly interval', function () {
zeroFill: true,
};
+ let mockWidth;
+
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(900);
+ });
+
beforeEach(() => {
vis = getVis(visLibParams);
mockUiState = getMockUiState();
@@ -341,12 +394,19 @@ describe('datumWidth - monthly interval', function () {
vis.destroy();
});
- it('should vary bar width when date histogram intervals are not equal', function () {
- expect(vis.handler.charts.length).to.equal(1);
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ mockWidth.mockRestore();
+ });
+
+ test('should vary bar width when date histogram intervals are not equal', function () {
+ expect(vis.handler.charts.length).toEqual(1);
const chart = vis.handler.charts[0];
const rects = $(chart.chartEl).find('.series rect');
- const januaryBarWidth = $(rects.get(0)).attr('width');
- const februaryBarWidth = $(rects.get(1)).attr('width');
- expect(februaryBarWidth).to.be.lessThan(januaryBarWidth);
+ const januaryBarWidth = parseInt($(rects.get(0)).attr('width'), 10);
+ const februaryBarWidth = parseInt($(rects.get(1)).attr('width'), 10);
+ expect(februaryBarWidth).toBeLessThan(januaryBarWidth);
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js
similarity index 65%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js
index 9fa51fb59ed48..8c727d225c6c3 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js
@@ -20,15 +20,19 @@
import _ from 'lodash';
import $ from 'jquery';
import d3 from 'd3';
-import expect from '@kbn/expect';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../../test_utils/public';
// Data
-import series from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
-import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg';
-import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg';
-import termsColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns';
-import stackedSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
+import series from '../../../fixtures/mock_data/date_histogram/_series';
+import seriesPosNeg from '../../../fixtures/mock_data/date_histogram/_series_pos_neg';
+import seriesNeg from '../../../fixtures/mock_data/date_histogram/_series_neg';
+import termsColumns from '../../../fixtures/mock_data/terms/_columns';
+import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series';
+import { getMockUiState } from '../../../fixtures/mocks';
import { getVis } from '../_vis_fixture';
// tuple, with the format [description, mode, data]
@@ -40,7 +44,26 @@ const dataTypesArray = [
['stackedSeries', stackedSeries],
];
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+let mockWidth;
+
describe('Vislib Heatmap Chart Test Suite', function () {
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(900);
+ });
+
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ mockWidth.mockRestore();
+ });
+
dataTypesArray.forEach(function (dataType) {
const name = dataType[0];
const data = dataType[1];
@@ -76,7 +99,7 @@ describe('Vislib Heatmap Chart Test Suite', function () {
vis.destroy();
});
- it('category axes should be rendered in reverse order', () => {
+ test('category axes should be rendered in reverse order', () => {
const renderedCategoryAxes = vis.handler.renderArray.filter((item) => {
return (
item.constructor &&
@@ -84,22 +107,22 @@ describe('Vislib Heatmap Chart Test Suite', function () {
item.axisConfig.get('type') === 'category'
);
});
- expect(vis.handler.categoryAxes.length).to.equal(renderedCategoryAxes.length);
- expect(vis.handler.categoryAxes[0].axisConfig.get('id')).to.equal(
+ expect(vis.handler.categoryAxes.length).toEqual(renderedCategoryAxes.length);
+ expect(vis.handler.categoryAxes[0].axisConfig.get('id')).toEqual(
renderedCategoryAxes[1].axisConfig.get('id')
);
- expect(vis.handler.categoryAxes[1].axisConfig.get('id')).to.equal(
+ expect(vis.handler.categoryAxes[1].axisConfig.get('id')).toEqual(
renderedCategoryAxes[0].axisConfig.get('id')
);
});
describe('addSquares method', function () {
- it('should append rects', function () {
+ test('should append rects', function () {
vis.handler.charts.forEach(function (chart) {
const numOfRects = chart.chartData.series.reduce((result, series) => {
return result + series.values.length;
}, 0);
- expect($(chart.chartEl).find('.series rect')).to.have.length(numOfRects);
+ expect($(chart.chartEl).find('.series rect')).toHaveLength(numOfRects);
});
});
});
@@ -120,53 +143,53 @@ describe('Vislib Heatmap Chart Test Suite', function () {
};
}
- it('should attach the brush if data is a set of ordered dates', function () {
+ test('should attach the brush if data is a set of ordered dates', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
const ordered = vis.handler.data.get('ordered');
const date = Boolean(ordered && ordered.date);
- expect(has.brush).to.be(date);
+ expect(has.brush).toBe(date);
});
});
- it('should attach a click event', function () {
+ test('should attach a click event', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
- expect(has.click).to.be(true);
+ expect(has.click).toBe(true);
});
});
- it('should attach a hover event', function () {
+ test('should attach a hover event', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
- expect(has.mouseOver).to.be(true);
+ expect(has.mouseOver).toBe(true);
});
});
});
describe('draw method', function () {
- it('should return a function', function () {
+ test('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
- expect(_.isFunction(chart.draw())).to.be(true);
+ expect(_.isFunction(chart.draw())).toBe(true);
});
});
- it('should return a yMin and yMax', function () {
+ test('should return a yMin and yMax', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.not.be(undefined);
- expect(domain[1]).to.not.be(undefined);
+ expect(domain[0]).not.toBe(undefined);
+ expect(domain[1]).not.toBe(undefined);
});
});
});
- it('should define default colors', function () {
- expect(mockUiState.get('vis.defaultColors')).to.not.be(undefined);
+ test('should define default colors', function () {
+ expect(mockUiState.get('vis.defaultColors')).not.toBe(undefined);
});
- it('should set custom range', function () {
+ test('should set custom range', function () {
vis.destroy();
generateVis({
setColorRange: true,
@@ -178,14 +201,14 @@ describe('Vislib Heatmap Chart Test Suite', function () {
],
});
const labels = vis.getLegendLabels();
- expect(labels[0]).to.be('0 - 200');
- expect(labels[1]).to.be('200 - 400');
- expect(labels[2]).to.be('400 - 500');
- expect(labels[3]).to.be('500 - Infinity');
+ expect(labels[0]).toBe('0 - 200');
+ expect(labels[1]).toBe('200 - 400');
+ expect(labels[2]).toBe('400 - 500');
+ expect(labels[3]).toBe('500 - Infinity');
});
- it('should show correct Y axis title', function () {
- expect(vis.handler.categoryAxes[1].axisConfig.get('title.text')).to.equal('');
+ test('should show correct Y axis title', function () {
+ expect(vis.handler.categoryAxes[1].axisConfig.get('title.text')).toEqual('');
});
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js
similarity index 67%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js
index dae92c831cd8d..a84c74c095051 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js
@@ -18,18 +18,22 @@
*/
import d3 from 'd3';
-import expect from '@kbn/expect';
import $ from 'jquery';
import _ from 'lodash';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../../test_utils/public';
// Data
-import seriesPos from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
-import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg';
-import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg';
-import histogramColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_columns';
-import rangeRows from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows';
-import termSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
+import seriesPos from '../../../fixtures/mock_data/date_histogram/_series';
+import seriesPosNeg from '../../../fixtures/mock_data/date_histogram/_series_pos_neg';
+import seriesNeg from '../../../fixtures/mock_data/date_histogram/_series_neg';
+import histogramColumns from '../../../fixtures/mock_data/histogram/_columns';
+import rangeRows from '../../../fixtures/mock_data/range/_rows';
+import termSeries from '../../../fixtures/mock_data/terms/_series';
+import { getMockUiState } from '../../../fixtures/mocks';
import { getVis } from '../_vis_fixture';
const dataTypes = [
@@ -41,7 +45,23 @@ const dataTypes = [
['term series', termSeries],
];
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+
describe('Vislib Line Chart', function () {
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
dataTypes.forEach(function (type) {
const name = type[0];
const data = type[1];
@@ -94,37 +114,37 @@ describe('Vislib Line Chart', function () {
// listeners, however, I was not able to test for the listener
// function being present. I will need to update this test
// in the future.
- it('should attach a brush g element', function () {
+ test('should attach a brush g element', function () {
vis.handler.charts.forEach(function () {
- expect(onBrush).to.be(true);
+ expect(onBrush).toBe(true);
});
});
- it('should attach a click event', function () {
+ test('should attach a click event', function () {
vis.handler.charts.forEach(function () {
- expect(onClick).to.be(true);
+ expect(onClick).toBe(true);
});
});
- it('should attach a hover event', function () {
+ test('should attach a hover event', function () {
vis.handler.charts.forEach(function () {
- expect(onMouseOver).to.be(true);
+ expect(onMouseOver).toBe(true);
});
});
});
describe('addCircles method', function () {
- it('should append circles', function () {
+ test('should append circles', function () {
vis.handler.charts.forEach(function (chart) {
- expect($(chart.chartEl).find('circle').length).to.be.greaterThan(0);
+ expect($(chart.chartEl).find('circle').length).toBeGreaterThan(0);
});
});
});
describe('addLines method', function () {
- it('should append a paths', function () {
+ test('should append a paths', function () {
vis.handler.charts.forEach(function (chart) {
- expect($(chart.chartEl).find('path').length).to.be.greaterThan(0);
+ expect($(chart.chartEl).find('path').length).toBeGreaterThan(0);
});
});
});
@@ -132,7 +152,7 @@ describe('Vislib Line Chart', function () {
// Cannot seem to get these tests to work on the box
// They however pass in the browsers
//describe('addClipPath method', function () {
- // it('should append a clipPath', function () {
+ // test('should append a clipPath', function () {
// vis.handler.charts.forEach(function (chart) {
// expect($(chart.chartEl).find('clipPath').length).to.be(1);
// });
@@ -140,27 +160,27 @@ describe('Vislib Line Chart', function () {
//});
describe('draw method', function () {
- it('should return a function', function () {
+ test('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
- expect(chart.draw()).to.be.a(Function);
+ expect(chart.draw()).toBeInstanceOf(Function);
});
});
- it('should return a yMin and yMax', function () {
+ test('should return a yMin and yMax', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.not.be(undefined);
- expect(domain[1]).to.not.be(undefined);
+ expect(domain[0]).not.toBe(undefined);
+ expect(domain[1]).not.toBe(undefined);
});
});
- it('should render a zero axis line', function () {
+ test('should render a zero axis line', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
if (yAxis.yMin < 0 && yAxis.yMax > 0) {
- expect($(chart.chartEl).find('line.zero-line').length).to.be(1);
+ expect($(chart.chartEl).find('line.zero-line').length).toBe(1);
}
});
});
@@ -172,14 +192,14 @@ describe('Vislib Line Chart', function () {
vis.render(data, mockUiState);
});
- it('should return yAxis extents equal to data extents', function () {
+ test('should return yAxis extents equal to data extents', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const min = vis.handler.valueAxes[0].axisScale.getYMin();
const max = vis.handler.valueAxes[0].axisScale.getYMax();
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1]).toEqual(max);
});
});
});
@@ -191,21 +211,21 @@ describe('Vislib Line Chart', function () {
vis.render(data, mockUiState);
});
- it('should return yAxis extents equal to data extents with boundsMargin', function () {
+ test('should return yAxis extents equal to data extents with boundsMargin', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const min = vis.handler.valueAxes[0].axisScale.getYMin();
const max = vis.handler.valueAxes[0].axisScale.getYMax();
const domain = yAxis.getScale().domain();
if (min < 0 && max < 0) {
- expect(domain[0]).to.equal(min);
- expect(domain[1] - boundsMarginValue).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1] - boundsMarginValue).toEqual(max);
} else if (min > 0 && max > 0) {
- expect(domain[0] + boundsMarginValue).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0] + boundsMarginValue).toEqual(min);
+ expect(domain[1]).toEqual(max);
} else {
- expect(domain[0]).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1]).toEqual(max);
}
});
});
diff --git a/src/test_utils/public/helpers/index.ts b/src/test_utils/public/helpers/index.ts
index c8447743ee287..fcc0102c76683 100644
--- a/src/test_utils/public/helpers/index.ts
+++ b/src/test_utils/public/helpers/index.ts
@@ -25,4 +25,9 @@ export { WithMemoryRouter, WithRoute, reactRouterMock } from './router_helpers';
export * from './utils';
-export { setSVGElementGetBBox, setHTMLElementOffset } from './jsdom_svg_mocks';
+export {
+ setSVGElementGetBBox,
+ setHTMLElementOffset,
+ setHTMLElementClientSizes,
+ setSVGElementGetComputedTextLength,
+} from './jsdom_svg_mocks';
diff --git a/src/test_utils/public/helpers/jsdom_svg_mocks.ts b/src/test_utils/public/helpers/jsdom_svg_mocks.ts
index dbc8266f663f1..6ef4204baa2ff 100644
--- a/src/test_utils/public/helpers/jsdom_svg_mocks.ts
+++ b/src/test_utils/public/helpers/jsdom_svg_mocks.ts
@@ -17,6 +17,20 @@
* under the License.
*/
+export const setHTMLElementClientSizes = (width: number, height: number) => {
+ const spyWidth = jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get');
+ spyWidth.mockReturnValue(width);
+ const spyHeight = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get');
+ spyHeight.mockReturnValue(height);
+
+ return {
+ mockRestore: () => {
+ spyWidth.mockRestore();
+ spyHeight.mockRestore();
+ },
+ };
+};
+
export const setSVGElementGetBBox = (
width: number,
height: number,
@@ -41,6 +55,20 @@ export const setSVGElementGetBBox = (
};
};
+export const setSVGElementGetComputedTextLength = (width: number) => {
+ const SVGElementPrototype = SVGElement.prototype as any;
+ const originalGetComputedTextLength = SVGElementPrototype.getComputedTextLength;
+
+ // getComputedTextLength is not in the SVGElement.prototype object by default, so we cannot use jest.spyOn for that case
+ SVGElementPrototype.getComputedTextLength = jest.fn(() => width);
+
+ return {
+ mockRestore: () => {
+ SVGElementPrototype.getComputedTextLength = originalGetComputedTextLength;
+ },
+ };
+};
+
export const setHTMLElementOffset = (width: number, height: number) => {
const offsetWidthSpy = jest.spyOn(window.HTMLElement.prototype, 'offsetWidth', 'get');
offsetWidthSpy.mockReturnValue(width);
diff --git a/src/test_utils/public/index.ts b/src/test_utils/public/index.ts
index 4f46dfe1578db..e57f1ae8ea7a9 100644
--- a/src/test_utils/public/index.ts
+++ b/src/test_utils/public/index.ts
@@ -17,4 +17,9 @@
* under the License.
*/
-export { setSVGElementGetBBox, setHTMLElementOffset } from './helpers';
+export {
+ setSVGElementGetBBox,
+ setHTMLElementOffset,
+ setHTMLElementClientSizes,
+ setSVGElementGetComputedTextLength,
+} from './helpers';
diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js
index 949a01ff7873a..47741c1ab8a0d 100644
--- a/test/functional/apps/discover/_discover.js
+++ b/test/functional/apps/discover/_discover.js
@@ -96,25 +96,32 @@ export default function ({ getService, getPageObjects }) {
it('should modify the time range when a bar is clicked', async function () {
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.discover.clickHistogramBar();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
const time = await PageObjects.timePicker.getTimeConfig();
expect(time.start).to.be('Sep 21, 2015 @ 09:00:00.000');
expect(time.end).to.be('Sep 21, 2015 @ 12:00:00.000');
- const rowData = await PageObjects.discover.getDocTableField(1);
- expect(rowData).to.have.string('Sep 21, 2015 @ 11:59:22.316');
+ await retry.waitFor('doc table to contain the right search result', async () => {
+ const rowData = await PageObjects.discover.getDocTableField(1);
+ log.debug(`The first timestamp value in doc table: ${rowData}`);
+ return rowData.includes('Sep 21, 2015 @ 11:59:22.316');
+ });
});
it('should modify the time range when the histogram is brushed', async function () {
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.discover.brushHistogram();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
expect(Math.round(newDurationHours)).to.be(24);
- const rowData = await PageObjects.discover.getDocTableField(1);
- log.debug(`The first timestamp value in doc table: ${rowData}`);
- expect(Date.parse(rowData)).to.be.within(
- Date.parse('Sep 20, 2015 @ 17:30:00.000'),
- Date.parse('Sep 20, 2015 @ 23:30:00.000')
- );
+
+ await retry.waitFor('doc table to contain the right search result', async () => {
+ const rowData = await PageObjects.discover.getDocTableField(1);
+ log.debug(`The first timestamp value in doc table: ${rowData}`);
+ const dateParsed = Date.parse(rowData);
+ //compare against the parsed date of Sep 20, 2015 @ 17:30:00.000 and Sep 20, 2015 @ 23:30:00.000
+ return dateParsed >= 1442770200000 && dateParsed <= 1442791800000;
+ });
});
it('should show correct initial chart interval of Auto', async function () {
diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js
index f6a092ecb79a8..9bcf7fd2d73b5 100644
--- a/test/functional/apps/discover/_doc_navigation.js
+++ b/test/functional/apps/discover/_doc_navigation.js
@@ -20,14 +20,20 @@
import expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
+ const log = getService('log');
const docTable = getService('docTable');
+ const filterBar = getService('filterBar');
const testSubjects = getService('testSubjects');
- const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
+ const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'context']);
const esArchiver = getService('esArchiver');
const retry = getService('retry');
- describe('doc link in discover', function contextSize() {
- before(async function () {
+ // Flaky: https://github.com/elastic/kibana/issues/71216
+ describe.skip('doc link in discover', function contextSize() {
+ beforeEach(async function () {
+ log.debug('load kibana index with default index pattern');
+ await esArchiver.loadIfNeeded('discover');
+
await esArchiver.loadIfNeeded('logstash_functional');
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setDefaultAbsoluteRange();
@@ -50,5 +56,27 @@ export default function ({ getService, getPageObjects }) {
const hasDocHit = await testSubjects.exists('doc-hit');
expect(hasDocHit).to.be(true);
});
+
+ it('add filter should create an exists filter if value is null (#7189)', async function () {
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ // Filter special document
+ await filterBar.addFilter('agent', 'is', 'Missing/Fields');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ // navigate to the doc view
+ await docTable.clickRowToggle({ rowIndex: 0 });
+
+ const details = await docTable.getDetailsRow();
+ await docTable.addInclusiveFilter(details, 'referer');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ const hasInclusiveFilter = await filterBar.hasFilter('referer', 'exists', true, false, true);
+ expect(hasInclusiveFilter).to.be(true);
+
+ await docTable.removeInclusiveFilter(details, 'referer');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ const hasExcludeFilter = await filterBar.hasFilter('referer', 'exists', true, false, false);
+ expect(hasExcludeFilter).to.be(true);
+ });
});
}
diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js
index 61bb5f7cfee6f..6b423bf6eeb1d 100644
--- a/test/functional/apps/discover/_saved_queries.js
+++ b/test/functional/apps/discover/_saved_queries.js
@@ -20,6 +20,7 @@
import expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
+ const retry = getService('retry');
const log = getService('log');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
@@ -93,7 +94,10 @@ export default function ({ getService, getPageObjects }) {
expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true);
expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime);
expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime);
- expect(await PageObjects.discover.getHitCount()).to.be('2,792');
+ await retry.waitFor(
+ 'the right hit count',
+ async () => (await PageObjects.discover.getHitCount()) === '2,792'
+ );
expect(await savedQueryManagementComponent.getCurrentlyLoadedQueryID()).to.be('OkResponse');
});
@@ -149,7 +153,6 @@ export default function ({ getService, getPageObjects }) {
expect(await queryBar.getQueryString()).to.eql('');
});
- // https://github.com/elastic/kibana/issues/63505
it('allows clearing if non default language was remembered in localstorage', async () => {
await queryBar.switchQueryLanguage('lucene');
await PageObjects.common.navigateToApp('discover'); // makes sure discovered is reloaded without any state in url
@@ -160,9 +163,7 @@ export default function ({ getService, getPageObjects }) {
await queryBar.expectQueryLanguageOrFail('lucene');
});
- // fails: bug in discover https://github.com/elastic/kibana/issues/63561
- // unskip this test when bug is fixed
- it.skip('changing language removes saved query', async () => {
+ it('changing language removes saved query', async () => {
await savedQueryManagementComponent.loadSavedQuery('OkResponse');
await queryBar.switchQueryLanguage('lucene');
expect(await queryBar.getQueryString()).to.eql('');
diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.js b/test/functional/apps/visualize/_data_table_nontimeindex.js
index d64629a65c2c3..fd06257a91ff4 100644
--- a/test/functional/apps/visualize/_data_table_nontimeindex.js
+++ b/test/functional/apps/visualize/_data_table_nontimeindex.js
@@ -112,8 +112,7 @@ export default function ({ getService, getPageObjects }) {
expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']);
});
- // bug https://github.com/elastic/kibana/issues/68977
- describe.skip('data table with date histogram', async () => {
+ describe('data table with date histogram', async () => {
before(async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickDataTable();
@@ -123,7 +122,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.visEditor.clickBucket('Split rows');
await PageObjects.visEditor.selectAggregation('Date Histogram');
await PageObjects.visEditor.selectField('@timestamp');
- await PageObjects.visEditor.setInterval('Daily');
+ await PageObjects.visEditor.setInterval('Day');
await PageObjects.visEditor.clickGo();
});
diff --git a/test/functional/fixtures/es_archiver/logstash_functional/data.json.gz b/test/functional/fixtures/es_archiver/logstash_functional/data.json.gz
index a212c34e2ead6..a4f889da61128 100644
Binary files a/test/functional/fixtures/es_archiver/logstash_functional/data.json.gz and b/test/functional/fixtures/es_archiver/logstash_functional/data.json.gz differ
diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts
index d058695ea6819..03d21aa4aa52f 100644
--- a/test/functional/page_objects/management/saved_objects_page.ts
+++ b/test/functional/page_objects/management/saved_objects_page.ts
@@ -87,13 +87,15 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv
async waitTableIsLoaded() {
return retry.try(async () => {
- const exists = await find.existsByDisplayedByCssSelector(
- '*[data-test-subj="savedObjectsTable"] .euiBasicTable-loading'
+ const isLoaded = await find.existsByDisplayedByCssSelector(
+ '*[data-test-subj="savedObjectsTable"] :not(.euiBasicTable-loading)'
);
- if (exists) {
+
+ if (isLoaded) {
+ return true;
+ } else {
throw new Error('Waiting');
}
- return true;
});
}
diff --git a/test/functional/services/doc_table.ts b/test/functional/services/doc_table.ts
index 52593de68705b..1ac8de69ee5f4 100644
--- a/test/functional/services/doc_table.ts
+++ b/test/functional/services/doc_table.ts
@@ -58,6 +58,11 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont
: (await this.getBodyRows())[options.rowIndex];
}
+ public async getDetailsRow(): Promise
{
+ const table = await this.getTable();
+ return await table.findByCssSelector('[data-test-subj~="docTableDetailsRow"]');
+ }
+
public async getAnchorDetailsRow(): Promise {
const table = await this.getTable();
return await table.findByCssSelector(
@@ -133,6 +138,22 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont
await PageObjects.header.awaitGlobalLoadingIndicatorHidden();
}
+ public async getRemoveInclusiveFilterButton(
+ tableDocViewRow: WebElementWrapper
+ ): Promise {
+ return await tableDocViewRow.findByTestSubject(`~removeInclusiveFilterButton`);
+ }
+
+ public async removeInclusiveFilter(
+ detailsRow: WebElementWrapper,
+ fieldName: WebElementWrapper
+ ): Promise {
+ const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName);
+ const addInclusiveFilterButton = await this.getRemoveInclusiveFilterButton(tableDocViewRow);
+ await addInclusiveFilterButton.click();
+ await PageObjects.header.awaitGlobalLoadingIndicatorHidden();
+ }
+
public async getAddExistsFilterButton(
tableDocViewRow: WebElementWrapper
): Promise {
diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts
index f6531f8d872c2..98ab1babd60fe 100644
--- a/test/functional/services/filter_bar.ts
+++ b/test/functional/services/filter_bar.ts
@@ -31,17 +31,21 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon
* @param key field name
* @param value filter value
* @param enabled filter status
+ * @param pinned filter pinned status
+ * @param negated filter including or excluding value
*/
public async hasFilter(
key: string,
value: string,
enabled: boolean = true,
- pinned: boolean = false
+ pinned: boolean = false,
+ negated: boolean = false
): Promise {
const filterActivationState = enabled ? 'enabled' : 'disabled';
const filterPinnedState = pinned ? 'pinned' : 'unpinned';
+ const filterNegatedState = negated ? 'filter-negated' : '';
return testSubjects.exists(
- `filter filter-${filterActivationState} filter-key-${key} filter-value-${value} filter-${filterPinnedState}`,
+ `filter filter-${filterActivationState} filter-key-${key} filter-value-${value} filter-${filterPinnedState} ${filterNegatedState}`,
{
allowHidden: true,
}
diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts
new file mode 100644
index 0000000000000..1fd927d82f186
--- /dev/null
+++ b/x-pack/plugins/apm/common/anomaly_detection.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface ServiceAnomalyStats {
+ transactionType?: string;
+ anomalyScore?: number;
+ actualValue?: number;
+ jobId?: string;
+}
diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts
index 43f3585d0ebb2..b50db270ef544 100644
--- a/x-pack/plugins/apm/common/service_map.ts
+++ b/x-pack/plugins/apm/common/service_map.ts
@@ -15,11 +15,13 @@ import {
SPAN_SUBTYPE,
SPAN_TYPE,
} from './elasticsearch_fieldnames';
+import { ServiceAnomalyStats } from './anomaly_detection';
export interface ServiceConnectionNode extends cytoscape.NodeDataDefinition {
[SERVICE_NAME]: string;
[SERVICE_ENVIRONMENT]: string | null;
[AGENT_NAME]: string;
+ serviceAnomalyStats?: ServiceAnomalyStats;
}
export interface ExternalConnectionNode extends cytoscape.NodeDataDefinition {
[SPAN_DESTINATION_SERVICE_RESOURCE]: string;
@@ -37,8 +39,10 @@ export interface Connection {
export interface ServiceNodeMetrics {
avgMemoryUsage: number | null;
avgCpuUsage: number | null;
- avgTransactionDuration: number | null;
- avgRequestsPerMinute: number | null;
+ transactionStats: {
+ avgTransactionDuration: number | null;
+ avgRequestsPerMinute: number | null;
+ };
avgErrorsPerMinute: number | null;
}
diff --git a/x-pack/plugins/apm/common/utils/range_filter.ts b/x-pack/plugins/apm/common/utils/range_filter.ts
index 08062cbf76bc6..9ffec18d95fb0 100644
--- a/x-pack/plugins/apm/common/utils/range_filter.ts
+++ b/x-pack/plugins/apm/common/utils/range_filter.ts
@@ -4,13 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export function rangeFilter(
- start: number,
- end: number,
- timestampField = '@timestamp'
-) {
+export function rangeFilter(start: number, end: number) {
return {
- [timestampField]: {
+ '@timestamp': {
gte: start,
lte: end,
format: 'epoch_millis',
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx
new file mode 100644
index 0000000000000..410ba8b5027fb
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx
@@ -0,0 +1,158 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import styled from 'styled-components';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiIconTip,
+ EuiHealth,
+} from '@elastic/eui';
+import { useTheme } from '../../../../hooks/useTheme';
+import { fontSize, px } from '../../../../style/variables';
+import { asInteger, asDuration } from '../../../../utils/formatters';
+import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink';
+import { getSeverityColor, popoverWidth } from '../cytoscapeOptions';
+import { getSeverity } from '../../../../../common/ml_job_constants';
+import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types';
+import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection';
+
+const HealthStatusTitle = styled(EuiTitle)`
+ display: inline;
+ text-transform: uppercase;
+`;
+
+const VerticallyCentered = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
+const SubduedText = styled.span`
+ color: ${({ theme }) => theme.eui.euiTextSubduedColor};
+`;
+
+const EnableText = styled.section`
+ color: ${({ theme }) => theme.eui.euiTextSubduedColor};
+ line-height: 1.4;
+ font-size: ${fontSize};
+ width: ${px(popoverWidth)};
+`;
+
+export const ContentLine = styled.section`
+ line-height: 2;
+`;
+
+interface Props {
+ serviceName: string;
+ serviceAnomalyStats: ServiceAnomalyStats | undefined;
+}
+export function AnomalyDetection({ serviceName, serviceAnomalyStats }: Props) {
+ const theme = useTheme();
+
+ const anomalyScore = serviceAnomalyStats?.anomalyScore;
+ const anomalySeverity = getSeverity(anomalyScore);
+ const actualValue = serviceAnomalyStats?.actualValue;
+ const mlJobId = serviceAnomalyStats?.jobId;
+ const transactionType =
+ serviceAnomalyStats?.transactionType ?? TRANSACTION_REQUEST;
+ const hasAnomalyDetectionScore = anomalyScore !== undefined;
+
+ return (
+ <>
+
+
+ {ANOMALY_DETECTION_TITLE}
+
+
+
+ {!mlJobId && {ANOMALY_DETECTION_DISABLED_TEXT} }
+
+ {hasAnomalyDetectionScore && (
+
+
+
+
+
+ {ANOMALY_DETECTION_SCORE_METRIC}
+
+
+
+
+ {getDisplayedAnomalyScore(anomalyScore as number)}
+ {actualValue && (
+ ({asDuration(actualValue)})
+ )}
+
+
+
+
+ )}
+ {mlJobId && !hasAnomalyDetectionScore && (
+ {ANOMALY_DETECTION_NO_DATA_TEXT}
+ )}
+ {mlJobId && (
+
+
+ {ANOMALY_DETECTION_LINK}
+
+
+ )}
+ >
+ );
+}
+
+function getDisplayedAnomalyScore(score: number) {
+ if (score > 0 && score < 1) {
+ return '< 1';
+ }
+ return asInteger(score);
+}
+
+const ANOMALY_DETECTION_TITLE = i18n.translate(
+ 'xpack.apm.serviceMap.anomalyDetectionPopoverTitle',
+ { defaultMessage: 'Anomaly Detection' }
+);
+
+const ANOMALY_DETECTION_TOOLTIP = i18n.translate(
+ 'xpack.apm.serviceMap.anomalyDetectionPopoverTooltip',
+ {
+ defaultMessage:
+ 'Service health indicators are powered by the anomaly detection feature in machine learning',
+ }
+);
+
+const ANOMALY_DETECTION_SCORE_METRIC = i18n.translate(
+ 'xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric',
+ { defaultMessage: 'Score (max.)' }
+);
+
+const ANOMALY_DETECTION_LINK = i18n.translate(
+ 'xpack.apm.serviceMap.anomalyDetectionPopoverLink',
+ { defaultMessage: 'View anomalies' }
+);
+
+const ANOMALY_DETECTION_DISABLED_TEXT = i18n.translate(
+ 'xpack.apm.serviceMap.anomalyDetectionPopoverDisabled',
+ {
+ defaultMessage:
+ 'Display service health indicators by enabling anomaly detection in APM settings.',
+ }
+);
+
+const ANOMALY_DETECTION_NO_DATA_TEXT = i18n.translate(
+ 'xpack.apm.serviceMap.anomalyDetectionPopoverNoData',
+ {
+ defaultMessage: `We couldn't find an anomaly score within the selected time range. See details in the anomaly explorer.`,
+ }
+);
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx
index 78779bdcc2052..c696a93773ceb 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx
@@ -15,7 +15,7 @@ import React, { MouseEvent } from 'react';
import { Buttons } from './Buttons';
import { Info } from './Info';
import { ServiceMetricFetcher } from './ServiceMetricFetcher';
-import { popoverMinWidth } from '../cytoscapeOptions';
+import { popoverWidth } from '../cytoscapeOptions';
interface ContentsProps {
isService: boolean;
@@ -60,7 +60,7 @@ export function Contents({
@@ -68,16 +68,12 @@ export function Contents({
- {/* //TODO [APM ML] add service health stats here:
- isService && (
-
-
-
-
- )*/}
{isService ? (
-
+
) : (
)}
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx
index 2edd36f0d1380..ccf147ed1d90d 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx
@@ -12,40 +12,33 @@ storiesOf('app/ServiceMap/Popover/ServiceMetricList', module)
.add('example', () => (
- ))
- .add('loading', () => (
-
))
.add('some null values', () => (
))
.add('all null values', () => (
));
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx
index 718e43984d7f3..957678877a134 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx
@@ -5,23 +5,38 @@
*/
import React from 'react';
+import {
+ EuiLoadingSpinner,
+ EuiFlexGroup,
+ EuiHorizontalRule,
+ EuiText,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { isNumber } from 'lodash';
import { ServiceNodeMetrics } from '../../../../../common/service_map';
-import { useFetcher } from '../../../../hooks/useFetcher';
+import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { ServiceMetricList } from './ServiceMetricList';
+import { AnomalyDetection } from './AnomalyDetection';
+import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection';
interface ServiceMetricFetcherProps {
serviceName: string;
+ serviceAnomalyStats: ServiceAnomalyStats | undefined;
}
export function ServiceMetricFetcher({
serviceName,
+ serviceAnomalyStats,
}: ServiceMetricFetcherProps) {
const {
urlParams: { start, end, environment },
} = useUrlParams();
- const { data = {} as ServiceNodeMetrics, status } = useFetcher(
+ const {
+ data = { transactionStats: {} } as ServiceNodeMetrics,
+ status,
+ } = useFetcher(
(callApmApi) => {
if (serviceName && start && end) {
return callApmApi({
@@ -35,7 +50,62 @@ export function ServiceMetricFetcher({
preservePreviousData: false,
}
);
- const isLoading = status === 'loading';
- return ;
+ const isLoading =
+ status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
+
+ if (isLoading) {
+ return ;
+ }
+
+ const {
+ avgCpuUsage,
+ avgErrorsPerMinute,
+ avgMemoryUsage,
+ transactionStats: { avgRequestsPerMinute, avgTransactionDuration },
+ } = data;
+
+ const hasServiceData = [
+ avgCpuUsage,
+ avgErrorsPerMinute,
+ avgMemoryUsage,
+ avgRequestsPerMinute,
+ avgTransactionDuration,
+ ].some((stat) => isNumber(stat));
+
+ if (environment && !hasServiceData) {
+ return (
+
+ {i18n.translate('xpack.apm.serviceMap.popoverMetrics.noDataText', {
+ defaultMessage: `No data for selected environment. Try switching to another environment.`,
+ })}
+
+ );
+ }
+ return (
+ <>
+ {serviceAnomalyStats && (
+ <>
+
+
+ >
+ )}
+
+ >
+ );
+}
+
+function LoadingSpinner() {
+ return (
+
+
+
+ );
}
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx
index d66be9c61e42d..f82f434e7ded1 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isNumber } from 'lodash';
import React from 'react';
@@ -12,18 +11,6 @@ import styled from 'styled-components';
import { ServiceNodeMetrics } from '../../../../../common/service_map';
import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters';
-function LoadingSpinner() {
- return (
-
-
-
- );
-}
-
export const ItemRow = styled('tr')`
line-height: 2;
`;
@@ -37,17 +24,13 @@ export const ItemDescription = styled('td')`
text-align: right;
`;
-interface ServiceMetricListProps extends ServiceNodeMetrics {
- isLoading: boolean;
-}
+type ServiceMetricListProps = ServiceNodeMetrics;
export function ServiceMetricList({
- avgTransactionDuration,
- avgRequestsPerMinute,
avgErrorsPerMinute,
avgCpuUsage,
avgMemoryUsage,
- isLoading,
+ transactionStats,
}: ServiceMetricListProps) {
const listItems = [
{
@@ -57,8 +40,8 @@ export function ServiceMetricList({
defaultMessage: 'Trans. duration (avg.)',
}
),
- description: isNumber(avgTransactionDuration)
- ? asDuration(avgTransactionDuration)
+ description: isNumber(transactionStats.avgTransactionDuration)
+ ? asDuration(transactionStats.avgTransactionDuration)
: null,
},
{
@@ -68,8 +51,10 @@ export function ServiceMetricList({
defaultMessage: 'Req. per minute (avg.)',
}
),
- description: isNumber(avgRequestsPerMinute)
- ? `${avgRequestsPerMinute.toFixed(2)} ${tpmUnit('request')}`
+ description: isNumber(transactionStats.avgRequestsPerMinute)
+ ? `${transactionStats.avgRequestsPerMinute.toFixed(2)} ${tpmUnit(
+ 'request'
+ )}`
: null,
},
{
@@ -100,9 +85,7 @@ export function ServiceMetricList({
},
];
- return isLoading ? (
-
- ) : (
+ return (
{listItems.map(
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts
index 5a2a3d2a2644e..dfcfbee1806a4 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts
@@ -10,10 +10,11 @@ import {
SPAN_DESTINATION_SERVICE_RESOURCE,
} from '../../../../common/elasticsearch_fieldnames';
import { EuiTheme } from '../../../../../observability/public';
-import { severity } from '../../../../common/ml_job_constants';
+import { severity, getSeverity } from '../../../../common/ml_job_constants';
import { defaultIcon, iconForNode } from './icons';
+import { ServiceAnomalyStats } from '../../../../common/anomaly_detection';
-export const popoverMinWidth = 280;
+export const popoverWidth = 280;
export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) {
switch (nodeSeverity) {
@@ -29,12 +30,19 @@ export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) {
}
}
+function getNodeSeverity(el: cytoscape.NodeSingular) {
+ const serviceAnomalyStats: ServiceAnomalyStats | undefined = el.data(
+ 'serviceAnomalyStats'
+ );
+ return getSeverity(serviceAnomalyStats?.anomalyScore);
+}
+
function getBorderColorFn(
theme: EuiTheme
): cytoscape.Css.MapperFunction {
return (el: cytoscape.NodeSingular) => {
- const hasAnomalyDetectionJob = el.data('ml_job_id') !== undefined;
- const nodeSeverity = el.data('anomaly_severity');
+ const hasAnomalyDetectionJob = el.data('serviceAnomalyStats') !== undefined;
+ const nodeSeverity = getNodeSeverity(el);
if (hasAnomalyDetectionJob) {
return (
getSeverityColor(theme, nodeSeverity) || theme.eui.euiColorMediumShade
@@ -51,7 +59,7 @@ const getBorderStyle: cytoscape.Css.MapperFunction<
cytoscape.NodeSingular,
cytoscape.Css.LineStyle
> = (el: cytoscape.NodeSingular) => {
- const nodeSeverity = el.data('anomaly_severity');
+ const nodeSeverity = getNodeSeverity(el);
if (nodeSeverity === severity.critical) {
return 'double';
} else {
@@ -60,7 +68,7 @@ const getBorderStyle: cytoscape.Css.MapperFunction<
};
function getBorderWidth(el: cytoscape.NodeSingular) {
- const nodeSeverity = el.data('anomaly_severity');
+ const nodeSeverity = getNodeSeverity(el);
if (nodeSeverity === severity.minor || nodeSeverity === severity.major) {
return 4;
diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx
index 1e6015a9589b0..2f41b9fedd1d1 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx
@@ -25,7 +25,7 @@ export function AgentConfigurations() {
(callApmApi) =>
callApmApi({ pathname: '/api/apm/settings/agent-configuration' }),
[],
- { preservePreviousData: false }
+ { preservePreviousData: false, showToastOnError: false }
);
useTrackPageview({ app: 'apm', path: 'agent_configuration' });
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
index 81655bc46c336..4ef3d78a7d303 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
@@ -10,9 +10,15 @@ import { i18n } from '@kbn/i18n';
import { EuiPanel } from '@elastic/eui';
import { JobsList } from './jobs_list';
import { AddEnvironments } from './add_environments';
-import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
+import { useFetcher } from '../../../../hooks/useFetcher';
import { LicensePrompt } from '../../../shared/LicensePrompt';
import { useLicense } from '../../../../hooks/useLicense';
+import { APIReturnType } from '../../../../services/rest/createCallApmApi';
+
+const DEFAULT_VALUE: APIReturnType<'/api/apm/settings/anomaly-detection'> = {
+ jobs: [],
+ hasLegacyJobs: false,
+};
export const AnomalyDetection = () => {
const license = useLicense();
@@ -20,17 +26,13 @@ export const AnomalyDetection = () => {
const [viewAddEnvironments, setViewAddEnvironments] = useState(false);
- const { refetch, data = [], status } = useFetcher(
+ const { refetch, data = DEFAULT_VALUE, status } = useFetcher(
(callApmApi) =>
callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }),
[],
- { preservePreviousData: false }
+ { preservePreviousData: false, showToastOnError: false }
);
- const isLoading =
- status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
- const hasFetchFailure = status === FETCH_STATUS.FAILURE;
-
if (!hasValidLicense) {
return (
@@ -66,7 +68,7 @@ export const AnomalyDetection = () => {
{viewAddEnvironments ? (
environment)}
+ currentEnvironments={data.jobs.map(({ environment }) => environment)}
onCreateJobSuccess={() => {
refetch();
setViewAddEnvironments(false);
@@ -77,9 +79,9 @@ export const AnomalyDetection = () => {
/>
) : (
{
setViewAddEnvironments(true);
}}
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
index 30b4805011f03..674b4492c2c9c 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
@@ -16,12 +16,14 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
import { AnomalyDetectionJobByEnv } from '../../../../../typings/anomaly_detection';
import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink';
import { MLLink } from '../../../shared/Links/MachineLearningLinks/MLLink';
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
+import { LegacyJobsCallout } from './legacy_jobs_callout';
const columns: Array> = [
{
@@ -60,17 +62,22 @@ const columns: Array> = [
];
interface Props {
- isLoading: boolean;
- hasFetchFailure: boolean;
+ status: FETCH_STATUS;
onAddEnvironments: () => void;
anomalyDetectionJobsByEnv: AnomalyDetectionJobByEnv[];
+ hasLegacyJobs: boolean;
}
export const JobsList = ({
- isLoading,
- hasFetchFailure,
+ status,
onAddEnvironments,
anomalyDetectionJobsByEnv,
+ hasLegacyJobs,
}: Props) => {
+ const isLoading =
+ status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
+
+ const hasFetchFailure = status === FETCH_STATUS.FAILURE;
+
return (
@@ -131,6 +138,8 @@ export const JobsList = ({
items={isLoading || hasFetchFailure ? [] : anomalyDetectionJobsByEnv}
/>
+
+ {hasLegacyJobs && }
);
};
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx
new file mode 100644
index 0000000000000..54053097ab02e
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiCallOut, EuiButton } from '@elastic/eui';
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
+
+export function LegacyJobsCallout() {
+ const { core } = useApmPluginContext();
+ return (
+
+
+ {i18n.translate(
+ 'xpack.apm.settings.anomaly_detection.legacy_jobs.body',
+ {
+ defaultMessage:
+ 'We have discovered legacy Machine Learning jobs from our previous integration which are no longer being used in the APM app',
+ }
+ )}
+
+
+ {i18n.translate(
+ 'xpack.apm.settings.anomaly_detection.legacy_jobs.button',
+ { defaultMessage: 'Review jobs' }
+ )}
+
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx
index b4cf3a65fea35..c832d3ded6175 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx
@@ -18,8 +18,24 @@ describe('MLJobLink', () => {
{ search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location
);
- expect(href).toEqual(
- `/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))`
+ expect(href).toMatchInlineSnapshot(
+ `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))"`
+ );
+ });
+ it('should produce the correct URL with jobId, serviceName, and transactionType', async () => {
+ const href = await getRenderedHref(
+ () => (
+
+ ),
+ { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location
+ );
+
+ expect(href).toMatchInlineSnapshot(
+ `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request)))"`
);
});
});
diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx
index 1e1f9ea5f23b7..f3c5b49287293 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx
@@ -5,24 +5,35 @@
*/
import React from 'react';
-import { MLLink } from './MLLink';
+import { EuiLink } from '@elastic/eui';
+import { useTimeSeriesExplorerHref } from './useTimeSeriesExplorerHref';
interface Props {
jobId: string;
external?: boolean;
+ serviceName?: string;
+ transactionType?: string;
}
-export const MLJobLink: React.FC = (props) => {
- const query = {
- ml: { jobIds: [props.jobId] },
- };
+export const MLJobLink: React.FC = ({
+ jobId,
+ serviceName,
+ transactionType,
+ external,
+ children,
+}) => {
+ const href = useTimeSeriesExplorerHref({
+ jobId,
+ serviceName,
+ transactionType,
+ });
return (
-
);
};
diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts
new file mode 100644
index 0000000000000..625b9205b6ce0
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts
@@ -0,0 +1,58 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import url from 'url';
+import querystring from 'querystring';
+import rison from 'rison-node';
+import { useLocation } from '../../../../hooks/useLocation';
+import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
+import { getTimepickerRisonData } from '../rison_helpers';
+
+export function useTimeSeriesExplorerHref({
+ jobId,
+ serviceName,
+ transactionType,
+}: {
+ jobId: string;
+ serviceName?: string;
+ transactionType?: string;
+}) {
+ const { core } = useApmPluginContext();
+ const location = useLocation();
+
+ const search = querystring.stringify(
+ {
+ _g: rison.encode({
+ ml: { jobIds: [jobId] },
+ ...getTimepickerRisonData(location.search),
+ }),
+ ...(serviceName && transactionType
+ ? {
+ _a: rison.encode({
+ mlTimeSeriesExplorer: {
+ entities: {
+ 'service.name': serviceName,
+ 'transaction.type': transactionType,
+ },
+ },
+ }),
+ }
+ : null),
+ },
+ undefined,
+ undefined,
+ {
+ encodeURIComponent(str: string) {
+ return str;
+ },
+ }
+ );
+
+ return url.format({
+ pathname: core.http.basePath.prepend('/app/ml'),
+ hash: url.format({ pathname: '/timeseriesexplorer', search }),
+ });
+}
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/constants.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/constants.ts
new file mode 100644
index 0000000000000..bfc4fcde09972
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/constants.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const ML_MODULE_ID_APM_TRANSACTION = 'apm_transaction';
+export const APM_ML_JOB_GROUP = 'apm';
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
index 406097805775d..e723393a24013 100644
--- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
@@ -6,6 +6,7 @@
import { Logger } from 'kibana/server';
import uuid from 'uuid/v4';
+import { snakeCase } from 'lodash';
import { PromiseReturnType } from '../../../../observability/typings/common';
import { Setup } from '../helpers/setup_request';
import {
@@ -14,9 +15,7 @@ import {
PROCESSOR_EVENT,
} from '../../../common/elasticsearch_fieldnames';
import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
-
-const ML_MODULE_ID_APM_TRANSACTION = 'apm_transaction';
-export const ML_GROUP_NAME_APM = 'apm';
+import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants';
export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType<
typeof createAnomalyDetectionJobs
@@ -78,13 +77,12 @@ async function createAnomalyDetectionJob({
environment: string;
indexPatternName?: string | undefined;
}) {
- const convertedEnvironmentName = convertToMLIdentifier(environment);
const randomToken = uuid().substr(-4);
return ml.modules.setup({
moduleId: ML_MODULE_ID_APM_TRANSACTION,
- prefix: `${ML_GROUP_NAME_APM}-${convertedEnvironmentName}-${randomToken}-`,
- groups: [ML_GROUP_NAME_APM, convertedEnvironmentName],
+ prefix: `${APM_ML_JOB_GROUP}-${snakeCase(environment)}-${randomToken}-`,
+ groups: [APM_ML_JOB_GROUP],
indexPatternName,
query: {
bool: {
@@ -101,7 +99,11 @@ async function createAnomalyDetectionJob({
jobOverrides: [
{
custom_settings: {
- job_tags: { environment },
+ job_tags: {
+ environment,
+ // identifies this as an APM ML job & facilitates future migrations
+ apm_ml_version: 2,
+ },
},
},
],
@@ -117,7 +119,3 @@ const ENVIRONMENT_NOT_DEFINED_FILTER = {
},
},
};
-
-export function convertToMLIdentifier(value: string) {
- return value.replace(/\s+/g, '_').toLowerCase();
-}
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts
index 252c87e9263db..8fdebeb597eaf 100644
--- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts
@@ -5,56 +5,34 @@
*/
import { Logger } from 'kibana/server';
-import { PromiseReturnType } from '../../../../observability/typings/common';
import { Setup } from '../helpers/setup_request';
-import { AnomalyDetectionJobByEnv } from '../../../typings/anomaly_detection';
-import { ML_GROUP_NAME_APM } from './create_anomaly_detection_jobs';
+import { getMlJobsWithAPMGroup } from './get_ml_jobs_by_group';
-export type AnomalyDetectionJobsAPIResponse = PromiseReturnType<
- typeof getAnomalyDetectionJobs
->;
-export async function getAnomalyDetectionJobs(
- setup: Setup,
- logger: Logger
-): Promise {
+export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) {
const { ml } = setup;
if (!ml) {
return [];
}
- try {
- const mlCapabilities = await ml.mlSystem.mlCapabilities();
- if (
- !(
- mlCapabilities.mlFeatureEnabledInSpace &&
- mlCapabilities.isPlatinumOrTrialLicense
- )
- ) {
- logger.warn(
- 'Anomaly detection integration is not availble for this user.'
- );
- return [];
- }
- } catch (error) {
- logger.warn('Unable to get ML capabilities.');
- logger.error(error);
- return [];
- }
- try {
- const { jobs } = await ml.anomalyDetectors.jobs(ML_GROUP_NAME_APM);
- return jobs
- .map((job) => {
- const environment = job.custom_settings?.job_tags?.environment ?? '';
- return {
- job_id: job.job_id,
- environment,
- };
- })
- .filter((job) => job.environment);
- } catch (error) {
- if (error.statusCode !== 404) {
- logger.warn('Unable to get APM ML jobs.');
- logger.error(error);
- }
+
+ const mlCapabilities = await ml.mlSystem.mlCapabilities();
+ if (
+ !(
+ mlCapabilities.mlFeatureEnabledInSpace &&
+ mlCapabilities.isPlatinumOrTrialLicense
+ )
+ ) {
+ logger.warn('Anomaly detection integration is not availble for this user.');
return [];
}
+
+ const response = await getMlJobsWithAPMGroup(ml);
+ return response.jobs
+ .filter((job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2)
+ .map((job) => {
+ const environment = job.custom_settings?.job_tags?.environment ?? '';
+ return {
+ job_id: job.job_id,
+ environment,
+ };
+ });
}
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.ts
new file mode 100644
index 0000000000000..5c0a3d17648aa
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Setup } from '../helpers/setup_request';
+import { APM_ML_JOB_GROUP } from './constants';
+
+// returns ml jobs containing "apm" group
+// workaround: the ML api returns 404 when no jobs are found. This is handled so instead of throwing an empty response is returned
+export async function getMlJobsWithAPMGroup(ml: NonNullable) {
+ try {
+ return await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP);
+ } catch (e) {
+ if (e.statusCode === 404) {
+ return { count: 0, jobs: [] };
+ }
+
+ throw e;
+ }
+}
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts
new file mode 100644
index 0000000000000..bf502607fcc1d
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Setup } from '../helpers/setup_request';
+import { getMlJobsWithAPMGroup } from './get_ml_jobs_by_group';
+
+// Determine whether there are any legacy ml jobs.
+// A legacy ML job has a job id that ends with "high_mean_response_time" and created_by=ml-module-apm-transaction
+export async function hasLegacyJobs(setup: Setup) {
+ const { ml } = setup;
+
+ if (!ml) {
+ return false;
+ }
+
+ const response = await getMlJobsWithAPMGroup(ml);
+ return response.jobs.some(
+ (job) =>
+ job.job_id.endsWith('high_mean_response_time') &&
+ job.custom_settings?.created_by === 'ml-module-apm-transaction'
+ );
+}
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
index c648cf4cc116a..e3161b49b315d 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
@@ -65,4 +65,40 @@ describe('data telemetry collection tasks', () => {
});
});
});
+
+ describe('integrations', () => {
+ const integrationsTask = tasks.find((task) => task.name === 'integrations');
+
+ it('returns the count of ML jobs', async () => {
+ const transportRequest = jest
+ .fn()
+ .mockResolvedValueOnce({ body: { count: 1 } });
+
+ expect(
+ await integrationsTask?.executor({ indices, transportRequest } as any)
+ ).toEqual({
+ integrations: {
+ ml: {
+ all_jobs_count: 1,
+ },
+ },
+ });
+ });
+
+ describe('with no data', () => {
+ it('returns a count of 0', async () => {
+ const transportRequest = jest.fn().mockResolvedValueOnce({});
+
+ expect(
+ await integrationsTask?.executor({ indices, transportRequest } as any)
+ ).toEqual({
+ integrations: {
+ ml: {
+ all_jobs_count: 0,
+ },
+ },
+ });
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
index f27af9a2cc516..4bbaaf3e86e78 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
@@ -4,31 +4,31 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { flatten, merge, sortBy, sum } from 'lodash';
-import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
+import { TelemetryTask } from '.';
import { AGENT_NAMES } from '../../../../common/agent_name';
-import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
import {
- PROCESSOR_EVENT,
- SERVICE_NAME,
AGENT_NAME,
AGENT_VERSION,
+ CLOUD_AVAILABILITY_ZONE,
+ CLOUD_PROVIDER,
+ CLOUD_REGION,
ERROR_GROUP_ID,
- TRANSACTION_NAME,
PARENT_ID,
+ PROCESSOR_EVENT,
SERVICE_FRAMEWORK_NAME,
SERVICE_FRAMEWORK_VERSION,
SERVICE_LANGUAGE_NAME,
SERVICE_LANGUAGE_VERSION,
+ SERVICE_NAME,
SERVICE_RUNTIME_NAME,
SERVICE_RUNTIME_VERSION,
+ TRANSACTION_NAME,
USER_AGENT_ORIGINAL,
- CLOUD_AVAILABILITY_ZONE,
- CLOUD_PROVIDER,
- CLOUD_REGION,
} from '../../../../common/elasticsearch_fieldnames';
-import { Span } from '../../../../typings/es_schemas/ui/span';
import { APMError } from '../../../../typings/es_schemas/ui/apm_error';
-import { TelemetryTask } from '.';
+import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
+import { Span } from '../../../../typings/es_schemas/ui/span';
+import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
import { APMTelemetry } from '../types';
const TIME_RANGES = ['1d', 'all'] as const;
@@ -465,17 +465,17 @@ export const tasks: TelemetryTask[] = [
{
name: 'integrations',
executor: async ({ transportRequest }) => {
- const apmJobs = ['*-high_mean_response_time'];
+ const apmJobs = ['apm-*', '*-high_mean_response_time'];
const response = (await transportRequest({
method: 'get',
path: `/_ml/anomaly_detectors/${apmJobs.join(',')}`,
- })) as { data?: { count: number } };
+ })) as { body?: { count: number } };
return {
integrations: {
ml: {
- all_jobs_count: response.data?.count ?? 0,
+ all_jobs_count: response.body?.count ?? 0,
},
},
};
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts
new file mode 100644
index 0000000000000..3e5ef5eb37b02
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts
@@ -0,0 +1,166 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Logger } from 'kibana/server';
+import { Setup, SetupTimeRange } from '../helpers/setup_request';
+import { PromiseReturnType } from '../../../typings/common';
+import {
+ TRANSACTION_PAGE_LOAD,
+ TRANSACTION_REQUEST,
+} from '../../../common/transaction_types';
+import { ServiceAnomalyStats } from '../../../common/anomaly_detection';
+import { APM_ML_JOB_GROUP } from '../anomaly_detection/constants';
+
+export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: {} };
+
+export type ServiceAnomaliesResponse = PromiseReturnType<
+ typeof getServiceAnomalies
+>;
+
+export async function getServiceAnomalies({
+ setup,
+ logger,
+ environment,
+}: {
+ setup: Setup & SetupTimeRange;
+ logger: Logger;
+ environment?: string;
+}) {
+ const { ml, start, end } = setup;
+
+ if (!ml) {
+ logger.warn('Anomaly detection plugin is not available.');
+ return DEFAULT_ANOMALIES;
+ }
+ const mlCapabilities = await ml.mlSystem.mlCapabilities();
+ if (!mlCapabilities.mlFeatureEnabledInSpace) {
+ logger.warn('Anomaly detection feature is not enabled for the space.');
+ return DEFAULT_ANOMALIES;
+ }
+ if (!mlCapabilities.isPlatinumOrTrialLicense) {
+ logger.warn(
+ 'Unable to create anomaly detection jobs due to insufficient license.'
+ );
+ return DEFAULT_ANOMALIES;
+ }
+
+ let mlJobIds: string[] = [];
+ try {
+ mlJobIds = await getMLJobIds(ml, environment);
+ } catch (error) {
+ logger.error(error);
+ return DEFAULT_ANOMALIES;
+ }
+
+ const params = {
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ { term: { result_type: 'record' } },
+ { terms: { job_id: mlJobIds } },
+ {
+ range: {
+ timestamp: { gte: start, lte: end, format: 'epoch_millis' },
+ },
+ },
+ {
+ terms: {
+ // Only retrieving anomalies for transaction types "request" and "page-load"
+ by_field_value: [TRANSACTION_REQUEST, TRANSACTION_PAGE_LOAD],
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ services: {
+ terms: { field: 'partition_field_value' },
+ aggs: {
+ top_score: {
+ top_hits: {
+ sort: { record_score: 'desc' },
+ _source: { includes: ['actual', 'job_id', 'by_field_value'] },
+ size: 1,
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+ const response = await ml.mlSystem.mlAnomalySearch(params);
+ return {
+ mlJobIds,
+ serviceAnomalies: transformResponseToServiceAnomalies(
+ response as ServiceAnomaliesAggResponse
+ ),
+ };
+}
+
+interface ServiceAnomaliesAggResponse {
+ aggregations: {
+ services: {
+ buckets: Array<{
+ key: string;
+ top_score: {
+ hits: {
+ hits: Array<{
+ sort: [number];
+ _source: {
+ actual: [number];
+ job_id: string;
+ by_field_value: string;
+ };
+ }>;
+ };
+ };
+ }>;
+ };
+ };
+}
+
+function transformResponseToServiceAnomalies(
+ response: ServiceAnomaliesAggResponse
+): Record {
+ const serviceAnomaliesMap = response.aggregations.services.buckets.reduce(
+ (statsByServiceName, { key: serviceName, top_score: topScoreAgg }) => {
+ return {
+ ...statsByServiceName,
+ [serviceName]: {
+ transactionType: topScoreAgg.hits.hits[0]?._source?.by_field_value,
+ anomalyScore: topScoreAgg.hits.hits[0]?.sort?.[0],
+ actualValue: topScoreAgg.hits.hits[0]?._source?.actual?.[0],
+ jobId: topScoreAgg.hits.hits[0]?._source?.job_id,
+ },
+ };
+ },
+ {}
+ );
+ return serviceAnomaliesMap;
+}
+
+export async function getMLJobIds(
+ ml: Required['ml'],
+ environment?: string
+) {
+ const response = await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP);
+ // to filter out legacy jobs we are filtering by the existence of `apm_ml_version` in `custom_settings`
+ // and checking that it is compatable.
+ const mlJobs = response.jobs.filter(
+ (job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2
+ );
+ if (environment) {
+ const matchingMLJob = mlJobs.find(
+ (job) => job.custom_settings?.job_tags?.environment === environment
+ );
+ if (!matchingMLJob) {
+ throw new Error(`ML job Not Found for environment "${environment}".`);
+ }
+ return [matchingMLJob.job_id];
+ }
+ return mlJobs.map((job) => job.job_id);
+}
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
index 4d488cd1a5509..ea2bb14efdfc7 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { chunk } from 'lodash';
+import { Logger } from 'kibana/server';
import {
AGENT_NAME,
SERVICE_ENVIRONMENT,
@@ -16,11 +17,17 @@ import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { transformServiceMapResponses } from './transform_service_map_responses';
import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids';
import { getTraceSampleIds } from './get_trace_sample_ids';
+import {
+ getServiceAnomalies,
+ ServiceAnomaliesResponse,
+ DEFAULT_ANOMALIES,
+} from './get_service_anomalies';
export interface IEnvOptions {
setup: Setup & SetupTimeRange;
serviceName?: string;
environment?: string;
+ logger: Logger;
}
async function getConnectionData({
@@ -132,13 +139,23 @@ export type ServicesResponse = PromiseReturnType;
export type ServiceMapAPIResponse = PromiseReturnType;
export async function getServiceMap(options: IEnvOptions) {
- const [connectionData, servicesData] = await Promise.all([
+ const { logger } = options;
+ const anomaliesPromise: Promise = getServiceAnomalies(
+ options
+ ).catch((error) => {
+ logger.warn(`Unable to retrieve anomalies for service maps.`);
+ logger.error(error);
+ return DEFAULT_ANOMALIES;
+ });
+ const [connectionData, servicesData, anomalies] = await Promise.all([
getConnectionData(options),
getServicesData(options),
+ anomaliesPromise,
]);
return transformServiceMapResponses({
...connectionData,
services: servicesData,
+ anomalies,
});
}
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
index e521efa687388..be92bfe5a0099 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
@@ -12,11 +12,17 @@ import {
SERVICE_ENVIRONMENT,
SERVICE_NAME,
TRANSACTION_DURATION,
+ TRANSACTION_TYPE,
METRIC_SYSTEM_CPU_PERCENT,
METRIC_SYSTEM_FREE_MEMORY,
METRIC_SYSTEM_TOTAL_MEMORY,
} from '../../../common/elasticsearch_fieldnames';
import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory';
+import {
+ TRANSACTION_REQUEST,
+ TRANSACTION_PAGE_LOAD,
+} from '../../../common/transaction_types';
+import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
interface Options {
setup: Setup & SetupTimeRange;
@@ -37,12 +43,23 @@ export async function getServiceMapServiceNodeInfo({
}: Options & { serviceName: string; environment?: string }) {
const { start, end } = setup;
+ const environmentNotDefinedFilter = {
+ bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] },
+ };
+
const filter: ESFilter[] = [
{ range: rangeFilter(start, end) },
{ term: { [SERVICE_NAME]: serviceName } },
- ...(environment ? [{ term: { [SERVICE_ENVIRONMENT]: environment } }] : []),
];
+ if (environment) {
+ filter.push(
+ environment === ENVIRONMENT_NOT_DEFINED
+ ? environmentNotDefinedFilter
+ : { term: { [SERVICE_ENVIRONMENT]: environment } }
+ );
+ }
+
const minutes = Math.abs((end - start) / (1000 * 60));
const taskParams = {
@@ -53,19 +70,19 @@ export async function getServiceMapServiceNodeInfo({
const [
errorMetrics,
- transactionMetrics,
+ transactionStats,
cpuMetrics,
memoryMetrics,
] = await Promise.all([
getErrorMetrics(taskParams),
- getTransactionMetrics(taskParams),
+ getTransactionStats(taskParams),
getCpuMetrics(taskParams),
getMemoryMetrics(taskParams),
]);
return {
...errorMetrics,
- ...transactionMetrics,
+ transactionStats,
...cpuMetrics,
...memoryMetrics,
};
@@ -99,7 +116,7 @@ async function getErrorMetrics({ setup, minutes, filter }: TaskParameters) {
};
}
-async function getTransactionMetrics({
+async function getTransactionStats({
setup,
filter,
minutes,
@@ -109,17 +126,28 @@ async function getTransactionMetrics({
}> {
const { indices, client } = setup;
- const response = await client.search({
+ const params = {
index: indices['apm_oss.transactionIndices'],
body: {
- size: 1,
+ size: 0,
query: {
bool: {
- filter: filter.concat({
- term: {
- [PROCESSOR_EVENT]: 'transaction',
+ filter: [
+ ...filter,
+ {
+ term: {
+ [PROCESSOR_EVENT]: 'transaction',
+ },
},
- }),
+ {
+ terms: {
+ [TRANSACTION_TYPE]: [
+ TRANSACTION_REQUEST,
+ TRANSACTION_PAGE_LOAD,
+ ],
+ },
+ },
+ ],
},
},
track_total_hits: true,
@@ -131,14 +159,12 @@ async function getTransactionMetrics({
},
},
},
- });
-
+ };
+ const response = await client.search(params);
+ const docCount = response.hits.total.value;
return {
avgTransactionDuration: response.aggregations?.duration.value ?? null,
- avgRequestsPerMinute:
- response.hits.total.value > 0
- ? response.hits.total.value / minutes
- : null,
+ avgRequestsPerMinute: docCount > 0 ? docCount / minutes : null,
};
}
diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts
index 1e26634bdf0f1..7e4bcfdda7382 100644
--- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts
@@ -35,6 +35,18 @@ const javaService = {
[AGENT_NAME]: 'java',
};
+const anomalies = {
+ mlJobIds: ['apm-test-1234-ml-module-name'],
+ serviceAnomalies: {
+ 'opbeans-test': {
+ transactionType: 'request',
+ actualValue: 10000,
+ anomalyScore: 50,
+ jobId: 'apm-test-1234-ml-module-name',
+ },
+ },
+};
+
describe('transformServiceMapResponses', () => {
it('maps external destinations to internal services', () => {
const response: ServiceMapResponse = {
@@ -51,6 +63,7 @@ describe('transformServiceMapResponses', () => {
destination: nodejsExternal,
},
],
+ anomalies,
};
const { elements } = transformServiceMapResponses(response);
@@ -89,6 +102,7 @@ describe('transformServiceMapResponses', () => {
},
},
],
+ anomalies,
};
const { elements } = transformServiceMapResponses(response);
@@ -126,6 +140,7 @@ describe('transformServiceMapResponses', () => {
},
},
],
+ anomalies,
};
const { elements } = transformServiceMapResponses(response);
@@ -150,6 +165,7 @@ describe('transformServiceMapResponses', () => {
destination: nodejsService,
},
],
+ anomalies,
};
const { elements } = transformServiceMapResponses(response);
diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts
index 2e394f44b25b1..7f5e34f68f922 100644
--- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts
@@ -18,6 +18,7 @@ import {
ExternalConnectionNode,
} from '../../../common/service_map';
import { ConnectionsResponse, ServicesResponse } from './get_service_map';
+import { ServiceAnomaliesResponse } from './get_service_anomalies';
function getConnectionNodeId(node: ConnectionNode): string {
if ('span.destination.service.resource' in node) {
@@ -63,10 +64,11 @@ export function getServiceNodes(allNodes: ConnectionNode[]) {
export type ServiceMapResponse = ConnectionsResponse & {
services: ServicesResponse;
+ anomalies: ServiceAnomaliesResponse;
};
export function transformServiceMapResponses(response: ServiceMapResponse) {
- const { discoveredServices, services, connections } = response;
+ const { discoveredServices, services, connections, anomalies } = response;
const allNodes = getAllNodes(services, connections);
const serviceNodes = getServiceNodes(allNodes);
@@ -100,21 +102,23 @@ export function transformServiceMapResponses(response: ServiceMapResponse) {
serviceName = node[SERVICE_NAME];
}
- const matchedServiceNodes = serviceNodes.filter(
- (serviceNode) => serviceNode[SERVICE_NAME] === serviceName
- );
+ const matchedServiceNodes = serviceNodes
+ .filter((serviceNode) => serviceNode[SERVICE_NAME] === serviceName)
+ .map((serviceNode) => pickBy(serviceNode, identity));
+ const mergedServiceNode = Object.assign({}, ...matchedServiceNodes);
+
+ const serviceAnomalyStats = serviceName
+ ? anomalies.serviceAnomalies[serviceName]
+ : null;
if (matchedServiceNodes.length) {
return {
...map,
- [node.id]: Object.assign(
- {
- id: matchedServiceNodes[0][SERVICE_NAME],
- },
- ...matchedServiceNodes.map((serviceNode) =>
- pickBy(serviceNode, identity)
- )
- ),
+ [node.id]: {
+ id: matchedServiceNodes[0][SERVICE_NAME],
+ ...mergedServiceNode,
+ ...(serviceAnomalyStats ? { serviceAnomalyStats } : null),
+ },
};
}
diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts
index a3e2f708b0b22..50123131a42e7 100644
--- a/x-pack/plugins/apm/server/routes/service_map.ts
+++ b/x-pack/plugins/apm/server/routes/service_map.ts
@@ -37,11 +37,12 @@ export const serviceMapRoute = createRoute(() => ({
}
context.licensing.featureUsage.notifyUsage(APM_SERVICE_MAPS_FEATURE_NAME);
+ const logger = context.logger;
const setup = await setupRequest(context, request);
const {
query: { serviceName, environment },
} = context.params;
- return getServiceMap({ setup, serviceName, environment });
+ return getServiceMap({ setup, serviceName, environment, logger });
},
}));
diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
index 67eca0da946d0..7009470e1ff17 100644
--- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
+++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
@@ -10,6 +10,7 @@ import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly
import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs';
import { setupRequest } from '../../lib/helpers/setup_request';
import { getAllEnvironments } from '../../lib/environments/get_all_environments';
+import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs';
// get ML anomaly detection jobs for each environment
export const anomalyDetectionJobsRoute = createRoute(() => ({
@@ -17,7 +18,11 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({
path: '/api/apm/settings/anomaly-detection',
handler: async ({ context, request }) => {
const setup = await setupRequest(context, request);
- return await getAnomalyDetectionJobs(setup, context.logger);
+ const jobs = await getAnomalyDetectionJobs(setup, context.logger);
+ return {
+ jobs,
+ hasLegacyJobs: await hasLegacyJobs(setup),
+ };
},
}));
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 225432375dc75..e5037a6477aca 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
@@ -5,6 +5,8 @@
*/
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 DELETE_PHASE_POLICY = {
version: 1,
@@ -26,7 +28,7 @@ export const DELETE_PHASE_POLICY = {
min_age: '0ms',
actions: {
wait_for_snapshot: {
- policy: 'my_snapshot_policy',
+ policy: SNAPSHOT_POLICY_NAME,
},
delete: {
delete_searchable_snapshot: true,
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 d6c955e0c0813..cba496ee0f212 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
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import React from 'react';
import { act } from 'react-dom/test-utils';
import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils';
@@ -14,6 +15,25 @@ import { TestSubjects } from '../helpers';
import { EditPolicy } from '../../../public/application/sections/edit_policy';
import { indexLifecycleManagementStore } from '../../../public/application/store';
+jest.mock('@elastic/eui', () => {
+ const original = jest.requireActual('@elastic/eui');
+
+ return {
+ ...original,
+ // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions,
+ // which does not produce a valid component wrapper
+ EuiComboBox: (props: any) => (
+ {
+ props.onChange([syntheticEvent['0']]);
+ }}
+ />
+ ),
+ };
+});
+
const testBedConfig: TestBedConfig = {
store: () => indexLifecycleManagementStore(),
memoryRouter: {
@@ -34,9 +54,11 @@ export interface EditPolicyTestBed extends TestBed {
export const setup = async (): Promise => {
const testBed = await initTestBed();
- const setWaitForSnapshotPolicy = (snapshotPolicyName: string) => {
- const { component, form } = testBed;
- form.setInputValue('waitForSnapshotField', snapshotPolicyName, true);
+ const setWaitForSnapshotPolicy = async (snapshotPolicyName: string) => {
+ const { component } = testBed;
+ act(() => {
+ testBed.find('snapshotPolicyCombobox').simulate('change', [{ label: snapshotPolicyName }]);
+ });
component.update();
};
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
index 8753f01376d42..06829e6ef6f1e 100644
--- 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
@@ -7,11 +7,10 @@
import { act } from 'react-dom/test-utils';
import { setupEnvironment } from '../helpers/setup_environment';
-
import { EditPolicyTestBed, setup } from './edit_policy.helpers';
-import { DELETE_PHASE_POLICY } from './constants';
import { API_BASE_PATH } from '../../../common/constants';
+import { DELETE_PHASE_POLICY, NEW_SNAPSHOT_POLICY_NAME, SNAPSHOT_POLICY_NAME } from './constants';
window.scrollTo = jest.fn();
@@ -25,6 +24,10 @@ describe(' ', () => {
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();
@@ -35,16 +38,18 @@ describe(' ', () => {
});
test('wait for snapshot policy field should correctly display snapshot policy name', () => {
- expect(testBed.find('waitForSnapshotField').props().value).toEqual(
- DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy
- );
+ expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([
+ {
+ label: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy,
+ value: 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;
- const newPolicyName = 'my_new_snapshot_policy';
- actions.setWaitForSnapshotPolicy(newPolicyName);
+ await actions.setWaitForSnapshotPolicy(NEW_SNAPSHOT_POLICY_NAME);
await actions.savePolicy();
const expected = {
@@ -56,7 +61,7 @@ describe(' ', () => {
actions: {
...DELETE_PHASE_POLICY.policy.phases.delete.actions,
wait_for_snapshot: {
- policy: newPolicyName,
+ policy: NEW_SNAPSHOT_POLICY_NAME,
},
},
},
@@ -69,6 +74,15 @@ describe(' ', () => {
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;
@@ -92,5 +106,31 @@ describe(' ', () => {
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();
+ });
});
});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts
index f41742fc104ff..04f58f93939ca 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SinonFakeServer, fakeServer } from 'sinon';
+import { fakeServer, SinonFakeServer } from 'sinon';
import { API_BASE_PATH } from '../../../common/constants';
export const init = () => {
@@ -27,7 +27,19 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
]);
};
+ const setLoadSnapshotPolicies = (response: any = [], error?: { status: number; body: any }) => {
+ const status = error ? error.status : 200;
+ const body = error ? error.body : response;
+
+ server.respondWith('GET', `${API_BASE_PATH}/snapshot_policies`, [
+ status,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(body),
+ ]);
+ };
+
return {
setLoadPolicies,
+ setLoadSnapshotPolicies,
};
};
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts
index 3cff2e3ab050f..7b227f822fa97 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts
@@ -4,4 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export type TestSubjects = 'waitForSnapshotField' | 'savePolicyButton';
+export type TestSubjects =
+ | 'snapshotPolicyCombobox'
+ | 'savePolicyButton'
+ | 'customPolicyCallout'
+ | 'noPoliciesCallout'
+ | 'policiesErrorCallout';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js
index 299bf28778ab4..34d1c0f8de216 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js
@@ -7,17 +7,12 @@
import React, { PureComponent, Fragment } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiDescribedFormGroup,
- EuiSwitch,
- EuiFieldText,
- EuiTextColor,
- EuiFormRow,
-} from '@elastic/eui';
+import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui';
import { PHASE_DELETE, PHASE_ENABLED, PHASE_WAIT_FOR_SNAPSHOT_POLICY } from '../../../../constants';
import { ActiveBadge, LearnMoreLink, OptionalLabel, PhaseErrorMessage } from '../../../components';
import { MinAgeInput } from '../min_age_input';
+import { SnapshotPolicies } from '../snapshot_policies';
export class DeletePhase extends PureComponent {
static propTypes = {
@@ -125,10 +120,9 @@ export class DeletePhase extends PureComponent {
}
>
- setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, e.target.value)}
+ onChange={(value) => setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, value)}
/>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts
new file mode 100644
index 0000000000000..f33ce81eb6157
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { SnapshotPolicies } from './snapshot_policies';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx
new file mode 100644
index 0000000000000..76eae0f906d0c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx
@@ -0,0 +1,157 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment } from 'react';
+
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+
+import {
+ EuiButtonIcon,
+ EuiCallOut,
+ EuiComboBox,
+ EuiComboBoxOptionOption,
+ EuiSpacer,
+} from '@elastic/eui';
+
+import { useLoadSnapshotPolicies } from '../../../../services/api';
+
+interface Props {
+ value: string;
+ onChange: (value: string) => void;
+}
+export const SnapshotPolicies: React.FunctionComponent = ({ value, onChange }) => {
+ const { error, isLoading, data, sendRequest } = useLoadSnapshotPolicies();
+
+ const policies = data.map((name: string) => ({
+ label: name,
+ value: name,
+ }));
+
+ const onComboChange = (options: EuiComboBoxOptionOption[]) => {
+ if (options.length > 0) {
+ onChange(options[0].label);
+ } else {
+ onChange('');
+ }
+ };
+
+ const onCreateOption = (newValue: string) => {
+ onChange(newValue);
+ };
+
+ let calloutContent;
+ if (error) {
+ calloutContent = (
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+ );
+ } else if (data.length === 0) {
+ calloutContent = (
+
+
+
+ }
+ >
+
+
+
+ );
+ } else if (value && !data.includes(value)) {
+ calloutContent = (
+
+
+
+ }
+ >
+
+
+
+ );
+ }
+
+ return (
+
+
+ {calloutContent}
+
+ );
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js
index dad259681eb7a..500ab44d96694 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js
@@ -254,7 +254,7 @@ export class PolicyTable extends Component {
icon: 'list',
onClick: () => {
this.props.navigateToApp('management', {
- path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`)}`,
+ path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`, true)}`,
});
},
});
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts
similarity index 56%
rename from x-pack/plugins/index_lifecycle_management/public/application/services/api.js
rename to x-pack/plugins/index_lifecycle_management/public/application/services/api.ts
index 6b46d6e6ea735..065fb3bcebca7 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js
+++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts
@@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { METRIC_TYPE } from '@kbn/analytics';
+import { trackUiMetric } from './ui_metric';
+
import {
UIM_POLICY_DELETE,
UIM_POLICY_ATTACH_INDEX,
@@ -12,14 +15,13 @@ import {
UIM_INDEX_RETRY_STEP,
} from '../constants';
-import { trackUiMetric } from './ui_metric';
-import { sendGet, sendPost, sendDelete } from './http';
+import { sendGet, sendPost, sendDelete, useRequest } from './http';
export async function loadNodes() {
return await sendGet(`nodes/list`);
}
-export async function loadNodeDetails(selectedNodeAttrs) {
+export async function loadNodeDetails(selectedNodeAttrs: string) {
return await sendGet(`nodes/${selectedNodeAttrs}/details`);
}
@@ -27,45 +29,53 @@ export async function loadIndexTemplates() {
return await sendGet(`templates`);
}
-export async function loadPolicies(withIndices) {
+export async function loadPolicies(withIndices: boolean) {
return await sendGet('policies', { withIndices });
}
-export async function savePolicy(policy) {
+export async function savePolicy(policy: any) {
return await sendPost(`policies`, policy);
}
-export async function deletePolicy(policyName) {
+export async function deletePolicy(policyName: string) {
const response = await sendDelete(`policies/${encodeURIComponent(policyName)}`);
// Only track successful actions.
- trackUiMetric('count', UIM_POLICY_DELETE);
+ trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_DELETE);
return response;
}
-export const retryLifecycleForIndex = async (indexNames) => {
+export const retryLifecycleForIndex = async (indexNames: string[]) => {
const response = await sendPost(`index/retry`, { indexNames });
// Only track successful actions.
- trackUiMetric('count', UIM_INDEX_RETRY_STEP);
+ trackUiMetric(METRIC_TYPE.COUNT, UIM_INDEX_RETRY_STEP);
return response;
};
-export const removeLifecycleForIndex = async (indexNames) => {
+export const removeLifecycleForIndex = async (indexNames: string[]) => {
const response = await sendPost(`index/remove`, { indexNames });
// Only track successful actions.
- trackUiMetric('count', UIM_POLICY_DETACH_INDEX);
+ trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_DETACH_INDEX);
return response;
};
-export const addLifecyclePolicyToIndex = async (body) => {
+export const addLifecyclePolicyToIndex = async (body: any) => {
const response = await sendPost(`index/add`, body);
// Only track successful actions.
- trackUiMetric('count', UIM_POLICY_ATTACH_INDEX);
+ trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_ATTACH_INDEX);
return response;
};
-export const addLifecyclePolicyToTemplate = async (body) => {
+export const addLifecyclePolicyToTemplate = async (body: any) => {
const response = await sendPost(`template`, body);
// Only track successful actions.
- trackUiMetric('count', UIM_POLICY_ATTACH_INDEX_TEMPLATE);
+ trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_ATTACH_INDEX_TEMPLATE);
return response;
};
+
+export const useLoadSnapshotPolicies = () => {
+ return useRequest({
+ path: `snapshot_policies`,
+ method: 'get',
+ initialData: [],
+ });
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts
index 47e96ea28bb8c..c54ee15fd69bf 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts
@@ -4,6 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import {
+ UseRequestConfig,
+ useRequest as _useRequest,
+ Error,
+} from '../../../../../../src/plugins/es_ui_shared/public';
+
let _httpClient: any;
export function init(httpClient: any): void {
@@ -24,10 +30,14 @@ export function sendPost(path: string, payload: any): any {
return _httpClient.post(getFullPath(path), { body: JSON.stringify(payload) });
}
-export function sendGet(path: string, query: any): any {
+export function sendGet(path: string, query?: any): any {
return _httpClient.get(getFullPath(path), { query });
}
export function sendDelete(path: string): any {
return _httpClient.delete(getFullPath(path));
}
+
+export const useRequest = (config: UseRequestConfig) => {
+ return _useRequest(_httpClient, { ...config, path: getFullPath(config.path) });
+};
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/index.ts
new file mode 100644
index 0000000000000..19fbc45010ea2
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/index.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { RouteDependencies } from '../../../types';
+import { registerFetchRoute } from './register_fetch_route';
+
+export function registerSnapshotPoliciesRoutes(dependencies: RouteDependencies) {
+ registerFetchRoute(dependencies);
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts
new file mode 100644
index 0000000000000..7a52648e29ee8
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { LegacyAPICaller } from 'src/core/server';
+
+import { RouteDependencies } from '../../../types';
+import { addBasePath } from '../../../services';
+
+async function fetchSnapshotPolicies(callAsCurrentUser: LegacyAPICaller): Promise {
+ const params = {
+ method: 'GET',
+ path: '/_slm/policy',
+ };
+
+ return await callAsCurrentUser('transport.request', params);
+}
+
+export function registerFetchRoute({ router, license, lib }: RouteDependencies) {
+ router.get(
+ { path: addBasePath('/snapshot_policies'), validate: false },
+ license.guardApiRoute(async (context, request, response) => {
+ try {
+ const policiesByName = await fetchSnapshotPolicies(
+ context.core.elasticsearch.legacy.client.callAsCurrentUser
+ );
+ return response.ok({ body: Object.keys(policiesByName) });
+ } catch (e) {
+ if (lib.isEsError(e)) {
+ return response.customError({
+ statusCode: e.statusCode,
+ body: e,
+ });
+ }
+ // Case: default
+ return response.internalError({ body: e });
+ }
+ })
+ );
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts
index 35996721854c6..f7390debbe177 100644
--- a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts
@@ -10,10 +10,12 @@ import { registerIndexRoutes } from './api/index';
import { registerNodesRoutes } from './api/nodes';
import { registerPoliciesRoutes } from './api/policies';
import { registerTemplatesRoutes } from './api/templates';
+import { registerSnapshotPoliciesRoutes } from './api/snapshot_policies';
export function registerApiRoutes(dependencies: RouteDependencies) {
registerIndexRoutes(dependencies);
registerNodesRoutes(dependencies);
registerPoliciesRoutes(dependencies);
registerTemplatesRoutes(dependencies);
+ registerSnapshotPoliciesRoutes(dependencies);
}
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
index d85db94d4a970..ad445f75f047c 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
@@ -34,7 +34,11 @@ export const services = {
services.uiMetricService.setup({ reportUiStats() {} } as any);
setExtensionsService(services.extensionsService);
setUiMetricService(services.uiMetricService);
-const appDependencies = { services, core: { getUrlForApp: () => {} }, plugins: {} } as any;
+const appDependencies = {
+ services,
+ core: { getUrlForApp: () => {} },
+ plugins: {},
+} as any;
export const setupEnvironment = () => {
// Mock initialization of services
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts
index ecea230ecab85..9397ce21ba827 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts
@@ -166,7 +166,7 @@ export const setup = async (overridingDependencies: any = {}): Promise ({
name,
- timeStampField: { name: '@timestamp', mapping: { type: 'date' } },
+ timeStampField: { name: '@timestamp' },
indices: [
{
name: 'indexName',
diff --git a/x-pack/plugins/index_management/common/types/data_streams.ts b/x-pack/plugins/index_management/common/types/data_streams.ts
index 772ed43459bcf..d1936c4426b49 100644
--- a/x-pack/plugins/index_management/common/types/data_streams.ts
+++ b/x-pack/plugins/index_management/common/types/data_streams.ts
@@ -6,9 +6,6 @@
interface TimestampFieldFromEs {
name: string;
- mapping: {
- type: string;
- };
}
type TimestampField = TimestampFieldFromEs;
diff --git a/x-pack/plugins/index_management/public/application/sections/home/home.tsx b/x-pack/plugins/index_management/public/application/sections/home/home.tsx
index 7bd04cdbf0c91..ee8970a3c4509 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/home.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/home.tsx
@@ -144,7 +144,6 @@ export const IndexManagementHome: React.FunctionComponent
-
index.name);
const selectedIndexNames = Object.keys(selectedIndicesMap);
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
index fe6c9ad3d8e07..de2fc29ec8543 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
@@ -18,8 +18,9 @@ import {
EuiFlexItem,
EuiCodeBlock,
} from '@elastic/eui';
+import { useAppContext } from '../../../../../app_context';
import { TemplateDeserialized } from '../../../../../../../common';
-import { getILMPolicyPath } from '../../../../../services/navigation';
+import { getILMPolicyPath } from '../../../../../services/routing';
interface Props {
templateDetails: TemplateDeserialized;
@@ -51,6 +52,10 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails })
const numIndexPatterns = indexPatterns.length;
+ const {
+ core: { getUrlForApp },
+ } = useAppContext();
+
return (
@@ -153,7 +158,13 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails })
{ilmPolicy && ilmPolicy.name ? (
- {ilmPolicy.name}
+
+ {ilmPolicy.name}
+
) : (
i18nTexts.none
)}
diff --git a/x-pack/plugins/index_management/public/application/services/navigation.ts b/x-pack/plugins/index_management/public/application/services/navigation.ts
deleted file mode 100644
index 3b4977bb63751..0000000000000
--- a/x-pack/plugins/index_management/public/application/services/navigation.ts
+++ /dev/null
@@ -1,21 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-export const getIndexListUri = (filter: any) => {
- if (filter) {
- // React router tries to decode url params but it can't because the browser partially
- // decodes them. So we have to encode both the URL and the filter to get it all to
- // work correctly for filters with URL unsafe characters in them.
- return encodeURI(`/indices/filter/${encodeURIComponent(filter)}`);
- }
-
- // If no filter, URI is already safe so no need to encode.
- return '/indices';
-};
-
-export const getILMPolicyPath = (policyName: string) => {
- return encodeURI(`/policies/edit/${encodeURIComponent(policyName)}`);
-};
diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts
index 8831fa2368f47..68bf06409e6ab 100644
--- a/x-pack/plugins/index_management/public/application/services/routing.ts
+++ b/x-pack/plugins/index_management/public/application/services/routing.ts
@@ -31,6 +31,28 @@ export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => {
return encodeURI(url);
};
+export const getILMPolicyPath = (policyName: string) => {
+ return encodeURI(
+ `/data/index_lifecycle_management/policies/edit/${encodeURIComponent(policyName)}`
+ );
+};
+
+export const getIndexListUri = (filter?: string, includeHiddenIndices?: boolean) => {
+ const hiddenIndicesParam =
+ typeof includeHiddenIndices !== 'undefined' ? includeHiddenIndices : false;
+ if (filter) {
+ // React router tries to decode url params but it can't because the browser partially
+ // decodes them. So we have to encode both the URL and the filter to get it all to
+ // work correctly for filters with URL unsafe characters in them.
+ return encodeURI(
+ `/indices?includeHiddenIndices=${hiddenIndicesParam}&filter=${encodeURIComponent(filter)}`
+ );
+ }
+
+ // If no filter, URI is already safe so no need to encode.
+ return '/indices';
+};
+
export const decodePathFromReactRouter = (pathname: string): string => {
let decodedPath;
try {
diff --git a/x-pack/plugins/index_management/public/index.ts b/x-pack/plugins/index_management/public/index.ts
index 7a76fff7f3ec6..a2e9a41feb165 100644
--- a/x-pack/plugins/index_management/public/index.ts
+++ b/x-pack/plugins/index_management/public/index.ts
@@ -13,4 +13,4 @@ export const plugin = () => {
export { IndexManagementPluginSetup };
-export { getIndexListUri } from './application/services/navigation';
+export { getIndexListUri } from './application/services/routing';
diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts
index 5f4e625348333..b91c7b4650180 100644
--- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts
+++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts
@@ -17,7 +17,9 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou
const { callAsCurrentUser } = ctx.dataManagement!.client;
try {
- const dataStreams = await callAsCurrentUser('dataManagement.getDataStreams');
+ const { data_streams: dataStreams } = await callAsCurrentUser(
+ 'dataManagement.getDataStreams'
+ );
const body = deserializeDataStreamList(dataStreams);
return res.ok({ body });
@@ -50,7 +52,10 @@ export function registerGetOneRoute({ router, license, lib: { isEsError } }: Rou
const { callAsCurrentUser } = ctx.dataManagement!.client;
try {
- const dataStream = await callAsCurrentUser('dataManagement.getDataStream', { name });
+ const { data_streams: dataStream } = await callAsCurrentUser(
+ 'dataManagement.getDataStream',
+ { name }
+ );
if (dataStream[0]) {
const body = deserializeDataStream(dataStream[0]);
diff --git a/x-pack/plugins/ingest_manager/common/services/agent_status.ts b/x-pack/plugins/ingest_manager/common/services/agent_status.ts
index b1d92d3a78e65..6489c30308771 100644
--- a/x-pack/plugins/ingest_manager/common/services/agent_status.ts
+++ b/x-pack/plugins/ingest_manager/common/services/agent_status.ts
@@ -5,63 +5,52 @@
*/
import {
- AGENT_TYPE_TEMPORARY,
AGENT_POLLING_THRESHOLD_MS,
AGENT_TYPE_PERMANENT,
- AGENT_TYPE_EPHEMERAL,
AGENT_SAVED_OBJECT_TYPE,
} from '../constants';
import { Agent, AgentStatus } from '../types';
export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentStatus {
- const { type, last_checkin: lastCheckIn } = agent;
- const msLastCheckIn = new Date(lastCheckIn || 0).getTime();
- const msSinceLastCheckIn = new Date().getTime() - msLastCheckIn;
- const intervalsSinceLastCheckIn = Math.floor(msSinceLastCheckIn / AGENT_POLLING_THRESHOLD_MS);
+ const { last_checkin: lastCheckIn } = agent;
+
if (!agent.active) {
return 'inactive';
}
+ if (!agent.last_checkin) {
+ return 'enrolling';
+ }
if (agent.unenrollment_started_at && !agent.unenrolled_at) {
return 'unenrolling';
}
- if (agent.current_error_events.length > 0) {
+
+ const msLastCheckIn = new Date(lastCheckIn || 0).getTime();
+ const msSinceLastCheckIn = new Date().getTime() - msLastCheckIn;
+ const intervalsSinceLastCheckIn = Math.floor(msSinceLastCheckIn / AGENT_POLLING_THRESHOLD_MS);
+
+ if (agent.last_checkin_status === 'error') {
return 'error';
}
- switch (type) {
- case AGENT_TYPE_PERMANENT:
- if (intervalsSinceLastCheckIn >= 4) {
- return 'error';
- }
- case AGENT_TYPE_TEMPORARY:
- if (intervalsSinceLastCheckIn >= 3) {
- return 'offline';
- }
- case AGENT_TYPE_EPHEMERAL:
- if (intervalsSinceLastCheckIn >= 3) {
- return 'inactive';
- }
+ if (agent.last_checkin_status === 'degraded') {
+ return 'degraded';
+ }
+ if (intervalsSinceLastCheckIn >= 4) {
+ return 'offline';
}
+
return 'online';
}
export function buildKueryForOnlineAgents() {
- return `(${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} and ${AGENT_SAVED_OBJECT_TYPE}.last_checkin >= now-${
- (4 * AGENT_POLLING_THRESHOLD_MS) / 1000
- }s) or (${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_TEMPORARY} and ${AGENT_SAVED_OBJECT_TYPE}.last_checkin >= now-${
- (3 * AGENT_POLLING_THRESHOLD_MS) / 1000
- }s) or (${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_EPHEMERAL} and ${AGENT_SAVED_OBJECT_TYPE}.last_checkin >= now-${
- (3 * AGENT_POLLING_THRESHOLD_MS) / 1000
- }s)`;
+ return `not (${buildKueryForOfflineAgents()}) AND not (${buildKueryForErrorAgents()})`;
}
-export function buildKueryForOfflineAgents() {
- return `${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_TEMPORARY} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${
- (3 * AGENT_POLLING_THRESHOLD_MS) / 1000
- }s`;
+export function buildKueryForErrorAgents() {
+ return `( ${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:error or ${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:degraded )`;
}
-export function buildKueryForErrorAgents() {
- return `${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${
+export function buildKueryForOfflineAgents() {
+ return `((${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${
(4 * AGENT_POLLING_THRESHOLD_MS) / 1000
- }s`;
+ }s) AND not ( ${buildKueryForErrorAgents()} ))`;
}
diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts
index 1f4718acc2c1f..d3789c58a2c22 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts
@@ -11,7 +11,16 @@ export type AgentType =
| typeof AGENT_TYPE_PERMANENT
| typeof AGENT_TYPE_TEMPORARY;
-export type AgentStatus = 'offline' | 'error' | 'online' | 'inactive' | 'warning' | 'unenrolling';
+export type AgentStatus =
+ | 'offline'
+ | 'error'
+ | 'online'
+ | 'inactive'
+ | 'warning'
+ | 'enrolling'
+ | 'unenrolling'
+ | 'degraded';
+
export type AgentActionType = 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE' | 'UNENROLL';
export interface NewAgentAction {
type: AgentActionType;
@@ -82,6 +91,7 @@ interface AgentBase {
config_id?: string;
config_revision?: number | null;
last_checkin?: string;
+ last_checkin_status?: 'error' | 'online' | 'degraded';
user_provided_metadata: AgentMetadata;
local_metadata: AgentMetadata;
}
diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
index 1105c8ee7ca82..ed7d73ab0b719 100644
--- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
+++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
@@ -47,6 +47,7 @@ export interface PostAgentCheckinRequest {
agentId: string;
};
body: {
+ status?: 'online' | 'error' | 'degraded';
local_metadata?: Record;
events?: NewAgentEvent[];
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx
index 30204603e764c..36a8bf908ddd7 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx
@@ -178,11 +178,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
}
if (selectedStatus.length) {
- if (kuery) {
- kuery = `(${kuery}) and`;
- }
-
- kuery = selectedStatus
+ const kueryStatus = selectedStatus
.map((status) => {
switch (status) {
case 'online':
@@ -196,6 +192,12 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
return '';
})
.join(' or ');
+
+ if (kuery) {
+ kuery = `(${kuery}) and ${kueryStatus}`;
+ } else {
+ kuery = kueryStatus;
+ }
}
const agentsRequest = useGetAgents(
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx
index e4dfa520259eb..7c6c95cab420f 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx
@@ -53,6 +53,22 @@ const Status = {
/>
),
+ Degraded: (
+
+
+
+ ),
+ Enrolling: (
+
+
+
+ ),
Unenrolling: (
= {};
+ const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, data.events);
if (updatedErrorEvents) {
updateData.current_error_events = JSON.stringify(updatedErrorEvents);
}
- if (localMetadata) {
- updateData.local_metadata = localMetadata;
+ if (data.localMetadata) {
+ updateData.local_metadata = data.localMetadata;
+ }
+
+ if (data.status !== agent.last_checkin_status) {
+ updateData.last_checkin_status = data.status;
}
if (Object.keys(updateData).length > 0) {
await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, updateData);
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts
index 96e006b78f00f..994ecc64c82a7 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts
@@ -59,7 +59,7 @@ export function agentCheckinStateConnectedAgentsFactory() {
const internalSOClient = getInternalUserSOClient();
const now = new Date().toISOString();
const updates: Array> = [
- ...connectedAgentsIds.values(),
+ ...agentToUpdate.values(),
].map((agentId) => ({
type: AGENT_SAVED_OBJECT_TYPE,
id: agentId,
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
index 8140b1e6de470..f216cd541eb21 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
@@ -33,6 +33,7 @@ describe('Agent status service', () => {
type: AGENT_TYPE_PERMANENT,
attributes: {
active: true,
+ last_checkin: new Date().toISOString(),
local_metadata: {},
user_provided_metadata: {},
},
@@ -40,4 +41,36 @@ describe('Agent status service', () => {
const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
expect(status).toEqual('online');
});
+
+ it('should return enrolling when agent is active but never checkin', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.get = jest.fn().mockReturnValue({
+ id: 'id',
+ type: AGENT_TYPE_PERMANENT,
+ attributes: {
+ active: true,
+ local_metadata: {},
+ user_provided_metadata: {},
+ },
+ } as SavedObject);
+ const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
+ expect(status).toEqual('enrolling');
+ });
+
+ it('should return unenrolling when agent is unenrolling', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.get = jest.fn().mockReturnValue({
+ id: 'id',
+ type: AGENT_TYPE_PERMANENT,
+ attributes: {
+ active: true,
+ last_checkin: new Date().toISOString(),
+ unenrollment_started_at: new Date().toISOString(),
+ local_metadata: {},
+ user_provided_metadata: {},
+ },
+ } as SavedObject);
+ const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
+ expect(status).toEqual('unenrolling');
+ });
});
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
index 848e65b7931eb..7437321163749 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
@@ -99,7 +99,8 @@ exports[`tests loading base.yml: base.yml 1`] = `
"package": {
"name": "nginx"
},
- "managed_by": "ingest-manager"
+ "managed_by": "ingest-manager",
+ "managed": true
}
}
`;
@@ -203,7 +204,8 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
"package": {
"name": "coredns"
},
- "managed_by": "ingest-manager"
+ "managed_by": "ingest-manager",
+ "managed": true
}
}
`;
@@ -1691,7 +1693,8 @@ exports[`tests loading system.yml: system.yml 1`] = `
"package": {
"name": "system"
},
- "managed_by": "ingest-manager"
+ "managed_by": "ingest-manager",
+ "managed": true
}
}
`;
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
index e7867532ed176..77ad96952269f 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
@@ -317,6 +317,7 @@ function getBaseTemplate(
name: packageName,
},
managed_by: 'ingest-manager',
+ managed: true,
},
};
}
diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
index a508c33e0347b..3e9209efcac04 100644
--- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
+++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
@@ -32,6 +32,9 @@ export const PostAgentCheckinRequestSchema = {
agentId: schema.string(),
}),
body: schema.object({
+ status: schema.maybe(
+ schema.oneOf([schema.literal('online'), schema.literal('error'), schema.literal('degraded')])
+ ),
local_metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())),
events: schema.maybe(schema.arrayOf(NewAgentEventSchema)),
}),
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
index 0d60bd588f710..4a79f30a17a05 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
@@ -324,8 +324,11 @@ describe('IndexPattern Data Panel', () => {
};
}
- async function testExistenceLoading(stateChanges?: unknown, propChanges?: unknown) {
- const props = testProps();
+ async function testExistenceLoading(
+ stateChanges?: unknown,
+ propChanges?: unknown,
+ props = testProps()
+ ) {
const inst = mountWithIntl( );
await act(async () => {
@@ -536,6 +539,25 @@ describe('IndexPattern Data Panel', () => {
expect(core.http.post).toHaveBeenCalledTimes(2);
expect(overlapCount).toEqual(0);
});
+
+ it("should default to empty dsl if query can't be parsed", async () => {
+ const props = {
+ ...testProps(),
+ query: {
+ language: 'kuery',
+ query: '@timestamp : NOT *',
+ },
+ };
+ await testExistenceLoading(undefined, undefined, props);
+
+ expect((props.core.http.post as jest.Mock).mock.calls[0][1].body).toContain(
+ JSON.stringify({
+ must_not: {
+ match_all: {},
+ },
+ })
+ );
+ });
});
describe('displaying field list', () => {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
index eb7940634d78e..91c068c2b4fab 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
@@ -22,7 +22,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { DataPublicPluginStart } from 'src/plugins/data/public';
+import { DataPublicPluginStart, EsQueryConfig, Query, Filter } from 'src/plugins/data/public';
import { DatasourceDataPanelProps, DataType, StateSetter } from '../types';
import { ChildDragDropProvider, DragContextState } from '../drag_drop';
import { FieldItem } from './field_item';
@@ -74,6 +74,27 @@ const fieldTypeNames: Record = {
ip: i18n.translate('xpack.lens.datatypes.ipAddress', { defaultMessage: 'IP' }),
};
+// Wrapper around esQuery.buildEsQuery, handling errors (e.g. because a query can't be parsed) by
+// returning a query dsl object not matching anything
+function buildSafeEsQuery(
+ indexPattern: IIndexPattern,
+ query: Query,
+ filters: Filter[],
+ queryConfig: EsQueryConfig
+) {
+ try {
+ return esQuery.buildEsQuery(indexPattern, query, filters, queryConfig);
+ } catch (e) {
+ return {
+ bool: {
+ must_not: {
+ match_all: {},
+ },
+ },
+ };
+ }
+}
+
export function IndexPatternDataPanel({
setState,
state,
@@ -106,7 +127,7 @@ export function IndexPatternDataPanel({
timeFieldName: indexPatterns[id].timeFieldName,
}));
- const dslQuery = esQuery.buildEsQuery(
+ const dslQuery = buildSafeEsQuery(
indexPatterns[currentIndexPatternId] as IIndexPattern,
query,
filters,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
index 815725f4331a6..fabf9e9e9bfff 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
@@ -198,10 +198,12 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
className={`lnsFieldItem__info ${infoIsOpen ? 'lnsFieldItem__info-isOpen' : ''}`}
data-test-subj={`lnsFieldListPanelField-${field.name}`}
onClick={() => {
- togglePopover();
+ if (exists) {
+ togglePopover();
+ }
}}
onKeyPress={(event) => {
- if (event.key === 'ENTER') {
+ if (exists && event.key === 'ENTER') {
togglePopover();
}
}}
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
index a3bb32337f9f8..096f26eb22fe3 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features , request an extension now.
"`;
+exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome subscription features , request an extension now.
"`;
-exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features , request an extension now.
"`;
+exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome subscription features , request an extension now.
"`;
-exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features , request an extension now.
"`;
+exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome subscription features , request an extension now.
"`;
-exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features , request an extension now.
"`;
+exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome subscription features , request an extension now.
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
index cb2a41dadbe9e..0a5656aa266bc 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`RevertToBasic component should display when license is about to expire 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features .
"`;
+exports[`RevertToBasic component should display when license is about to expire 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features .
"`;
-exports[`RevertToBasic component should display when license is expired 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features .
"`;
+exports[`RevertToBasic component should display when license is expired 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features .
"`;
-exports[`RevertToBasic component should display when trial is active 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features .
"`;
+exports[`RevertToBasic component should display when trial is active 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features .
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
index 9370b77e29560..9da8bb958941b 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`StartTrial component when trial is allowed display for basic license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other Platinum features have to offer.
"`;
+exports[`StartTrial component when trial is allowed display for basic license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other subscription features have to offer.
"`;
-exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other Platinum features have to offer.
"`;
+exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other subscription features have to offer.
"`;
-exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other Platinum features have to offer.
"`;
+exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other subscription features have to offer.
"`;
-exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other Platinum features have to offer.
"`;
+exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other subscription features have to offer.
"`;
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js
index fb1ea026abaa0..77fb10c71091d 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js
@@ -19,13 +19,13 @@ export const RequestTrialExtension = ({ shouldShowRequestTrialExtension }) => {
),
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js
index a1a46d8616554..24b51cccb4e45 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js
@@ -82,13 +82,13 @@ export class RevertToBasic extends React.PureComponent {
),
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx
index 65d40f1de2009..7220f377cf386 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx
@@ -94,14 +94,14 @@ export class StartTrial extends Component {
),
@@ -236,15 +236,15 @@ export class StartTrial extends Component {
const description = (
),
diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts
index 185de02d555b7..7f7a90eeba5a2 100644
--- a/x-pack/plugins/lists/common/constants.mock.ts
+++ b/x-pack/plugins/lists/common/constants.mock.ts
@@ -41,6 +41,8 @@ export const OPERATOR = 'included';
export const ENTRY_VALUE = 'some host name';
export const MATCH = 'match';
export const MATCH_ANY = 'match_any';
+export const MAX_IMPORT_PAYLOAD_BYTES = 40000000;
+export const IMPORT_BUFFER_SIZE = 1000;
export const LIST = 'list';
export const EXISTS = 'exists';
export const NESTED = 'nested';
diff --git a/x-pack/plugins/lists/server/config.mock.ts b/x-pack/plugins/lists/server/config.mock.ts
new file mode 100644
index 0000000000000..3cf5040c73675
--- /dev/null
+++ b/x-pack/plugins/lists/server/config.mock.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ IMPORT_BUFFER_SIZE,
+ LIST_INDEX,
+ LIST_ITEM_INDEX,
+ MAX_IMPORT_PAYLOAD_BYTES,
+} from '../common/constants.mock';
+
+import { ConfigType } from './config';
+
+export const getConfigMock = (): Partial => ({
+ listIndex: LIST_INDEX,
+ listItemIndex: LIST_ITEM_INDEX,
+});
+
+export const getConfigMockDecoded = (): ConfigType => ({
+ enabled: true,
+ importBufferSize: IMPORT_BUFFER_SIZE,
+ listIndex: LIST_INDEX,
+ listItemIndex: LIST_ITEM_INDEX,
+ maxImportPayloadBytes: MAX_IMPORT_PAYLOAD_BYTES,
+});
diff --git a/x-pack/plugins/lists/server/config.test.ts b/x-pack/plugins/lists/server/config.test.ts
new file mode 100644
index 0000000000000..60501322dcfa2
--- /dev/null
+++ b/x-pack/plugins/lists/server/config.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ConfigSchema, ConfigType } from './config';
+import { getConfigMock, getConfigMockDecoded } from './config.mock';
+
+describe('config_schema', () => {
+ test('it works with expected basic mock data set and defaults', () => {
+ expect(ConfigSchema.validate(getConfigMock())).toEqual(getConfigMockDecoded());
+ });
+
+ test('it throws if given an invalid value', () => {
+ const mock: Partial & { madeUpValue: string } = {
+ madeUpValue: 'something',
+ ...getConfigMock(),
+ };
+ expect(() => ConfigSchema.validate(mock)).toThrow(
+ '[madeUpValue]: definition for this key is missing'
+ );
+ });
+
+ test('it throws if the "maxImportPayloadBytes" value is 0', () => {
+ const mock: ConfigType = {
+ ...getConfigMockDecoded(),
+ maxImportPayloadBytes: 0,
+ };
+ expect(() => ConfigSchema.validate(mock)).toThrow(
+ '[maxImportPayloadBytes]: Value must be equal to or greater than [1].'
+ );
+ });
+
+ test('it throws if the "maxImportPayloadBytes" value is less than 0', () => {
+ const mock: ConfigType = {
+ ...getConfigMockDecoded(),
+ maxImportPayloadBytes: -1,
+ };
+ expect(() => ConfigSchema.validate(mock)).toThrow(
+ '[maxImportPayloadBytes]: Value must be equal to or greater than [1].'
+ );
+ });
+
+ test('it throws if the "importBufferSize" value is 0', () => {
+ const mock: ConfigType = {
+ ...getConfigMockDecoded(),
+ importBufferSize: 0,
+ };
+ expect(() => ConfigSchema.validate(mock)).toThrow(
+ '[importBufferSize]: Value must be equal to or greater than [1].'
+ );
+ });
+
+ test('it throws if the "importBufferSize" value is less than 0', () => {
+ const mock: ConfigType = {
+ ...getConfigMockDecoded(),
+ importBufferSize: -1,
+ };
+ expect(() => ConfigSchema.validate(mock)).toThrow(
+ '[importBufferSize]: Value must be equal to or greater than [1].'
+ );
+ });
+});
diff --git a/x-pack/plugins/lists/server/config.ts b/x-pack/plugins/lists/server/config.ts
index f2fa7e8801033..0fcc68419f8fe 100644
--- a/x-pack/plugins/lists/server/config.ts
+++ b/x-pack/plugins/lists/server/config.ts
@@ -8,8 +8,10 @@ import { TypeOf, schema } from '@kbn/config-schema';
export const ConfigSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
+ importBufferSize: schema.number({ defaultValue: 1000, min: 1 }),
listIndex: schema.string({ defaultValue: '.lists' }),
listItemIndex: schema.string({ defaultValue: '.items' }),
+ maxImportPayloadBytes: schema.number({ defaultValue: 40000000, min: 1 }),
});
export type ConfigType = TypeOf;
diff --git a/x-pack/plugins/lists/server/create_config.ts b/x-pack/plugins/lists/server/create_config.ts
index 7e2e639ce7a35..e46c71798eb9f 100644
--- a/x-pack/plugins/lists/server/create_config.ts
+++ b/x-pack/plugins/lists/server/create_config.ts
@@ -12,12 +12,6 @@ import { ConfigType } from './config';
export const createConfig$ = (
context: PluginInitializerContext
-): Observable<
- Readonly<{
- enabled: boolean;
- listIndex: string;
- listItemIndex: string;
- }>
-> => {
+): Observable> => {
return context.config.create().pipe(map((config) => config));
};
diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts
index cdd674a19ceb6..b4f2639f24923 100644
--- a/x-pack/plugins/lists/server/plugin.ts
+++ b/x-pack/plugins/lists/server/plugin.ts
@@ -48,7 +48,7 @@ export class ListPlugin
core.http.registerRouteHandlerContext('lists', this.createRouteHandlerContext());
const router = core.http.createRouter();
- initRoutes(router);
+ initRoutes(router, config);
return {
getExceptionListClient: (savedObjectsClient, user): ExceptionListClient => {
diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts
index d75199140ea8e..2e629d7516dd1 100644
--- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts
+++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts
@@ -4,50 +4,40 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Readable } from 'stream';
-
import { IRouter } from 'kibana/server';
+import { schema } from '@kbn/config-schema';
import { LIST_ITEM_URL } from '../../common/constants';
import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps';
import { validate } from '../../common/siem_common_deps';
-import { importListItemQuerySchema, importListItemSchema, listSchema } from '../../common/schemas';
-
-import { getListClient } from '.';
+import { importListItemQuerySchema, listSchema } from '../../common/schemas';
+import { ConfigType } from '../config';
-export interface HapiReadableStream extends Readable {
- hapi: {
- filename: string;
- };
-}
+import { createStreamFromBuffer } from './utils/create_stream_from_buffer';
-/**
- * Special interface since we are streaming in a file through a reader
- */
-export interface ImportListItemHapiFileSchema {
- file: HapiReadableStream;
-}
+import { getListClient } from '.';
-export const importListItemRoute = (router: IRouter): void => {
+export const importListItemRoute = (router: IRouter, config: ConfigType): void => {
router.post(
{
options: {
body: {
- output: 'stream',
+ accepts: ['multipart/form-data'],
+ maxBytes: config.maxImportPayloadBytes,
+ parse: false,
},
tags: ['access:lists'],
},
path: `${LIST_ITEM_URL}/_import`,
validate: {
- body: buildRouteValidation(
- importListItemSchema
- ),
+ body: schema.buffer(),
query: buildRouteValidation(importListItemQuerySchema),
},
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
+ const stream = createStreamFromBuffer(request.body);
const { deserializer, list_id: listId, serializer, type } = request.query;
const lists = getListClient(context);
if (listId != null) {
@@ -63,7 +53,7 @@ export const importListItemRoute = (router: IRouter): void => {
listId,
meta: undefined,
serializer: list.serializer,
- stream: request.body.file,
+ stream,
type: list.type,
});
@@ -74,26 +64,21 @@ export const importListItemRoute = (router: IRouter): void => {
return response.ok({ body: validated ?? {} });
}
} else if (type != null) {
- const { filename } = request.body.file.hapi;
- // TODO: Should we prevent the same file from being uploaded multiple times?
- const list = await lists.createListIfItDoesNotExist({
- description: `File uploaded from file system of ${filename}`,
+ const importedList = await lists.importListItemsToStream({
deserializer,
- id: filename,
+ listId: undefined,
meta: undefined,
- name: filename,
serializer,
+ stream,
type,
});
- await lists.importListItemsToStream({
- deserializer: list.deserializer,
- listId: list.id,
- meta: undefined,
- serializer: list.serializer,
- stream: request.body.file,
- type: list.type,
- });
- const [validated, errors] = validate(list, listSchema);
+ if (importedList == null) {
+ return siemResponse.error({
+ body: 'Unable to parse a valid fileName during import',
+ statusCode: 400,
+ });
+ }
+ const [validated, errors] = validate(importedList, listSchema);
if (errors != null) {
return siemResponse.error({ body: errors, statusCode: 500 });
} else {
diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts
index e74fa471734b0..ffd8afd54913f 100644
--- a/x-pack/plugins/lists/server/routes/init_routes.ts
+++ b/x-pack/plugins/lists/server/routes/init_routes.ts
@@ -6,6 +6,8 @@
import { IRouter } from 'kibana/server';
+import { ConfigType } from '../config';
+
import {
createExceptionListItemRoute,
createExceptionListRoute,
@@ -36,7 +38,7 @@ import {
updateListRoute,
} from '.';
-export const initRoutes = (router: IRouter): void => {
+export const initRoutes = (router: IRouter, config: ConfigType): void => {
// lists
createListRoute(router);
readListRoute(router);
@@ -52,7 +54,7 @@ export const initRoutes = (router: IRouter): void => {
deleteListItemRoute(router);
patchListItemRoute(router);
exportListItemRoute(router);
- importListItemRoute(router);
+ importListItemRoute(router, config);
findListItemRoute(router);
// indexes of lists
diff --git a/x-pack/plugins/lists/server/routes/utils/create_stream_from_buffer.ts b/x-pack/plugins/lists/server/routes/utils/create_stream_from_buffer.ts
new file mode 100644
index 0000000000000..3dcf03617bcbc
--- /dev/null
+++ b/x-pack/plugins/lists/server/routes/utils/create_stream_from_buffer.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Readable } from 'stream';
+
+export const createStreamFromBuffer = (buffer: Buffer): Readable => {
+ const stream = new Readable();
+ stream.push(buffer);
+ stream.push(null);
+ return stream;
+};
diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts
index a283269271bd0..ad1511e28f80a 100644
--- a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts
+++ b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts
@@ -4,15 +4,44 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { IMPORT_BUFFER_SIZE } from '../../../common/constants.mock';
+
import { BufferLines } from './buffer_lines';
import { TestReadable } from './test_readable.mock';
describe('buffer_lines', () => {
+ test('it will throw if given a buffer size of zero', () => {
+ expect(() => {
+ new BufferLines({ bufferSize: 0, input: new TestReadable() });
+ }).toThrow('bufferSize must be greater than zero');
+ });
+
+ test('it will throw if given a buffer size of -1', () => {
+ expect(() => {
+ new BufferLines({ bufferSize: -1, input: new TestReadable() });
+ }).toThrow('bufferSize must be greater than zero');
+ });
+
test('it can read a single line', (done) => {
const input = new TestReadable();
input.push('line one\n');
input.push(null);
- const bufferedLine = new BufferLines({ input });
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
+ let linesToTest: string[] = [];
+ bufferedLine.on('lines', (lines: string[]) => {
+ linesToTest = [...linesToTest, ...lines];
+ });
+ bufferedLine.on('close', () => {
+ expect(linesToTest).toEqual(['line one']);
+ done();
+ });
+ });
+
+ test('it can read a single line using a buffer size of 1', (done) => {
+ const input = new TestReadable();
+ input.push('line one\n');
+ input.push(null);
+ const bufferedLine = new BufferLines({ bufferSize: 1, input });
let linesToTest: string[] = [];
bufferedLine.on('lines', (lines: string[]) => {
linesToTest = [...linesToTest, ...lines];
@@ -28,7 +57,23 @@ describe('buffer_lines', () => {
input.push('line one\n');
input.push('line two\n');
input.push(null);
- const bufferedLine = new BufferLines({ input });
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
+ let linesToTest: string[] = [];
+ bufferedLine.on('lines', (lines: string[]) => {
+ linesToTest = [...linesToTest, ...lines];
+ });
+ bufferedLine.on('close', () => {
+ expect(linesToTest).toEqual(['line one', 'line two']);
+ done();
+ });
+ });
+
+ test('it can read two lines using a buffer size of 1', (done) => {
+ const input = new TestReadable();
+ input.push('line one\n');
+ input.push('line two\n');
+ input.push(null);
+ const bufferedLine = new BufferLines({ bufferSize: 1, input });
let linesToTest: string[] = [];
bufferedLine.on('lines', (lines: string[]) => {
linesToTest = [...linesToTest, ...lines];
@@ -44,7 +89,7 @@ describe('buffer_lines', () => {
input.push('line one\n');
input.push('line one\n');
input.push(null);
- const bufferedLine = new BufferLines({ input });
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
let linesToTest: string[] = [];
bufferedLine.on('lines', (lines: string[]) => {
linesToTest = [...linesToTest, ...lines];
@@ -58,7 +103,7 @@ describe('buffer_lines', () => {
test('it can close out without writing any lines', (done) => {
const input = new TestReadable();
input.push(null);
- const bufferedLine = new BufferLines({ input });
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
let linesToTest: string[] = [];
bufferedLine.on('lines', (lines: string[]) => {
linesToTest = [...linesToTest, ...lines];
@@ -71,7 +116,7 @@ describe('buffer_lines', () => {
test('it can read 200 lines', (done) => {
const input = new TestReadable();
- const bufferedLine = new BufferLines({ input });
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
let linesToTest: string[] = [];
const size200: string[] = new Array(200).fill(null).map((_, index) => `${index}\n`);
size200.forEach((element) => input.push(element));
@@ -84,4 +129,66 @@ describe('buffer_lines', () => {
done();
});
});
+
+ test('it can read an example multi-part message', (done) => {
+ const input = new TestReadable();
+ input.push('--boundary\n');
+ input.push('Content-type: text/plain\n');
+ input.push('Content-Disposition: form-data; name="fieldName"; filename="filename.text"\n');
+ input.push('\n');
+ input.push('127.0.0.1\n');
+ input.push('127.0.0.2\n');
+ input.push('127.0.0.3\n');
+ input.push('\n');
+ input.push('--boundary--\n');
+ input.push(null);
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
+ let linesToTest: string[] = [];
+ bufferedLine.on('lines', (lines: string[]) => {
+ linesToTest = [...linesToTest, ...lines];
+ });
+ bufferedLine.on('close', () => {
+ expect(linesToTest).toEqual(['127.0.0.1', '127.0.0.2', '127.0.0.3']);
+ done();
+ });
+ });
+
+ test('it can read an empty multi-part message', (done) => {
+ const input = new TestReadable();
+ input.push('--boundary\n');
+ input.push('Content-type: text/plain\n');
+ input.push('Content-Disposition: form-data; name="fieldName"; filename="filename.text"\n');
+ input.push('\n');
+ input.push('\n');
+ input.push('--boundary--\n');
+ input.push(null);
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
+ let linesToTest: string[] = [];
+ bufferedLine.on('lines', (lines: string[]) => {
+ linesToTest = [...linesToTest, ...lines];
+ });
+ bufferedLine.on('close', () => {
+ expect(linesToTest).toEqual([]);
+ done();
+ });
+ });
+
+ test('it can read a fileName from a multipart message', (done) => {
+ const input = new TestReadable();
+ input.push('--boundary\n');
+ input.push('Content-type: text/plain\n');
+ input.push('Content-Disposition: form-data; name="fieldName"; filename="filename.text"\n');
+ input.push('\n');
+ input.push('--boundary--\n');
+ input.push(null);
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
+ let fileNameToTest: string;
+ bufferedLine.on('fileName', (fileName: string) => {
+ fileNameToTest = fileName;
+ });
+ bufferedLine.on('close', () => {
+ expect(fileNameToTest).toEqual('filename.text');
+ done();
+ });
+ });
});
diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.ts
index 4ff84268f5e0c..dc257eadb7438 100644
--- a/x-pack/plugins/lists/server/services/items/buffer_lines.ts
+++ b/x-pack/plugins/lists/server/services/items/buffer_lines.ts
@@ -7,18 +7,50 @@
import readLine from 'readline';
import { Readable } from 'stream';
-const BUFFER_SIZE = 100;
-
export class BufferLines extends Readable {
private set = new Set();
- constructor({ input }: { input: NodeJS.ReadableStream }) {
+ private boundary: string | null = null;
+ private readableText: boolean = false;
+ private paused: boolean = false;
+ private bufferSize: number;
+ constructor({ input, bufferSize }: { input: NodeJS.ReadableStream; bufferSize: number }) {
super({ encoding: 'utf-8' });
+ if (bufferSize <= 0) {
+ throw new RangeError('bufferSize must be greater than zero');
+ }
+ this.bufferSize = bufferSize;
+
const readline = readLine.createInterface({
input,
});
+ // We are parsing multipart/form-data involving boundaries as fast as we can to get
+ // * The filename if it exists and emit it
+ // * The actual content within the multipart/form-data
readline.on('line', (line) => {
- this.push(line);
+ if (this.boundary == null && line.startsWith('--')) {
+ this.boundary = `${line}--`;
+ } else if (this.boundary != null && !this.readableText && line.trim() !== '') {
+ if (line.startsWith('Content-Disposition')) {
+ const fileNameMatch = RegExp('filename="(?.+)"');
+ const matches = fileNameMatch.exec(line);
+ if (matches?.groups?.fileName != null) {
+ this.emit('fileName', matches.groups.fileName);
+ }
+ }
+ } else if (this.boundary != null && !this.readableText && line.trim() === '') {
+ // we are ready to be readable text now for parsing
+ this.readableText = true;
+ } else if (this.readableText && line.trim() === '') {
+ // skip and do nothing as this is either a empty line or an upcoming end is about to happen
+ } else if (this.boundary != null && this.readableText && line === this.boundary) {
+ // we are at the end of the stream
+ this.boundary = null;
+ this.readableText = false;
+ } else {
+ // we have actual content to push
+ this.push(line);
+ }
});
readline.on('close', () => {
@@ -26,23 +58,54 @@ export class BufferLines extends Readable {
});
}
- public _read(): void {
- // No operation but this is required to be implemented
+ public _read(): void {}
+
+ public pause(): this {
+ this.paused = true;
+ return this;
}
- public push(line: string | null): boolean {
- if (line == null) {
- this.emit('lines', Array.from(this.set));
- this.set.clear();
- this.emit('close');
- return true;
+ public resume(): this {
+ this.paused = false;
+ return this;
+ }
+
+ private emptyBuffer(): void {
+ const arrayFromSet = Array.from(this.set);
+ if (arrayFromSet.length === 0) {
+ this.emit('lines', []);
} else {
+ while (arrayFromSet.length) {
+ const spliced = arrayFromSet.splice(0, this.bufferSize);
+ this.emit('lines', spliced);
+ }
+ }
+ this.set.clear();
+ }
+
+ public push(line: string | null): boolean {
+ if (line != null) {
this.set.add(line);
- if (this.set.size > BUFFER_SIZE) {
- this.emit('lines', Array.from(this.set));
- this.set.clear();
+ if (this.paused) {
+ return false;
+ } else {
+ if (this.set.size > this.bufferSize) {
+ this.emptyBuffer();
+ }
return true;
+ }
+ } else {
+ if (this.paused) {
+ // If we paused but have buffered all of the available data
+ // we should do wait for 10(ms) and check again if we are paused
+ // or not.
+ setTimeout(() => {
+ this.push(line);
+ }, 10);
+ return false;
} else {
+ this.emptyBuffer();
+ this.emit('close');
return true;
}
}
diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts
index 7fbdc900fe2a4..76bd47d217107 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts
@@ -36,6 +36,7 @@ describe('crete_list_item', () => {
body,
id: LIST_ITEM_ID,
index: LIST_ITEM_INDEX,
+ refresh: 'wait_for',
};
expect(options.callCluster).toBeCalledWith('index', expected);
});
diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts
index 333f34946828a..aa17fc00b25c6 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts
@@ -71,6 +71,7 @@ export const createListItem = async ({
body,
id,
index: listItemIndex,
+ refresh: 'wait_for',
});
return {
diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts
index 4ab1bfb856846..b2cc0da669e42 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts
@@ -33,6 +33,7 @@ describe('crete_list_item_bulk', () => {
secondRecord,
],
index: LIST_ITEM_INDEX,
+ refresh: 'wait_for',
});
});
@@ -70,6 +71,7 @@ describe('crete_list_item_bulk', () => {
},
],
index: '.items',
+ refresh: 'wait_for',
});
});
});
diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts
index 447c0f6bf95cc..91e9587aa676a 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts
@@ -80,9 +80,13 @@ export const createListItemsBulk = async ({
},
[]
);
-
- await callCluster('bulk', {
- body,
- index: listItemIndex,
- });
+ try {
+ await callCluster('bulk', {
+ body,
+ index: listItemIndex,
+ refresh: 'wait_for',
+ });
+ } catch (error) {
+ // TODO: Log out the error with return values from the bulk insert into another index or saved object
+ }
};
diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts
index ea338d9dd3791..b14bddb1268f8 100644
--- a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts
+++ b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts
@@ -47,6 +47,7 @@ describe('delete_list_item', () => {
const deleteQuery = {
id: LIST_ITEM_ID,
index: LIST_ITEM_INDEX,
+ refresh: 'wait_for',
};
expect(options.callCluster).toBeCalledWith('delete', deleteQuery);
});
diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts
index b006aed6f6dde..baeced4b09995 100644
--- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts
@@ -28,6 +28,7 @@ export const deleteListItem = async ({
await callCluster('delete', {
id,
index: listItemIndex,
+ refresh: 'wait_for',
});
}
return listItem;
diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts
index bf1608334ef24..f658a51730d97 100644
--- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts
+++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts
@@ -52,6 +52,7 @@ describe('delete_list_item_by_value', () => {
},
},
index: '.items',
+ refresh: 'wait_for',
};
expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery);
});
diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts
index 3551cb75dc5bc..880402fca1bfa 100644
--- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts
+++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts
@@ -48,6 +48,7 @@ export const deleteListItemByValue = async ({
},
},
index: listItemIndex,
+ refresh: 'wait_for',
});
return listItems;
};
diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts
index 24cd11cbb65e4..eb20f1cfe3b30 100644
--- a/x-pack/plugins/lists/server/services/items/update_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts
@@ -62,6 +62,7 @@ export const updateListItem = async ({
},
id: listItem.id,
index: listItemIndex,
+ refresh: 'wait_for',
});
return {
created_at: listItem.created_at,
diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts
index b7e30e0a1c308..d868351fc4b33 100644
--- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts
+++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts
@@ -5,14 +5,24 @@
*/
import { getCallClusterMock } from '../../../common/get_call_cluster.mock';
import { ImportListItemsToStreamOptions, WriteBufferToItemsOptions } from '../items';
-import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from '../../../common/constants.mock';
+import {
+ LIST_ID,
+ LIST_INDEX,
+ LIST_ITEM_INDEX,
+ META,
+ TYPE,
+ USER,
+} from '../../../common/constants.mock';
+import { getConfigMockDecoded } from '../../config.mock';
import { TestReadable } from './test_readable.mock';
export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStreamOptions => ({
callCluster: getCallClusterMock(),
+ config: getConfigMockDecoded(),
deserializer: undefined,
listId: LIST_ID,
+ listIndex: LIST_INDEX,
listItemIndex: LIST_ITEM_INDEX,
meta: META,
serializer: undefined,
diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts
index 31b2b74c88431..2bffe338e9075 100644
--- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts
+++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts
@@ -8,20 +8,26 @@ import { Readable } from 'stream';
import { LegacyAPICaller } from 'kibana/server';
+import { createListIfItDoesNotExist } from '../lists/create_list_if_it_does_not_exist';
import {
DeserializerOrUndefined,
+ ListIdOrUndefined,
+ ListSchema,
MetaOrUndefined,
SerializerOrUndefined,
Type,
} from '../../../common/schemas';
+import { ConfigType } from '../../config';
import { BufferLines } from './buffer_lines';
import { createListItemsBulk } from './create_list_items_bulk';
export interface ImportListItemsToStreamOptions {
+ listId: ListIdOrUndefined;
+ config: ConfigType;
+ listIndex: string;
deserializer: DeserializerOrUndefined;
serializer: SerializerOrUndefined;
- listId: string;
stream: Readable;
callCluster: LegacyAPICaller;
listItemIndex: string;
@@ -31,34 +37,72 @@ export interface ImportListItemsToStreamOptions {
}
export const importListItemsToStream = ({
+ config,
deserializer,
serializer,
listId,
stream,
callCluster,
listItemIndex,
+ listIndex,
type,
user,
meta,
-}: ImportListItemsToStreamOptions): Promise => {
- return new Promise((resolve) => {
- const readBuffer = new BufferLines({ input: stream });
+}: ImportListItemsToStreamOptions): Promise => {
+ return new Promise((resolve) => {
+ const readBuffer = new BufferLines({ bufferSize: config.importBufferSize, input: stream });
+ let fileName: string | undefined;
+ let list: ListSchema | null = null;
+ readBuffer.on('fileName', async (fileNameEmitted: string) => {
+ readBuffer.pause();
+ fileName = fileNameEmitted;
+ if (listId == null) {
+ list = await createListIfItDoesNotExist({
+ callCluster,
+ description: `File uploaded from file system of ${fileNameEmitted}`,
+ deserializer,
+ id: fileNameEmitted,
+ listIndex,
+ meta,
+ name: fileNameEmitted,
+ serializer,
+ type,
+ user,
+ });
+ }
+ readBuffer.resume();
+ });
+
readBuffer.on('lines', async (lines: string[]) => {
- await writeBufferToItems({
- buffer: lines,
- callCluster,
- deserializer,
- listId,
- listItemIndex,
- meta,
- serializer,
- type,
- user,
- });
+ if (listId != null) {
+ await writeBufferToItems({
+ buffer: lines,
+ callCluster,
+ deserializer,
+ listId,
+ listItemIndex,
+ meta,
+ serializer,
+ type,
+ user,
+ });
+ } else if (fileName != null) {
+ await writeBufferToItems({
+ buffer: lines,
+ callCluster,
+ deserializer,
+ listId: fileName,
+ listItemIndex,
+ meta,
+ serializer,
+ type,
+ user,
+ });
+ }
});
readBuffer.on('close', () => {
- resolve();
+ resolve(list);
});
});
};
diff --git a/x-pack/plugins/lists/server/services/lists/create_list.test.ts b/x-pack/plugins/lists/server/services/lists/create_list.test.ts
index 43af08bcaf7ff..e328df710ebe1 100644
--- a/x-pack/plugins/lists/server/services/lists/create_list.test.ts
+++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts
@@ -52,6 +52,7 @@ describe('crete_list', () => {
body,
id: LIST_ID,
index: LIST_INDEX,
+ refresh: 'wait_for',
};
expect(options.callCluster).toBeCalledWith('index', expected);
});
diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts
index 3925fa5f0170c..3d396cf4d5af9 100644
--- a/x-pack/plugins/lists/server/services/lists/create_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/create_list.ts
@@ -67,6 +67,7 @@ export const createList = async ({
body,
id,
index: listIndex,
+ refresh: 'wait_for',
});
return {
id: response._id,
diff --git a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts
new file mode 100644
index 0000000000000..84f5ac0308191
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { LegacyAPICaller } from 'kibana/server';
+
+import {
+ Description,
+ DeserializerOrUndefined,
+ Id,
+ ListSchema,
+ MetaOrUndefined,
+ Name,
+ SerializerOrUndefined,
+ Type,
+} from '../../../common/schemas';
+
+import { getList } from './get_list';
+import { createList } from './create_list';
+
+export interface CreateListIfItDoesNotExistOptions {
+ id: Id;
+ type: Type;
+ name: Name;
+ deserializer: DeserializerOrUndefined;
+ serializer: SerializerOrUndefined;
+ description: Description;
+ callCluster: LegacyAPICaller;
+ listIndex: string;
+ user: string;
+ meta: MetaOrUndefined;
+ dateNow?: string;
+ tieBreaker?: string;
+}
+
+export const createListIfItDoesNotExist = async ({
+ id,
+ name,
+ type,
+ description,
+ deserializer,
+ callCluster,
+ listIndex,
+ user,
+ meta,
+ serializer,
+ dateNow,
+ tieBreaker,
+}: CreateListIfItDoesNotExistOptions): Promise => {
+ const list = await getList({ callCluster, id, listIndex });
+ if (list == null) {
+ return createList({
+ callCluster,
+ dateNow,
+ description,
+ deserializer,
+ id,
+ listIndex,
+ meta,
+ name,
+ serializer,
+ tieBreaker,
+ type,
+ user,
+ });
+ } else {
+ return list;
+ }
+};
diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts
index b9f1ec4d400be..029b6226a7375 100644
--- a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts
+++ b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts
@@ -47,6 +47,7 @@ describe('delete_list', () => {
const deleteByQuery = {
body: { query: { term: { list_id: LIST_ID } } },
index: LIST_ITEM_INDEX,
+ refresh: 'wait_for',
};
expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery);
});
@@ -59,6 +60,7 @@ describe('delete_list', () => {
const deleteQuery = {
id: LIST_ID,
index: LIST_INDEX,
+ refresh: 'wait_for',
};
expect(options.callCluster).toHaveBeenNthCalledWith(2, 'delete', deleteQuery);
});
diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts
index 64359b7273274..152048ca9cac6 100644
--- a/x-pack/plugins/lists/server/services/lists/delete_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts
@@ -36,11 +36,13 @@ export const deleteList = async ({
},
},
index: listItemIndex,
+ refresh: 'wait_for',
});
await callCluster('delete', {
id,
index: listIndex,
+ refresh: 'wait_for',
});
return list;
}
diff --git a/x-pack/plugins/lists/server/services/lists/list_client.mock.ts b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts
index 43a01a3ca62dc..e5036d561ddc6 100644
--- a/x-pack/plugins/lists/server/services/lists/list_client.mock.ts
+++ b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts
@@ -9,7 +9,12 @@ import { getFoundListSchemaMock } from '../../../common/schemas/response/found_l
import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock';
import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock';
import { getCallClusterMock } from '../../../common/get_call_cluster.mock';
-import { LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock';
+import {
+ IMPORT_BUFFER_SIZE,
+ LIST_INDEX,
+ LIST_ITEM_INDEX,
+ MAX_IMPORT_PAYLOAD_BYTES,
+} from '../../../common/constants.mock';
import { ListClient } from './list_client';
@@ -59,8 +64,10 @@ export const getListClientMock = (): ListClient => {
callCluster: getCallClusterMock(),
config: {
enabled: true,
+ importBufferSize: IMPORT_BUFFER_SIZE,
listIndex: LIST_INDEX,
listItemIndex: LIST_ITEM_INDEX,
+ maxImportPayloadBytes: MAX_IMPORT_PAYLOAD_BYTES,
},
spaceId: 'default',
user: 'elastic',
diff --git a/x-pack/plugins/lists/server/services/lists/list_client.ts b/x-pack/plugins/lists/server/services/lists/list_client.ts
index be9da1a1c69f5..4acc2e7092491 100644
--- a/x-pack/plugins/lists/server/services/lists/list_client.ts
+++ b/x-pack/plugins/lists/server/services/lists/list_client.ts
@@ -70,6 +70,7 @@ import {
UpdateListItemOptions,
UpdateListOptions,
} from './list_client_types';
+import { createListIfItDoesNotExist } from './create_list_if_it_does_not_exist';
export class ListClient {
private readonly spaceId: string;
@@ -140,12 +141,20 @@ export class ListClient {
type,
meta,
}: CreateListIfItDoesNotExistOptions): Promise => {
- const list = await this.getList({ id });
- if (list == null) {
- return this.createList({ description, deserializer, id, meta, name, serializer, type });
- } else {
- return list;
- }
+ const { callCluster, user } = this;
+ const listIndex = this.getListIndex();
+ return createListIfItDoesNotExist({
+ callCluster,
+ description,
+ deserializer,
+ id,
+ listIndex,
+ meta,
+ name,
+ serializer,
+ type,
+ user,
+ });
};
public getListIndexExists = async (): Promise => {
@@ -325,13 +334,16 @@ export class ListClient {
listId,
stream,
meta,
- }: ImportListItemsToStreamOptions): Promise => {
- const { callCluster, user } = this;
+ }: ImportListItemsToStreamOptions): Promise => {
+ const { callCluster, user, config } = this;
const listItemIndex = this.getListItemIndex();
+ const listIndex = this.getListIndex();
return importListItemsToStream({
callCluster,
+ config,
deserializer,
listId,
+ listIndex,
listItemIndex,
meta,
serializer,
diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts
index 26e147a6fa130..68a018fa2fc16 100644
--- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts
+++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts
@@ -16,6 +16,7 @@ import {
Id,
IdOrUndefined,
ListId,
+ ListIdOrUndefined,
MetaOrUndefined,
Name,
NameOrUndefined,
@@ -86,9 +87,9 @@ export interface ExportListItemsToStreamOptions {
}
export interface ImportListItemsToStreamOptions {
+ listId: ListIdOrUndefined;
deserializer: DeserializerOrUndefined;
serializer: SerializerOrUndefined;
- listId: string;
type: Type;
stream: Readable;
meta: MetaOrUndefined;
diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts
index c7cc30aaae908..f84ca787eaa7c 100644
--- a/x-pack/plugins/lists/server/services/lists/update_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/update_list.ts
@@ -55,6 +55,7 @@ export const updateList = async ({
body: { doc },
id,
index: listIndex,
+ refresh: 'wait_for',
});
return {
created_at: list.created_at,
diff --git a/x-pack/plugins/ml/common/constants/field_types.ts b/x-pack/plugins/ml/common/constants/field_types.ts
index 9402e4c20e46f..93641fd45c499 100644
--- a/x-pack/plugins/ml/common/constants/field_types.ts
+++ b/x-pack/plugins/ml/common/constants/field_types.ts
@@ -17,3 +17,6 @@ export enum ML_JOB_FIELD_TYPES {
export const MLCATEGORY = 'mlcategory';
export const DOC_COUNT = 'doc_count';
+
+// List of system fields we don't want to display.
+export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score'];
diff --git a/x-pack/plugins/ml/common/constants/new_job.ts b/x-pack/plugins/ml/common/constants/new_job.ts
index 751413bb6485a..d5c532234fd2b 100644
--- a/x-pack/plugins/ml/common/constants/new_job.ts
+++ b/x-pack/plugins/ml/common/constants/new_job.ts
@@ -17,6 +17,7 @@ export enum CREATED_BY_LABEL {
MULTI_METRIC = 'multi-metric-wizard',
POPULATION = 'population-wizard',
CATEGORIZATION = 'categorization-wizard',
+ APM_TRANSACTION = 'ml-module-apm-transaction',
}
export const DEFAULT_MODEL_MEMORY_LIMIT = '10MB';
diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts
index e2c4f1bae1a10..744f9c4d759dd 100644
--- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts
+++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts
@@ -65,7 +65,7 @@ export interface Detector {
function: string;
over_field_name?: string;
partition_field_name?: string;
- use_null?: string;
+ use_null?: boolean;
custom_rules?: CustomRule[];
}
export interface AnalysisLimits {
@@ -80,7 +80,7 @@ export interface DataDescription {
}
export interface ModelPlotConfig {
- enabled: boolean;
+ enabled?: boolean;
annotations_enabled?: boolean;
terms?: string;
}
diff --git a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx
index 83e7b82986cf8..d71a180cd2206 100644
--- a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx
+++ b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx
@@ -11,13 +11,17 @@ import { mount } from 'enzyme';
import { EuiSelect } from '@elastic/eui';
+import { UrlStateProvider } from '../../../util/url_state';
+
import { SelectInterval } from './select_interval';
describe('SelectInterval', () => {
test('creates correct initial selected value', () => {
const wrapper = mount(
-
+
+
+
);
const select = wrapper.find(EuiSelect);
@@ -29,7 +33,9 @@ describe('SelectInterval', () => {
test('currently selected value is updated correctly on click', (done) => {
const wrapper = mount(
-
+
+
+
);
const select = wrapper.find(EuiSelect).first();
diff --git a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx
index 484a0c395f3f8..cb4f80bfe6809 100644
--- a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx
+++ b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx
@@ -11,13 +11,17 @@ import { mount } from 'enzyme';
import { EuiSuperSelect } from '@elastic/eui';
+import { UrlStateProvider } from '../../../util/url_state';
+
import { SelectSeverity } from './select_severity';
describe('SelectSeverity', () => {
test('creates correct severity options and initial selected value', () => {
const wrapper = mount(
-
+
+
+
);
const select = wrapper.find(EuiSuperSelect);
@@ -65,7 +69,9 @@ describe('SelectSeverity', () => {
test('state for currently selected value is updated correctly on click', (done) => {
const wrapper = mount(
-
+
+
+
);
diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx
index 859d649416267..3a4875fa243fd 100644
--- a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx
+++ b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx
@@ -60,7 +60,7 @@ function getTabs(disableLinks: boolean): Tab[] {
name: i18n.translate('xpack.ml.navMenu.settingsTabLinkText', {
defaultMessage: 'Settings',
}),
- disabled: false,
+ disabled: disableLinks,
},
];
}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
index aa637f71db1cc..618ea5184007d 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
@@ -121,16 +121,24 @@ export interface DfAnalyticsExplainResponse {
}
export interface Eval {
- meanSquaredError: number | string;
+ mse: number | string;
+ msle: number | string;
+ huber: number | string;
rSquared: number | string;
error: null | string;
}
export interface RegressionEvaluateResponse {
regression: {
+ huber: {
+ value: number;
+ };
mse: {
value: number;
};
+ msle: {
+ value: number;
+ };
r_squared: {
value: number;
};
@@ -414,19 +422,37 @@ export const useRefreshAnalyticsList = (
const DEFAULT_SIG_FIGS = 3;
-export function getValuesFromResponse(response: RegressionEvaluateResponse) {
- let meanSquaredError = response?.regression?.mse?.value;
+interface RegressionEvaluateExtractedResponse {
+ mse: number | string;
+ msle: number | string;
+ huber: number | string;
+ r_squared: number | string;
+}
- if (meanSquaredError) {
- meanSquaredError = Number(meanSquaredError.toPrecision(DEFAULT_SIG_FIGS));
- }
+export const EMPTY_STAT = '--';
- let rSquared = response?.regression?.r_squared?.value;
- if (rSquared) {
- rSquared = Number(rSquared.toPrecision(DEFAULT_SIG_FIGS));
+export function getValuesFromResponse(response: RegressionEvaluateResponse) {
+ const results: RegressionEvaluateExtractedResponse = {
+ mse: EMPTY_STAT,
+ msle: EMPTY_STAT,
+ huber: EMPTY_STAT,
+ r_squared: EMPTY_STAT,
+ };
+
+ if (response?.regression) {
+ for (const statType in response.regression) {
+ if (response.regression.hasOwnProperty(statType)) {
+ let currentStatValue =
+ response.regression[statType as keyof RegressionEvaluateResponse['regression']]?.value;
+ if (currentStatValue) {
+ currentStatValue = Number(currentStatValue.toPrecision(DEFAULT_SIG_FIGS));
+ }
+ results[statType as keyof RegressionEvaluateExtractedResponse] = currentStatValue;
+ }
+ }
}
- return { meanSquaredError, rSquared };
+ return results;
}
interface ResultsSearchBoolQuery {
bool: Dictionary;
@@ -490,13 +516,22 @@ export function getEvalQueryBody({
return query;
}
+export enum REGRESSION_STATS {
+ MSE = 'mse',
+ MSLE = 'msle',
+ R_SQUARED = 'rSquared',
+ HUBER = 'huber',
+}
+
interface EvaluateMetrics {
classification: {
multiclass_confusion_matrix: object;
};
regression: {
r_squared: object;
- mean_squared_error: object;
+ mse: object;
+ msle: object;
+ huber: object;
};
}
@@ -541,7 +576,9 @@ export const loadEvalData = async ({
},
regression: {
r_squared: {},
- mean_squared_error: {},
+ mse: {},
+ msle: {},
+ huber: {},
},
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
index b83dd2e4329e0..9dae54b6537b3 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
@@ -71,12 +71,8 @@ export const ConfigurationStepForm: FC = ({
EuiComboBoxOptionOption[]
>([]);
const [includesTableItems, setIncludesTableItems] = useState([]);
- const [maxDistinctValuesError, setMaxDistinctValuesError] = useState(
- undefined
- );
- const [unsupportedFieldsError, setUnsupportedFieldsError] = useState(
- undefined
- );
+ const [maxDistinctValuesError, setMaxDistinctValuesError] = useState();
+ const [unsupportedFieldsError, setUnsupportedFieldsError] = useState();
const { setEstimatedModelMemoryLimit, setFormState } = actions;
const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts
index bf3ab01549139..0935ed15a1a4a 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts
@@ -12,9 +12,6 @@ import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../com
export const CATEGORICAL_TYPES = new Set(['ip', 'keyword']);
-// List of system fields we want to ignore for the numeric field check.
-export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score'];
-
// Regression supports numeric fields. Classification supports categorical, numeric, and boolean.
export const shouldAddAsDepVarOption = (field: Field, jobType: AnalyticsJobType) => {
if (field.id === EVENT_RATE_FIELD_ID) return false;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx
index 0a4ba67831818..88c89df86b29a 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx
@@ -11,8 +11,9 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics';
import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields';
+import { OMIT_FIELDS } from '../../../../../../../common/constants/field_types';
import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../common/fields';
-import { OMIT_FIELDS, CATEGORICAL_TYPES } from './form_options_validation';
+import { CATEGORICAL_TYPES } from './form_options_validation';
import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx
index a4d86b48006e8..8a41eb4b8a865 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx
@@ -26,7 +26,7 @@ export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({
state,
}) => {
const { form, isJobCreated } = state;
- const { description, jobId, destinationIndex } = form;
+ const { description, jobId, destinationIndex, resultsField } = form;
const detailsFirstCol: ListItems[] = [
{
@@ -37,6 +37,19 @@ export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({
},
];
+ if (
+ resultsField !== undefined &&
+ typeof resultsField === 'string' &&
+ resultsField.trim() !== ''
+ ) {
+ detailsFirstCol.push({
+ title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.resultsField', {
+ defaultMessage: 'Results field',
+ }),
+ description: resultsField,
+ });
+ }
+
const detailsSecondCol: ListItems[] = [
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobDescription', {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx
index d846ae95c2c7e..168d5e31f57c3 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx
@@ -47,6 +47,7 @@ export const DetailsStepForm: FC = ({
jobIdExists,
jobIdInvalidMaxLength,
jobIdValid,
+ resultsField,
} = form;
const forceInput = useRef(null);
@@ -195,6 +196,22 @@ export const DetailsStepForm: FC = ({
data-test-subj="mlAnalyticsCreateJobFlyoutDestinationIndexInput"
/>
+
+ setFormState({ resultsField: e.target.value })}
+ data-test-subj="mlAnalyticsCreateJobWizardResultsFieldInput"
+ />
+
= ({ jobConfig, jobStatus, searchQuery }) => {
const {
@@ -82,18 +94,19 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
genErrorEval.eval &&
isRegressionEvaluateResponse(genErrorEval.eval)
) {
- const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval);
+ const { mse, msle, huber, r_squared } = getValuesFromResponse(genErrorEval.eval);
setGeneralizationEval({
- meanSquaredError,
- rSquared,
+ mse,
+ msle,
+ huber,
+ rSquared: r_squared,
error: null,
});
setIsLoadingGeneralization(false);
} else {
setIsLoadingGeneralization(false);
setGeneralizationEval({
- meanSquaredError: '--',
- rSquared: '--',
+ ...EMPTY_STATS,
error: genErrorEval.error,
});
}
@@ -118,18 +131,19 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
trainingErrorEval.eval &&
isRegressionEvaluateResponse(trainingErrorEval.eval)
) {
- const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval);
+ const { mse, msle, huber, r_squared } = getValuesFromResponse(trainingErrorEval.eval);
setTrainingEval({
- meanSquaredError,
- rSquared,
+ mse,
+ msle,
+ huber,
+ rSquared: r_squared,
error: null,
});
setIsLoadingTraining(false);
} else {
setIsLoadingTraining(false);
setTrainingEval({
- meanSquaredError: '--',
- rSquared: '--',
+ ...EMPTY_STATS,
error: trainingErrorEval.error,
});
}
@@ -274,22 +288,48 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
-
+
+ {/* First row stats */}
-
+
+
+
+
+
+
+
+
+ {/* Second row stats */}
-
+
+
+
+
+
+
+
+
@@ -331,22 +371,48 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
-
+
+ {/* First row stats */}
-
+
+
+
+
+
+
+
+
+ {/* Second row stats */}
-
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx
index 1b4461b2bb075..114ec75efb2e7 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx
@@ -6,58 +6,99 @@
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
+import { REGRESSION_STATS } from '../../../../common/analytics';
interface Props {
isLoading: boolean;
title: number | string;
- isMSE: boolean;
+ statType: REGRESSION_STATS;
dataTestSubj: string;
}
-const meanSquaredErrorText = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText',
- {
- defaultMessage: 'Mean squared error',
- }
-);
-const rSquaredText = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText',
- {
- defaultMessage: 'R squared',
- }
-);
-const meanSquaredErrorTooltipContent = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent',
- {
- defaultMessage:
- 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.',
- }
-);
-const rSquaredTooltipContent = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent',
- {
- defaultMessage:
- 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.',
- }
-);
+const statDescriptions = {
+ [REGRESSION_STATS.MSE]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText',
+ {
+ defaultMessage: 'Mean squared error',
+ }
+ ),
+ [REGRESSION_STATS.MSLE]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.msleText',
+ {
+ defaultMessage: 'Mean squared logarithmic error',
+ }
+ ),
+ [REGRESSION_STATS.R_SQUARED]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText',
+ {
+ defaultMessage: 'R squared',
+ }
+ ),
+ [REGRESSION_STATS.HUBER]: (
+
+ {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.huberLinkText', {
+ defaultMessage: 'Pseudo Huber loss function',
+ })}
+
+ ),
+ }}
+ />
+ ),
+};
+
+const tooltipContent = {
+ [REGRESSION_STATS.MSE]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent',
+ {
+ defaultMessage:
+ 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.',
+ }
+ ),
+ [REGRESSION_STATS.MSLE]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.msleTooltipContent',
+ {
+ defaultMessage:
+ 'Average squared difference between the logarithm of the predicted values and the logarithm of the actual (ground truth) value',
+ }
+ ),
+ [REGRESSION_STATS.R_SQUARED]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent',
+ {
+ defaultMessage:
+ 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.',
+ }
+ ),
+};
-export const EvaluateStat: FC = ({ isLoading, isMSE, title, dataTestSubj }) => (
+export const EvaluateStat: FC = ({ isLoading, statType, title, dataTestSubj }) => (
-
+ {statType !== REGRESSION_STATS.HUBER && (
+
+ )}
);
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts
index 006cccf3b4610..9db32e298691e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts
@@ -131,7 +131,7 @@ describe('Analytics job clone action', () => {
},
analyzed_fields: {
includes: [],
- excludes: [],
+ excludes: ['excluded_field'],
},
model_memory_limit: '350mb',
allow_lazy_start: false,
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx
index f8b6fdfbe2119..280ec544c1e5e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx
@@ -247,6 +247,7 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo
},
results_field: {
optional: true,
+ formKey: 'resultsField',
defaultValue: DEFAULT_RESULTS_FIELD,
},
},
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
index 405231aef5774..4080f6cd7a77e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
@@ -39,6 +39,7 @@ import {
import { getAnalyticsFactory } from '../../services/analytics_service';
import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns';
import { ExpandedRow } from './expanded_row';
+import { stringMatch } from '../../../../../util/string_utils';
import {
ProgressBar,
mlInMemoryTableFactory,
@@ -65,14 +66,6 @@ function getItemIdToExpandedRowMap(
}, {} as ItemIdToExpandedRowMap);
}
-function stringMatch(str: string | undefined, substr: any) {
- return (
- typeof str === 'string' &&
- typeof substr === 'string' &&
- (str.toLowerCase().match(substr.toLowerCase()) === null) === false
- );
-}
-
const MlInMemoryTable = mlInMemoryTableFactory();
interface Props {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx
index 4d029ff1d9546..5276fedff0fde 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx
@@ -29,6 +29,7 @@ import { getDataFrameAnalyticsProgressPhase, isCompletedAnalyticsJob } from './c
import {
isRegressionAnalysis,
ANALYSIS_CONFIG_TYPE,
+ REGRESSION_STATS,
isRegressionEvaluateResponse,
} from '../../../../common/analytics';
import { ExpandedRowMessagesPane } from './expanded_row_messages_pane';
@@ -44,7 +45,7 @@ function getItemDescription(value: any) {
interface LoadedStatProps {
isLoading: boolean;
evalData: Eval;
- resultProperty: 'meanSquaredError' | 'rSquared';
+ resultProperty: REGRESSION_STATS;
}
const LoadedStat: FC = ({ isLoading, evalData, resultProperty }) => {
@@ -61,7 +62,7 @@ interface Props {
item: DataFrameAnalyticsListRow;
}
-const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null };
+const defaultEval: Eval = { mse: '', msle: '', huber: '', rSquared: '', error: null };
export const ExpandedRow: FC = ({ item }) => {
const [trainingEval, setTrainingEval] = useState(defaultEval);
@@ -94,17 +95,21 @@ export const ExpandedRow: FC = ({ item }) => {
genErrorEval.eval &&
isRegressionEvaluateResponse(genErrorEval.eval)
) {
- const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval);
+ const { mse, msle, huber, r_squared } = getValuesFromResponse(genErrorEval.eval);
setGeneralizationEval({
- meanSquaredError,
- rSquared,
+ mse,
+ msle,
+ huber,
+ rSquared: r_squared,
error: null,
});
setIsLoadingGeneralization(false);
} else {
setIsLoadingGeneralization(false);
setGeneralizationEval({
- meanSquaredError: '',
+ mse: '',
+ msle: '',
+ huber: '',
rSquared: '',
error: genErrorEval.error,
});
@@ -124,17 +129,21 @@ export const ExpandedRow: FC = ({ item }) => {
trainingErrorEval.eval &&
isRegressionEvaluateResponse(trainingErrorEval.eval)
) {
- const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval);
+ const { mse, msle, huber, r_squared } = getValuesFromResponse(trainingErrorEval.eval);
setTrainingEval({
- meanSquaredError,
- rSquared,
+ mse,
+ msle,
+ huber,
+ rSquared: r_squared,
error: null,
});
setIsLoadingTraining(false);
} else {
setIsLoadingTraining(false);
setTrainingEval({
- meanSquaredError: '',
+ mse: '',
+ msle: '',
+ huber: '',
rSquared: '',
error: genErrorEval.error,
});
@@ -221,7 +230,17 @@ export const ExpandedRow: FC = ({ item }) => {
+ ),
+ },
+ {
+ title: 'generalization mean squared logarithmic error',
+ description: (
+
),
},
@@ -231,7 +250,17 @@ export const ExpandedRow: FC = ({ item }) => {
+ ),
+ },
+ {
+ title: 'generalization pseudo huber loss function',
+ description: (
+
),
},
@@ -241,7 +270,17 @@ export const ExpandedRow: FC = ({ item }) => {
+ ),
+ },
+ {
+ title: 'training mean squared logarithmic error',
+ description: (
+
),
},
@@ -251,7 +290,17 @@ export const ExpandedRow: FC = ({ item }) => {
+ ),
+ },
+ {
+ title: 'training pseudo huber loss function',
+ description: (
+
),
}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx
index e75d938116991..cb46a88fa3b21 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx
@@ -30,17 +30,21 @@ export const useActions = (
actions: EuiTableActionsColumnType['actions'];
modals: JSX.Element | null;
} => {
- const deleteAction = useDeleteAction();
- const editAction = useEditAction();
- const startAction = useStartAction();
-
let modals: JSX.Element | null = null;
const actions: EuiTableActionsColumnType['actions'] = [
getViewAction(isManagementTable),
];
+ // isManagementTable will be the same for the lifecycle of the component
+ // Disabling lint error to fix console error in management list due to action hooks using deps not initialized in management
if (isManagementTable === false) {
+ /* eslint-disable react-hooks/rules-of-hooks */
+ const deleteAction = useDeleteAction();
+ const editAction = useEditAction();
+ const startAction = useStartAction();
+ /* eslint-disable react-hooks/rules-of-hooks */
+
modals = (
<>
{startAction.isModalVisible && }
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
index 81d35679443b8..b344e44c97d59 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
@@ -144,6 +144,11 @@ export const validateAdvancedEditor = (state: State): State => {
const destinationIndexNameValid = isValidIndexName(destinationIndexName);
const destinationIndexPatternTitleExists =
state.indexPatternsMap[destinationIndexName] !== undefined;
+
+ const resultsFieldEmptyString =
+ typeof jobConfig?.dest?.results_field === 'string' &&
+ jobConfig?.dest?.results_field.trim() === '';
+
const mml = jobConfig.model_memory_limit;
const modelMemoryLimitEmpty = mml === '' || mml === undefined;
if (!modelMemoryLimitEmpty && mml !== undefined) {
@@ -292,6 +297,18 @@ export const validateAdvancedEditor = (state: State): State => {
});
}
+ if (resultsFieldEmptyString) {
+ state.advancedEditorMessages.push({
+ error: i18n.translate(
+ 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.resultsFieldEmptyString',
+ {
+ defaultMessage: 'The results field must not be an empty string.',
+ }
+ ),
+ message: '',
+ });
+ }
+
if (dependentVariableEmpty) {
state.advancedEditorMessages.push({
error: i18n.translate(
@@ -336,6 +353,7 @@ export const validateAdvancedEditor = (state: State): State => {
sourceIndexNameValid &&
!destinationIndexNameEmpty &&
destinationIndexNameValid &&
+ !resultsFieldEmptyString &&
!dependentVariableEmpty &&
!modelMemoryLimitEmpty &&
numTopFeatureImportanceValuesValid &&
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
index cedbe9094cb20..0d425c8ead4a2 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
@@ -82,6 +82,7 @@ export interface State {
previousJobType: null | AnalyticsJobType;
requiredFieldsError: string | undefined;
randomizeSeed: undefined | number;
+ resultsField: undefined | string;
sourceIndex: EsIndexName;
sourceIndexNameEmpty: boolean;
sourceIndexNameValid: boolean;
@@ -147,6 +148,7 @@ export const getInitialState = (): State => ({
previousJobType: null,
requiredFieldsError: undefined,
randomizeSeed: undefined,
+ resultsField: undefined,
sourceIndex: '',
sourceIndexNameEmpty: true,
sourceIndexNameValid: false,
@@ -198,6 +200,13 @@ export const getJobConfigFromFormState = (
model_memory_limit: formState.modelMemoryLimit,
};
+ const resultsFieldEmpty =
+ typeof formState?.resultsField === 'string' && formState?.resultsField.trim() === '';
+
+ if (jobConfig.dest && !resultsFieldEmpty) {
+ jobConfig.dest.results_field = formState.resultsField;
+ }
+
if (
formState.jobType === ANALYSIS_CONFIG_TYPE.REGRESSION ||
formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION
@@ -277,6 +286,7 @@ export function getCloneFormStateFromJobConfig(
const resultState: Partial = {
jobType,
description: analyticsJobConfig.description ?? '',
+ resultsField: analyticsJobConfig.dest.results_field,
sourceIndex: Array.isArray(analyticsJobConfig.source.index)
? analyticsJobConfig.source.index.join(',')
: analyticsJobConfig.source.index,
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts
index 7d1f456d2334f..a08821c65bfe7 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts
+++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts
@@ -10,13 +10,12 @@ import { getToastNotifications } from '../../../util/dependency_cache';
import { IndexPattern } from '../../../../../../../../src/plugins/data/public';
import { SavedSearchQuery } from '../../../contexts/ml';
+import { OMIT_FIELDS } from '../../../../../common/constants/field_types';
import { IndexPatternTitle } from '../../../../../common/types/kibana';
import { ml } from '../../../services/ml_api_service';
import { FieldRequestConfig } from '../common';
-// List of system fields we don't want to display.
-const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score'];
// Maximum number of examples to obtain for text type fields.
const MAX_EXAMPLES_DEFAULT: number = 10;
diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
index 068f43a140c90..f356d79c0a8e1 100644
--- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
+++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
@@ -9,12 +9,10 @@ import { useUrlState } from '../../util/url_state';
import { SWIMLANE_TYPE } from '../explorer_constants';
import { AppStateSelectedCells } from '../explorer_utils';
-export const useSelectedCells = (): [
- AppStateSelectedCells | undefined,
- (swimlaneSelectedCells: AppStateSelectedCells) => void
-] => {
- const [appState, setAppState] = useUrlState('_a');
-
+export const useSelectedCells = (
+ appState: any,
+ setAppState: ReturnType[1]
+): [AppStateSelectedCells | undefined, (swimlaneSelectedCells: AppStateSelectedCells) => void] => {
// keep swimlane selection, restore selectedCells from AppState
const selectedCells = useMemo(() => {
return appState?.mlExplorerSwimlane?.selectedType !== undefined
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js
index 15c54fc5b3a46..569eca4aba949 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js
@@ -11,6 +11,7 @@ import rison from 'rison-node';
import { mlJobService } from '../../../services/job_service';
import { ml } from '../../../services/ml_api_service';
import { getToastNotifications } from '../../../util/dependency_cache';
+import { stringMatch } from '../../../util/string_utils';
import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states';
import { parseInterval } from '../../../../../common/util/parse_interval';
import { i18n } from '@kbn/i18n';
@@ -350,14 +351,6 @@ export function checkForAutoStartDatafeed() {
}
}
-function stringMatch(str, substr) {
- return (
- typeof str === 'string' &&
- typeof substr === 'string' &&
- (str.toLowerCase().match(substr.toLowerCase()) === null) === false
- );
-}
-
function jobProperty(job, prop) {
const propMap = {
job_state: 'jobState',
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts
index d8c4dab150fb5..29e8aafffef7e 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts
@@ -226,16 +226,23 @@ export class JobCreator {
this._calendars = calendars;
}
- public set modelPlot(enable: boolean) {
- if (enable) {
- this._job_config.model_plot_config = {
- enabled: true,
- };
- } else {
- delete this._job_config.model_plot_config;
+ private _initModelPlotConfig() {
+ // initialize configs to false if they are missing
+ if (this._job_config.model_plot_config === undefined) {
+ this._job_config.model_plot_config = {};
+ }
+ if (this._job_config.model_plot_config.enabled === undefined) {
+ this._job_config.model_plot_config.enabled = false;
+ }
+ if (this._job_config.model_plot_config.annotations_enabled === undefined) {
+ this._job_config.model_plot_config.annotations_enabled = false;
}
}
+ public set modelPlot(enable: boolean) {
+ this._initModelPlotConfig();
+ this._job_config.model_plot_config!.enabled = enable;
+ }
public get modelPlot() {
return (
this._job_config.model_plot_config !== undefined &&
@@ -243,6 +250,15 @@ export class JobCreator {
);
}
+ public set modelChangeAnnotations(enable: boolean) {
+ this._initModelPlotConfig();
+ this._job_config.model_plot_config!.annotations_enabled = enable;
+ }
+
+ public get modelChangeAnnotations() {
+ return this._job_config.model_plot_config?.annotations_enabled === true;
+ }
+
public set useDedicatedIndex(enable: boolean) {
this._useDedicatedIndex = enable;
if (enable) {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx
index 9a158f78c39be..18bd6f7fc6e23 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx
@@ -14,6 +14,7 @@ import {
EuiHorizontalRule,
} from '@elastic/eui';
import { ModelPlotSwitch } from './components/model_plot';
+import { AnnotationsSwitch } from './components/annotations';
import { DedicatedIndexSwitch } from './components/dedicated_index';
import { ModelMemoryLimitInput } from '../../../common/model_memory_limit';
import { JobCreatorContext } from '../../../job_creator_context';
@@ -41,6 +42,7 @@ export const AdvancedSection: FC = ({ advancedExpanded, setAdvancedExpand
+
@@ -68,6 +70,7 @@ export const AdvancedSection: FC = ({ advancedExpanded, setAdvancedExpand
>
+
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/annotations_switch.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/annotations_switch.tsx
new file mode 100644
index 0000000000000..9defbb12207e2
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/annotations_switch.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useState, useContext, useEffect } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiCallOut, EuiSpacer, EuiSwitch } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { JobCreatorContext } from '../../../../../job_creator_context';
+import { Description } from './description';
+
+export const AnnotationsSwitch: FC = () => {
+ const { jobCreator, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext);
+ const [annotationsEnabled, setAnnotationsEnabled] = useState(jobCreator.modelChangeAnnotations);
+ const [showCallOut, setShowCallout] = useState(
+ jobCreator.modelPlot && !jobCreator.modelChangeAnnotations
+ );
+
+ useEffect(() => {
+ jobCreator.modelChangeAnnotations = annotationsEnabled;
+ jobCreatorUpdate();
+ }, [annotationsEnabled]);
+
+ useEffect(() => {
+ setShowCallout(jobCreator.modelPlot && !annotationsEnabled);
+ }, [jobCreatorUpdated, annotationsEnabled]);
+
+ function toggleAnnotations() {
+ setAnnotationsEnabled(!annotationsEnabled);
+ }
+
+ return (
+ <>
+
+
+
+ {showCallOut && (
+
+ }
+ color="primary"
+ iconType="help"
+ />
+ )}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/description.tsx
new file mode 100644
index 0000000000000..92b07ff8d0910
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/description.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo, FC } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui';
+
+export const Description: FC = memo(({ children }) => {
+ const title = i18n.translate(
+ 'xpack.ml.newJob.wizard.jobDetailsStep.advancedSection.enableModelPlotAnnotations.title',
+ {
+ defaultMessage: 'Enable model change annotations',
+ }
+ );
+ return (
+ {title}}
+ description={
+
+ }
+ >
+
+ <>{children}>
+
+
+ );
+});
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/index.ts
new file mode 100644
index 0000000000000..04bd97e140055
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { AnnotationsSwitch } from './annotations_switch';
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx
index 171c7bbdd550c..48b044e5371de 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx
@@ -125,6 +125,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => {
if (jobCreator.type === JOB_TYPE.SINGLE_METRIC) {
jobCreator.modelPlot = true;
+ jobCreator.modelChangeAnnotations = true;
}
if (mlContext.currentSavedSearch !== null) {
diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx
index 281493c4e31b7..f1b8083f19ccf 100644
--- a/x-pack/plugins/ml/public/application/routing/router.tsx
+++ b/x-pack/plugins/ml/public/application/routing/router.tsx
@@ -12,6 +12,7 @@ import { IUiSettingsClient, ChromeStart } from 'kibana/public';
import { ChromeBreadcrumb } from 'kibana/public';
import { IndexPatternsContract } from 'src/plugins/data/public';
import { MlContext, MlContextValue } from '../contexts/ml';
+import { UrlStateProvider } from '../util/url_state';
import * as routes from './routes';
@@ -48,21 +49,23 @@ export const MlRouter: FC<{ pageDeps: PageDependencies }> = ({ pageDeps }) => {
return (
-
- {Object.entries(routes).map(([name, route]) => (
- {
- window.setTimeout(() => {
- setBreadcrumbs(route.breadcrumbs);
- });
- return route.render(props, pageDeps);
- }}
- />
- ))}
-
+
+
+ {Object.entries(routes).map(([name, route]) => (
+ {
+ window.setTimeout(() => {
+ setBreadcrumbs(route.breadcrumbs);
+ });
+ return route.render(props, pageDeps);
+ }}
+ />
+ ))}
+
+
);
};
diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
index 52b4408d1ac5b..7a7865c9bd738 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
@@ -152,7 +152,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim
const [tableInterval] = useTableInterval();
const [tableSeverity] = useTableSeverity();
- const [selectedCells, setSelectedCells] = useSelectedCells();
+ const [selectedCells, setSelectedCells] = useSelectedCells(appState, setAppState);
useEffect(() => {
explorerService.setSelectedCells(selectedCells);
}, [JSON.stringify(selectedCells)]);
diff --git a/x-pack/plugins/ml/public/application/util/string_utils.d.ts b/x-pack/plugins/ml/public/application/util/string_utils.d.ts
deleted file mode 100644
index 531e44e3e78c1..0000000000000
--- a/x-pack/plugins/ml/public/application/util/string_utils.d.ts
+++ /dev/null
@@ -1,21 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-export function escapeForElasticsearchQuery(str: string): string;
-
-export function replaceStringTokens(
- str: string,
- valuesByTokenName: {},
- encodeForURI: boolean
-): string;
-
-export function detectorToString(dtr: any): string;
-
-export function sortByKey(list: any, reverse: boolean, comparator?: any): any;
-
-export function toLocaleString(x: number): string;
-
-export function mlEscape(str: string): string;
diff --git a/x-pack/plugins/ml/public/application/util/string_utils.test.ts b/x-pack/plugins/ml/public/application/util/string_utils.test.ts
index 25f1cbd3abac3..034c406afb4b2 100644
--- a/x-pack/plugins/ml/public/application/util/string_utils.test.ts
+++ b/x-pack/plugins/ml/public/application/util/string_utils.test.ts
@@ -4,10 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { CustomUrlAnomalyRecordDoc } from '../../../common/types/custom_urls';
+import { Detector } from '../../../common/types/anomaly_detection_jobs';
+
import {
replaceStringTokens,
detectorToString,
- sortByKey,
toLocaleString,
mlEscape,
escapeForElasticsearchQuery,
@@ -15,7 +17,7 @@ import {
describe('ML - string utils', () => {
describe('replaceStringTokens', () => {
- const testRecord = {
+ const testRecord: CustomUrlAnomalyRecordDoc = {
job_id: 'test_job',
result_type: 'record',
probability: 0.0191711,
@@ -30,6 +32,10 @@ describe('ML - string utils', () => {
testfield1: 'test$tring=[+-?]',
testfield2: '{<()>}',
testfield3: 'host=\\\\test@uk.dev',
+ earliest: '0',
+ latest: '0',
+ is_interim: false,
+ initial_record_score: 0,
};
test('returns correct values without URI encoding', () => {
@@ -68,17 +74,17 @@ describe('ML - string utils', () => {
describe('detectorToString', () => {
test('returns the correct descriptions for detectors', () => {
- const detector1 = {
+ const detector1: Detector = {
function: 'count',
};
- const detector2 = {
+ const detector2: Detector = {
function: 'count',
by_field_name: 'airline',
use_null: false,
};
- const detector3 = {
+ const detector3: Detector = {
function: 'mean',
field_name: 'CPUUtilization',
partition_field_name: 'region',
@@ -95,50 +101,6 @@ describe('ML - string utils', () => {
});
});
- describe('sortByKey', () => {
- const obj = {
- zebra: 'stripes',
- giraffe: 'neck',
- elephant: 'trunk',
- };
-
- const valueComparator = function (value: string) {
- return value;
- };
-
- test('returns correct ordering with default comparator', () => {
- const result = sortByKey(obj, false);
- const keys = Object.keys(result);
- expect(keys[0]).toBe('elephant');
- expect(keys[1]).toBe('giraffe');
- expect(keys[2]).toBe('zebra');
- });
-
- test('returns correct ordering with default comparator and order reversed', () => {
- const result = sortByKey(obj, true);
- const keys = Object.keys(result);
- expect(keys[0]).toBe('zebra');
- expect(keys[1]).toBe('giraffe');
- expect(keys[2]).toBe('elephant');
- });
-
- test('returns correct ordering with comparator', () => {
- const result = sortByKey(obj, false, valueComparator);
- const keys = Object.keys(result);
- expect(keys[0]).toBe('giraffe');
- expect(keys[1]).toBe('zebra');
- expect(keys[2]).toBe('elephant');
- });
-
- test('returns correct ordering with comparator and order reversed', () => {
- const result = sortByKey(obj, true, valueComparator);
- const keys = Object.keys(result);
- expect(keys[0]).toBe('elephant');
- expect(keys[1]).toBe('zebra');
- expect(keys[2]).toBe('giraffe');
- });
- });
-
describe('toLocaleString', () => {
test('returns correct comma placement for large numbers', () => {
expect(toLocaleString(1)).toBe('1');
diff --git a/x-pack/plugins/ml/public/application/util/string_utils.js b/x-pack/plugins/ml/public/application/util/string_utils.ts
similarity index 75%
rename from x-pack/plugins/ml/public/application/util/string_utils.js
rename to x-pack/plugins/ml/public/application/util/string_utils.ts
index 7411820ba3239..aa283fd71bf79 100644
--- a/x-pack/plugins/ml/public/application/util/string_utils.js
+++ b/x-pack/plugins/ml/public/application/util/string_utils.ts
@@ -10,6 +10,9 @@
import _ from 'lodash';
import d3 from 'd3';
+import { CustomUrlAnomalyRecordDoc } from '../../../common/types/custom_urls';
+import { Detector } from '../../../common/types/anomaly_detection_jobs';
+
// Replaces all instances of dollar delimited tokens in the specified String
// with corresponding values from the supplied object, optionally
// encoding the replacement for a URI component.
@@ -17,7 +20,11 @@ import d3 from 'd3';
// and valuesByTokenName of {"airline":"AAL"}, will return
// 'http://www.google.co.uk/#q=airline+code+AAL'.
// If a corresponding key is not found in valuesByTokenName, then the String is not replaced.
-export function replaceStringTokens(str, valuesByTokenName, encodeForURI) {
+export function replaceStringTokens(
+ str: string,
+ valuesByTokenName: CustomUrlAnomalyRecordDoc,
+ encodeForURI: boolean
+) {
return String(str).replace(/\$([^?&$\'"]+)\$/g, (match, name) => {
// Use lodash get to allow nested JSON fields to be retrieved.
let tokenValue = _.get(valuesByTokenName, name, null);
@@ -31,7 +38,7 @@ export function replaceStringTokens(str, valuesByTokenName, encodeForURI) {
}
// creates the default description for a given detector
-export function detectorToString(dtr) {
+export function detectorToString(dtr: Detector): string {
const BY_TOKEN = ' by ';
const OVER_TOKEN = ' over ';
const USE_NULL_OPTION = ' use_null=';
@@ -73,7 +80,7 @@ export function detectorToString(dtr) {
}
// wrap a the inputed string in quotes if it contains non-word characters
-function quoteField(field) {
+function quoteField(field: string): string {
if (field.match(/\W/g)) {
return '"' + field + '"';
} else {
@@ -81,28 +88,10 @@ function quoteField(field) {
}
}
-// re-order an object based on the value of the keys
-export function sortByKey(list, reverse, comparator) {
- let keys = _.sortBy(_.keys(list), (key) => {
- return comparator ? comparator(list[key], key) : key;
- });
-
- if (reverse) {
- keys = keys.reverse();
- }
-
- return _.zipObject(
- keys,
- _.map(keys, (key) => {
- return list[key];
- })
- );
-}
-
// add commas to large numbers
// Number.toLocaleString is not supported on safari
-export function toLocaleString(x) {
- let result = x;
+export function toLocaleString(x: number): string {
+ let result = x.toString();
if (x && typeof x === 'number') {
const parts = x.toString().split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
@@ -112,8 +101,8 @@ export function toLocaleString(x) {
}
// escape html characters
-export function mlEscape(str) {
- const entityMap = {
+export function mlEscape(str: string): string {
+ const entityMap: { [escapeChar: string]: string } = {
'&': '&',
'<': '<',
'>': '>',
@@ -125,7 +114,7 @@ export function mlEscape(str) {
}
// Escapes reserved characters for use in Elasticsearch query terms.
-export function escapeForElasticsearchQuery(str) {
+export function escapeForElasticsearchQuery(str: string): string {
// Escape with a leading backslash any of the characters that
// Elastic document may cause a syntax error when used in queries:
// + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
@@ -133,27 +122,24 @@ export function escapeForElasticsearchQuery(str) {
return String(str).replace(/[-[\]{}()+!<>=?:\/\\^"~*&|\s]/g, '\\$&');
}
-export function calculateTextWidth(txt, isNumber, elementSelection) {
- txt = isNumber ? d3.format(',')(txt) : txt;
- let svg = elementSelection;
- let $el;
- if (elementSelection === undefined) {
- // Create a temporary selection to append the label to.
- // Note styling of font will be inherited from CSS of page.
- const $body = d3.select('body');
- $el = $body.append('div');
- svg = $el.append('svg');
- }
+export function calculateTextWidth(txt: string | number, isNumber: boolean) {
+ txt = isNumber && typeof txt === 'number' ? d3.format(',')(txt) : txt;
+
+ // Create a temporary selection to append the label to.
+ // Note styling of font will be inherited from CSS of page.
+ const $body = d3.select('body');
+ const $el = $body.append('div');
+ const svg = $el.append('svg');
const tempLabelText = svg
.append('g')
.attr('class', 'temp-axis-label tick')
.selectAll('text.temp.axis')
- .data('a')
+ .data(['a'])
.enter()
.append('text')
.text(txt);
- const width = tempLabelText[0][0].getBBox().width;
+ const width = (tempLabelText[0][0] as SVGSVGElement).getBBox().width;
d3.select('.temp-axis-label').remove();
if ($el !== undefined) {
@@ -161,3 +147,11 @@ export function calculateTextWidth(txt, isNumber, elementSelection) {
}
return Math.ceil(width);
}
+
+export function stringMatch(str: string | undefined, substr: any) {
+ return (
+ typeof str === 'string' &&
+ typeof substr === 'string' &&
+ (str.toLowerCase().match(substr.toLowerCase()) === null) === false
+ );
+}
diff --git a/x-pack/plugins/ml/public/application/util/url_state.test.ts b/x-pack/plugins/ml/public/application/util/url_state.test.tsx
similarity index 82%
rename from x-pack/plugins/ml/public/application/util/url_state.test.ts
rename to x-pack/plugins/ml/public/application/util/url_state.test.tsx
index 0813f2e3da97f..9c03369648554 100644
--- a/x-pack/plugins/ml/public/application/util/url_state.test.ts
+++ b/x-pack/plugins/ml/public/application/util/url_state.test.tsx
@@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { renderHook, act } from '@testing-library/react-hooks';
-import { getUrlState, useUrlState } from './url_state';
+import React, { FC } from 'react';
+import { render, act } from '@testing-library/react';
+import { parseUrlState, useUrlState, UrlStateProvider } from './url_state';
const mockHistoryPush = jest.fn();
@@ -22,7 +23,7 @@ jest.mock('react-router-dom', () => ({
describe('getUrlState', () => {
test('properly decode url with _g and _a', () => {
expect(
- getUrlState(
+ parseUrlState(
"?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d"
)
).toEqual({
@@ -64,13 +65,19 @@ describe('useUrlState', () => {
});
test('pushes a properly encoded search string to history', () => {
- const { result } = renderHook(() => useUrlState('_a'));
+ const TestComponent: FC = () => {
+ const [, setUrlState] = useUrlState('_a');
+ return setUrlState({ query: {} })}>ButtonText ;
+ };
+
+ const { getByText } = render(
+
+
+
+ );
act(() => {
- const [, setUrlState] = result.current;
- setUrlState({
- query: {},
- });
+ getByText('ButtonText').click();
});
expect(mockHistoryPush).toHaveBeenCalledWith({
diff --git a/x-pack/plugins/ml/public/application/util/url_state.ts b/x-pack/plugins/ml/public/application/util/url_state.tsx
similarity index 54%
rename from x-pack/plugins/ml/public/application/util/url_state.ts
rename to x-pack/plugins/ml/public/application/util/url_state.tsx
index beff5340ce7e4..c288a00bb06da 100644
--- a/x-pack/plugins/ml/public/application/util/url_state.ts
+++ b/x-pack/plugins/ml/public/application/util/url_state.tsx
@@ -5,7 +5,7 @@
*/
import { parse, stringify } from 'query-string';
-import { useCallback } from 'react';
+import React, { createContext, useCallback, useContext, useMemo, FC } from 'react';
import { isEqual } from 'lodash';
import { decode, encode } from 'rison-node';
import { useHistory, useLocation } from 'react-router-dom';
@@ -14,8 +14,16 @@ import { Dictionary } from '../../../common/types/common';
import { getNestedProperty } from './object_utils';
-export type SetUrlState = (attribute: string | Dictionary, value?: any) => void;
-export type UrlState = [Dictionary, SetUrlState];
+type Accessor = '_a' | '_g';
+export type SetUrlState = (
+ accessor: Accessor,
+ attribute: string | Dictionary,
+ value?: any
+) => void;
+export interface UrlState {
+ searchString: string;
+ setUrlState: SetUrlState;
+}
/**
* Set of URL query parameters that require the rison serialization.
@@ -30,7 +38,7 @@ function isRisonSerializationRequired(queryParam: string): boolean {
return risonSerializedParams.has(queryParam);
}
-export function getUrlState(search: string): Dictionary {
+export function parseUrlState(search: string): Dictionary {
const urlState: Dictionary = {};
const parsedQueryString = parse(search, { sort: false });
@@ -56,14 +64,23 @@ export function getUrlState(search: string): Dictionary {
// - `history.push()` is the successor of `save`.
// - The exposed state and set call make use of the above and make sure that
// different urlStates(e.g. `_a` / `_g`) don't overwrite each other.
-export const useUrlState = (accessor: string): UrlState => {
+// This uses a context to be able to maintain only one instance
+// of the url state. It gets passed down with `UrlStateProvider`
+// and can be used via `useUrlState`.
+export const urlStateStore = createContext({
+ searchString: '',
+ setUrlState: () => {},
+});
+const { Provider } = urlStateStore;
+export const UrlStateProvider: FC = ({ children }) => {
const history = useHistory();
- const { search } = useLocation();
+ const { search: searchString } = useLocation();
- const setUrlState = useCallback(
- (attribute: string | Dictionary, value?: any) => {
- const urlState = getUrlState(search);
- const parsedQueryString = parse(search, { sort: false });
+ const setUrlState: SetUrlState = useCallback(
+ (accessor: Accessor, attribute: string | Dictionary, value?: any) => {
+ const prevSearchString = searchString;
+ const urlState = parseUrlState(prevSearchString);
+ const parsedQueryString = parse(prevSearchString, { sort: false });
if (!Object.prototype.hasOwnProperty.call(urlState, accessor)) {
urlState[accessor] = {};
@@ -71,7 +88,7 @@ export const useUrlState = (accessor: string): UrlState => {
if (typeof attribute === 'string') {
if (isEqual(getNestedProperty(urlState, `${accessor}.${attribute}`), value)) {
- return;
+ return prevSearchString;
}
urlState[accessor][attribute] = value;
@@ -83,7 +100,10 @@ export const useUrlState = (accessor: string): UrlState => {
}
try {
- const oldLocationSearch = stringify(parsedQueryString, { sort: false, encode: false });
+ const oldLocationSearchString = stringify(parsedQueryString, {
+ sort: false,
+ encode: false,
+ });
Object.keys(urlState).forEach((a) => {
if (isRisonSerializationRequired(a)) {
@@ -92,20 +112,41 @@ export const useUrlState = (accessor: string): UrlState => {
parsedQueryString[a] = urlState[a];
}
});
- const newLocationSearch = stringify(parsedQueryString, { sort: false, encode: false });
+ const newLocationSearchString = stringify(parsedQueryString, {
+ sort: false,
+ encode: false,
+ });
- if (oldLocationSearch !== newLocationSearch) {
- history.push({
- search: stringify(parsedQueryString, { sort: false }),
- });
+ if (oldLocationSearchString !== newLocationSearchString) {
+ const newSearchString = stringify(parsedQueryString, { sort: false });
+ history.push({ search: newSearchString });
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Could not save url state', error);
}
},
- [search]
+ [searchString]
);
- return [getUrlState(search)[accessor], setUrlState];
+ return {children} ;
+};
+
+export const useUrlState = (accessor: Accessor) => {
+ const { searchString, setUrlState: setUrlStateContext } = useContext(urlStateStore);
+
+ const urlState = useMemo(() => {
+ const fullUrlState = parseUrlState(searchString);
+ if (typeof fullUrlState === 'object') {
+ return fullUrlState[accessor];
+ }
+ return undefined;
+ }, [searchString]);
+
+ const setUrlState = useCallback(
+ (attribute: string | Dictionary, value?: any) =>
+ setUrlStateContext(accessor, attribute, value),
+ [accessor, setUrlStateContext]
+ );
+ return [urlState, setUrlState];
};
diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts
index 0b544d4eca0ed..78e05c9a6d07b 100644
--- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts
+++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts
@@ -175,9 +175,11 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
const { jobId } = request.params;
+ const body = request.body;
+
const results = await context.ml!.mlClient.callAsCurrentUser('ml.addJob', {
jobId,
- body: request.body,
+ body,
});
return response.ok({
body: results,
diff --git a/x-pack/plugins/observability/public/assets/illustration_dark.svg b/x-pack/plugins/observability/public/assets/illustration_dark.svg
new file mode 100644
index 0000000000000..44815a7455144
--- /dev/null
+++ b/x-pack/plugins/observability/public/assets/illustration_dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/x-pack/plugins/observability/public/assets/illustration_light.svg b/x-pack/plugins/observability/public/assets/illustration_light.svg
new file mode 100644
index 0000000000000..1690c68fd595a
--- /dev/null
+++ b/x-pack/plugins/observability/public/assets/illustration_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/x-pack/plugins/observability/public/assets/observability_overview.png b/x-pack/plugins/observability/public/assets/observability_overview.png
deleted file mode 100644
index 70be08af9745a..0000000000000
Binary files a/x-pack/plugins/observability/public/assets/observability_overview.png and /dev/null differ
diff --git a/x-pack/plugins/observability/public/components/app/news/index.test.tsx b/x-pack/plugins/observability/public/components/app/news/index.test.tsx
deleted file mode 100644
index cae6b4aec0c62..0000000000000
--- a/x-pack/plugins/observability/public/components/app/news/index.test.tsx
+++ /dev/null
@@ -1,16 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React from 'react';
-import { render } from '../../../utils/test_helper';
-import { News } from './';
-
-describe('News', () => {
- it('renders resources with all elements', () => {
- const { getByText, getAllByText } = render( );
- expect(getByText("What's new")).toBeInTheDocument();
- expect(getAllByText('Read full story')).not.toEqual([]);
- });
-});
diff --git a/x-pack/plugins/observability/public/components/app/news/mock/news.mock.data.ts b/x-pack/plugins/observability/public/components/app/news/mock/news.mock.data.ts
deleted file mode 100644
index 5c623bb9134eb..0000000000000
--- a/x-pack/plugins/observability/public/components/app/news/mock/news.mock.data.ts
+++ /dev/null
@@ -1,34 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-export const news = [
- {
- title: 'Have SIEM questions?',
- description:
- 'Join our growing community of Elastic SIEM users to discuss the configuration and use of Elastic SIEM for threat detection and response.',
- link_url: 'https://discuss.elastic.co/c/security/siem/?blade=securitysolutionfeed',
- image_url:
- 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed',
- },
- {
- title: 'Elastic SIEM on-demand training course — free for a limited time',
- description:
- 'With this self-paced, on-demand course, you will learn how to leverage Elastic SIEM to drive your security operations and threat hunting. This course is designed for security analysts and practitioners who have used other SIEMs or are familiar with SIEM concepts.',
- link_url:
- 'https://training.elastic.co/elearning/security-analytics/elastic-siem-fundamentals-promo?blade=securitysolutionfeed',
- image_url:
- 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt50f58e0358ebea9d/5c30508693d9791a70cd73ad/illustration-specialization-course-page-security.svg?blade=securitysolutionfeed',
- },
- {
- title: 'New to Elastic SIEM? Take our on-demand training course',
- description:
- 'With this self-paced, on-demand course, you will learn how to leverage Elastic SIEM to drive your security operations and threat hunting. This course is designed for security analysts and practitioners who have used other SIEMs or are familiar with SIEM concepts.',
- link_url:
- 'https://www.elastic.co/training/specializations/security-analytics/elastic-siem-fundamentals?blade=securitysolutionfeed',
- image_url:
- 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt50f58e0358ebea9d/5c30508693d9791a70cd73ad/illustration-specialization-course-page-security.svg?blade=securitysolutionfeed',
- },
-];
diff --git a/x-pack/plugins/observability/public/components/app/news/index.scss b/x-pack/plugins/observability/public/components/app/news_feed/index.scss
similarity index 100%
rename from x-pack/plugins/observability/public/components/app/news/index.scss
rename to x-pack/plugins/observability/public/components/app/news_feed/index.scss
diff --git a/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx b/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx
new file mode 100644
index 0000000000000..c71130b57c33f
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { NewsItem } from '../../../services/get_news_feed';
+import { render } from '../../../utils/test_helper';
+import { NewsFeed } from './';
+
+const newsFeedItems = [
+ {
+ title: {
+ en: 'Elastic introduces OpenTelemetry integration',
+ },
+ description: {
+ en:
+ 'We are pleased to announce the availability of the Elastic OpenTelemetry integration — available on Elastic Cloud, or when you download Elastic APM.',
+ },
+ link_url: {
+ en:
+ 'https://www.elastic.co/blog/elastic-apm-opentelemetry-integration?blade=observabilitysolutionfeed',
+ },
+ image_url: {
+ en: 'foo.png',
+ },
+ },
+ {
+ title: {
+ en: 'Kubernetes observability tutorial: Log monitoring and analysis',
+ },
+ description: {
+ en:
+ 'Learn how Elastic Observability makes it easy to monitor and detect anomalies in millions of logs from thousands of containers running hundreds of microservices — while Kubernetes scales applications with changing pod counts. All from a single UI.',
+ },
+ link_url: {
+ en:
+ 'https://www.elastic.co/blog/kubernetes-observability-tutorial-k8s-log-monitoring-and-analysis-elastic-stack?blade=observabilitysolutionfeed',
+ },
+ image_url: null,
+ },
+ {
+ title: {
+ en: 'Kubernetes observability tutorial: K8s cluster setup and demo app deployment',
+ },
+ description: {
+ en:
+ 'This blog will walk you through configuring the environment you will be using for the Kubernetes observability tutorial blog series. We will be deploying Elasticsearch Service, a Minikube single-node Kubernetes cluster setup, and a demo app.',
+ },
+ link_url: {
+ en:
+ 'https://www.elastic.co/blog/kubernetes-observability-tutorial-k8s-cluster-setup-demo-app-deployment?blade=observabilitysolutionfeed',
+ },
+ image_url: {
+ en: null,
+ },
+ },
+] as NewsItem[];
+describe('News', () => {
+ it('renders resources with all elements', () => {
+ const { getByText, getAllByText, queryAllByTestId } = render(
+
+ );
+ expect(getByText("What's new")).toBeInTheDocument();
+ expect(getAllByText('Read full story').length).toEqual(3);
+ expect(queryAllByTestId('news_image').length).toEqual(1);
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/app/news/index.tsx b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx
similarity index 53%
rename from x-pack/plugins/observability/public/components/app/news/index.tsx
rename to x-pack/plugins/observability/public/components/app/news_feed/index.tsx
index 41a4074f47976..2fbd6659bcb5a 100644
--- a/x-pack/plugins/observability/public/components/app/news/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
+ EuiErrorBoundary,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
@@ -12,51 +13,51 @@ import {
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { truncate } from 'lodash';
import React, { useContext } from 'react';
import { ThemeContext } from 'styled-components';
+import { NewsItem as INewsItem } from '../../../services/get_news_feed';
import './index.scss';
-import { truncate } from 'lodash';
-import { news as newsMockData } from './mock/news.mock.data';
-interface NewsItem {
- title: string;
- description: string;
- link_url: string;
- image_url: string;
+interface Props {
+ items: INewsItem[];
}
-export const News = () => {
- const newsItems: NewsItem[] = newsMockData;
+export const NewsFeed = ({ items }: Props) => {
return (
-
-
-
-
- {i18n.translate('xpack.observability.news.title', {
- defaultMessage: "What's new",
- })}
-
-
-
- {newsItems.map((item, index) => (
-
-
+ // The news feed is manually added/edited, to prevent any errors caused by typos or missing fields,
+ // wraps the component with EuiErrorBoundary to avoid breaking the entire page.
+
+
+
+
+
+ {i18n.translate('xpack.observability.news.title', {
+ defaultMessage: "What's new",
+ })}
+
+
- ))}
-
+ {items.map((item, index) => (
+
+
+
+ ))}
+
+
);
};
const limitString = (string: string, limit: number) => truncate(string, { length: limit });
-const NewsItem = ({ item }: { item: NewsItem }) => {
+const NewsItem = ({ item }: { item: INewsItem }) => {
const theme = useContext(ThemeContext);
return (
- {item.title}
+ {item.title.en}
@@ -65,11 +66,11 @@ const NewsItem = ({ item }: { item: NewsItem }) => {
- {limitString(item.description, 128)}
+ {limitString(item.description.en, 128)}
-
+
{i18n.translate('xpack.observability.news.readFullStory', {
defaultMessage: 'Read full story',
@@ -79,16 +80,19 @@ const NewsItem = ({ item }: { item: NewsItem }) => {
-
-
-
+ {item.image_url?.en && (
+
+
+
+ )}
diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx
index b614095641250..512f4428d9bf2 100644
--- a/x-pack/plugins/observability/public/pages/landing/index.tsx
+++ b/x-pack/plugins/observability/public/pages/landing/index.tsx
@@ -84,7 +84,9 @@ export const LandingPage = () => {
size="xl"
alt="observability overview image"
url={core.http.basePath.prepend(
- '/plugins/observability/assets/observability_overview.png'
+ `/plugins/observability/assets/illustration_${
+ theme.darkMode ? 'dark' : 'light'
+ }.svg`
)}
/>
diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx
index 9caac7f9d86f4..3674e69ab5702 100644
--- a/x-pack/plugins/observability/public/pages/overview/index.tsx
+++ b/x-pack/plugins/observability/public/pages/overview/index.tsx
@@ -16,6 +16,7 @@ import { LogsSection } from '../../components/app/section/logs';
import { MetricsSection } from '../../components/app/section/metrics';
import { UptimeSection } from '../../components/app/section/uptime';
import { DatePicker, TimePickerTime } from '../../components/shared/data_picker';
+import { NewsFeed } from '../../components/app/news_feed';
import { fetchHasData } from '../../data_handler';
import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher';
import { UI_SETTINGS, useKibanaUISettings } from '../../hooks/use_kibana_ui_settings';
@@ -26,6 +27,7 @@ import { getParsedDate } from '../../utils/date';
import { getBucketSize } from '../../utils/get_bucket_size';
import { getEmptySections } from './empty_section';
import { LoadingObservability } from './loading_observability';
+import { getNewsFeed } from '../../services/get_news_feed';
interface Props {
routeParams: RouteParams<'/overview'>;
@@ -48,6 +50,8 @@ export const OverviewPage = ({ routeParams }: Props) => {
return getObservabilityAlerts({ core });
}, []);
+ const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), []);
+
const theme = useContext(ThemeContext);
const timePickerTime = useKibanaUISettings(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS);
@@ -190,6 +194,12 @@ export const OverviewPage = ({ routeParams }: Props) => {
+
+ {!!newsFeed?.items?.length && (
+
+
+
+ )}
diff --git a/x-pack/plugins/observability/public/pages/overview/mock/news_feed.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/news_feed.mock.ts
new file mode 100644
index 0000000000000..b23d095e2775b
--- /dev/null
+++ b/x-pack/plugins/observability/public/pages/overview/mock/news_feed.mock.ts
@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const newsFeedFetchData = async () => {
+ return {
+ items: [
+ {
+ title: {
+ en: 'Elastic introduces OpenTelemetry integration',
+ },
+ description: {
+ en:
+ 'We are pleased to announce the availability of the Elastic OpenTelemetry integration — available on Elastic Cloud, or when you download Elastic APM.',
+ },
+ link_text: null,
+ link_url: {
+ en:
+ 'https://www.elastic.co/blog/elastic-apm-opentelemetry-integration?blade=observabilitysolutionfeed',
+ },
+ languages: null,
+ badge: null,
+ image_url: null,
+ publish_on: '2020-07-02T00:00:00',
+ expire_on: '2021-05-02T00:00:00',
+ hash: '012caf3e161127d618ae8cc95e3e63f009a45d343eedf2f5e369cc95b1f9d9d3',
+ },
+ {
+ title: {
+ en: 'Kubernetes observability tutorial: Log monitoring and analysis',
+ },
+ description: {
+ en:
+ 'Learn how Elastic Observability makes it easy to monitor and detect anomalies in millions of logs from thousands of containers running hundreds of microservices — while Kubernetes scales applications with changing pod counts. All from a single UI.',
+ },
+ link_text: null,
+ link_url: {
+ en:
+ 'https://www.elastic.co/blog/kubernetes-observability-tutorial-k8s-log-monitoring-and-analysis-elastic-stack?blade=observabilitysolutionfeed',
+ },
+ languages: null,
+ badge: null,
+ image_url: null,
+ publish_on: '2020-06-23T00:00:00',
+ expire_on: '2021-06-23T00:00:00',
+ hash: '79a28cb9be717e82df80bf32c27e5d475e56d0d315be694b661d133f9a58b3b3',
+ },
+ {
+ title: {
+ en: 'Kubernetes observability tutorial: K8s cluster setup and demo app deployment',
+ },
+ description: {
+ en:
+ 'This blog will walk you through configuring the environment you will be using for the Kubernetes observability tutorial blog series. We will be deploying Elasticsearch Service, a Minikube single-node Kubernetes cluster setup, and a demo app.',
+ },
+ link_text: null,
+ link_url: {
+ en:
+ 'https://www.elastic.co/blog/kubernetes-observability-tutorial-k8s-cluster-setup-demo-app-deployment?blade=observabilitysolutionfeed',
+ },
+ languages: null,
+ badge: null,
+ image_url: null,
+ publish_on: '2020-06-23T00:00:00',
+ expire_on: '2021-06-23T00:00:00',
+ hash: 'ad682c355af3d4470a14df116df3b441e941661b291cdac62335615e7c6f13c2',
+ },
+ ],
+ };
+};
diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx
index b88614b22e81a..896cad7b72ecd 100644
--- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx
+++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx
@@ -17,6 +17,7 @@ import { fetchUptimeData, emptyResponse as emptyUptimeResponse } from './mock/up
import { EuiThemeProvider } from '../../typings';
import { OverviewPage } from './';
import { alertsFetchData } from './mock/alerts.mock';
+import { newsFeedFetchData } from './mock/news_feed.mock';
const core = {
http: {
@@ -102,6 +103,14 @@ const coreWithAlerts = ({
},
} as unknown) as AppMountContext['core'];
+const coreWithNewsFeed = ({
+ ...core,
+ http: {
+ ...core.http,
+ get: newsFeedFetchData,
+ },
+} as unknown) as AppMountContext['core'];
+
function unregisterAll() {
unregisterDataHandler({ appName: 'apm' });
unregisterDataHandler({ appName: 'infra_logs' });
@@ -337,6 +346,45 @@ storiesOf('app/Overview', module)
);
});
+storiesOf('app/Overview', module)
+ .addDecorator((storyFn) => (
+
+
+ {storyFn()} )
+
+
+ ))
+ .add('logs, metrics, APM, Uptime and News feed', () => {
+ unregisterAll();
+ registerDataHandler({
+ appName: 'apm',
+ fetchData: fetchApmData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_logs',
+ fetchData: fetchLogsData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'infra_metrics',
+ fetchData: fetchMetricsData,
+ hasData: async () => true,
+ });
+ registerDataHandler({
+ appName: 'uptime',
+ fetchData: fetchUptimeData,
+ hasData: async () => true,
+ });
+ return (
+
+ );
+ });
+
storiesOf('app/Overview', module)
.addDecorator((storyFn) => (
diff --git a/x-pack/plugins/observability/public/services/get_news_feed.test.ts b/x-pack/plugins/observability/public/services/get_news_feed.test.ts
new file mode 100644
index 0000000000000..49eb2da803ab6
--- /dev/null
+++ b/x-pack/plugins/observability/public/services/get_news_feed.test.ts
@@ -0,0 +1,98 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { getNewsFeed } from './get_news_feed';
+import { AppMountContext } from 'kibana/public';
+
+describe('getNewsFeed', () => {
+ it('Returns empty array when api throws exception', async () => {
+ const core = ({
+ http: {
+ get: async () => {
+ throw new Error('Boom');
+ },
+ },
+ } as unknown) as AppMountContext['core'];
+
+ const newsFeed = await getNewsFeed({ core });
+ expect(newsFeed.items).toEqual([]);
+ });
+ it('Returns array with the news feed', async () => {
+ const core = ({
+ http: {
+ get: async () => {
+ return {
+ items: [
+ {
+ title: {
+ en: 'Elastic introduces OpenTelemetry integration',
+ },
+ description: {
+ en:
+ 'We are pleased to announce the availability of the Elastic OpenTelemetry integration — available on Elastic Cloud, or when you download Elastic APM.',
+ },
+ link_text: null,
+ link_url: {
+ en:
+ 'https://www.elastic.co/blog/elastic-apm-opentelemetry-integration?blade=observabilitysolutionfeed',
+ },
+ languages: null,
+ badge: null,
+ image_url: null,
+ publish_on: '2020-07-02T00:00:00',
+ expire_on: '2021-05-02T00:00:00',
+ hash: '012caf3e161127d618ae8cc95e3e63f009a45d343eedf2f5e369cc95b1f9d9d3',
+ },
+ {
+ title: {
+ en: 'Kubernetes observability tutorial: Log monitoring and analysis',
+ },
+ description: {
+ en:
+ 'Learn how Elastic Observability makes it easy to monitor and detect anomalies in millions of logs from thousands of containers running hundreds of microservices — while Kubernetes scales applications with changing pod counts. All from a single UI.',
+ },
+ link_text: null,
+ link_url: {
+ en:
+ 'https://www.elastic.co/blog/kubernetes-observability-tutorial-k8s-log-monitoring-and-analysis-elastic-stack?blade=observabilitysolutionfeed',
+ },
+ languages: null,
+ badge: null,
+ image_url: null,
+ publish_on: '2020-06-23T00:00:00',
+ expire_on: '2021-06-23T00:00:00',
+ hash: '79a28cb9be717e82df80bf32c27e5d475e56d0d315be694b661d133f9a58b3b3',
+ },
+ {
+ title: {
+ en:
+ 'Kubernetes observability tutorial: K8s cluster setup and demo app deployment',
+ },
+ description: {
+ en:
+ 'This blog will walk you through configuring the environment you will be using for the Kubernetes observability tutorial blog series. We will be deploying Elasticsearch Service, a Minikube single-node Kubernetes cluster setup, and a demo app.',
+ },
+ link_text: null,
+ link_url: {
+ en:
+ 'https://www.elastic.co/blog/kubernetes-observability-tutorial-k8s-cluster-setup-demo-app-deployment?blade=observabilitysolutionfeed',
+ },
+ languages: null,
+ badge: null,
+ image_url: null,
+ publish_on: '2020-06-23T00:00:00',
+ expire_on: '2021-06-23T00:00:00',
+ hash: 'ad682c355af3d4470a14df116df3b441e941661b291cdac62335615e7c6f13c2',
+ },
+ ],
+ };
+ },
+ },
+ } as unknown) as AppMountContext['core'];
+
+ const newsFeed = await getNewsFeed({ core });
+ expect(newsFeed.items.length).toEqual(3);
+ });
+});
diff --git a/x-pack/plugins/observability/public/services/get_news_feed.ts b/x-pack/plugins/observability/public/services/get_news_feed.ts
new file mode 100644
index 0000000000000..3a6e60fa74188
--- /dev/null
+++ b/x-pack/plugins/observability/public/services/get_news_feed.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { AppMountContext } from 'kibana/public';
+
+export interface NewsItem {
+ title: { en: string };
+ description: { en: string };
+ link_url: { en: string };
+ image_url?: { en: string } | null;
+}
+
+interface NewsFeed {
+ items: NewsItem[];
+}
+
+export async function getNewsFeed({ core }: { core: AppMountContext['core'] }): Promise {
+ try {
+ return await core.http.get('https://feeds.elastic.co/observability-solution/v8.0.0.json');
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error('Error while fetching news feed', e);
+ return { items: [] };
+ }
+}
diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.ts
index 1bbabbad2834a..49855a30c16f6 100644
--- a/x-pack/plugins/observability/public/services/get_observability_alerts.ts
+++ b/x-pack/plugins/observability/public/services/get_observability_alerts.ts
@@ -22,6 +22,8 @@ export async function getObservabilityAlerts({ core }: { core: AppMountContext['
);
});
} catch (e) {
+ // eslint-disable-next-line no-console
+ console.error('Error while fetching alerts', e);
return [];
}
}
diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts
index 961a046c846e4..9a9f445de0b13 100644
--- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts
@@ -28,9 +28,9 @@ export { runTaskFnFactory } from './server/execute_job';
export const getExportType = (): ExportTypeDefinition<
JobParamsPanelCsv,
- ImmediateCreateJobFn,
+ ImmediateCreateJobFn,
JobParamsPanelCsv,
- ImmediateExecuteFn
+ ImmediateExecuteFn
> => ({
...metadata,
jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE,
diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts
index dafac04017607..da9810b03aff6 100644
--- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/create_job/index.ts
@@ -20,15 +20,15 @@ import {
} from '../../types';
import { createJobSearch } from './create_job_search';
-export type ImmediateCreateJobFn = (
- jobParams: JobParamsType,
+export type ImmediateCreateJobFn = (
+ jobParams: JobParamsPanelCsv,
headers: KibanaRequest['headers'],
context: RequestHandlerContext,
req: KibanaRequest
) => Promise<{
type: string | null;
title: string;
- jobParams: JobParamsType;
+ jobParams: JobParamsPanelCsv;
}>;
interface VisData {
@@ -37,9 +37,10 @@ interface VisData {
panel: SearchPanel;
}
-export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting, parentLogger) {
+export const scheduleTaskFnFactory: ScheduleTaskFnFactory = function createJobFactoryFn(
+ reporting,
+ parentLogger
+) {
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));
const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']);
diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts
index 26b7a24907f40..912ae0809cf92 100644
--- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/server/execute_job.ts
@@ -7,39 +7,43 @@
import { i18n } from '@kbn/i18n';
import { KibanaRequest, RequestHandlerContext } from 'src/core/server';
import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants';
-import { cryptoFactory } from '../../../lib';
import { RunTaskFnFactory, ScheduledTaskParams, TaskRunResult } from '../../../types';
import { CsvResultFromSearch } from '../../csv/types';
-import { FakeRequest, JobParamsPanelCsv, SearchPanel } from '../types';
+import { JobParamsPanelCsv, SearchPanel } from '../types';
import { createGenerateCsv } from './lib';
+/*
+ * The run function receives the full request which provides the un-encrypted
+ * headers, so encrypted headers are not part of these kind of job params
+ */
+type ImmediateJobParams = Omit, 'headers'>;
+
/*
* ImmediateExecuteFn receives the job doc payload because the payload was
* generated in the ScheduleFn
*/
-export type ImmediateExecuteFn = (
+export type ImmediateExecuteFn = (
jobId: null,
- job: ScheduledTaskParams,
+ job: ImmediateJobParams,
context: RequestHandlerContext,
req: KibanaRequest
) => Promise;
-export const runTaskFnFactory: RunTaskFnFactory> = function executeJobFactoryFn(reporting, parentLogger) {
- const config = reporting.getConfig();
- const crypto = cryptoFactory(config.get('encryptionKey'));
+export const runTaskFnFactory: RunTaskFnFactory = function executeJobFactoryFn(
+ reporting,
+ parentLogger
+) {
const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']);
const generateCsv = createGenerateCsv(reporting, parentLogger);
- return async function runTask(jobId: string | null, job, context, req) {
+ return async function runTask(jobId: string | null, job, context, request) {
// There will not be a jobID for "immediate" generation.
// jobID is only for "queued" jobs
// Use the jobID as a logging tag or "immediate"
const jobLogger = logger.clone([jobId === null ? 'immediate' : jobId]);
const { jobParams } = job;
- const { isImmediate, panel, visType } = jobParams as JobParamsPanelCsv & { panel: SearchPanel };
+ const { panel, visType } = jobParams as JobParamsPanelCsv & { panel: SearchPanel };
if (!panel) {
i18n.translate(
@@ -50,54 +54,13 @@ export const runTaskFnFactory: RunTaskFnFactory;
- const serializedEncryptedHeaders = job.headers;
- try {
- if (typeof serializedEncryptedHeaders !== 'string') {
- throw new Error(
- i18n.translate(
- 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.missingJobHeadersErrorMessage',
- {
- defaultMessage: 'Job headers are missing',
- }
- )
- );
- }
- decryptedHeaders = (await crypto.decrypt(serializedEncryptedHeaders)) as Record<
- string,
- unknown
- >;
- } catch (err) {
- jobLogger.error(err);
- throw new Error(
- i18n.translate(
- 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.failedToDecryptReportJobDataErrorMessage',
- {
- defaultMessage:
- 'Failed to decrypt report job data. Please ensure that {encryptionKey} is set and re-generate this report. {err}',
- values: { encryptionKey: 'xpack.reporting.encryptionKey', err },
- }
- )
- );
- }
-
- requestObject = { headers: decryptedHeaders };
- }
-
let content: string;
let maxSizeReached = false;
let size = 0;
try {
const generateResults: CsvResultFromSearch = await generateCsv(
context,
- requestObject,
+ request,
visType as string,
panel,
jobParams
diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts
index 835b352953dfe..c182fe49a31f6 100644
--- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts
@@ -23,10 +23,6 @@ export interface JobParamsPanelCsv {
visType?: string;
}
-export interface ScheduledTaskParamsPanelCsv extends ScheduledTaskParams {
- jobParams: JobParamsPanelCsv;
-}
-
export interface SavedObjectServiceError {
statusCode: number;
error?: string;
diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject.ts
deleted file mode 100644
index b8326406743b7..0000000000000
--- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject.ts
+++ /dev/null
@@ -1,85 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { schema } from '@kbn/config-schema';
-import { get } from 'lodash';
-import { HandlerErrorFunction, HandlerFunction, QueuedJobPayload } from './types';
-import { ReportingCore } from '../';
-import { API_BASE_GENERATE_V1, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../common/constants';
-import { getJobParamsFromRequest } from '../export_types/csv_from_savedobject/server/lib/get_job_params_from_request';
-import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing';
-
-/*
- * This function registers API Endpoints for queuing Reporting jobs. The API inputs are:
- * - saved object type and ID
- * - time range and time zone
- * - application state:
- * - filters
- * - query bar
- * - local (transient) changes the user made to the saved object
- */
-export function registerGenerateCsvFromSavedObject(
- reporting: ReportingCore,
- handleRoute: HandlerFunction,
- handleRouteError: HandlerErrorFunction
-) {
- const setupDeps = reporting.getPluginSetupDeps();
- const userHandler = authorizedUserPreRoutingFactory(reporting);
- const { router } = setupDeps;
- router.post(
- {
- path: `${API_BASE_GENERATE_V1}/csv/saved-object/{savedObjectType}:{savedObjectId}`,
- validate: {
- params: schema.object({
- savedObjectType: schema.string({ minLength: 2 }),
- savedObjectId: schema.string({ minLength: 2 }),
- }),
- body: schema.object({
- state: schema.object({}),
- timerange: schema.object({
- timezone: schema.string({ defaultValue: 'UTC' }),
- min: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])),
- max: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])),
- }),
- }),
- },
- },
- userHandler(async (user, context, req, res) => {
- /*
- * 1. Build `jobParams` object: job data that execution will need to reference in various parts of the lifecycle
- * 2. Pass the jobParams and other common params to `handleRoute`, a shared function to enqueue the job with the params
- * 3. Ensure that details for a queued job were returned
- */
- let result: QueuedJobPayload;
- try {
- const jobParams = getJobParamsFromRequest(req, { isImmediate: false });
- result = await handleRoute(
- user,
- CSV_FROM_SAVEDOBJECT_JOB_TYPE,
- jobParams,
- context,
- req,
- res
- );
- } catch (err) {
- return handleRouteError(res, err);
- }
-
- if (get(result, 'source.job') == null) {
- return res.badRequest({
- body: `The Export handler is expected to return a result with job info! ${result}`,
- });
- }
-
- return res.ok({
- body: result,
- headers: {
- 'content-type': 'application/json',
- },
- });
- })
- );
-}
diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts
index 7d93a36c85bc8..97441bba70984 100644
--- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts
+++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts
@@ -10,7 +10,6 @@ import { API_BASE_GENERATE_V1 } from '../../common/constants';
import { scheduleTaskFnFactory } from '../export_types/csv_from_savedobject/server/create_job';
import { runTaskFnFactory } from '../export_types/csv_from_savedobject/server/execute_job';
import { getJobParamsFromRequest } from '../export_types/csv_from_savedobject/server/lib/get_job_params_from_request';
-import { ScheduledTaskParamsPanelCsv } from '../export_types/csv_from_savedobject/types';
import { LevelLogger as Logger } from '../lib';
import { TaskRunResult } from '../types';
import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing';
@@ -64,12 +63,8 @@ export function registerGenerateCsvFromSavedObjectImmediate(
const runTaskFn = runTaskFnFactory(reporting, logger);
try {
- const jobDocPayload: ScheduledTaskParamsPanelCsv = await scheduleTaskFn(
- jobParams,
- req.headers,
- context,
- req
- );
+ // FIXME: no scheduleTaskFn for immediate download
+ const jobDocPayload = await scheduleTaskFn(jobParams, req.headers, context, req);
const {
content_type: jobOutputContentType,
content: jobOutputContent,
@@ -91,11 +86,12 @@ export function registerGenerateCsvFromSavedObjectImmediate(
return res.ok({
body: jobOutputContent || '',
headers: {
- 'content-type': jobOutputContentType,
+ 'content-type': jobOutputContentType ? jobOutputContentType : [],
'accept-ranges': 'none',
},
});
} catch (err) {
+ logger.error(err);
return handleError(res, err);
}
})
diff --git a/x-pack/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generation.test.ts
index 7de7c68122125..c73c443d2390b 100644
--- a/x-pack/plugins/reporting/server/routes/generation.test.ts
+++ b/x-pack/plugins/reporting/server/routes/generation.test.ts
@@ -4,16 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
+import { of } from 'rxjs';
+import sinon from 'sinon';
import { setupServer } from 'src/core/server/test_utils';
-import { registerJobGenerationRoutes } from './generation';
-import { createMockReportingCore } from '../test_helpers';
+import supertest from 'supertest';
import { ReportingCore } from '..';
import { ExportTypesRegistry } from '../lib/export_types_registry';
-import { ExportTypeDefinition } from '../types';
-import { LevelLogger } from '../lib';
-import { of } from 'rxjs';
+import { createMockReportingCore } from '../test_helpers';
+import { createMockLevelLogger } from '../test_helpers/create_mock_levellogger';
+import { registerJobGenerationRoutes } from './generation';
type setupServerReturn = UnwrapPromise>;
@@ -21,7 +21,8 @@ describe('POST /api/reporting/generate', () => {
const reportingSymbol = Symbol('reporting');
let server: setupServerReturn['server'];
let httpSetup: setupServerReturn['httpSetup'];
- let exportTypesRegistry: ExportTypesRegistry;
+ let mockExportTypesRegistry: ExportTypesRegistry;
+ let callClusterStub: any;
let core: ReportingCore;
const config = {
@@ -29,7 +30,7 @@ describe('POST /api/reporting/generate', () => {
const key = args.join('.');
switch (key) {
case 'queue.indexInterval':
- return 10000;
+ return 'year';
case 'queue.timeout':
return 10000;
case 'index':
@@ -42,56 +43,45 @@ describe('POST /api/reporting/generate', () => {
}),
kbnConfig: { get: jest.fn() },
};
- const mockLogger = ({
- error: jest.fn(),
- debug: jest.fn(),
- } as unknown) as jest.Mocked;
+ const mockLogger = createMockLevelLogger();
beforeEach(async () => {
({ server, httpSetup } = await setupServer(reportingSymbol));
httpSetup.registerRouteHandlerContext(reportingSymbol, 'reporting', () => ({}));
- const mockDeps = ({
+
+ callClusterStub = sinon.stub().resolves({});
+
+ const mockSetupDeps = ({
elasticsearch: {
- legacy: {
- client: { callAsInternalUser: jest.fn() },
- },
+ legacy: { client: { callAsInternalUser: callClusterStub } },
},
security: {
- license: {
- isEnabled: () => true,
- },
+ license: { isEnabled: () => true },
authc: {
- getCurrentUser: () => ({
- id: '123',
- roles: ['superuser'],
- username: 'Tom Riddle',
- }),
+ getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }),
},
},
router: httpSetup.createRouter(''),
- licensing: {
- license$: of({
- isActive: true,
- isAvailable: true,
- type: 'gold',
- }),
- },
+ licensing: { license$: of({ isActive: true, isAvailable: true, type: 'gold' }) },
} as unknown) as any;
- core = await createMockReportingCore(config, mockDeps);
- exportTypesRegistry = new ExportTypesRegistry();
- exportTypesRegistry.register({
+
+ core = await createMockReportingCore(config, mockSetupDeps);
+
+ mockExportTypesRegistry = new ExportTypesRegistry();
+ mockExportTypesRegistry.register({
id: 'printablePdf',
+ name: 'not sure why this field exists',
jobType: 'printable_pdf',
jobContentEncoding: 'base64',
jobContentExtension: 'pdf',
validLicenses: ['basic', 'gold'],
- } as ExportTypeDefinition);
- core.getExportTypesRegistry = () => exportTypesRegistry;
+ scheduleTaskFnFactory: () => () => ({ scheduleParamsTest: { test1: 'yes' } }),
+ runTaskFnFactory: () => () => ({ runParamsTest: { test2: 'yes' } }),
+ });
+ core.getExportTypesRegistry = () => mockExportTypesRegistry;
});
afterEach(async () => {
- mockLogger.debug.mockReset();
- mockLogger.error.mockReset();
await server.stop();
});
@@ -147,14 +137,9 @@ describe('POST /api/reporting/generate', () => {
);
});
- it('returns 400 if job handler throws an error', async () => {
- const errorText = 'you found me';
- core.getEnqueueJob = async () =>
- jest.fn().mockImplementation(() => ({
- toJSON: () => {
- throw new Error(errorText);
- },
- }));
+ it('returns 500 if job handler throws an error', async () => {
+ // throw an error from enqueueJob
+ core.getEnqueueJob = jest.fn().mockRejectedValue('Sorry, this tests says no');
registerJobGenerationRoutes(core, mockLogger);
@@ -163,9 +148,27 @@ describe('POST /api/reporting/generate', () => {
await supertest(httpSetup.server.listener)
.post('/api/reporting/generate/printablePdf')
.send({ jobParams: `abc` })
- .expect(400)
+ .expect(500);
+ });
+
+ it(`returns 200 if job handler doesn't error`, async () => {
+ callClusterStub.withArgs('index').resolves({ _id: 'foo', _index: 'foo-index' });
+
+ registerJobGenerationRoutes(core, mockLogger);
+
+ await server.start();
+
+ await supertest(httpSetup.server.listener)
+ .post('/api/reporting/generate/printablePdf')
+ .send({ jobParams: `abc` })
+ .expect(200)
.then(({ body }) => {
- expect(body.message).toMatchInlineSnapshot(`"${errorText}"`);
+ expect(body).toMatchObject({
+ job: {
+ id: expect.any(String),
+ },
+ path: expect.any(String),
+ });
});
});
});
diff --git a/x-pack/plugins/reporting/server/routes/generation.ts b/x-pack/plugins/reporting/server/routes/generation.ts
index b4c81e698ce71..017e875931ae2 100644
--- a/x-pack/plugins/reporting/server/routes/generation.ts
+++ b/x-pack/plugins/reporting/server/routes/generation.ts
@@ -11,7 +11,6 @@ import { ReportingCore } from '../';
import { API_BASE_URL } from '../../common/constants';
import { LevelLogger as Logger } from '../lib';
import { registerGenerateFromJobParams } from './generate_from_jobparams';
-import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject';
import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate';
import { HandlerFunction } from './types';
@@ -43,24 +42,32 @@ export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Lo
return res.forbidden({ body: licenseResults.message });
}
- const enqueueJob = await reporting.getEnqueueJob();
- const job = await enqueueJob(exportTypeId, jobParams, user, context, req);
-
- // return the queue's job information
- const jobJson = job.toJSON();
- const downloadBaseUrl = getDownloadBaseUrl(reporting);
-
- return res.ok({
- headers: {
- 'content-type': 'application/json',
- },
- body: {
- path: `${downloadBaseUrl}/${jobJson.id}`,
- job: jobJson,
- },
- });
+ try {
+ const enqueueJob = await reporting.getEnqueueJob();
+ const job = await enqueueJob(exportTypeId, jobParams, user, context, req);
+
+ // return the queue's job information
+ const jobJson = job.toJSON();
+ const downloadBaseUrl = getDownloadBaseUrl(reporting);
+
+ return res.ok({
+ headers: {
+ 'content-type': 'application/json',
+ },
+ body: {
+ path: `${downloadBaseUrl}/${jobJson.id}`,
+ job: jobJson,
+ },
+ });
+ } catch (err) {
+ logger.error(err);
+ throw err;
+ }
};
+ /*
+ * Error should already have been logged by the time we get here
+ */
function handleError(res: typeof kibanaResponseFactory, err: Error | Boom) {
if (err instanceof Boom) {
return res.customError({
@@ -87,12 +94,10 @@ export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Lo
});
}
- return res.badRequest({
- body: err.message,
- });
+ // unknown error, can't convert to 4xx
+ throw err;
}
registerGenerateFromJobParams(reporting, handler, handleError);
- registerGenerateCsvFromSavedObject(reporting, handler, handleError); // FIXME: remove this https://github.com/elastic/kibana/issues/62986
registerGenerateCsvFromSavedObjectImmediate(reporting, handleError, logger);
}
diff --git a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts
index a8492481e6b13..651f1c34fee6c 100644
--- a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts
+++ b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts
@@ -46,20 +46,20 @@ export function downloadJobResponseHandlerFactory(reporting: ReportingCore) {
});
}
- const response = getDocumentPayload(doc);
+ const payload = getDocumentPayload(doc);
- if (!WHITELISTED_JOB_CONTENT_TYPES.includes(response.contentType)) {
+ if (!payload.contentType || !WHITELISTED_JOB_CONTENT_TYPES.includes(payload.contentType)) {
return res.badRequest({
- body: `Unsupported content-type of ${response.contentType} specified by job output`,
+ body: `Unsupported content-type of ${payload.contentType} specified by job output`,
});
}
return res.custom({
- body: typeof response.content === 'string' ? Buffer.from(response.content) : response.content,
- statusCode: response.statusCode,
+ body: typeof payload.content === 'string' ? Buffer.from(payload.content) : payload.content,
+ statusCode: payload.statusCode,
headers: {
- ...response.headers,
- 'content-type': response.contentType,
+ ...payload.headers,
+ 'content-type': payload.contentType || '',
},
});
};
diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
index 427a6362a7258..95b06aa39f07e 100644
--- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
+++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
@@ -22,6 +22,7 @@ import { ReportingInternalSetup, ReportingInternalStart } from '../core';
import { ReportingStartDeps } from '../types';
import { ReportingStore } from '../lib';
import { createMockLevelLogger } from './create_mock_levellogger';
+import { Report } from '../lib/store';
(initializeBrowserDriverFactory as jest.Mock<
Promise
@@ -47,7 +48,7 @@ const createMockPluginStart = (
const store = new ReportingStore(mockReportingCore, logger);
return {
browserDriverFactory: startMock.browserDriverFactory,
- enqueueJob: startMock.enqueueJob,
+ enqueueJob: startMock.enqueueJob || jest.fn().mockResolvedValue(new Report({} as any)),
esqueue: startMock.esqueue,
savedObjects: startMock.savedObjects || { getScopedClient: jest.fn() },
uiSettings: startMock.uiSettings || { asScopedToClient: () => ({ get: jest.fn() }) },
diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx
index 743510d45107e..d83d5ef3f6468 100644
--- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx
+++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx
@@ -45,7 +45,9 @@ describe(' ', () => {
let history: ScopedHistory;
beforeEach(() => {
- history = (scopedHistoryMock.create() as unknown) as ScopedHistory;
+ history = (scopedHistoryMock.create({
+ createHref: jest.fn((location) => location.pathname!),
+ }) as unknown) as ScopedHistory;
apiClientMock = rolesAPIClientMock.create();
apiClientMock.getRoles.mockResolvedValue([
{
@@ -135,15 +137,19 @@ describe(' ', () => {
});
expect(wrapper.find(PermissionDenied)).toHaveLength(0);
- expect(
- wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-test-role-1"]')
- ).toHaveLength(1);
- expect(
- wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-disabled-role"]')
- ).toHaveLength(1);
+
+ const editButton = wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-test-role-1"]');
+ expect(editButton).toHaveLength(1);
+ expect(editButton.prop('href')).toBe('/edit/test-role-1');
+
+ const cloneButton = wrapper.find(
+ 'EuiButtonIcon[data-test-subj="clone-role-action-test-role-1"]'
+ );
+ expect(cloneButton).toHaveLength(1);
+ expect(cloneButton.prop('href')).toBe('/clone/test-role-1');
expect(
- wrapper.find('EuiButtonIcon[data-test-subj="clone-role-action-test-role-1"]')
+ wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-disabled-role"]')
).toHaveLength(1);
expect(
wrapper.find('EuiButtonIcon[data-test-subj="clone-role-action-disabled-role"]')
diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx
index 051c16f03d342..c2ea119100722 100644
--- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx
+++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx
@@ -262,7 +262,7 @@ export class RolesGridPage extends Component {
iconType={'copy'}
{...reactRouterNavigate(
this.props.history,
- getRoleManagementHref('edit', role.name)
+ getRoleManagementHref('clone', role.name)
)}
/>
);
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index a34a76361f799..7cd5692176ee3 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -36,7 +36,7 @@ export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C
export const ENDPOINT_METADATA_INDEX = 'metrics-endpoint.metadata-*';
export enum SecurityPageName {
- alerts = 'alerts',
+ detections = 'detections',
overview = 'overview',
hosts = 'hosts',
network = 'network',
@@ -46,12 +46,12 @@ export enum SecurityPageName {
}
export const APP_OVERVIEW_PATH = `${APP_PATH}/overview`;
-export const APP_ALERTS_PATH = `${APP_PATH}/alerts`;
+export const APP_DETECTIONS_PATH = `${APP_PATH}/detections`;
export const APP_HOSTS_PATH = `${APP_PATH}/hosts`;
export const APP_NETWORK_PATH = `${APP_PATH}/network`;
export const APP_TIMELINES_PATH = `${APP_PATH}/timelines`;
export const APP_CASES_PATH = `${APP_PATH}/cases`;
-export const APP_MANAGEMENT_PATH = `${APP_PATH}/management`;
+export const APP_MANAGEMENT_PATH = `${APP_PATH}/administration`;
/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */
export const DEFAULT_INDEX_PATTERN = [
@@ -165,13 +165,6 @@ export const showAllOthersBucket: string[] = [
'user.name',
];
-/**
- * CreateTemplateTimelineBtn
- * https://github.com/elastic/kibana/pull/66613
- * Remove the comment here to enable template timeline
- */
-export const disableTemplate = false;
-
/*
* This should be set to true after https://github.com/elastic/kibana/pull/67496 is merged
*/
diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
index 6720f3523d5c7..339e5554ccb12 100644
--- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
@@ -1036,8 +1036,8 @@ export class EndpointDocGenerator {
config: {
artifact_manifest: {
value: {
- manifest_version: 'v0',
- schema_version: '1.0.0',
+ manifest_version: 'WzAsMF0=',
+ schema_version: 'v1',
artifacts: {},
},
},
diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/common.ts b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts
index fdb2570314cd0..014673ebe6398 100644
--- a/x-pack/plugins/security_solution/common/endpoint/schema/common.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts
@@ -10,6 +10,7 @@ export const compressionAlgorithm = t.keyof({
none: null,
zlib: null,
});
+export type CompressionAlgorithm = t.TypeOf;
export const encryptionAlgorithm = t.keyof({
none: null,
@@ -20,7 +21,7 @@ export const identifier = t.string;
export const manifestVersion = t.string;
export const manifestSchemaVersion = t.keyof({
- '1.0.0': null,
+ v1: null,
});
export type ManifestSchemaVersion = t.TypeOf;
diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts
index 2cf5930a83bee..90d254b15e8b3 100644
--- a/x-pack/plugins/security_solution/common/types/timeline/index.ts
+++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts
@@ -50,6 +50,16 @@ const SavedDataProviderQueryMatchRuntimeType = runtimeTypes.partial({
queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType),
});
+export enum DataProviderType {
+ default = 'default',
+ template = 'template',
+}
+
+export const DataProviderTypeLiteralRt = runtimeTypes.union([
+ runtimeTypes.literal(DataProviderType.default),
+ runtimeTypes.literal(DataProviderType.template),
+]);
+
const SavedDataProviderRuntimeType = runtimeTypes.partial({
id: unionWithNullType(runtimeTypes.string),
name: unionWithNullType(runtimeTypes.string),
@@ -58,6 +68,7 @@ const SavedDataProviderRuntimeType = runtimeTypes.partial({
kqlQuery: unionWithNullType(runtimeTypes.string),
queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType),
and: unionWithNullType(runtimeTypes.array(SavedDataProviderQueryMatchRuntimeType)),
+ type: unionWithNullType(DataProviderTypeLiteralRt),
});
/*
@@ -154,7 +165,7 @@ export type TimelineStatusLiteralWithNull = runtimeTypes.TypeOf<
>;
/**
- * Template timeline type
+ * Timeline template type
*/
export enum TemplateTimelineType {
@@ -229,8 +240,8 @@ export interface SavedTimelineNote extends runtimeTypes.TypeOf {
context('Closing alerts', () => {
beforeEach(() => {
esArchiverLoad('alerts');
- loginAndWaitForPage(ALERTS_URL);
+ loginAndWaitForPage(DETECTIONS_URL);
});
it('Closes and opens alerts', () => {
@@ -162,7 +162,7 @@ describe.skip('Alerts', () => {
context('Opening alerts', () => {
beforeEach(() => {
esArchiverLoad('closed_alerts');
- loginAndWaitForPage(ALERTS_URL);
+ loginAndWaitForPage(DETECTIONS_URL);
});
it('Open one alert when more than one closed alerts are selected', () => {
@@ -208,7 +208,7 @@ describe.skip('Alerts', () => {
context('Marking alerts as in-progress', () => {
beforeEach(() => {
esArchiverLoad('alerts');
- loginAndWaitForPage(ALERTS_URL);
+ loginAndWaitForPage(DETECTIONS_URL);
});
it('Mark one alert in progress when more than one open alerts are selected', () => {
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts
index 5cad0b9c3260c..20cf624b3360d 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts
@@ -26,7 +26,7 @@ import {
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
-import { ALERTS_URL } from '../urls/navigation';
+import { DETECTIONS_URL } from '../urls/navigation';
describe('Alerts detection rules', () => {
before(() => {
@@ -38,7 +38,7 @@ describe('Alerts detection rules', () => {
});
it('Sorts by activated rules', () => {
- loginAndWaitForPageWithoutDateRange(ALERTS_URL);
+ loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
index 684570450aa05..81832b3d9edea 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
@@ -62,7 +62,7 @@ import {
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
-import { ALERTS_URL } from '../urls/navigation';
+import { DETECTIONS_URL } from '../urls/navigation';
// Flaky: https://github.com/elastic/kibana/issues/67814
describe.skip('Detection rules, custom', () => {
@@ -75,7 +75,7 @@ describe.skip('Detection rules, custom', () => {
});
it('Creates and activates a new custom rule', () => {
- loginAndWaitForPageWithoutDateRange(ALERTS_URL);
+ loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
@@ -170,7 +170,7 @@ describe.skip('Detection rules, custom', () => {
describe('Deletes custom rules', () => {
beforeEach(() => {
esArchiverLoad('custom_rules');
- loginAndWaitForPageWithoutDateRange(ALERTS_URL);
+ loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts
index fdab3016de8de..a7e6652613493 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts
@@ -13,11 +13,12 @@ import { exportFirstRule } from '../tasks/alerts_detection_rules';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
-import { ALERTS_URL } from '../urls/navigation';
+import { DETECTIONS_URL } from '../urls/navigation';
const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson';
-describe('Export rules', () => {
+// Flakky: https://github.com/elastic/kibana/issues/69849
+describe.skip('Export rules', () => {
before(() => {
esArchiverLoad('export_rule');
cy.server();
@@ -32,7 +33,7 @@ describe('Export rules', () => {
});
it('Exports a custom rule', () => {
- loginAndWaitForPageWithoutDateRange(ALERTS_URL);
+ loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts
index 19957a53dd701..b6b30ef550eb1 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts
@@ -58,7 +58,7 @@ import {
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
-import { ALERTS_URL } from '../urls/navigation';
+import { DETECTIONS_URL } from '../urls/navigation';
describe('Detection rules, machine learning', () => {
before(() => {
@@ -70,7 +70,7 @@ describe('Detection rules, machine learning', () => {
});
it('Creates and activates a new ml rule', () => {
- loginAndWaitForPageWithoutDateRange(ALERTS_URL);
+ loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts
index d3cbb05d7fc17..986a7c7177a79 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_prebuilt.spec.ts
@@ -31,7 +31,7 @@ import {
import { esArchiverLoadEmptyKibana, esArchiverUnloadEmptyKibana } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
-import { ALERTS_URL } from '../urls/navigation';
+import { DETECTIONS_URL } from '../urls/navigation';
import { totalNumberOfPrebuiltRules } from '../objects/rule';
@@ -48,7 +48,7 @@ describe('Alerts rules, prebuilt rules', () => {
const expectedNumberOfRules = totalNumberOfPrebuiltRules;
const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`;
- loginAndWaitForPageWithoutDateRange(ALERTS_URL);
+ loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
@@ -73,7 +73,7 @@ describe('Deleting prebuilt rules', () => {
const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`;
esArchiverLoadEmptyKibana();
- loginAndWaitForPageWithoutDateRange(ALERTS_URL);
+ loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts
index 10dc4fdd44486..b37aabf4825fc 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts
@@ -15,12 +15,13 @@ import {
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPage } from '../tasks/login';
-import { ALERTS_URL } from '../urls/navigation';
+import { DETECTIONS_URL } from '../urls/navigation';
-describe('Alerts timeline', () => {
+// Flakky: https://github.com/elastic/kibana/issues/71220
+describe.skip('Alerts timeline', () => {
beforeEach(() => {
esArchiverLoad('timeline_alerts');
- loginAndWaitForPage(ALERTS_URL);
+ loginAndWaitForPage(DETECTIONS_URL);
});
afterEach(() => {
diff --git a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts
index ea3a78c77152a..e4f0ec2c4828f 100644
--- a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts
@@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
- ALERTS,
CASES,
+ DETECTIONS,
HOSTS,
MANAGEMENT,
NETWORK,
@@ -17,21 +17,21 @@ import { loginAndWaitForPage } from '../tasks/login';
import { navigateFromHeaderTo } from '../tasks/security_header';
import {
- ALERTS_URL,
+ DETECTIONS_URL,
CASES_URL,
HOSTS_URL,
KIBANA_HOME,
- MANAGEMENT_URL,
+ ADMINISTRATION_URL,
NETWORK_URL,
OVERVIEW_URL,
TIMELINES_URL,
} from '../urls/navigation';
import { openKibanaNavigation, navigateFromKibanaCollapsibleTo } from '../tasks/kibana_navigation';
import {
- ALERTS_PAGE,
CASES_PAGE,
+ DETECTIONS_PAGE,
HOSTS_PAGE,
- MANAGEMENT_PAGE,
+ ADMINISTRATION_PAGE,
NETWORK_PAGE,
OVERVIEW_PAGE,
TIMELINES_PAGE,
@@ -47,9 +47,9 @@ describe('top-level navigation common to all pages in the Security app', () => {
cy.url().should('include', OVERVIEW_URL);
});
- it('navigates to the Alerts page', () => {
- navigateFromHeaderTo(ALERTS);
- cy.url().should('include', ALERTS_URL);
+ it('navigates to the Detections page', () => {
+ navigateFromHeaderTo(DETECTIONS);
+ cy.url().should('include', DETECTIONS_URL);
});
it('navigates to the Hosts page', () => {
@@ -72,9 +72,9 @@ describe('top-level navigation common to all pages in the Security app', () => {
cy.url().should('include', CASES_URL);
});
- it('navigates to the Management page', () => {
+ it('navigates to the Administration page', () => {
navigateFromHeaderTo(MANAGEMENT);
- cy.url().should('include', MANAGEMENT_URL);
+ cy.url().should('include', ADMINISTRATION_URL);
});
});
@@ -90,9 +90,9 @@ describe('Kibana navigation to all pages in the Security app ', () => {
cy.url().should('include', OVERVIEW_URL);
});
- it('navigates to the Alerts page', () => {
- navigateFromKibanaCollapsibleTo(ALERTS_PAGE);
- cy.url().should('include', ALERTS_URL);
+ it('navigates to the Detections page', () => {
+ navigateFromKibanaCollapsibleTo(DETECTIONS_PAGE);
+ cy.url().should('include', DETECTIONS_URL);
});
it('navigates to the Hosts page', () => {
@@ -115,8 +115,8 @@ describe('Kibana navigation to all pages in the Security app ', () => {
cy.url().should('include', CASES_URL);
});
- it('navigates to the Management page', () => {
- navigateFromKibanaCollapsibleTo(MANAGEMENT_PAGE);
- cy.url().should('include', MANAGEMENT_URL);
+ it('navigates to the Administration page', () => {
+ navigateFromKibanaCollapsibleTo(ADMINISTRATION_PAGE);
+ cy.url().should('include', ADMINISTRATION_URL);
});
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts
index 911fd7e0f3483..205a49fc771cf 100644
--- a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts
@@ -9,9 +9,9 @@ import { loginAndWaitForPage } from '../tasks/login';
import { DETECTIONS } from '../urls/navigation';
describe('URL compatibility', () => {
- it('Redirects to Alerts from old Detections URL', () => {
+ it('Redirects to Detection alerts from old Detections URL', () => {
loginAndWaitForPage(DETECTIONS);
- cy.url().should('include', '/security/alerts');
+ cy.url().should('include', '/security/detections');
});
});
diff --git a/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts b/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts
index 2f7956ce370bc..68352c6e584cc 100644
--- a/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const ALERTS_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Alerts"]';
+export const DETECTIONS_PAGE =
+ '[data-test-subj="collapsibleNavGroup-security"] [title="Detections"]';
export const CASES_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Cases"]';
@@ -12,8 +13,8 @@ export const HOSTS_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [titl
export const KIBANA_NAVIGATION_TOGGLE = '[data-test-subj="toggleNavButton"]';
-export const MANAGEMENT_PAGE =
- '[data-test-subj="collapsibleNavGroup-security"] [title="Management"]';
+export const ADMINISTRATION_PAGE =
+ '[data-test-subj="collapsibleNavGroup-security"] [title="Administration"]';
export const NETWORK_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Network"]';
diff --git a/x-pack/plugins/security_solution/cypress/screens/security_header.ts b/x-pack/plugins/security_solution/cypress/screens/security_header.ts
index 17d8aed1c2d21..20fcae60415ae 100644
--- a/x-pack/plugins/security_solution/cypress/screens/security_header.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/security_header.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const ALERTS = '[data-test-subj="navigation-alerts"]';
+export const DETECTIONS = '[data-test-subj="navigation-detections"]';
export const BREADCRUMBS = '[data-test-subj="breadcrumbs"] a';
diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts
index c673cf34b6dae..14282b84b5ffc 100644
--- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts
@@ -9,7 +9,7 @@ export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]';
export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]';
export const DRAGGABLE_HEADER =
- '[data-test-subj="headers-group"] [data-test-subj="draggable-header"]';
+ '[data-test-subj="events-viewer-panel"] [data-test-subj="headers-group"] [data-test-subj="draggable-header"]';
export const HEADERS_GROUP = '[data-test-subj="headers-group"]';
@@ -21,7 +21,8 @@ export const ID_TOGGLE_FIELD = '[data-test-subj="toggle-field-_id"]';
export const PROVIDER_BADGE = '[data-test-subj="providerBadge"]';
-export const REMOVE_COLUMN = '[data-test-subj="remove-column"]';
+export const REMOVE_COLUMN =
+ '[data-test-subj="events-viewer-panel"] [data-test-subj="remove-column"]';
export const RESET_FIELDS =
'[data-test-subj="events-viewer-panel"] [data-test-subj="reset-fields"]';
diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
index 761fd2c1e6a0b..37ce9094dc594 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
@@ -27,8 +27,6 @@ import {
import { drag, drop } from '../tasks/common';
-export const hostExistsQuery = 'host.name: *';
-
export const addDescriptionToTimeline = (description: string) => {
cy.get(TIMELINE_DESCRIPTION).type(`${description}{enter}`);
cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).click().invoke('text').should('not.equal', 'Updating');
@@ -79,7 +77,6 @@ export const openTimelineSettings = () => {
};
export const populateTimeline = () => {
- executeTimelineKQL(hostExistsQuery);
cy.get(SERVER_SIDE_EVENT_COUNT)
.invoke('text')
.then((strCount) => {
diff --git a/x-pack/plugins/security_solution/cypress/urls/navigation.ts b/x-pack/plugins/security_solution/cypress/urls/navigation.ts
index 9da9abf388e4d..b53b06db5beda 100644
--- a/x-pack/plugins/security_solution/cypress/urls/navigation.ts
+++ b/x-pack/plugins/security_solution/cypress/urls/navigation.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const ALERTS_URL = 'app/security/alerts';
+export const DETECTIONS_URL = 'app/security/detections';
export const CASES_URL = '/app/security/cases';
export const DETECTIONS = '/app/siem#/detections';
export const HOSTS_URL = '/app/security/hosts/allHosts';
@@ -16,7 +16,7 @@ export const HOSTS_PAGE_TAB_URLS = {
uncommonProcesses: '/app/security/hosts/uncommonProcesses',
};
export const KIBANA_HOME = '/app/home#/';
-export const MANAGEMENT_URL = '/app/security/management';
+export const ADMINISTRATION_URL = '/app/security/administration';
export const NETWORK_URL = '/app/security/network';
export const OVERVIEW_URL = '/app/security/overview';
export const TIMELINES_URL = '/app/security/timelines';
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.test.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.test.ts
deleted file mode 100644
index ad4f5cf8b4aa8..0000000000000
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.test.ts
+++ /dev/null
@@ -1,274 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { cloneDeep } from 'lodash/fp';
-
-import { mockEcsData } from '../../../common/mock/mock_ecs';
-import { Filter } from '../../../../../../../src/plugins/data/public';
-import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider';
-import { mockDataProviders } from '../../../timelines/components/timeline/data_providers/mock/mock_data_providers';
-
-import {
- getStringArray,
- replaceTemplateFieldFromQuery,
- replaceTemplateFieldFromMatchFilters,
- reformatDataProviderWithNewValue,
-} from './helpers';
-
-describe('helpers', () => {
- let mockEcsDataClone = cloneDeep(mockEcsData);
- beforeEach(() => {
- mockEcsDataClone = cloneDeep(mockEcsData);
- });
- describe('getStringOrStringArray', () => {
- test('it should correctly return a string array', () => {
- const value = getStringArray('x', {
- x: 'The nickname of the developer we all :heart:',
- });
- expect(value).toEqual(['The nickname of the developer we all :heart:']);
- });
-
- test('it should correctly return a string array with a single element', () => {
- const value = getStringArray('x', {
- x: ['The nickname of the developer we all :heart:'],
- });
- expect(value).toEqual(['The nickname of the developer we all :heart:']);
- });
-
- test('it should correctly return a string array with two elements of strings', () => {
- const value = getStringArray('x', {
- x: ['The nickname of the developer we all :heart:', 'We are all made of stars'],
- });
- expect(value).toEqual([
- 'The nickname of the developer we all :heart:',
- 'We are all made of stars',
- ]);
- });
-
- test('it should correctly return a string array with deep elements', () => {
- const value = getStringArray('x.y.z', {
- x: { y: { z: 'zed' } },
- });
- expect(value).toEqual(['zed']);
- });
-
- test('it should correctly return a string array with a non-existent value', () => {
- const value = getStringArray('non.existent', {
- x: { y: { z: 'zed' } },
- });
- expect(value).toEqual([]);
- });
-
- test('it should trace an error if the value is not a string', () => {
- const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console;
- const value = getStringArray('a', { a: 5 }, mockConsole);
- expect(value).toEqual([]);
- expect(
- mockConsole.trace
- ).toHaveBeenCalledWith(
- 'Data type that is not a string or string array detected:',
- 5,
- 'when trying to access field:',
- 'a',
- 'from data object of:',
- { a: 5 }
- );
- });
-
- test('it should trace an error if the value is an array of mixed values', () => {
- const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console;
- const value = getStringArray('a', { a: ['hi', 5] }, mockConsole);
- expect(value).toEqual([]);
- expect(
- mockConsole.trace
- ).toHaveBeenCalledWith(
- 'Data type that is not a string or string array detected:',
- ['hi', 5],
- 'when trying to access field:',
- 'a',
- 'from data object of:',
- { a: ['hi', 5] }
- );
- });
- });
-
- describe('replaceTemplateFieldFromQuery', () => {
- test('given an empty query string this returns an empty query string', () => {
- const replacement = replaceTemplateFieldFromQuery('', mockEcsDataClone[0]);
- expect(replacement).toEqual('');
- });
-
- test('given a query string with spaces this returns an empty query string', () => {
- const replacement = replaceTemplateFieldFromQuery(' ', mockEcsDataClone[0]);
- expect(replacement).toEqual('');
- });
-
- test('it should replace a query with a template value such as apache from a mock template', () => {
- const replacement = replaceTemplateFieldFromQuery(
- 'host.name: placeholdertext',
- mockEcsDataClone[0]
- );
- expect(replacement).toEqual('host.name: apache');
- });
-
- test('it should replace a template field with an ECS value that is not an array', () => {
- mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case
- const replacement = replaceTemplateFieldFromQuery('host.name: *', mockEcsDataClone[0]);
- expect(replacement).toEqual('host.name: *');
- });
-
- test('it should NOT replace a query with a template value that is not part of the template fields array', () => {
- const replacement = replaceTemplateFieldFromQuery(
- 'user.id: placeholdertext',
- mockEcsDataClone[0]
- );
- expect(replacement).toEqual('user.id: placeholdertext');
- });
- });
-
- describe('replaceTemplateFieldFromMatchFilters', () => {
- test('given an empty query filter this will return an empty filter', () => {
- const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]);
- expect(replacement).toEqual([]);
- });
-
- test('given a query filter this will return that filter with the placeholder replaced', () => {
- const filters: Filter[] = [
- {
- meta: {
- type: 'phrase',
- key: 'host.name',
- alias: 'alias',
- disabled: false,
- negate: false,
- params: { query: 'Braden' },
- },
- query: { match_phrase: { 'host.name': 'Braden' } },
- },
- ];
- const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]);
- const expected: Filter[] = [
- {
- meta: {
- type: 'phrase',
- key: 'host.name',
- alias: 'alias',
- disabled: false,
- negate: false,
- params: { query: 'apache' },
- },
- query: { match_phrase: { 'host.name': 'apache' } },
- },
- ];
- expect(replacement).toEqual(expected);
- });
-
- test('given a query filter with a value not in the templateFields, this will NOT replace the placeholder value', () => {
- const filters: Filter[] = [
- {
- meta: {
- type: 'phrase',
- key: 'user.id',
- alias: 'alias',
- disabled: false,
- negate: false,
- params: { query: 'Evan' },
- },
- query: { match_phrase: { 'user.id': 'Evan' } },
- },
- ];
- const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]);
- const expected: Filter[] = [
- {
- meta: {
- type: 'phrase',
- key: 'user.id',
- alias: 'alias',
- disabled: false,
- negate: false,
- params: { query: 'Evan' },
- },
- query: { match_phrase: { 'user.id': 'Evan' } },
- },
- ];
- expect(replacement).toEqual(expected);
- });
- });
-
- describe('reformatDataProviderWithNewValue', () => {
- test('it should replace a query with a template value such as apache from a mock data provider', () => {
- const mockDataProvider: DataProvider = mockDataProviders[0];
- mockDataProvider.queryMatch.field = 'host.name';
- mockDataProvider.id = 'Braden';
- mockDataProvider.name = 'Braden';
- mockDataProvider.queryMatch.value = 'Braden';
- const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]);
- expect(replacement).toEqual({
- id: 'apache',
- name: 'apache',
- enabled: true,
- excluded: false,
- kqlQuery: '',
- queryMatch: {
- field: 'host.name',
- value: 'apache',
- operator: ':',
- displayField: undefined,
- displayValue: undefined,
- },
- and: [],
- });
- });
-
- test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => {
- mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case
- const mockDataProvider: DataProvider = mockDataProviders[0];
- mockDataProvider.queryMatch.field = 'host.name';
- mockDataProvider.id = 'Braden';
- mockDataProvider.name = 'Braden';
- mockDataProvider.queryMatch.value = 'Braden';
- const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]);
- expect(replacement).toEqual({
- id: 'apache',
- name: 'apache',
- enabled: true,
- excluded: false,
- kqlQuery: '',
- queryMatch: {
- field: 'host.name',
- value: 'apache',
- operator: ':',
- displayField: undefined,
- displayValue: undefined,
- },
- and: [],
- });
- });
-
- test('it should NOT replace a query with a template value that is not part of a template such as user.id', () => {
- const mockDataProvider: DataProvider = mockDataProviders[0];
- mockDataProvider.queryMatch.field = 'user.id';
- mockDataProvider.id = 'my-id';
- mockDataProvider.name = 'Rebecca';
- mockDataProvider.queryMatch.value = 'Rebecca';
- const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]);
- expect(replacement).toEqual({
- id: 'my-id',
- name: 'Rebecca',
- enabled: true,
- excluded: false,
- kqlQuery: '',
- queryMatch: {
- field: 'user.id',
- value: 'Rebecca',
- operator: ':',
- displayField: undefined,
- displayValue: undefined,
- },
- and: [],
- });
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx
index 88e9d4179a971..543a4634ceecc 100644
--- a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx
@@ -9,7 +9,7 @@ import { SecurityPageName } from '../types';
import { SiemNavTab } from '../../common/components/navigation/types';
import {
APP_OVERVIEW_PATH,
- APP_ALERTS_PATH,
+ APP_DETECTIONS_PATH,
APP_HOSTS_PATH,
APP_NETWORK_PATH,
APP_TIMELINES_PATH,
@@ -25,12 +25,12 @@ export const navTabs: SiemNavTab = {
disabled: false,
urlKey: 'overview',
},
- [SecurityPageName.alerts]: {
- id: SecurityPageName.alerts,
- name: i18n.Alerts,
- href: APP_ALERTS_PATH,
+ [SecurityPageName.detections]: {
+ id: SecurityPageName.detections,
+ name: i18n.DETECTION_ENGINE,
+ href: APP_DETECTIONS_PATH,
disabled: false,
- urlKey: 'alerts',
+ urlKey: 'detections',
},
[SecurityPageName.hosts]: {
id: SecurityPageName.hosts,
@@ -63,7 +63,7 @@ export const navTabs: SiemNavTab = {
},
[SecurityPageName.management]: {
id: SecurityPageName.management,
- name: i18n.MANAGEMENT,
+ name: i18n.ADMINISTRATION,
href: APP_MANAGEMENT_PATH,
disabled: false,
urlKey: SecurityPageName.management,
diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx
index 03e48282cb754..8f03945df437c 100644
--- a/x-pack/plugins/security_solution/public/app/home/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/index.tsx
@@ -17,6 +17,7 @@ import { UseUrlState } from '../../common/components/url_state';
import { useWithSource } from '../../common/containers/source';
import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline';
import { navTabs } from './home_navigations';
+import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index';
const WrappedByAutoSizer = styled.div`
height: 100%;
@@ -55,9 +56,17 @@ export const HomePage: React.FC = ({ children }) => {
}),
[windowHeight]
);
+ const { signalIndexExists, signalIndexName } = useSignalIndex();
+
+ const indexToAdd = useMemo(() => {
+ if (signalIndexExists && signalIndexName != null) {
+ return [signalIndexName];
+ }
+ return null;
+ }, [signalIndexExists, signalIndexName]);
const [showTimeline] = useShowTimeline();
- const { browserFields, indexPattern, indicesExist } = useWithSource();
+ const { browserFields, indexPattern, indicesExist } = useWithSource('default', indexToAdd);
return (
diff --git a/x-pack/plugins/security_solution/public/app/home/translations.ts b/x-pack/plugins/security_solution/public/app/home/translations.ts
index f5a08e6395f1f..bee1dfe333851 100644
--- a/x-pack/plugins/security_solution/public/app/home/translations.ts
+++ b/x-pack/plugins/security_solution/public/app/home/translations.ts
@@ -25,7 +25,7 @@ export const DETECTION_ENGINE = i18n.translate(
}
);
-export const Alerts = i18n.translate('xpack.securitySolution.navigation.alerts', {
+export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', {
defaultMessage: 'Alerts',
});
@@ -37,6 +37,6 @@ export const CASE = i18n.translate('xpack.securitySolution.navigation.case', {
defaultMessage: 'Cases',
});
-export const MANAGEMENT = i18n.translate('xpack.securitySolution.navigation.management', {
- defaultMessage: 'Management',
+export const ADMINISTRATION = i18n.translate('xpack.securitySolution.navigation.administration', {
+ defaultMessage: 'Administration',
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx
index 2de957039efe6..bf134a02dd822 100644
--- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx
@@ -26,7 +26,7 @@ import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../containers
import { useGetCases, UpdateCase } from '../../containers/use_get_cases';
import { useGetCasesStatus } from '../../containers/use_get_cases_status';
import { useDeleteCases } from '../../containers/use_delete_cases';
-import { EuiBasicTableOnChange } from '../../../alerts/pages/detection_engine/rules/types';
+import { EuiBasicTableOnChange } from '../../../detections/pages/detection_engine/rules/types';
import { Panel } from '../../../common/components/panel';
import {
UtilityBar,
diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts
index 8403050a13114..b1ab509417fe5 100644
--- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts
+++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts
@@ -30,7 +30,7 @@ export const ALERTS_TABLE_TITLE = i18n.translate(
export const ALERTS_GRAPH_TITLE = i18n.translate(
'xpack.securitySolution.alertsView.alertsGraphTitle',
{
- defaultMessage: 'External alert count',
+ defaultMessage: 'External alert trend',
}
);
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts
index 4fb4e5d30ca7a..ba328eff62e51 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts
@@ -182,6 +182,11 @@ export const addProviderToTimeline = ({
}
};
+const linkFields: Record = {
+ 'signal.rule.name': 'signal.rule.id',
+ 'event.module': 'rule.reference',
+};
+
export const addFieldToTimelineColumns = ({
upsertColumn = timelineActions.upsertColumn,
browserFields,
@@ -202,6 +207,7 @@ export const addFieldToTimelineColumns = ({
description: isString(column.description) ? column.description : undefined,
example: isString(column.example) ? column.example : undefined,
id: fieldId,
+ linkField: linkFields[fieldId] ?? undefined,
type: column.type,
aggregatable: column.aggregatable,
width: DEFAULT_COLUMN_MIN_WIDTH,
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
index ec56751b4cbd2..2a079ce015f0d 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
@@ -14,13 +14,13 @@ import { wait } from '../../lib/helpers';
import { mockEventViewerResponse } from './mock';
import { StatefulEventsViewer } from '.';
import { defaultHeaders } from './default_headers';
-import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns';
+import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns';
import { mockBrowserFields } from '../../containers/source/mock';
import { eventsDefaultModel } from './default_model';
import { useMountAppended } from '../../utils/use_mount_appended';
const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock;
-jest.mock('../../../alerts/containers/detection_engine/rules/fetch_index_patterns');
+jest.mock('../../../detections/containers/detection_engine/rules/fetch_index_patterns');
mockUseFetchIndexPatterns.mockImplementation(() => [
{
browserFields: mockBrowserFields,
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx
index edab0e3a98456..a5f4dc0c5ed6f 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx
@@ -14,12 +14,12 @@ import { useMountAppended } from '../../utils/use_mount_appended';
import { mockEventViewerResponse } from './mock';
import { StatefulEventsViewer } from '.';
-import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns';
+import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns';
import { mockBrowserFields } from '../../containers/source/mock';
import { eventsDefaultModel } from './default_model';
const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock;
-jest.mock('../../../alerts/containers/detection_engine/rules/fetch_index_patterns');
+jest.mock('../../../detections/containers/detection_engine/rules/fetch_index_patterns');
mockUseFetchIndexPatterns.mockImplementation(() => [
{
browserFields: mockBrowserFields,
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
index 1645db371802c..02b3571421f67 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
@@ -21,7 +21,7 @@ import { OnChangeItemsPerPage } from '../../../timelines/components/timeline/eve
import { Filter } from '../../../../../../../src/plugins/data/public';
import { useUiSetting } from '../../lib/kibana';
import { EventsViewer } from './events_viewer';
-import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns';
+import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns';
import { InspectButtonContainer } from '../inspect';
export interface OwnProps {
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
index 5221e170574b3..be89aa8e33718 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
@@ -34,7 +34,7 @@ import { errorToToaster, displaySuccessToast, useStateToaster } from '../../toas
import { ExceptionBuilder } from '../builder';
import { Loader } from '../../loader';
import { useAddOrUpdateException } from '../use_add_exception';
-import { useSignalIndex } from '../../../../alerts/containers/detection_engine/alerts/use_signal_index';
+import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list';
import { AddExceptionComments } from '../add_exception_comments';
import {
@@ -44,7 +44,7 @@ import {
entryHasListType,
entryHasNonEcsType,
} from '../helpers';
-import { useFetchIndexPatterns } from '../../../../alerts/containers/detection_engine/rules';
+import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
export interface AddExceptionOnClick {
ruleName: string;
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx
new file mode 100644
index 0000000000000..791782b0f0152
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx
@@ -0,0 +1,438 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { mount } from 'enzyme';
+import React from 'react';
+import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
+
+import { EntryItemComponent } from './entry_item';
+import {
+ isOperator,
+ isNotOperator,
+ isOneOfOperator,
+ isNotOneOfOperator,
+ isInListOperator,
+ isNotInListOperator,
+ existsOperator,
+ doesNotExistOperator,
+} from '../../autocomplete/operators';
+import {
+ fields,
+ getField,
+} from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts';
+import { getFoundListSchemaMock } from '../../../../../../lists/common/schemas/response/found_list_schema.mock';
+import { getEmptyValue } from '../../empty_value';
+
+// mock out lists hook
+const mockStart = jest.fn();
+const mockResult = getFoundListSchemaMock();
+jest.mock('../../../../common/lib/kibana');
+jest.mock('../../../../lists_plugin_deps', () => {
+ const originalModule = jest.requireActual('../../../../lists_plugin_deps');
+
+ return {
+ ...originalModule,
+ useFindLists: () => ({
+ loading: false,
+ start: mockStart.mockReturnValue(mockResult),
+ result: mockResult,
+ error: undefined,
+ }),
+ };
+});
+
+describe('EntryItemComponent', () => {
+ test('it renders fields disabled if "isLoading" is "true"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(
+ wrapper.find('[data-test-subj="exceptionBuilderEntryField"] input').props().disabled
+ ).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"] input').props().disabled
+ ).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"] input').props().disabled
+ ).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldFormRow"]')).toHaveLength(0);
+ });
+
+ test('it renders field labels if "showLabel" is "true"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldFormRow"]')).not.toEqual(0);
+ });
+
+ test('it renders field values correctly when operator is "isOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual('is');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual(
+ '1234'
+ );
+ });
+
+ test('it renders field values correctly when operator is "isNotOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'is not'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual(
+ '1234'
+ );
+ });
+
+ test('it renders field values correctly when operator is "isOneOfOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'is one of'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').text()).toEqual(
+ '1234'
+ );
+ });
+
+ test('it renders field values correctly when operator is "isNotOneOfOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'is not one of'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').text()).toEqual(
+ '1234'
+ );
+ });
+
+ test('it renders field values correctly when operator is "isInListOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'is in list'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldList"]').text()).toEqual(
+ 'some name'
+ );
+ });
+
+ test('it renders field values correctly when operator is "isNotInListOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'is not in list'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldList"]').text()).toEqual(
+ 'some name'
+ );
+ });
+
+ test('it renders field values correctly when operator is "existsOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'exists'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldExists"]').text()).toEqual(
+ getEmptyValue()
+ );
+ expect(
+ wrapper.find('[data-test-subj="exceptionBuilderEntryFieldExists"] input').props().disabled
+ ).toBeTruthy();
+ });
+
+ test('it renders field values correctly when operator is "doesNotExistOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'does not exist'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldExists"]').text()).toEqual(
+ getEmptyValue()
+ );
+ expect(
+ wrapper.find('[data-test-subj="exceptionBuilderEntryFieldExists"] input').props().disabled
+ ).toBeTruthy();
+ });
+
+ test('it invokes "onChange" when new field is selected and resets operator and value fields', () => {
+ const mockOnChange = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ ((wrapper.find(EuiComboBox).at(0).props() as unknown) as {
+ onChange: (a: EuiComboBoxOptionOption[]) => void;
+ }).onChange([{ label: 'machine.os' }]);
+
+ expect(mockOnChange).toHaveBeenCalledWith(
+ { field: 'machine.os', operator: 'included', type: 'match', value: undefined },
+ 0
+ );
+ });
+
+ test('it invokes "onChange" when new operator is selected and resets value field', () => {
+ const mockOnChange = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ ((wrapper.find(EuiComboBox).at(1).props() as unknown) as {
+ onChange: (a: EuiComboBoxOptionOption[]) => void;
+ }).onChange([{ label: 'is not' }]);
+
+ expect(mockOnChange).toHaveBeenCalledWith(
+ { field: 'ip', operator: 'excluded', type: 'match', value: '' },
+ 0
+ );
+ });
+
+ test('it invokes "onChange" when new value field is entered for match operator', () => {
+ const mockOnChange = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ ((wrapper.find(EuiComboBox).at(2).props() as unknown) as {
+ onCreateOption: (a: string) => void;
+ }).onCreateOption('126.45.211.34');
+
+ expect(mockOnChange).toHaveBeenCalledWith(
+ { field: 'ip', operator: 'excluded', type: 'match', value: '126.45.211.34' },
+ 0
+ );
+ });
+
+ test('it invokes "onChange" when new value field is entered for match_any operator', () => {
+ const mockOnChange = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ ((wrapper.find(EuiComboBox).at(2).props() as unknown) as {
+ onCreateOption: (a: string) => void;
+ }).onCreateOption('126.45.211.34');
+
+ expect(mockOnChange).toHaveBeenCalledWith(
+ { field: 'ip', operator: 'included', type: 'match_any', value: ['126.45.211.34'] },
+ 0
+ );
+ });
+
+ test('it invokes "onChange" when new value field is entered for list operator', () => {
+ const mockOnChange = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ ((wrapper.find(EuiComboBox).at(2).props() as unknown) as {
+ onChange: (a: EuiComboBoxOptionOption[]) => void;
+ }).onChange([{ label: 'some name' }]);
+
+ expect(mockOnChange).toHaveBeenCalledWith(
+ {
+ field: 'ip',
+ operator: 'excluded',
+ type: 'list',
+ list: { id: 'some-list-id', type: 'ip' },
+ },
+ 0
+ );
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx
index 39a1e1bdbad5a..0f5000c8c0abe 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx
@@ -67,13 +67,13 @@ export const EntryItemComponent: React.FC = ({
{
field: entry.field != null ? entry.field.name : undefined,
type: OperatorTypeEnum.MATCH,
- operator: isOperator.operator,
+ operator: entry.operator.operator,
value: newField,
},
entryIndex
);
},
- [onChange, entryIndex, entry.field]
+ [onChange, entryIndex, entry.field, entry.operator.operator]
);
const handleFieldMatchAnyValueChange = useCallback(
@@ -82,13 +82,13 @@ export const EntryItemComponent: React.FC = ({
{
field: entry.field != null ? entry.field.name : undefined,
type: OperatorTypeEnum.MATCH_ANY,
- operator: isOperator.operator,
+ operator: entry.operator.operator,
value: newField,
},
entryIndex
);
},
- [onChange, entryIndex, entry.field]
+ [onChange, entryIndex, entry.field, entry.operator.operator]
);
const handleFieldListValueChange = useCallback(
@@ -97,13 +97,13 @@ export const EntryItemComponent: React.FC = ({
{
field: entry.field != null ? entry.field.name : undefined,
type: OperatorTypeEnum.LIST,
- operator: isOperator.operator,
+ operator: entry.operator.operator,
list: { id: newField.id, type: newField.type },
},
entryIndex
);
},
- [onChange, entryIndex, entry.field]
+ [onChange, entryIndex, entry.field, entry.operator.operator]
);
const renderFieldInput = (isFirst: boolean): JSX.Element => {
@@ -114,9 +114,9 @@ export const EntryItemComponent: React.FC = ({
selectedField={entry.field}
isLoading={isLoading}
isClearable={false}
- isDisabled={indexPattern == null}
+ isDisabled={isLoading}
onChange={handleFieldChange}
- data-test-subj="filterFieldSuggestionList"
+ data-test-subj="exceptionBuilderEntryField"
/>
);
@@ -137,11 +137,11 @@ export const EntryItemComponent: React.FC = ({
placeholder={i18n.EXCEPTION_OPERATOR_PLACEHOLDER}
selectedField={entry.field}
operator={entry.operator}
- isDisabled={false}
+ isDisabled={isLoading}
isLoading={false}
isClearable={false}
onChange={handleOperatorChange}
- data-test-subj="filterFieldSuggestionList"
+ data-test-subj="exceptionBuilderEntryOperator"
/>
);
@@ -165,12 +165,12 @@ export const EntryItemComponent: React.FC = ({
placeholder={i18n.EXCEPTION_FIELD_VALUE_PLACEHOLDER}
selectedField={entry.field}
selectedValue={value}
- isDisabled={false}
+ isDisabled={isLoading}
isLoading={isLoading}
isClearable={false}
indexPattern={indexPattern}
onChange={handleFieldMatchValueChange}
- data-test-subj="filterFieldSuggestionList"
+ data-test-subj="exceptionBuilderEntryFieldMatch"
/>
);
case OperatorTypeEnum.MATCH_ANY:
@@ -180,12 +180,12 @@ export const EntryItemComponent: React.FC = ({
placeholder={i18n.EXCEPTION_FIELD_VALUE_PLACEHOLDER}
selectedField={entry.field}
selectedValue={values}
- isDisabled={false}
+ isDisabled={isLoading}
isLoading={isLoading}
isClearable={false}
indexPattern={indexPattern}
onChange={handleFieldMatchAnyValueChange}
- data-test-subj="filterFieldSuggestionList"
+ data-test-subj="exceptionBuilderEntryFieldMatchAny"
/>
);
case OperatorTypeEnum.LIST:
@@ -195,17 +195,18 @@ export const EntryItemComponent: React.FC = ({
selectedField={entry.field}
placeholder={i18n.EXCEPTION_FIELD_LISTS_PLACEHOLDER}
selectedValue={id}
- isLoading={false}
- isDisabled={false}
+ isLoading={isLoading}
+ isDisabled={isLoading}
isClearable={false}
onChange={handleFieldListValueChange}
+ data-test-subj="exceptionBuilderEntryFieldList"
/>
);
case OperatorTypeEnum.EXISTS:
return (
);
default:
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx
index 3afdf43ec7dfa..5e53ce3ba6578 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx
@@ -29,6 +29,7 @@ interface ExceptionListItemProps {
isLoading: boolean;
indexPattern: IIndexPattern;
andLogicIncluded: boolean;
+ onCheckAndLogic: (item: ExceptionsBuilderExceptionItem[]) => void;
onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void;
onExceptionItemChange: (item: ExceptionsBuilderExceptionItem, index: number) => void;
}
@@ -41,6 +42,7 @@ export const ExceptionListItemComponent = React.memo(
indexPattern,
isLoading,
andLogicIncluded,
+ onCheckAndLogic,
onDeleteExceptionItem,
onExceptionItemChange,
}) => {
@@ -70,11 +72,12 @@ export const ExceptionListItemComponent = React.memo(
onDeleteExceptionItem(updatedExceptionItem, exceptionItemIndex);
};
- const entries = useMemo(
- (): FormattedBuilderEntry[] =>
- indexPattern != null ? getFormattedBuilderEntries(indexPattern, exceptionItem.entries) : [],
- [indexPattern, exceptionItem.entries]
- );
+ const entries = useMemo((): FormattedBuilderEntry[] => {
+ onCheckAndLogic([exceptionItem]);
+ return indexPattern != null
+ ? getFormattedBuilderEntries(indexPattern, exceptionItem.entries)
+ : [];
+ }, [indexPattern, exceptionItem, onCheckAndLogic]);
const andBadge = useMemo((): JSX.Element => {
const badge = ;
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx
index c6376c34c768f..d3ed1dfc944fd 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx
@@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { ExceptionListItemComponent } from './exception_item';
-import { useFetchIndexPatterns } from '../../../../alerts/containers/detection_engine/rules/fetch_index_patterns';
+import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules/fetch_index_patterns';
import {
ExceptionListItemSchema,
NamespaceType,
@@ -77,15 +77,21 @@ export const ExceptionBuilder = ({
indexPatternConfig ?? []
);
+ const handleCheckAndLogic = (items: ExceptionsBuilderExceptionItem[]): void => {
+ setAndLogicIncluded((includesAnd: boolean): boolean => {
+ if (includesAnd) {
+ return true;
+ } else {
+ return items.filter(({ entries }) => entries.length > 1).length > 0;
+ }
+ });
+ };
+
// Bubble up changes to parent
useEffect(() => {
onChange({ exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete });
}, [onChange, exceptionsToDelete, exceptions]);
- const checkAndLogic = (items: ExceptionsBuilderExceptionItem[]): void => {
- setAndLogicIncluded(items.filter(({ entries }) => entries.length > 1).length > 0);
- };
-
const handleDeleteExceptionItem = (
item: ExceptionsBuilderExceptionItem,
itemIndex: number
@@ -100,7 +106,7 @@ export const ExceptionBuilder = ({
...existingExceptions.slice(0, itemIndex),
...existingExceptions.slice(itemIndex + 1),
];
- checkAndLogic(updatedExceptions);
+ handleCheckAndLogic(updatedExceptions);
return updatedExceptions;
});
@@ -118,7 +124,7 @@ export const ExceptionBuilder = ({
...exceptions.slice(index + 1),
];
- checkAndLogic(updatedExceptions);
+ handleCheckAndLogic(updatedExceptions);
setExceptions(updatedExceptions);
};
@@ -214,6 +220,7 @@ export const ExceptionBuilder = ({
isLoading={indexPatternLoading}
exceptionItemIndex={index}
andLogicIncluded={andLogicIncluded}
+ onCheckAndLogic={handleCheckAndLogic}
onDeleteExceptionItem={handleDeleteExceptionItem}
onExceptionItemChange={handleExceptionItemChange}
/>
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
index 4bec5778cd775..aa36b65e04b69 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
@@ -21,8 +21,8 @@ import {
EuiText,
} from '@elastic/eui';
import { alertsIndexPattern } from '../../../../../common/endpoint/constants';
-import { useFetchIndexPatterns } from '../../../../alerts/containers/detection_engine/rules';
-import { useSignalIndex } from '../../../../alerts/containers/detection_engine/alerts/use_signal_index';
+import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
+import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
import {
ExceptionListItemSchema,
CreateExceptionListItemSchema,
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx
index b167807a6edaa..018ca1d29c369 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx
@@ -7,7 +7,7 @@
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { KibanaServices } from '../../../common/lib/kibana';
-import * as alertsApi from '../../../alerts/containers/detection_engine/alerts/api';
+import * as alertsApi from '../../../detections/containers/detection_engine/alerts/api';
import * as listsApi from '../../../../../lists/public/exceptions/api';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getCreateExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/request/create_exception_list_item_schema.mock';
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx
index 2d793c89e48f1..267a9afd9cf6d 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx
@@ -14,8 +14,8 @@ import {
CreateExceptionListItemSchema,
UpdateExceptionListItemSchema,
} from '../../../lists_plugin_deps';
-import { updateAlertStatus } from '../../../alerts/containers/detection_engine/alerts/api';
-import { getUpdateAlertsQuery } from '../../../alerts/components/alerts_table/actions';
+import { updateAlertStatus } from '../../../detections/containers/detection_engine/alerts/api';
+import { getUpdateAlertsQuery } from '../../../detections/components/alerts_table/actions';
import { formatExceptionItemForUpdate } from './helpers';
/**
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx
index 1a031abc56f35..afc3568fd6c65 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx
@@ -6,10 +6,10 @@
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
-import * as rulesApi from '../../../alerts/containers/detection_engine/rules/api';
+import * as rulesApi from '../../../detections/containers/detection_engine/rules/api';
import * as listsApi from '../../../../../lists/public/exceptions/api';
import { getExceptionListSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_schema.mock';
-import { savedRuleMock } from '../../../alerts/containers/detection_engine/rules/mock';
+import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock';
import { createKibanaCoreStartMock } from '../../mock/kibana_core';
import { ExceptionListType } from '../../../lists_plugin_deps';
import { ListArray } from '../../../../common/detection_engine/schemas/types';
@@ -21,7 +21,7 @@ import {
} from './use_fetch_or_create_rule_exception_list';
const mockKibanaHttpService = createKibanaCoreStartMock().http;
-jest.mock('../../../alerts/containers/detection_engine/rules/api');
+jest.mock('../../../detections/containers/detection_engine/rules/api');
describe('useFetchOrCreateRuleExceptionList', () => {
let fetchRuleById: jest.SpyInstance>;
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx
index 5000a79287fc0..245ce192b3cfa 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx
@@ -11,9 +11,12 @@ import {
ExceptionListSchema,
CreateExceptionListSchema,
} from '../../../../../lists/common/schemas';
-import { Rule } from '../../../alerts/containers/detection_engine/rules/types';
+import { Rule } from '../../../detections/containers/detection_engine/rules/types';
import { List, ListArray } from '../../../../common/detection_engine/schemas/types';
-import { fetchRuleById, patchRule } from '../../../alerts/containers/detection_engine/rules/api';
+import {
+ fetchRuleById,
+ patchRule,
+} from '../../../detections/containers/detection_engine/rules/api';
import { fetchExceptionListById, addExceptionList } from '../../../lists_plugin_deps';
export type ReturnUseFetchOrCreateRuleExceptionList = [boolean, ExceptionListSchema | null];
diff --git a/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.tsx b/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.tsx
index 2e8d5f77afc83..33e26cd4db035 100644
--- a/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.tsx
@@ -9,7 +9,7 @@ import styled from 'styled-components';
import { isFunction } from 'lodash/fp';
import * as i18n from './translations';
-import { ExportDocumentsProps } from '../../../alerts/containers/detection_engine/rules';
+import { ExportDocumentsProps } from '../../../detections/containers/detection_engine/rules';
import { useStateToaster, errorToToaster } from '../toasters';
const InvisibleAnchor = styled.a`
diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
index 17fdf2163b58e..ba4f782499802 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
@@ -19,7 +19,7 @@ import * as i18n from './translations';
import { useWithSource } from '../../containers/source';
import { useGetUrlSearch } from '../navigation/use_get_url_search';
import { useKibana } from '../../lib/kibana';
-import { APP_ID, ADD_DATA_PATH, APP_ALERTS_PATH } from '../../../../common/constants';
+import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants';
import { LinkAnchor } from '../links';
const Wrapper = styled.header`
@@ -60,7 +60,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine
-
+
@@ -70,7 +70,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine
display="condensed"
navTabs={
hideDetectionEngine
- ? pickBy((_, key) => key !== SecurityPageName.alerts, navTabs)
+ ? pickBy((_, key) => key !== SecurityPageName.detections, navTabs)
: navTabs
}
/>
@@ -86,7 +86,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine
- {indicesExist && window.location.pathname.includes(APP_ALERTS_PATH) && (
+ {indicesExist && window.location.pathname.includes(APP_DETECTIONS_PATH) && (
diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx
index a42628cecff8e..d5d670b4c03ff 100644
--- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx
@@ -24,7 +24,7 @@ import React, { useCallback, useState } from 'react';
import {
ImportDataResponse,
ImportDataProps,
-} from '../../../alerts/containers/detection_engine/rules';
+} from '../../../detections/containers/detection_engine/rules';
import {
displayErrorToast,
displaySuccessToast,
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts
index 5c1c68b802726..dc5324adbac7d 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts
@@ -13,7 +13,7 @@ import { StartServices } from '../../../../types';
import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../hosts/pages/details/utils';
import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/ip_details';
import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../../cases/pages/utils';
-import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../alerts/pages/detection_engine/rules/utils';
+import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils';
import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../../timelines/pages';
import { SecurityPageName } from '../../../../app/types';
import {
@@ -59,7 +59,7 @@ const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState =>
spyState != null && spyState.pageName === SecurityPageName.case;
const isAlertsRoutes = (spyState: RouteSpyState) =>
- spyState != null && spyState.pageName === SecurityPageName.alerts;
+ spyState != null && spyState.pageName === SecurityPageName.detections;
export const getBreadcrumbsForRoute = (
object: RouteSpyState & TabNavigationProps,
@@ -103,7 +103,7 @@ export const getBreadcrumbsForRoute = (
];
}
if (isAlertsRoutes(spyState) && object.navTabs) {
- const tempNav: SearchNavTab = { urlKey: 'alerts', isDetailPage: false };
+ const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false };
let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)];
if (spyState.tabName != null) {
urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)];
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx
index 10f8b11b4d9c5..229e2d2402298 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx
@@ -92,12 +92,12 @@ describe('SIEM Navigation', () => {
{
detailName: undefined,
navTabs: {
- alerts: {
+ detections: {
disabled: false,
- href: '/app/security/alerts',
- id: 'alerts',
- name: 'Alerts',
- urlKey: 'alerts',
+ href: '/app/security/detections',
+ id: 'detections',
+ name: 'Detections',
+ urlKey: 'detections',
},
case: {
disabled: false,
@@ -108,9 +108,9 @@ describe('SIEM Navigation', () => {
},
management: {
disabled: false,
- href: '/app/security/management',
+ href: '/app/security/administration',
id: 'management',
- name: 'Management',
+ name: 'Administration',
urlKey: 'management',
},
hosts: {
@@ -197,12 +197,12 @@ describe('SIEM Navigation', () => {
filters: [],
flowTarget: undefined,
navTabs: {
- alerts: {
+ detections: {
disabled: false,
- href: '/app/security/alerts',
- id: 'alerts',
- name: 'Alerts',
- urlKey: 'alerts',
+ href: '/app/security/detections',
+ id: 'detections',
+ name: 'Detections',
+ urlKey: 'detections',
},
case: {
disabled: false,
@@ -220,9 +220,9 @@ describe('SIEM Navigation', () => {
},
management: {
disabled: false,
- href: '/app/security/management',
+ href: '/app/security/administration',
id: 'management',
- name: 'Management',
+ name: 'Administration',
urlKey: 'management',
},
network: {
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
index 80302be18355c..0489ebba738c8 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
@@ -45,7 +45,7 @@ export type SiemNavTabKey =
| SecurityPageName.overview
| SecurityPageName.hosts
| SecurityPageName.network
- | SecurityPageName.alerts
+ | SecurityPageName.detections
| SecurityPageName.timelines
| SecurityPageName.case
| SecurityPageName.management;
diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx
index da6ec784af6d4..c8232b0c3b3cb 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx
@@ -364,7 +364,7 @@ describe('StatefulTopN', () => {
field={field}
indexPattern={mockIndexPattern}
indexToAdd={null}
- timelineId={TimelineId.alertsPage}
+ timelineId={TimelineId.detectionsPage}
toggleTopN={jest.fn()}
onFilterAdded={jest.fn()}
value={value}
diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx
index 0b2f1f1e35cc7..807f1839973fa 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx
@@ -132,7 +132,8 @@ const StatefulTopNComponent: React.FC = ({
}
data-test-subj="top-n"
defaultView={
- timelineId === TimelineId.alertsPage || timelineId === TimelineId.alertsRulesDetailsPage
+ timelineId === TimelineId.detectionsPage ||
+ timelineId === TimelineId.detectionsRulesDetailsPage
? 'alert'
: options[0].value
}
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts
index 71faec88e85a0..1faff2594ce80 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts
@@ -8,7 +8,7 @@ export enum CONSTANTS {
appQuery = 'query',
caseDetails = 'case.details',
casePage = 'case.page',
- alertsPage = 'alerts.page',
+ detectionsPage = 'detections.page',
filters = 'filters',
hostsDetails = 'hosts.details',
hostsPage = 'hosts.page',
@@ -25,7 +25,7 @@ export enum CONSTANTS {
export type UrlStateType =
| 'case'
- | 'alerts'
+ | 'detections'
| 'host'
| 'network'
| 'overview'
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
index 7f4267bc5e2b3..6febf95aae01d 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
@@ -90,8 +90,8 @@ export const getUrlType = (pageName: string): UrlStateType => {
return 'host';
} else if (pageName === SecurityPageName.network) {
return 'network';
- } else if (pageName === SecurityPageName.alerts) {
- return 'alerts';
+ } else if (pageName === SecurityPageName.detections) {
+ return 'detections';
} else if (pageName === SecurityPageName.timelines) {
return 'timeline';
} else if (pageName === SecurityPageName.case) {
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx
index 20374affbdf89..eeeaacc25a15e 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx
@@ -193,7 +193,7 @@ describe('UrlStateContainer', () => {
wrapper.update();
await wait();
- if (CONSTANTS.alertsPage === page) {
+ if (CONSTANTS.detectionsPage === page) {
expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({
from: 11223344556677,
fromStr: 'now-1d/d',
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts
index 8ca43cb576d32..8881a82e5cd1c 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts
@@ -32,7 +32,7 @@ export const ALL_URL_STATE_KEYS: KeyUrlState[] = [
];
export const URL_STATE_KEYS: Record = {
- alerts: [
+ detections: [
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
@@ -80,7 +80,7 @@ export const URL_STATE_KEYS: Record = {
export type LocationTypes =
| CONSTANTS.caseDetails
| CONSTANTS.casePage
- | CONSTANTS.alertsPage
+ | CONSTANTS.detectionsPage
| CONSTANTS.hostsDetails
| CONSTANTS.hostsPage
| CONSTANTS.networkDetails
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx
index 221df436402dd..c97be1fdfb99b 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx
@@ -203,7 +203,7 @@ export const useUrlStateHooks = ({
}
});
} else if (pathName !== prevProps.pathName) {
- handleInitialize(type, pageName === SecurityPageName.alerts);
+ handleInitialize(type, pageName === SecurityPageName.detections);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isInitializing, history, pathName, pageName, prevProps, urlState]);
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 4eb66acdfad65..5248136437d7d 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
@@ -10,7 +10,7 @@ import { TimelineType, TimelineStatus } from '../../../common/types/timeline';
import { OpenTimelineResult } from '../../timelines/components/open_timeline/types';
import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../../graphql/types';
import { allTimelinesQuery } from '../../timelines/containers/all/index.gql_query';
-import { CreateTimelineProps } from '../../alerts/components/alerts_table/types';
+import { CreateTimelineProps } from '../../detections/components/alerts_table/types';
import { TimelineModel } from '../../timelines/store/timeline/model';
import { timelineDefaults } from '../../timelines/store/timeline/defaults';
export interface MockedProvidedQuery {
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/alerts_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/alerts_histogram.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/alerts_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/alerts_histogram.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/config.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/config.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/config.ts
rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/config.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/helpers.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/helpers.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx
similarity index 98%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx
index 2923446b8322d..59d97480418b7 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx
@@ -73,7 +73,7 @@ describe('AlertsHistogramPanel', () => {
preventDefault: jest.fn(),
});
- expect(mockNavigateToApp).toBeCalledWith('securitySolution:alerts', { path: '' });
+ expect(mockNavigateToApp).toBeCalledWith('securitySolution:detections', { path: '' });
});
});
});
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx
similarity index 99%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx
index 533a9d51a9bcd..ba12499b8f20e 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx
@@ -123,7 +123,7 @@ export const AlertsHistogramPanel = memo(
);
const kibana = useKibana();
const { navigateToApp } = kibana.services.application;
- const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.alerts);
+ const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.detections);
const totalAlerts = useMemo(
() =>
@@ -146,7 +146,7 @@ export const AlertsHistogramPanel = memo(
const goToDetectionEngine = useCallback(
(ev) => {
ev.preventDefault();
- navigateToApp(`${APP_ID}:${SecurityPageName.alerts}`, {
+ navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, {
path: getDetectionEngineUrl(urlSearch),
});
},
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/translations.ts
similarity index 99%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/translations.ts
index 6eaa0ba3fc4ec..e7c08914964a4 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/translations.ts
@@ -92,7 +92,7 @@ export const TOP = (fieldName: string) =>
export const HISTOGRAM_HEADER = i18n.translate(
'xpack.securitySolution.detectionEngine.alerts.histogram.headerTitle',
{
- defaultMessage: 'Alert count',
+ defaultMessage: 'Trend',
}
);
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/types.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/types.ts
rename to x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/types.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_info/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_info/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_info/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_info/query.dsl.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts
rename to x-pack/plugins/security_solution/public/detections/components/alerts_info/query.dsl.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_info/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_info/types.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_info/types.ts
rename to x-pack/plugins/security_solution/public/detections/components/alerts_info/types.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
similarity index 99%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
index bd62b79a3c54e..2fa7cfeedcd15 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
@@ -215,8 +215,8 @@ describe('alert actions', () => {
columnId: '@timestamp',
sortDirection: 'desc',
},
- status: TimelineStatus.active,
- title: 'Test rule - Duplicate',
+ status: TimelineStatus.draft,
+ title: '',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
similarity index 91%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
index ba392e9904cc4..24f292cf9135b 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
@@ -10,7 +10,14 @@ import moment from 'moment';
import { updateAlertStatus } from '../../containers/detection_engine/alerts/api';
import { SendAlertToTimelineActionProps, UpdateAlertStatusActionProps } from './types';
-import { TimelineNonEcsData, GetOneTimeline, TimelineResult, Ecs } from '../../../graphql/types';
+import {
+ TimelineNonEcsData,
+ GetOneTimeline,
+ TimelineResult,
+ Ecs,
+ TimelineStatus,
+ TimelineType,
+} from '../../../graphql/types';
import { oneTimelineQuery } from '../../../timelines/containers/one/index.gql_query';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import {
@@ -122,20 +129,31 @@ export const sendAlertToTimelineAction = async ({
if (!isEmpty(resultingTimeline)) {
const timelineTemplate: TimelineResult = omitTypenameInTimeline(resultingTimeline);
openAlertInBasicTimeline = false;
- const { timeline } = formatTimelineResultToModel(timelineTemplate, true);
+ const { timeline } = formatTimelineResultToModel(
+ timelineTemplate,
+ true,
+ timelineTemplate.timelineType ?? TimelineType.default
+ );
const query = replaceTemplateFieldFromQuery(
timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '',
- ecsData
+ ecsData,
+ timeline.timelineType
);
const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData);
const dataProviders = replaceTemplateFieldFromDataProviders(
timeline.dataProviders ?? [],
- ecsData
+ ecsData,
+ timeline.timelineType
);
+
createTimeline({
from,
timeline: {
...timeline,
+ title: '',
+ timelineType: TimelineType.default,
+ templateTimelineId: null,
+ status: TimelineStatus.draft,
dataProviders,
eventType: 'all',
filters,
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_filter_group/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_filter_group/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_filter_group/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_filter_group/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx
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
new file mode 100644
index 0000000000000..4decddd6b8886
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts
@@ -0,0 +1,482 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { cloneDeep } from 'lodash/fp';
+
+import { TimelineType } from '../../../../common/types/timeline';
+import { mockEcsData } from '../../../common/mock/mock_ecs';
+import { Filter } from '../../../../../../../src/plugins/data/public';
+import {
+ DataProvider,
+ DataProviderType,
+} from '../../../timelines/components/timeline/data_providers/data_provider';
+import { mockDataProviders } from '../../../timelines/components/timeline/data_providers/mock/mock_data_providers';
+
+import {
+ getStringArray,
+ replaceTemplateFieldFromQuery,
+ replaceTemplateFieldFromMatchFilters,
+ reformatDataProviderWithNewValue,
+} from './helpers';
+
+describe('helpers', () => {
+ let mockEcsDataClone = cloneDeep(mockEcsData);
+ beforeEach(() => {
+ mockEcsDataClone = cloneDeep(mockEcsData);
+ });
+ describe('getStringOrStringArray', () => {
+ test('it should correctly return a string array', () => {
+ const value = getStringArray('x', {
+ x: 'The nickname of the developer we all :heart:',
+ });
+ expect(value).toEqual(['The nickname of the developer we all :heart:']);
+ });
+
+ test('it should correctly return a string array with a single element', () => {
+ const value = getStringArray('x', {
+ x: ['The nickname of the developer we all :heart:'],
+ });
+ expect(value).toEqual(['The nickname of the developer we all :heart:']);
+ });
+
+ test('it should correctly return a string array with two elements of strings', () => {
+ const value = getStringArray('x', {
+ x: ['The nickname of the developer we all :heart:', 'We are all made of stars'],
+ });
+ expect(value).toEqual([
+ 'The nickname of the developer we all :heart:',
+ 'We are all made of stars',
+ ]);
+ });
+
+ test('it should correctly return a string array with deep elements', () => {
+ const value = getStringArray('x.y.z', {
+ x: { y: { z: 'zed' } },
+ });
+ expect(value).toEqual(['zed']);
+ });
+
+ test('it should correctly return a string array with a non-existent value', () => {
+ const value = getStringArray('non.existent', {
+ x: { y: { z: 'zed' } },
+ });
+ expect(value).toEqual([]);
+ });
+
+ test('it should trace an error if the value is not a string', () => {
+ const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console;
+ const value = getStringArray('a', { a: 5 }, mockConsole);
+ expect(value).toEqual([]);
+ expect(
+ mockConsole.trace
+ ).toHaveBeenCalledWith(
+ 'Data type that is not a string or string array detected:',
+ 5,
+ 'when trying to access field:',
+ 'a',
+ 'from data object of:',
+ { a: 5 }
+ );
+ });
+
+ test('it should trace an error if the value is an array of mixed values', () => {
+ const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console;
+ const value = getStringArray('a', { a: ['hi', 5] }, mockConsole);
+ expect(value).toEqual([]);
+ expect(
+ mockConsole.trace
+ ).toHaveBeenCalledWith(
+ 'Data type that is not a string or string array detected:',
+ ['hi', 5],
+ 'when trying to access field:',
+ 'a',
+ 'from data object of:',
+ { a: ['hi', 5] }
+ );
+ });
+ });
+
+ describe('replaceTemplateFieldFromQuery', () => {
+ describe('timelineType default', () => {
+ test('given an empty query string this returns an empty query string', () => {
+ const replacement = replaceTemplateFieldFromQuery(
+ '',
+ mockEcsDataClone[0],
+ TimelineType.default
+ );
+ expect(replacement).toEqual('');
+ });
+
+ test('given a query string with spaces this returns an empty query string', () => {
+ const replacement = replaceTemplateFieldFromQuery(
+ ' ',
+ mockEcsDataClone[0],
+ TimelineType.default
+ );
+ expect(replacement).toEqual('');
+ });
+
+ test('it should replace a query with a template value such as apache from a mock template', () => {
+ const replacement = replaceTemplateFieldFromQuery(
+ 'host.name: placeholdertext',
+ mockEcsDataClone[0],
+ TimelineType.default
+ );
+ expect(replacement).toEqual('host.name: apache');
+ });
+
+ test('it should replace a template field with an ECS value that is not an array', () => {
+ mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case
+ const replacement = replaceTemplateFieldFromQuery(
+ 'host.name: *',
+ mockEcsDataClone[0],
+ TimelineType.default
+ );
+ expect(replacement).toEqual('host.name: *');
+ });
+
+ test('it should NOT replace a query with a template value that is not part of the template fields array', () => {
+ const replacement = replaceTemplateFieldFromQuery(
+ 'user.id: placeholdertext',
+ mockEcsDataClone[0],
+ TimelineType.default
+ );
+ expect(replacement).toEqual('user.id: placeholdertext');
+ });
+ });
+
+ describe('timelineType template', () => {
+ test('given an empty query string this returns an empty query string', () => {
+ const replacement = replaceTemplateFieldFromQuery(
+ '',
+ mockEcsDataClone[0],
+ TimelineType.template
+ );
+ expect(replacement).toEqual('');
+ });
+
+ test('given a query string with spaces this returns an empty query string', () => {
+ const replacement = replaceTemplateFieldFromQuery(
+ ' ',
+ mockEcsDataClone[0],
+ TimelineType.template
+ );
+ expect(replacement).toEqual('');
+ });
+
+ test('it should NOT replace a query with a template value such as apache from a mock template', () => {
+ const replacement = replaceTemplateFieldFromQuery(
+ 'host.name: placeholdertext',
+ mockEcsDataClone[0],
+ TimelineType.template
+ );
+ expect(replacement).toEqual('host.name: placeholdertext');
+ });
+
+ test('it should NOT replace a template field with an ECS value that is not an array', () => {
+ mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case
+ const replacement = replaceTemplateFieldFromQuery(
+ 'host.name: *',
+ mockEcsDataClone[0],
+ TimelineType.default
+ );
+ expect(replacement).toEqual('host.name: *');
+ });
+
+ test('it should NOT replace a query with a template value that is not part of the template fields array', () => {
+ const replacement = replaceTemplateFieldFromQuery(
+ 'user.id: placeholdertext',
+ mockEcsDataClone[0],
+ TimelineType.default
+ );
+ expect(replacement).toEqual('user.id: placeholdertext');
+ });
+ });
+ });
+
+ describe('replaceTemplateFieldFromMatchFilters', () => {
+ test('given an empty query filter this will return an empty filter', () => {
+ const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]);
+ expect(replacement).toEqual([]);
+ });
+
+ test('given a query filter this will return that filter with the placeholder replaced', () => {
+ const filters: Filter[] = [
+ {
+ meta: {
+ type: 'phrase',
+ key: 'host.name',
+ alias: 'alias',
+ disabled: false,
+ negate: false,
+ params: { query: 'Braden' },
+ },
+ query: { match_phrase: { 'host.name': 'Braden' } },
+ },
+ ];
+ const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]);
+ const expected: Filter[] = [
+ {
+ meta: {
+ type: 'phrase',
+ key: 'host.name',
+ alias: 'alias',
+ disabled: false,
+ negate: false,
+ params: { query: 'apache' },
+ },
+ query: { match_phrase: { 'host.name': 'apache' } },
+ },
+ ];
+ expect(replacement).toEqual(expected);
+ });
+
+ test('given a query filter with a value not in the templateFields, this will NOT replace the placeholder value', () => {
+ const filters: Filter[] = [
+ {
+ meta: {
+ type: 'phrase',
+ key: 'user.id',
+ alias: 'alias',
+ disabled: false,
+ negate: false,
+ params: { query: 'Evan' },
+ },
+ query: { match_phrase: { 'user.id': 'Evan' } },
+ },
+ ];
+ const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]);
+ const expected: Filter[] = [
+ {
+ meta: {
+ type: 'phrase',
+ key: 'user.id',
+ alias: 'alias',
+ disabled: false,
+ negate: false,
+ params: { query: 'Evan' },
+ },
+ query: { match_phrase: { 'user.id': 'Evan' } },
+ },
+ ];
+ expect(replacement).toEqual(expected);
+ });
+ });
+
+ describe('reformatDataProviderWithNewValue', () => {
+ describe('timelineType default', () => {
+ test('it should replace a query with a template value such as apache from a mock data provider', () => {
+ const mockDataProvider: DataProvider = mockDataProviders[0];
+ mockDataProvider.queryMatch.field = 'host.name';
+ mockDataProvider.id = 'Braden';
+ mockDataProvider.name = 'Braden';
+ mockDataProvider.queryMatch.value = 'Braden';
+ const replacement = reformatDataProviderWithNewValue(
+ mockDataProvider,
+ mockEcsDataClone[0],
+ TimelineType.default
+ );
+ expect(replacement).toEqual({
+ id: 'apache',
+ name: 'apache',
+ enabled: true,
+ excluded: false,
+ kqlQuery: '',
+ queryMatch: {
+ field: 'host.name',
+ value: 'apache',
+ operator: ':',
+ displayField: undefined,
+ displayValue: undefined,
+ },
+ and: [],
+ type: TimelineType.default,
+ });
+ });
+
+ test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => {
+ mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case
+ const mockDataProvider: DataProvider = mockDataProviders[0];
+ mockDataProvider.queryMatch.field = 'host.name';
+ mockDataProvider.id = 'Braden';
+ mockDataProvider.name = 'Braden';
+ mockDataProvider.queryMatch.value = 'Braden';
+ const replacement = reformatDataProviderWithNewValue(
+ mockDataProvider,
+ mockEcsDataClone[0],
+ TimelineType.default
+ );
+ expect(replacement).toEqual({
+ id: 'apache',
+ name: 'apache',
+ enabled: true,
+ excluded: false,
+ kqlQuery: '',
+ queryMatch: {
+ field: 'host.name',
+ value: 'apache',
+ operator: ':',
+ displayField: undefined,
+ displayValue: undefined,
+ },
+ and: [],
+ type: TimelineType.default,
+ });
+ });
+
+ test('it should NOT replace a query with a template value that is not part of a template such as user.id', () => {
+ const mockDataProvider: DataProvider = mockDataProviders[0];
+ mockDataProvider.queryMatch.field = 'user.id';
+ mockDataProvider.id = 'my-id';
+ mockDataProvider.name = 'Rebecca';
+ mockDataProvider.queryMatch.value = 'Rebecca';
+ const replacement = reformatDataProviderWithNewValue(
+ mockDataProvider,
+ mockEcsDataClone[0],
+ TimelineType.default
+ );
+ expect(replacement).toEqual({
+ id: 'my-id',
+ name: 'Rebecca',
+ enabled: true,
+ excluded: false,
+ kqlQuery: '',
+ queryMatch: {
+ field: 'user.id',
+ value: 'Rebecca',
+ operator: ':',
+ displayField: undefined,
+ displayValue: undefined,
+ },
+ and: [],
+ type: TimelineType.default,
+ });
+ });
+ });
+
+ describe('timelineType template', () => {
+ test('it should replace a query with a template value such as apache from a mock data provider', () => {
+ const mockDataProvider: DataProvider = mockDataProviders[0];
+ mockDataProvider.queryMatch.field = 'host.name';
+ mockDataProvider.id = 'Braden';
+ mockDataProvider.name = 'Braden';
+ mockDataProvider.queryMatch.value = '{host.name}';
+ mockDataProvider.type = DataProviderType.template;
+ const replacement = reformatDataProviderWithNewValue(
+ mockDataProvider,
+ mockEcsDataClone[0],
+ TimelineType.template
+ );
+ expect(replacement).toEqual({
+ id: 'apache',
+ name: 'apache',
+ enabled: true,
+ excluded: false,
+ kqlQuery: '',
+ queryMatch: {
+ field: 'host.name',
+ value: 'apache',
+ operator: ':',
+ displayField: undefined,
+ displayValue: undefined,
+ },
+ and: [],
+ type: DataProviderType.default,
+ });
+ });
+
+ test('it should NOT replace a query for default data provider', () => {
+ const mockDataProvider: DataProvider = mockDataProviders[0];
+ mockDataProvider.queryMatch.field = 'host.name';
+ mockDataProvider.id = 'Braden';
+ mockDataProvider.name = 'Braden';
+ mockDataProvider.queryMatch.value = '{host.name}';
+ mockDataProvider.type = DataProviderType.default;
+ const replacement = reformatDataProviderWithNewValue(
+ mockDataProvider,
+ mockEcsDataClone[0],
+ TimelineType.template
+ );
+ expect(replacement).toEqual({
+ id: 'Braden',
+ name: 'Braden',
+ enabled: true,
+ excluded: false,
+ kqlQuery: '',
+ queryMatch: {
+ field: 'host.name',
+ value: '{host.name}',
+ operator: ':',
+ displayField: undefined,
+ displayValue: undefined,
+ },
+ and: [],
+ type: DataProviderType.default,
+ });
+ });
+
+ test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => {
+ mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case
+ const mockDataProvider: DataProvider = mockDataProviders[0];
+ mockDataProvider.queryMatch.field = 'host.name';
+ mockDataProvider.id = 'Braden';
+ mockDataProvider.name = 'Braden';
+ mockDataProvider.queryMatch.value = '{host.name}';
+ mockDataProvider.type = DataProviderType.template;
+ const replacement = reformatDataProviderWithNewValue(
+ mockDataProvider,
+ mockEcsDataClone[0],
+ TimelineType.template
+ );
+ expect(replacement).toEqual({
+ id: 'apache',
+ name: 'apache',
+ enabled: true,
+ excluded: false,
+ kqlQuery: '',
+ queryMatch: {
+ field: 'host.name',
+ value: 'apache',
+ operator: ':',
+ displayField: undefined,
+ displayValue: undefined,
+ },
+ and: [],
+ type: DataProviderType.default,
+ });
+ });
+
+ test('it should replace a query with a template value that is not part of a template such as user.id', () => {
+ const mockDataProvider: DataProvider = mockDataProviders[0];
+ mockDataProvider.queryMatch.field = 'user.id';
+ mockDataProvider.id = 'my-id';
+ mockDataProvider.name = 'Rebecca';
+ mockDataProvider.queryMatch.value = 'Rebecca';
+ mockDataProvider.type = DataProviderType.default;
+ const replacement = reformatDataProviderWithNewValue(
+ mockDataProvider,
+ mockEcsDataClone[0],
+ TimelineType.template
+ );
+ expect(replacement).toEqual({
+ id: 'my-id',
+ name: 'Rebecca',
+ enabled: true,
+ excluded: false,
+ kqlQuery: '',
+ queryMatch: {
+ field: 'user.id',
+ value: 'Rebecca',
+ operator: ':',
+ displayField: undefined,
+ displayValue: undefined,
+ },
+ and: [],
+ type: DataProviderType.default,
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts
similarity index 69%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.ts
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts
index 11a03b0426891..5025d782e2aa2 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts
@@ -8,9 +8,10 @@ import { get, isEmpty } from 'lodash/fp';
import { Filter, esKuery, KueryNode } from '../../../../../../../src/plugins/data/public';
import {
DataProvider,
+ DataProviderType,
DataProvidersAnd,
} from '../../../timelines/components/timeline/data_providers/data_provider';
-import { Ecs } from '../../../graphql/types';
+import { Ecs, TimelineType } from '../../../graphql/types';
interface FindValueToChangeInQuery {
field: string;
@@ -101,20 +102,28 @@ export const findValueToChangeInQuery = (
);
};
-export const replaceTemplateFieldFromQuery = (query: string, ecsData: Ecs): string => {
- if (query.trim() !== '') {
- const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query));
- return valueToChange.reduce((newQuery, vtc) => {
- const newValue = getStringArray(vtc.field, ecsData);
- if (newValue.length) {
- return newQuery.replace(vtc.valueToChange, newValue[0]);
- } else {
- return newQuery;
- }
- }, query);
- } else {
- return '';
+export const replaceTemplateFieldFromQuery = (
+ query: string,
+ ecsData: Ecs,
+ timelineType: TimelineType = TimelineType.default
+): string => {
+ if (timelineType === TimelineType.default) {
+ if (query.trim() !== '') {
+ const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query));
+ return valueToChange.reduce((newQuery, vtc) => {
+ const newValue = getStringArray(vtc.field, ecsData);
+ if (newValue.length) {
+ return newQuery.replace(vtc.valueToChange, newValue[0]);
+ } else {
+ return newQuery;
+ }
+ }, query);
+ } else {
+ return '';
+ }
}
+
+ return query.trim();
};
export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: Ecs): Filter[] =>
@@ -135,30 +144,64 @@ export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData:
export const reformatDataProviderWithNewValue = (
dataProvider: T,
- ecsData: Ecs
+ ecsData: Ecs,
+ timelineType: TimelineType = TimelineType.default
): T => {
- if (templateFields.includes(dataProvider.queryMatch.field)) {
- const newValue = getStringArray(dataProvider.queryMatch.field, ecsData);
- if (newValue.length) {
+ // Support for legacy "template-like" timeline behavior that is using hardcoded list of templateFields
+ if (timelineType === TimelineType.default) {
+ if (templateFields.includes(dataProvider.queryMatch.field)) {
+ const newValue = getStringArray(dataProvider.queryMatch.field, ecsData);
+ if (newValue.length) {
+ dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]);
+ dataProvider.name = newValue[0];
+ dataProvider.queryMatch.value = newValue[0];
+ dataProvider.queryMatch.displayField = undefined;
+ dataProvider.queryMatch.displayValue = undefined;
+ }
+ }
+ dataProvider.type = DataProviderType.default;
+ return dataProvider;
+ }
+
+ if (timelineType === TimelineType.template) {
+ if (
+ dataProvider.type === DataProviderType.template &&
+ dataProvider.queryMatch.operator === ':'
+ ) {
+ const newValue = getStringArray(dataProvider.queryMatch.field, ecsData);
+
+ if (!newValue.length) {
+ dataProvider.enabled = false;
+ }
+
dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]);
dataProvider.name = newValue[0];
dataProvider.queryMatch.value = newValue[0];
dataProvider.queryMatch.displayField = undefined;
dataProvider.queryMatch.displayValue = undefined;
+ dataProvider.type = DataProviderType.default;
+
+ return dataProvider;
}
+
+ dataProvider.type = dataProvider.type ?? DataProviderType.default;
+
+ return dataProvider;
}
+
return dataProvider;
};
export const replaceTemplateFieldFromDataProviders = (
dataProviders: DataProvider[],
- ecsData: Ecs
+ ecsData: Ecs,
+ timelineType: TimelineType = TimelineType.default
): DataProvider[] =>
dataProviders.map((dataProvider) => {
- const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData);
+ const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData, timelineType);
if (newDataProvider.and != null && !isEmpty(newDataProvider.and)) {
newDataProvider.and = newDataProvider.and.map((andDataProvider) =>
- reformatDataProviderWithNewValue(andDataProvider, ecsData)
+ reformatDataProviderWithNewValue(andDataProvider, ecsData, timelineType)
);
}
return newDataProvider;
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
similarity index 98%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
index 6cbf69f409dc4..81aebe95930ac 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
@@ -13,7 +13,7 @@ import { Dispatch } from 'redux';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { Filter, esQuery } from '../../../../../../../src/plugins/data/public';
import { TimelineIdLiteral } from '../../../../common/types/timeline';
-import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns';
+import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns';
import { StatefulEventsViewer } from '../../../common/components/events_viewer';
import { HeaderSection } from '../../../common/components/header_section';
import { combineQueries } from '../../../timelines/components/timeline/helpers';
@@ -375,7 +375,7 @@ export const AlertsTableComponent: React.FC = ({
loadingText: i18n.LOADING_ALERTS,
selectAll: canUserCRUD ? selectAll : false,
timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId })],
- title: i18n.ALERTS_TABLE_TITLE,
+ title: '',
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -408,7 +408,7 @@ export const AlertsTableComponent: React.FC = ({
if (loading || isEmpty(signalsIndex)) {
return (
-
+
);
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts
similarity index 96%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts
index c9cf4e484910c..0f55469bbfda2 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts
@@ -10,13 +10,6 @@ export const PAGE_TITLE = i18n.translate('xpack.securitySolution.detectionEngine
defaultMessage: 'Detection engine',
});
-export const ALERTS_TABLE_TITLE = i18n.translate(
- 'xpack.securitySolution.detectionEngine.alerts.tableTitle',
- {
- defaultMessage: 'Alert list',
- }
-);
-
export const ALERTS_DOCUMENT_TYPE = i18n.translate(
'xpack.securitySolution.detectionEngine.alerts.documentTypeTitle',
{
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/alerts_table/types.ts
rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.tsx b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/translations.ts b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/detection_engine_header_page/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/no_api_integration_callout/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/no_api_integration_callout/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/index.tsx b/x-pack/plugins/security_solution/public/detections/components/no_api_integration_callout/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/no_api_integration_callout/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/translations.ts b/x-pack/plugins/security_solution/public/detections/components/no_api_integration_callout/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/no_api_integration_callout/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/no_api_integration_callout/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/no_write_alerts_callout/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/no_write_alerts_callout/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/index.tsx b/x-pack/plugins/security_solution/public/detections/components/no_write_alerts_callout/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/no_write_alerts_callout/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/translations.ts b/x-pack/plugins/security_solution/public/detections/components/no_write_alerts_callout/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/no_write_alerts_callout/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/accordion_title/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/accordion_title/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/accordion_title/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/accordion_title/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/accordion_title/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/accordion_title/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/accordion_title/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/accordion_title/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/add_item_form/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/add_item_form/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/add_item_form/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/add_item_form/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/add_item_form/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/add_item_form/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/add_item_form/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/add_item_form/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/all_rules_tables/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/all_rules_tables/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/all_rules_tables/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/all_rules_tables/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/anomaly_threshold_slider/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/anomaly_threshold_slider/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/anomaly_threshold_slider/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/anomaly_threshold_slider/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/actions_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/actions_description.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/actions_description.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/actions_description.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/assets/list_tree_icon.svg b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/assets/list_tree_icon.svg
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/assets/list_tree_icon.svg
rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/assets/list_tree_icon.svg
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/ml_job_description.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/ml_job_description.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/ml_job_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/ml_job_description.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/throttle_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/throttle_description.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/throttle_description.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/throttle_description.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/translations.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/types.ts b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/description_step/types.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/mitre/helpers.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/mitre/helpers.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/mitre/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/mitre/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/mitre/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/mitre/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/mitre/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/ml_job_select/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/next_step/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/components/rules/next_step/__snapshots__/index.test.tsx.snap
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/next_step/__snapshots__/index.test.tsx.snap
rename to x-pack/plugins/security_solution/public/detections/components/rules/next_step/__snapshots__/index.test.tsx.snap
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/next_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/next_step/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/next_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/next_step/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/optional_field_label/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/optional_field_label/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/optional_field_label/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/optional_field_label/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/optional_field_label/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/optional_field_label/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/optional_field_label/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/optional_field_label/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pick_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pick_timeline/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/pick_timeline/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/pick_timeline/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pick_timeline/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pick_timeline/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/pick_timeline/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/pick_timeline/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx
similarity index 97%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx
index d82b930210ecd..f93f380469622 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx
@@ -36,7 +36,7 @@ const PrePackagedRulesPromptComponent: React.FC = (
const handlePreBuiltCreation = useCallback(() => {
createPrePackagedRules();
}, [createPrePackagedRules]);
- const { formatUrl } = useFormatUrl(SecurityPageName.alerts);
+ const { formatUrl } = useFormatUrl(SecurityPageName.detections);
const goToCreateRule = useCallback(
(ev) => {
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/update_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/update_callout.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/update_callout.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/update_callout.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/translations.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/query_bar/translations.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/query_bar/translations.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/read_only_callout/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/read_only_callout/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/read_only_callout/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/read_only_callout/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/read_only_callout/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/read_only_callout/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/read_only_callout/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/translations.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/translations.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_field/translations.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/translations.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx
similarity index 98%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx
index a5d0382ef8c8c..cf7a485c59cb3 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx
@@ -16,7 +16,7 @@ import styled from 'styled-components';
import { noop } from 'lodash/fp';
import { useHistory } from 'react-router-dom';
-import { Rule, exportRules } from '../../../../alerts/containers/detection_engine/rules';
+import { Rule, exportRules } from '../../../containers/detection_engine/rules';
import * as i18n from './translations';
import * as i18nActions from '../../../pages/detection_engine/rules/translations';
import { displaySuccessToast, useStateToaster } from '../../../../common/components/toasters';
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts
similarity index 85%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts
index 88fca3d95604e..e99894afeb63c 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { RuleStatusType } from '../../../../alerts/containers/detection_engine/rules';
+import { RuleStatusType } from '../../../containers/detection_engine/rules';
export const getStatusColor = (status: RuleStatusType | string | null) =>
status == null
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx
similarity index 96%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx
index 53be48bc98850..0ddf4d06fb0fc 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx
@@ -15,10 +15,7 @@ import {
import React, { memo, useCallback, useEffect, useState } from 'react';
import deepEqual from 'fast-deep-equal';
-import {
- useRuleStatus,
- RuleInfoStatus,
-} from '../../../../alerts/containers/detection_engine/rules';
+import { useRuleStatus, RuleInfoStatus } from '../../../containers/detection_engine/rules';
import { FormattedDate } from '../../../../common/components/formatted_date';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { getStatusColor } from './helpers';
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_status/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_status/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/__snapshots__/index.test.tsx.snap
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/__snapshots__/index.test.tsx.snap
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/__snapshots__/index.test.tsx.snap
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx
similarity index 97%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx
index c85676ce51052..73d66bf024a62 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_switch/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx
@@ -16,7 +16,7 @@ import styled from 'styled-components';
import React, { useCallback, useState, useEffect } from 'react';
import * as i18n from '../../../pages/detection_engine/rules/translations';
-import { enableRules } from '../../../../alerts/containers/detection_engine/rules';
+import { enableRules } from '../../../containers/detection_engine/rules';
import { enableRulesAction } from '../../../pages/detection_engine/rules/all/actions';
import { Action } from '../../../pages/detection_engine/rules/all/reducer';
import { useStateToaster, displayErrorToast } from '../../../../common/components/toasters';
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/schedule_item_form/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/select_rule_type/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/severity_badge/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/severity_badge/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/severity_badge/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/severity_badge/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/translations.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/status_icon/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/status_icon/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/status_icon/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/status_icon/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/status_icon/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/status_icon/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/status_icon/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/status_icon/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/data.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/data.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/default_value.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule_details/translations.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_content_wrapper/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_content_wrapper/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_content_wrapper/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_content_wrapper/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_content_wrapper/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_content_wrapper/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_content_wrapper/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_content_wrapper/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx
similarity index 99%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx
index b56e1794eef63..864f953bff1e1 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx
@@ -13,7 +13,7 @@ import { DEFAULT_INDEX_KEY } from '../../../../../common/constants';
import { isMlRule } from '../../../../../common/machine_learning/helpers';
import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions';
import { IIndexPattern } from '../../../../../../../../src/plugins/data/public';
-import { useFetchIndexPatterns } from '../../../../alerts/containers/detection_engine/rules';
+import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules';
import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
import { useMlCapabilities } from '../../../../common/components/ml_popover/hooks/use_ml_capabilities';
import { useUiSetting$ } from '../../../../common/lib/kibana';
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/schema.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/translations.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/types.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/types.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/types.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/types.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_panel/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_panel/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx
similarity index 99%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx
index 061b8b0f8c36e..7005bfb25f4a6 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx
@@ -88,7 +88,7 @@ const StepRuleActionsComponent: FC = ({
// TO DO need to make sure that logic is still valid
const kibanaAbsoluteUrl = useMemo(() => {
- const url = application.getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, {
+ const url = application.getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, {
absolute: true,
});
if (url != null && url.includes('app/security/alerts')) {
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/schema.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/translations.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/utils.test.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/utils.test.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/utils.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/utils.ts
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/schema.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/schema.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/schema.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/translations.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/step_schedule_rule/translations.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/translations.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/throttle_select_field/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/throttle_select_field/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/throttle_select_field/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/throttle_select_field/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/throttle_select_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/throttle_select_field/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/rules/throttle_select_field/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/throttle_select_field/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/user_info/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/components/user_info/index.tsx
rename to x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/__mocks__/api.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/__mocks__/api.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/__mocks__/api.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/api.test.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/api.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/mock.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/translations.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/types.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_privilege_user.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_privilege_user.test.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_privilege_user.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_privilege_user.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_query.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_query.test.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_query.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_signal_index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/alerts/use_signal_index.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/__mocks__/api.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.test.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/index.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/index.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/mock.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/persist_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/persist_rule.test.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/persist_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/persist_rule.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/translations.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/types.ts
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/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
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/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
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.test.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.test.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_tags.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_tags.test.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_tags.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_tags.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/index.ts b/x-pack/plugins/security_solution/public/detections/index.ts
similarity index 90%
rename from x-pack/plugins/security_solution/public/alerts/index.ts
rename to x-pack/plugins/security_solution/public/detections/index.ts
index a2e377a732936..d043127a3098b 100644
--- a/x-pack/plugins/security_solution/public/alerts/index.ts
+++ b/x-pack/plugins/security_solution/public/detections/index.ts
@@ -11,11 +11,11 @@ import { AlertsRoutes } from './routes';
import { SecuritySubPlugin } from '../app/types';
const ALERTS_TIMELINE_IDS: TimelineIdLiteral[] = [
- TimelineId.alertsRulesDetailsPage,
- TimelineId.alertsPage,
+ TimelineId.detectionsRulesDetailsPage,
+ TimelineId.detectionsPage,
];
-export class Alerts {
+export class Detections {
public setup() {}
public start(storage: Storage): SecuritySubPlugin {
diff --git a/x-pack/plugins/security_solution/public/alerts/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/mitre/mitre_tactics_techniques.ts
rename to x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/mitre/types.ts b/x-pack/plugins/security_solution/public/detections/mitre/types.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/mitre/types.ts
rename to x-pack/plugins/security_solution/public/detections/mitre/types.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
similarity index 97%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
index b39d51e2de95f..11f738320db6e 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
@@ -56,7 +56,7 @@ export const DetectionEnginePageComponent: React.FC = ({
} = useUserInfo();
const history = useHistory();
const [lastAlerts] = useAlertInfo({});
- const { formatUrl } = useFormatUrl(SecurityPageName.alerts);
+ const { formatUrl } = useFormatUrl(SecurityPageName.detections);
const updateDateRangeCallback = useCallback(
({ x }) => {
@@ -146,7 +146,7 @@ export const DetectionEnginePageComponent: React.FC = ({
/>
= ({
)}
-
+
>
);
};
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_no_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_no_signal_index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_no_signal_index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_no_signal_index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_no_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_no_signal_index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_no_signal_index.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_no_signal_index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_user_unauthenticated.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_user_unauthenticated.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/index.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts
similarity index 96%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts
index f1416bfbc41b5..2b86abf4255c6 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts
@@ -5,9 +5,9 @@
*/
import { esFilters } from '../../../../../../../../../../src/plugins/data/public';
-import { Rule, RuleError } from '../../../../../../alerts/containers/detection_engine/rules';
+import { Rule, RuleError } from '../../../../../containers/detection_engine/rules';
import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types';
-import { FieldValueQueryBar } from '../../../../../../alerts/components/rules/query_bar';
+import { FieldValueQueryBar } from '../../../../../components/rules/query_bar';
export const mockQueryBar: FieldValueQueryBar = {
query: {
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx
similarity index 98%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/actions.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx
index 5169ff009d63c..bad99039d0398 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/actions.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx
@@ -12,7 +12,7 @@ import {
duplicateRules,
enableRules,
Rule,
-} from '../../../../../alerts/containers/detection_engine/rules';
+} from '../../../../containers/detection_engine/rules';
import { getEditRuleUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine';
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx
similarity index 98%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/batch_actions.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx
index 2c94588ce128a..71cfbbf552d84 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/batch_actions.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx
@@ -15,7 +15,7 @@ import {
exportRulesAction,
} from './actions';
import { ActionToaster, displayWarningToast } from '../../../../../common/components/toasters';
-import { Rule } from '../../../../../alerts/containers/detection_engine/rules';
+import { Rule } from '../../../../containers/detection_engine/rules';
import * as detectionI18n from '../../translations';
interface GetBatchItems {
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
similarity index 99%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
index 030f510b7aa37..ea36a0cb0b48d 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
@@ -19,7 +19,7 @@ import * as H from 'history';
import React, { Dispatch } from 'react';
import { isMlRule } from '../../../../../../common/machine_learning/helpers';
-import { Rule, RuleStatus } from '../../../../../alerts/containers/detection_engine/rules';
+import { Rule, RuleStatus } from '../../../../containers/detection_engine/rules';
import { getEmptyTagValue } from '../../../../../common/components/empty_value';
import { FormattedDate } from '../../../../../common/components/formatted_date';
import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine';
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.tsx
similarity index 97%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/helpers.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.tsx
index 7350cec0115fb..062d7967bf301 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.tsx
@@ -7,7 +7,7 @@
import { bucketRulesResponse, showRulesTable } from './helpers';
import { mockRule, mockRuleError } from './__mocks__/mock';
import uuid from 'uuid';
-import { Rule, RuleError } from '../../../../../alerts/containers/detection_engine/rules';
+import { Rule, RuleError } from '../../../../containers/detection_engine/rules';
describe('AllRulesTable Helpers', () => {
const mockRule1: Readonly = mockRule(uuid.v4());
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts
similarity index 94%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/helpers.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts
index 632d03cebef71..0ebeb84d57468 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts
@@ -7,7 +7,7 @@
import {
BulkRuleResponse,
RuleResponseBuckets,
-} from '../../../../../alerts/containers/detection_engine/rules';
+} from '../../../../containers/detection_engine/rules';
/**
* Separates rules/errors from bulk rules API response (create/update/delete)
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx
similarity index 98%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx
index ae5c129befa5d..45e609e38202a 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx
@@ -80,7 +80,7 @@ jest.mock('./reducer', () => {
};
});
-jest.mock('../../../../../alerts/containers/detection_engine/rules', () => {
+jest.mock('../../../../containers/detection_engine/rules', () => {
return {
useRules: jest.fn().mockReturnValue([
false,
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx
similarity index 99%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx
index 65f7bb63c74e4..85dce907084e8 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx
@@ -24,7 +24,7 @@ import {
Rule,
PaginationOptions,
exportRules,
-} from '../../../../../alerts/containers/detection_engine/rules';
+} from '../../../../containers/detection_engine/rules';
import { HeaderSection } from '../../../../../common/components/header_section';
import {
UtilityBar,
@@ -142,7 +142,7 @@ export const AllRules = React.memo(
const [, dispatchToaster] = useStateToaster();
const mlCapabilities = useMlCapabilities();
const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules);
- const { formatUrl } = useFormatUrl(SecurityPageName.alerts);
+ const { formatUrl } = useFormatUrl(SecurityPageName.detections);
// TODO: Refactor license check + hasMlAdminPermissions to common check
const hasMlPermissions =
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/reducer.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts
similarity index 98%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/reducer.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts
index 3fe17fcaeeb9c..ff9b41bed06f5 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/reducer.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts
@@ -9,7 +9,7 @@ import {
FilterOptions,
PaginationOptions,
Rule,
-} from '../../../../../alerts/containers/detection_engine/rules';
+} from '../../../../containers/detection_engine/rules';
type LoadingRuleAction = 'duplicate' | 'enable' | 'disable' | 'export' | 'delete' | null;
export interface State {
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx
similarity index 95%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx
index c65271c3cc014..0f201fcbaa441 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx
@@ -16,8 +16,8 @@ import {
import { isEqual } from 'lodash/fp';
import * as i18n from '../../translations';
-import { FilterOptions } from '../../../../../../alerts/containers/detection_engine/rules';
-import { useTags } from '../../../../../../alerts/containers/detection_engine/rules/use_tags';
+import { FilterOptions } from '../../../../../containers/detection_engine/rules';
+import { useTags } from '../../../../../containers/detection_engine/rules/use_tags';
import { TagsFilterPopover } from './tags_filter_popover';
interface RulesTableFiltersProps {
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts
similarity index 99%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.test.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts
index bbfbbaae058d4..f402303c4c621 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { NewRule } from '../../../../../alerts/containers/detection_engine/rules';
+import { NewRule } from '../../../../containers/detection_engine/rules';
import {
DefineStepRuleJson,
ScheduleStepRuleJson,
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
similarity index 98%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
index b7cf94bb4f319..8331346b19ac9 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
@@ -12,7 +12,7 @@ import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/const
import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions';
import { RuleType } from '../../../../../../common/detection_engine/types';
import { isMlRule } from '../../../../../../common/machine_learning/helpers';
-import { NewRule } from '../../../../../alerts/containers/detection_engine/rules';
+import { NewRule } from '../../../../containers/detection_engine/rules';
import {
AboutStepRule,
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx
similarity index 98%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx
index 4be408039d6f6..6475b6f6b6b54 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx
@@ -9,7 +9,7 @@ import React, { useCallback, useRef, useState, useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import styled, { StyledComponent } from 'styled-components';
-import { usePersistRule } from '../../../../../alerts/containers/detection_engine/rules';
+import { usePersistRule } from '../../../../containers/detection_engine/rules';
import {
getRulesUrl,
@@ -293,7 +293,7 @@ const CreateRulePageComponent: React.FC = () => {
backOptions={{
href: getRulesUrl(),
text: i18n.BACK_TO_RULES,
- pageId: SecurityPageName.alerts,
+ pageId: SecurityPageName.detections,
}}
border
isLoading={isLoading || loading}
@@ -438,7 +438,7 @@ const CreateRulePageComponent: React.FC = () => {
-
+
>
);
};
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/translations.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx
similarity index 82%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx
index fc16bcd96f766..c44f1bf780944 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx
@@ -9,8 +9,8 @@ import { shallow } from 'enzyme';
import { TestProviders } from '../../../../../common/mock';
import { FailureHistory } from './failure_history';
-import { useRuleStatus } from '../../../../../alerts/containers/detection_engine/rules';
-jest.mock('../../../../../alerts/containers/detection_engine/rules');
+import { useRuleStatus } from '../../../../containers/detection_engine/rules';
+jest.mock('../../../../containers/detection_engine/rules');
describe('FailureHistory', () => {
beforeAll(() => {
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx
similarity index 95%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx
index 1030aaa30d752..610b7e32cec5f 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/failure_history.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx
@@ -15,10 +15,7 @@ import {
} from '@elastic/eui';
import React, { memo } from 'react';
-import {
- useRuleStatus,
- RuleInfoStatus,
-} from '../../../../../alerts/containers/detection_engine/rules';
+import { useRuleStatus, RuleInfoStatus } from '../../../../containers/detection_engine/rules';
import { HeaderSection } from '../../../../../common/components/header_section';
import * as i18n from './translations';
import { FormattedDate } from '../../../../../common/components/formatted_date';
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
similarity index 98%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
index bb6ef92a059cb..6ab08d94fa781 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
@@ -33,7 +33,7 @@ import {
} from '../../../../../common/components/link_to/redirect_to_detection_engine';
import { SiemSearchBar } from '../../../../../common/components/search_bar';
import { WrapperPage } from '../../../../../common/components/wrapper_page';
-import { useRule } from '../../../../../alerts/containers/detection_engine/rules';
+import { useRule } from '../../../../containers/detection_engine/rules';
import { useWithSource } from '../../../../../common/containers/source';
import { SpyRoute } from '../../../../../common/utils/route/spy_routes';
@@ -130,7 +130,7 @@ export const RuleDetailsPageComponent: FC = ({
const [lastAlerts] = useAlertInfo({ ruleId });
const mlCapabilities = useMlCapabilities();
const history = useHistory();
- const { formatUrl } = useFormatUrl(SecurityPageName.alerts);
+ const { formatUrl } = useFormatUrl(SecurityPageName.detections);
// TODO: Refactor license check + hasMlAdminPermissions to common check
const hasMlPermissions =
@@ -302,7 +302,7 @@ export const RuleDetailsPageComponent: FC = ({
backOptions={{
href: getRulesUrl(),
text: i18n.BACK_TO_RULES,
- pageId: SecurityPageName.alerts,
+ pageId: SecurityPageName.detections,
}}
border
subtitle={subTitle}
@@ -424,7 +424,7 @@ export const RuleDetailsPageComponent: FC = ({
{ruleId != null && (
= ({
)}
-
+
>
);
};
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/status_failed_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/status_failed_callout.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/status_failed_callout.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/status_failed_callout.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/translations.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx
similarity index 98%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx
index ba7444d8e8a52..777f7766993d0 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx
@@ -19,7 +19,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useParams, useHistory } from 'react-router-dom';
-import { useRule, usePersistRule } from '../../../../../alerts/containers/detection_engine/rules';
+import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules';
import { WrapperPage } from '../../../../../common/components/wrapper_page';
import {
getRuleDetailsUrl,
@@ -380,7 +380,7 @@ const EditRulePageComponent: FC = () => {
backOptions={{
href: getRuleDetailsUrl(ruleId ?? ''),
text: `${i18n.BACK_TO} ${rule?.name ?? ''}`,
- pageId: SecurityPageName.alerts,
+ pageId: SecurityPageName.detections,
}}
isLoading={isLoading}
title={i18n.PAGE_TITLE}
@@ -446,7 +446,7 @@ const EditRulePageComponent: FC = () => {
-
+
>
);
};
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/translations.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/translations.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/translations.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx
similarity index 99%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx
index b467f3334508d..f8969f06c8ef6 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx
@@ -18,7 +18,7 @@ import {
} from './helpers';
import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock';
import { esFilters } from '../../../../../../../../src/plugins/data/public';
-import { Rule } from '../../../../alerts/containers/detection_engine/rules';
+import { Rule } from '../../../containers/detection_engine/rules';
import {
AboutStepRule,
AboutStepRuleDetails,
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
similarity index 99%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
index 2a792f7d35eaa..bf49ed5be90fb 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
@@ -14,7 +14,7 @@ import { RuleAlertAction, RuleType } from '../../../../../common/detection_engin
import { isMlRule } from '../../../../../common/machine_learning/helpers';
import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions';
import { Filter } from '../../../../../../../../src/plugins/data/public';
-import { Rule } from '../../../../alerts/containers/detection_engine/rules';
+import { Rule } from '../../../containers/detection_engine/rules';
import { FormData, FormHook, FormSchema } from '../../../../shared_imports';
import {
AboutStepRule,
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx
similarity index 86%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.test.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx
index 7e6cd48ddc003..f0ad670ddb665 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx
@@ -9,7 +9,7 @@ import { shallow } from 'enzyme';
import { RulesPage } from './index';
import { useUserInfo } from '../../../components/user_info';
-import { usePrePackagedRules } from '../../../../alerts/containers/detection_engine/rules';
+import { usePrePackagedRules } from '../../../containers/detection_engine/rules';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@@ -24,7 +24,7 @@ jest.mock('react-router-dom', () => {
jest.mock('../../../../common/components/link_to');
jest.mock('../../../components/user_info');
-jest.mock('../../../../alerts/containers/detection_engine/rules');
+jest.mock('../../../containers/detection_engine/rules');
describe('RulesPage', () => {
beforeAll(() => {
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx
similarity index 95%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx
index 7684f710952e6..9cbc0e2aabfbe 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx
@@ -8,10 +8,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useCallback, useRef, useState } from 'react';
import { useHistory } from 'react-router-dom';
-import {
- usePrePackagedRules,
- importRules,
-} from '../../../../alerts/containers/detection_engine/rules';
+import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules';
import {
getDetectionEngineUrl,
getCreateRuleUrl,
@@ -66,7 +63,7 @@ const RulesPageComponent: React.FC = () => {
rulesNotInstalled,
rulesNotUpdated
);
- const { formatUrl } = useFormatUrl(SecurityPageName.alerts);
+ const { formatUrl } = useFormatUrl(SecurityPageName.detections);
const handleRefreshRules = useCallback(async () => {
if (refreshRulesData.current != null) {
@@ -126,8 +123,8 @@ const RulesPageComponent: React.FC = () => {
@@ -203,7 +200,7 @@ const RulesPageComponent: React.FC = () => {
/>
-
+
>
);
};
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts
similarity index 99%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts
index eee2aa9ff40cc..050c281d09350 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts
@@ -6,10 +6,10 @@
import { i18n } from '@kbn/i18n';
-export const BACK_TO_ALERTS = i18n.translate(
+export const BACK_TO_DETECTIONS = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.backOptionsHeader',
{
- defaultMessage: 'Back to alerts',
+ defaultMessage: 'Back to detections',
}
);
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/types.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts
similarity index 90%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.test.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts
index 91de1467a8310..32f96b519acc5 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.test.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts
@@ -23,6 +23,6 @@ describe('getBreadcrumbs', () => {
[],
getUrlForAppMock
)
- ).toEqual([{ href: 'securitySolution:alerts', text: 'Alerts' }]);
+ ).toEqual([{ href: 'securitySolution:detections', text: 'Detection alerts' }]);
});
});
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts
similarity index 87%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts
index 203a93acd849c..75d1df9406d25 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts
@@ -28,7 +28,7 @@ const getTabBreadcrumb = (pathname: string, search: string[], getUrlForApp: GetU
if (tabPath === 'alerts') {
return {
text: i18nDetections.ALERT,
- href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, {
+ href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, {
path: getDetectionEngineTabUrl(tabPath, !isEmpty(search[0]) ? search[0] : ''),
}),
};
@@ -37,7 +37,7 @@ const getTabBreadcrumb = (pathname: string, search: string[], getUrlForApp: GetU
if (tabPath === 'rules') {
return {
text: i18nRules.PAGE_TITLE,
- href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, {
+ href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, {
path: getRulesUrl(!isEmpty(search[0]) ? search[0] : ''),
}),
};
@@ -58,7 +58,7 @@ export const getBreadcrumbs = (
let breadcrumb = [
{
text: i18nDetections.PAGE_TITLE,
- href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, {
+ href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, {
path: !isEmpty(search[0]) ? search[0] : '',
}),
},
@@ -75,7 +75,7 @@ export const getBreadcrumbs = (
...breadcrumb,
{
text: params.state.ruleName,
- href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, {
+ href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, {
path: getRuleDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''),
}),
},
@@ -87,7 +87,7 @@ export const getBreadcrumbs = (
...breadcrumb,
{
text: i18nRules.ADD_PAGE_TITLE,
- href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, {
+ href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, {
path: getCreateRuleUrl(!isEmpty(search[0]) ? search[0] : ''),
}),
},
@@ -99,7 +99,7 @@ export const getBreadcrumbs = (
...breadcrumb,
{
text: i18nRules.EDIT_PAGE_TITLE,
- href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, {
+ href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, {
path: getEditRuleUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''),
}),
},
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts
similarity index 98%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts
index 8e91d9848d1ed..bfe5dfc012530 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts
@@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
export const PAGE_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.detectionsPageTitle',
{
- defaultMessage: 'Alerts',
+ defaultMessage: 'Detection alerts',
}
);
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/types.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/pages/detection_engine/types.ts
rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/types.ts
diff --git a/x-pack/plugins/security_solution/public/alerts/routes.tsx b/x-pack/plugins/security_solution/public/detections/routes.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/alerts/routes.tsx
rename to x-pack/plugins/security_solution/public/detections/routes.tsx
diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json
index 86ee84f2e8bf4..2b8b07cb6a24b 100644
--- a/x-pack/plugins/security_solution/public/graphql/introspection.json
+++ b/x-pack/plugins/security_solution/public/graphql/introspection.json
@@ -10015,6 +10015,14 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "type",
+ "description": "",
+ "args": [],
+ "type": { "kind": "ENUM", "name": "DataProviderType", "ofType": null },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "and",
"description": "",
@@ -10088,6 +10096,29 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "ENUM",
+ "name": "DataProviderType",
+ "description": "",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "default",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "template",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
{
"kind": "OBJECT",
"name": "DateRangePickerResult",
@@ -11253,6 +11284,12 @@
}
},
"defaultValue": null
+ },
+ {
+ "name": "type",
+ "description": "",
+ "type": { "kind": "ENUM", "name": "DataProviderType", "ofType": null },
+ "defaultValue": null
}
],
"interfaces": null,
diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts
index bf5725c2ddea5..2c8f2e63356e6 100644
--- a/x-pack/plugins/security_solution/public/graphql/types.ts
+++ b/x-pack/plugins/security_solution/public/graphql/types.ts
@@ -185,6 +185,8 @@ export interface DataProviderInput {
queryMatch?: Maybe;
and?: Maybe;
+
+ type?: Maybe;
}
export interface QueryMatchInput {
@@ -342,6 +344,11 @@ export enum TlsFields {
_id = '_id',
}
+export enum DataProviderType {
+ default = 'default',
+ template = 'template',
+}
+
export enum TimelineStatus {
active = 'active',
draft = 'draft',
@@ -2030,6 +2037,8 @@ export interface DataProviderResult {
queryMatch?: Maybe;
+ type?: Maybe;
+
and?: Maybe;
}
@@ -5523,6 +5532,8 @@ export namespace GetOneTimeline {
kqlQuery: Maybe;
+ type: Maybe;
+
queryMatch: Maybe;
and: Maybe;
diff --git a/x-pack/plugins/security_solution/public/helpers.ts b/x-pack/plugins/security_solution/public/helpers.ts
index 0dd66d06b78be..53fe185ef9a65 100644
--- a/x-pack/plugins/security_solution/public/helpers.ts
+++ b/x-pack/plugins/security_solution/public/helpers.ts
@@ -60,7 +60,7 @@ export const manageOldSiemRoutes = async (coreStart: CoreStart) => {
});
break;
case 'detections':
- application.navigateToApp(`${APP_ID}:${SecurityPageName.alerts}`, {
+ application.navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, {
replace: true,
path,
});
diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts
index 0fad1273c7279..4bc586bdee8a9 100644
--- a/x-pack/plugins/security_solution/public/management/common/constants.ts
+++ b/x-pack/plugins/security_solution/public/management/common/constants.ts
@@ -10,7 +10,7 @@ import { SecurityPageName } from '../../app/types';
// --[ ROUTING ]---------------------------------------------------------------------------
export const MANAGEMENT_APP_ID = `${APP_ID}:${SecurityPageName.management}`;
export const MANAGEMENT_ROUTING_ROOT_PATH = '';
-export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.endpoints})`;
+export const MANAGEMENT_ROUTING_HOSTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.hosts})`;
export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})`;
export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})/:policyId`;
@@ -21,5 +21,5 @@ export const MANAGEMENT_STORE_GLOBAL_NAMESPACE: ManagementStoreGlobalNamespace =
export const MANAGEMENT_STORE_POLICY_LIST_NAMESPACE = 'policyList';
/** Namespace within the Management state where policy details state is maintained */
export const MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE = 'policyDetails';
-/** Namespace within the Management state where endpoints state is maintained */
-export const MANAGEMENT_STORE_ENDPOINTS_NAMESPACE = 'endpoints';
+/** Namespace within the Management state where hosts state is maintained */
+export const MANAGEMENT_STORE_HOSTS_NAMESPACE = 'hosts';
diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts
index 92eb7717318d3..5add6b753a7a9 100644
--- a/x-pack/plugins/security_solution/public/management/common/routing.ts
+++ b/x-pack/plugins/security_solution/public/management/common/routing.ts
@@ -10,7 +10,7 @@ import { generatePath } from 'react-router-dom';
import querystring from 'querystring';
import {
- MANAGEMENT_ROUTING_ENDPOINTS_PATH,
+ MANAGEMENT_ROUTING_HOSTS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_PATH,
} from './constants';
@@ -32,11 +32,11 @@ const querystringStringify: (
) => string = querystring.stringify;
/** Make `selected_host` required */
-type EndpointDetailsUrlProps = Omit &
+type HostDetailsUrlProps = Omit &
Required>;
-export const getEndpointListPath = (
- props: { name: 'default' | 'endpointList' } & HostIndexUIQueryParams,
+export const getHostListPath = (
+ props: { name: 'default' | 'hostList' } & HostIndexUIQueryParams,
search?: string
) => {
const { name, ...queryParams } = props;
@@ -45,29 +45,27 @@ export const getEndpointListPath = (
);
const urlSearch = `${urlQueryParams && !isEmpty(search) ? '&' : ''}${search ?? ''}`;
- if (name === 'endpointList') {
- return `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, {
- tabName: ManagementSubTab.endpoints,
+ if (name === 'hostList') {
+ return `${generatePath(MANAGEMENT_ROUTING_HOSTS_PATH, {
+ tabName: ManagementSubTab.hosts,
})}${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`;
}
return `${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`;
};
-export const getEndpointDetailsPath = (
- props: { name: 'endpointDetails' | 'endpointPolicyResponse' } & EndpointDetailsUrlProps,
+export const getHostDetailsPath = (
+ props: { name: 'hostDetails' | 'hostPolicyResponse' } & HostDetailsUrlProps,
search?: string
) => {
const { name, ...queryParams } = props;
- queryParams.show = (props.name === 'endpointPolicyResponse'
+ queryParams.show = (props.name === 'hostPolicyResponse'
? 'policy_response'
: '') as HostIndexUIQueryParams['show'];
- const urlQueryParams = querystringStringify(
- queryParams
- );
+ const urlQueryParams = querystringStringify(queryParams);
const urlSearch = `${urlQueryParams && !isEmpty(search) ? '&' : ''}${search ?? ''}`;
- return `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, {
- tabName: ManagementSubTab.endpoints,
+ return `${generatePath(MANAGEMENT_ROUTING_HOSTS_PATH, {
+ tabName: ManagementSubTab.hosts,
})}${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`;
};
diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
index 5dd47d4e88028..c3d6cb48e4dae 100644
--- a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
@@ -103,7 +103,7 @@ const PolicyEmptyState = React.memo<{
);
});
-const EndpointsEmptyState = React.memo<{
+const HostsEmptyState = React.memo<{
loading: boolean;
onActionClick: (event: MouseEvent) => void;
actionDisabled: boolean;
@@ -113,14 +113,14 @@ const EndpointsEmptyState = React.memo<{
const policySteps = useMemo(
() => [
{
- title: i18n.translate('xpack.securitySolution.endpoint.endpointList.stepOneTitle', {
+ title: i18n.translate('xpack.securitySolution.endpoint.hostList.stepOneTitle', {
defaultMessage: 'Select a policy you created from the list below.',
}),
children: (
<>
@@ -138,7 +138,7 @@ const EndpointsEmptyState = React.memo<{
return loading ? (
@@ -146,7 +146,7 @@ const EndpointsEmptyState = React.memo<{
list
) : (
);
@@ -156,14 +156,14 @@ const EndpointsEmptyState = React.memo<{
),
},
{
- title: i18n.translate('xpack.securitySolution.endpoint.endpointList.stepTwoTitle', {
+ title: i18n.translate('xpack.securitySolution.endpoint.hostList.stepTwoTitle', {
defaultMessage:
'Head over to Ingest to deploy your Agent with Endpoint Security enabled.',
}),
children: (
@@ -178,18 +178,18 @@ const EndpointsEmptyState = React.memo<{
loading={loading}
onActionClick={onActionClick}
actionDisabled={actionDisabled}
- dataTestSubj="emptyEndpointsTable"
+ dataTestSubj="emptyHostsTable"
steps={policySteps}
headerComponent={
}
bodyComponent={
}
/>
@@ -271,7 +271,7 @@ const ManagementEmptyState = React.memo<{
);
PolicyEmptyState.displayName = 'PolicyEmptyState';
-EndpointsEmptyState.displayName = 'EndpointsEmptyState';
+HostsEmptyState.displayName = 'HostsEmptyState';
ManagementEmptyState.displayName = 'ManagementEmptyState';
-export { PolicyEmptyState, EndpointsEmptyState, ManagementEmptyState };
+export { PolicyEmptyState, HostsEmptyState, ManagementEmptyState };
diff --git a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx
index c3dbb93b369a9..8495628709d2a 100644
--- a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx
@@ -11,7 +11,7 @@ import { PageView, PageViewProps } from '../../common/components/endpoint/page_v
import { ManagementSubTab } from '../types';
import { SecurityPageName } from '../../app/types';
import { useFormatUrl } from '../../common/components/link_to';
-import { getEndpointListPath, getPoliciesPath } from '../common/routing';
+import { getHostListPath, getPoliciesPath } from '../common/routing';
import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler';
export const ManagementPageView = memo>((options) => {
@@ -19,7 +19,7 @@ export const ManagementPageView = memo>((options) =>
const { tabName } = useParams<{ tabName: ManagementSubTab }>();
const goToEndpoint = useNavigateByRouterEventHandler(
- getEndpointListPath({ name: 'endpointList' }, search)
+ getHostListPath({ name: 'hostList' }, search)
);
const goToPolicies = useNavigateByRouterEventHandler(getPoliciesPath(search));
@@ -31,11 +31,11 @@ export const ManagementPageView = memo>((options) =>
return [
{
name: i18n.translate('xpack.securitySolution.managementTabs.endpoints', {
- defaultMessage: 'Endpoints',
+ defaultMessage: 'Hosts',
}),
- id: ManagementSubTab.endpoints,
- isSelected: tabName === ManagementSubTab.endpoints,
- href: formatUrl(getEndpointListPath({ name: 'endpointList' })),
+ id: ManagementSubTab.hosts,
+ isSelected: tabName === ManagementSubTab.hosts,
+ href: formatUrl(getHostListPath({ name: 'hostList' })),
onClick: goToEndpoint,
},
{
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx
index ff7f522b9bc52..a970edd4d30f4 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx
@@ -7,19 +7,19 @@
import { Switch, Route } from 'react-router-dom';
import React, { memo } from 'react';
import { HostList } from './view';
-import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../common/constants';
+import { MANAGEMENT_ROUTING_HOSTS_PATH } from '../../common/constants';
import { NotFoundPage } from '../../../app/404';
/**
- * Provides the routing container for the endpoints related views
+ * Provides the routing container for the hosts related views
*/
-export const EndpointsContainer = memo(() => {
+export const HostsContainer = memo(() => {
return (
-
+
);
});
-EndpointsContainer.displayName = 'EndpointsContainer';
+HostsContainer.displayName = 'HostsContainer';
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts
index ae2ce9facc837..533b14e50f3dd 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts
@@ -24,7 +24,7 @@ import {
MiddlewareActionSpyHelper,
createSpyMiddleware,
} from '../../../../common/store/test_utils';
-import { getEndpointListPath } from '../../../common/routing';
+import { getHostListPath } from '../../../common/routing';
describe('host list pagination: ', () => {
let fakeCoreStart: jest.Mocked;
@@ -56,7 +56,7 @@ describe('host list pagination: ', () => {
queryParams = () => uiQueryParams(store.getState());
historyPush = (nextQueryParams: HostIndexUIQueryParams): void => {
- return history.push(getEndpointListPath({ name: 'endpointList', ...nextQueryParams }));
+ return history.push(getHostListPath({ name: 'hostList', ...nextQueryParams }));
};
});
@@ -70,7 +70,7 @@ describe('host list pagination: ', () => {
type: 'userChangedUrl',
payload: {
...history.location,
- pathname: getEndpointListPath({ name: 'endpointList' }),
+ pathname: getHostListPath({ name: 'hostList' }),
},
});
await waitForAction('serverReturnedHostList');
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
index e62c53e061a33..1c5c4fbac51ba 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
@@ -21,7 +21,7 @@ import { listData } from './selectors';
import { HostState } from '../types';
import { hostListReducer } from './reducer';
import { hostMiddlewareFactory } from './middleware';
-import { getEndpointListPath } from '../../../common/routing';
+import { getHostListPath } from '../../../common/routing';
describe('host list middleware', () => {
let fakeCoreStart: jest.Mocked;
@@ -60,7 +60,7 @@ describe('host list middleware', () => {
type: 'userChangedUrl',
payload: {
...history.location,
- pathname: getEndpointListPath({ name: 'endpointList' }),
+ pathname: getHostListPath({ name: 'hostList' }),
},
});
await waitForAction('serverReturnedHostList');
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
index e75d2129f61a5..4f47eaf565d8c 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
@@ -15,7 +15,7 @@ import {
HostPolicyResponseActionStatus,
} from '../../../../../common/endpoint/types';
import { HostState, HostIndexUIQueryParams } from '../types';
-import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../../common/constants';
+import { MANAGEMENT_ROUTING_HOSTS_PATH } from '../../../common/constants';
const PAGE_SIZES = Object.freeze([10, 20, 50]);
@@ -114,7 +114,7 @@ export const policyResponseError = (state: Immutable) => state.policy
export const isOnHostPage = (state: Immutable) => {
return (
matchPath(state.location?.pathname ?? '', {
- path: MANAGEMENT_ROUTING_ENDPOINTS_PATH,
+ path: MANAGEMENT_ROUTING_HOSTS_PATH,
exact: true,
}) !== null
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx
index 66abf993770a7..10ea271139e49 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx
@@ -26,7 +26,7 @@ import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants';
import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app';
-import { getEndpointDetailsPath, getPolicyDetailPath } from '../../../../common/routing';
+import { getHostDetailsPath, getPolicyDetailPath } from '../../../../common/routing';
import { SecurityPageName } from '../../../../../app/types';
import { useFormatUrl } from '../../../../../common/components/link_to';
import { AgentDetailsReassignConfigAction } from '../../../../../../../ingest_manager/public';
@@ -84,14 +84,14 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
const { selected_host, show, ...currentUrlParams } = queryParams;
return [
formatUrl(
- getEndpointDetailsPath({
- name: 'endpointPolicyResponse',
+ getHostDetailsPath({
+ name: 'hostPolicyResponse',
...currentUrlParams,
selected_host: details.host.id,
})
),
- getEndpointDetailsPath({
- name: 'endpointPolicyResponse',
+ getHostDetailsPath({
+ name: 'hostPolicyResponse',
...currentUrlParams,
selected_host: details.host.id,
}),
@@ -108,7 +108,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
onDoneNavigateTo: [
'securitySolution:management',
{
- path: getEndpointDetailsPath({ name: 'endpointDetails', selected_host: details.host.id }),
+ path: getHostDetailsPath({ name: 'hostDetails', selected_host: details.host.id }),
},
],
},
@@ -200,8 +200,8 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
description: details.host.hostname,
},
{
- title: i18n.translate('xpack.securitySolution.endpoint.host.details.sensorVersion', {
- defaultMessage: 'Sensor Version',
+ title: i18n.translate('xpack.securitySolution.endpoint.host.details.endpointVersion', {
+ defaultMessage: 'Endpoint Version',
}),
description: details.agent.version,
},
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
index 3d44b73858e90..e29d796325bd6 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
@@ -38,7 +38,7 @@ import { PolicyResponse } from './policy_response';
import { HostMetadata } from '../../../../../../common/endpoint/types';
import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
-import { getEndpointListPath } from '../../../../common/routing';
+import { getHostListPath } from '../../../../common/routing';
import { SecurityPageName } from '../../../../../app/types';
import { useFormatUrl } from '../../../../../common/components/link_to';
@@ -122,14 +122,14 @@ const PolicyResponseFlyoutPanel = memo<{
const [detailsUri, detailsRoutePath] = useMemo(
() => [
formatUrl(
- getEndpointListPath({
- name: 'endpointList',
+ getHostListPath({
+ name: 'hostList',
...queryParams,
selected_host: hostMeta.host.id,
})
),
- getEndpointListPath({
- name: 'endpointList',
+ getHostListPath({
+ name: 'hostList',
...queryParams,
selected_host: hostMeta.host.id,
}),
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts
index b048a8f69b5d2..d11335df875e9 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts
@@ -9,14 +9,14 @@ import { useMemo } from 'react';
import { useKibana } from '../../../../common/lib/kibana';
import { HostState } from '../types';
import {
- MANAGEMENT_STORE_ENDPOINTS_NAMESPACE,
+ MANAGEMENT_STORE_HOSTS_NAMESPACE,
MANAGEMENT_STORE_GLOBAL_NAMESPACE,
} from '../../../common/constants';
import { State } from '../../../../common/store';
export function useHostSelector(selector: (state: HostState) => TSelected) {
return useSelector(function (state: State) {
return selector(
- state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE] as HostState
+ state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_HOSTS_NAMESPACE] as HostState
);
});
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
index 9766cd6abd2b1..996b987ea2be3 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
@@ -44,7 +44,7 @@ describe('when on the hosts page', () => {
it('should show the empty state when there are no hosts or polices', async () => {
const renderResult = render();
- // Initially, there are no endpoints or policies, so we prompt to add policies first.
+ // Initially, there are no hosts or policies, so we prompt to add policies first.
const table = await renderResult.findByTestId('emptyPolicyTable');
expect(table).not.toBeNull();
});
@@ -79,8 +79,8 @@ describe('when on the hosts page', () => {
it('should show the no hosts empty state', async () => {
const renderResult = render();
- const emptyEndpointsTable = await renderResult.findByTestId('emptyEndpointsTable');
- expect(emptyEndpointsTable).not.toBeNull();
+ const emptyHostsTable = await renderResult.findByTestId('emptyHostsTable');
+ expect(emptyHostsTable).not.toBeNull();
});
it('should display the onboarding steps', async () => {
@@ -335,7 +335,7 @@ describe('when on the hosts page', () => {
const policyStatusLink = await renderResult.findByTestId('policyStatusValue');
expect(policyStatusLink).not.toBeNull();
expect(policyStatusLink.getAttribute('href')).toEqual(
- '/endpoints?page_index=0&page_size=10&selected_host=1&show=policy_response'
+ '/hosts?page_index=0&page_size=10&selected_host=1&show=policy_response'
);
});
@@ -549,7 +549,7 @@ describe('when on the hosts page', () => {
const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton');
expect(subHeaderBackLink.textContent).toBe('Endpoint Details');
expect(subHeaderBackLink.getAttribute('href')).toBe(
- '/endpoints?page_index=0&page_size=10&selected_host=1'
+ '/hosts?page_index=0&page_size=10&selected_host=1'
);
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
index d49335ca8de2c..492c75607a255 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
@@ -10,6 +10,8 @@ import {
EuiBasicTable,
EuiBasicTableColumn,
EuiText,
+ EuiTitle,
+ EuiSpacer,
EuiLink,
EuiHealth,
EuiToolTip,
@@ -33,7 +35,7 @@ import { CreateStructuredSelector } from '../../../../common/store';
import { Immutable, HostInfo } from '../../../../../common/endpoint/types';
import { SpyRoute } from '../../../../common/utils/route/spy_routes';
import { ManagementPageView } from '../../../components/management_page_view';
-import { PolicyEmptyState, EndpointsEmptyState } from '../../../components/management_empty_state';
+import { PolicyEmptyState, HostsEmptyState } from '../../../components/management_empty_state';
import { FormattedDate } from '../../../../common/components/formatted_date';
import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
import {
@@ -41,11 +43,7 @@ import {
AgentConfigDetailsDeployAgentAction,
} from '../../../../../../ingest_manager/public';
import { SecurityPageName } from '../../../../app/types';
-import {
- getEndpointListPath,
- getEndpointDetailsPath,
- getPolicyDetailPath,
-} from '../../../common/routing';
+import { getHostListPath, getHostDetailsPath, getPolicyDetailPath } from '../../../common/routing';
import { useFormatUrl } from '../../../../common/components/link_to';
import { HostAction } from '../store/action';
@@ -107,8 +105,8 @@ export const HostList = () => {
const { index, size } = page;
// FIXME: PT: if host details is open, table is not displaying correct number of rows
history.push(
- getEndpointListPath({
- name: 'endpointList',
+ getHostListPath({
+ name: 'hostList',
...queryParams,
page_index: JSON.stringify(index),
page_size: JSON.stringify(size),
@@ -127,12 +125,12 @@ export const HostList = () => {
state: {
onCancelNavigateTo: [
'securitySolution:management',
- { path: getEndpointListPath({ name: 'endpointList' }) },
+ { path: getHostListPath({ name: 'hostList' }) },
],
- onCancelUrl: formatUrl(getEndpointListPath({ name: 'endpointList' })),
+ onCancelUrl: formatUrl(getHostListPath({ name: 'hostList' })),
onSaveNavigateTo: [
'securitySolution:management',
- { path: getEndpointListPath({ name: 'endpointList' }) },
+ { path: getHostListPath({ name: 'hostList' }) },
],
},
}
@@ -145,7 +143,7 @@ export const HostList = () => {
state: {
onDoneNavigateTo: [
'securitySolution:management',
- { path: getEndpointListPath({ name: 'endpointList' }) },
+ { path: getHostListPath({ name: 'hostList' }) },
],
},
});
@@ -191,10 +189,10 @@ export const HostList = () => {
defaultMessage: 'Hostname',
}),
render: ({ hostname, id }: HostInfo['metadata']['host']) => {
- const toRoutePath = getEndpointDetailsPath(
+ const toRoutePath = getHostDetailsPath(
{
...queryParams,
- name: 'endpointDetails',
+ name: 'hostDetails',
selected_host: id,
},
search
@@ -259,8 +257,8 @@ export const HostList = () => {
}),
// eslint-disable-next-line react/display-name
render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => {
- const toRoutePath = getEndpointDetailsPath({
- name: 'endpointPolicyResponse',
+ const toRoutePath = getHostDetailsPath({
+ name: 'hostPolicyResponse',
selected_host: item.metadata.host.id,
});
const toRouteUrl = formatUrl(toRoutePath);
@@ -341,7 +339,7 @@ export const HostList = () => {
);
} else if (!policyItemsLoading && policyItems && policyItems.length > 0) {
return (
- {
+
+
+
+
+
+
+
+
+
+
+
+ >
+ }
>
{hasSelectedHost && }
{listData && listData.length > 0 && (
diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx
index 0e81b75d651ba..2cf07b9b4382e 100644
--- a/x-pack/plugins/security_solution/public/management/pages/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx
@@ -9,25 +9,25 @@ import { useHistory, Route, Switch } from 'react-router-dom';
import { PolicyContainer } from './policy';
import {
- MANAGEMENT_ROUTING_ENDPOINTS_PATH,
+ MANAGEMENT_ROUTING_HOSTS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_ROOT_PATH,
} from '../common/constants';
import { NotFoundPage } from '../../app/404';
-import { EndpointsContainer } from './endpoint_hosts';
-import { getEndpointListPath } from '../common/routing';
+import { HostsContainer } from './endpoint_hosts';
+import { getHostListPath } from '../common/routing';
export const ManagementContainer = memo(() => {
const history = useHistory();
return (
-
+
{
- history.replace(getEndpointListPath({ name: 'endpointList' }));
+ history.replace(getHostListPath({ name: 'hostList' }));
return null;
}}
/>
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
index 0bd623b27f4fb..102fd40c97672 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
@@ -43,8 +43,8 @@ describe('policy details: ', () => {
config: {
artifact_manifest: {
value: {
- manifest_version: 'v0',
- schema_version: '1.0.0',
+ manifest_version: 'WzAsMF0=',
+ schema_version: 'v1',
artifacts: {},
},
},
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
index 447a70ef998a9..fc120d9782e67 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
@@ -8,6 +8,8 @@ import React, { useCallback, useEffect, useMemo, CSSProperties, useState } from
import {
EuiBasicTable,
EuiText,
+ EuiTitle,
+ EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiTableFieldDataColumnType,
@@ -20,8 +22,8 @@ import {
EuiOverlayMask,
EuiConfirmModal,
EuiCallOut,
- EuiSpacer,
EuiButton,
+ EuiHorizontalRule,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -391,9 +393,27 @@ export const PolicyList = React.memo(() => {
+
+
+
+
+
+
+
+
+
+
+
+ >
+ }
headerRight={
{
/>
}
- bodyHeader={
- policyItems &&
- policyItems.length > 0 && (
-
+ >
+ {policyItems && policyItems.length > 0 && (
+ <>
+
- )
- }
- >
+
+ >
+ )}
{useMemo(() => {
return (
<>
diff --git a/x-pack/plugins/security_solution/public/management/store/middleware.ts b/x-pack/plugins/security_solution/public/management/store/middleware.ts
index 9ca170cce8b3d..a29da9bef5875 100644
--- a/x-pack/plugins/security_solution/public/management/store/middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/store/middleware.ts
@@ -12,7 +12,7 @@ import {
import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list';
import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details';
import {
- MANAGEMENT_STORE_ENDPOINTS_NAMESPACE,
+ MANAGEMENT_STORE_HOSTS_NAMESPACE,
MANAGEMENT_STORE_GLOBAL_NAMESPACE,
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_POLICY_LIST_NAMESPACE,
@@ -24,7 +24,7 @@ const policyListSelector = (state: State) =>
const policyDetailsSelector = (state: State) =>
state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE];
const endpointsSelector = (state: State) =>
- state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE];
+ state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_HOSTS_NAMESPACE];
export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = (
coreStart,
diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts
index 2ed3dfe86d2f8..f3c470fb1e8a3 100644
--- a/x-pack/plugins/security_solution/public/management/store/reducer.ts
+++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts
@@ -14,7 +14,7 @@ import {
initialPolicyListState,
} from '../pages/policy/store/policy_list/reducer';
import {
- MANAGEMENT_STORE_ENDPOINTS_NAMESPACE,
+ MANAGEMENT_STORE_HOSTS_NAMESPACE,
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_POLICY_LIST_NAMESPACE,
} from '../common/constants';
@@ -31,7 +31,7 @@ const immutableCombineReducers: ImmutableCombineReducers = combineReducers;
export const mockManagementState: Immutable = {
[MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(),
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(),
- [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialHostListState,
+ [MANAGEMENT_STORE_HOSTS_NAMESPACE]: initialHostListState,
};
/**
@@ -40,5 +40,5 @@ export const mockManagementState: Immutable = {
export const managementReducer = immutableCombineReducers({
[MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer,
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer,
- [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: hostListReducer,
+ [MANAGEMENT_STORE_HOSTS_NAMESPACE]: hostListReducer,
});
diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts
index 854e9faa0204d..cb21a236ddd7e 100644
--- a/x-pack/plugins/security_solution/public/management/types.ts
+++ b/x-pack/plugins/security_solution/public/management/types.ts
@@ -18,14 +18,14 @@ export type ManagementStoreGlobalNamespace = 'management';
export type ManagementState = CombinedState<{
policyList: PolicyListState;
policyDetails: PolicyDetailsState;
- endpoints: HostState;
+ hosts: HostState;
}>;
/**
* The management list of sub-tabs. Changes to these will impact the Router routes.
*/
export enum ManagementSubTab {
- endpoints = 'endpoints',
+ hosts = 'hosts',
policies = 'policy',
}
diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx
index 1aa114608b479..d2d9861e0ae1a 100644
--- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx
@@ -62,7 +62,7 @@ describe('Alerts by category', () => {
test('it renders the expected title', () => {
expect(wrapper.find('[data-test-subj="header-section-title"]').text()).toEqual(
- 'External alert count'
+ 'External alert trend'
);
});
diff --git a/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx
index ee048f0d61212..3758bd10bfc8f 100644
--- a/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx
@@ -7,13 +7,13 @@
import React, { memo } from 'react';
import { EuiCallOut, EuiButton, EuiButtonEmpty } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { getEndpointListPath } from '../../../management/common/routing';
+import { getHostListPath } from '../../../management/common/routing';
import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
import { useManagementFormatUrl } from '../../../management/components/hooks/use_management_format_url';
import { MANAGEMENT_APP_ID } from '../../../management/common/constants';
export const EndpointNotice = memo<{ onDismiss: () => void }>(({ onDismiss }) => {
- const endpointsPath = getEndpointListPath({ name: 'endpointList' });
+ const endpointsPath = getHostListPath({ name: 'hostList' });
const endpointsLink = useManagementFormatUrl(endpointsPath);
const handleGetStartedClick = useNavigateToAppEventHandler(MANAGEMENT_APP_ID, {
path: endpointsPath,
diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx
index 8f2b3c7495f0d..4f9784b1f84bf 100644
--- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx
@@ -45,7 +45,7 @@ const StatefulRecentTimelinesComponent = React.memo(
const { formatUrl } = useFormatUrl(SecurityPageName.timelines);
const { navigateToApp } = useKibana().services.application;
const onOpenTimeline: OnOpenTimeline = useCallback(
- ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => {
+ ({ duplicate, timelineId }) => {
queryTimelineById({
apolloClient,
duplicate,
diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx
index d91c2be214e8b..ddad72081645b 100644
--- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx
@@ -20,6 +20,7 @@ import {
OpenTimelineResult,
} from '../../../timelines/components/open_timeline/types';
import { WithHoverActions } from '../../../common/components/with_hover_actions';
+import { TimelineType } from '../../../../common/types/timeline';
import { RecentTimelineCounts } from './counts';
import * as i18n from './translations';
@@ -58,9 +59,19 @@ export const RecentTimelines = React.memo<{
{showHoverContent && (
-
+
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index 18072c25e6dde..6096a9b0e0bb8 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -34,7 +34,7 @@ import {
import {
APP_ID,
APP_ICON,
- APP_ALERTS_PATH,
+ APP_DETECTIONS_PATH,
APP_HOSTS_PATH,
APP_OVERVIEW_PATH,
APP_NETWORK_PATH,
@@ -48,6 +48,15 @@ import { ConfigureEndpointPackageConfig } from './management/pages/policy/view/i
import { State, createStore, createInitialState } from './common/store';
import { SecurityPageName } from './app/types';
import { manageOldSiemRoutes } from './helpers';
+import {
+ OVERVIEW,
+ HOSTS,
+ NETWORK,
+ TIMELINES,
+ DETECTION_ENGINE,
+ CASE,
+ ADMINISTRATION,
+} from './app/home/translations';
export class Plugin implements IPlugin {
private kibanaVersion: string;
@@ -95,10 +104,12 @@ export class Plugin implements IPlugin {
+ mount: async () => {
const [{ application }] = await core.getStartServices();
application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { replace: true });
return () => true;
@@ -107,9 +118,7 @@ export class Plugin implements IPlugin {
const [
{ coreStart, store, services, storage },
{ renderApp, composeLibs },
- { alertsSubPlugin },
+ { detectionsSubPlugin },
] = await Promise.all([
mountSecurityFactory(),
this.downloadAssets(),
@@ -159,14 +166,14 @@ export class Plugin implements IPlugin = (
activeDescendantId: null,
selectedDescendantId: null,
processEntityIdOfSelectedDescendant: null,
- panelToDisplay: null,
},
action
) => {
@@ -39,11 +38,6 @@ const uiReducer: Reducer = (
selectedDescendantId: action.payload.nodeId,
processEntityIdOfSelectedDescendant: action.payload.selectedProcessId,
};
- } else if (action.type === 'appDisplayedDifferentPanel') {
- return {
- ...uiState,
- panelToDisplay: action.payload,
- };
} else if (
action.type === 'userBroughtProcessIntoView' ||
action.type === 'appDetectedNewIdFromQueryParams'
diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
index e54193ab394a5..2bc254d118d33 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
@@ -127,11 +127,6 @@ export const uiSelectedDescendantProcessId = composeSelectors(
uiSelectors.selectedDescendantProcessId
);
-/**
- * The current panel to display
- */
-export const currentPanelView = composeSelectors(uiStateSelector, uiSelectors.currentPanelView);
-
/**
* Returns the camera state from within ResolverState
*/
diff --git a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts
index bddc7d34abf1c..494d8884329c6 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts
@@ -39,8 +39,3 @@ export const selectedDescendantProcessId = createSelector(
return processEntityIdOfSelectedDescendant;
}
);
-
-// Select the current panel to be displayed
-export const currentPanelView = (uiState: ResolverUIState) => {
- return uiState.panelToDisplay;
-};
diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts
index 5dd9a944b88ea..2025762a0605c 100644
--- a/x-pack/plugins/security_solution/public/resolver/types.ts
+++ b/x-pack/plugins/security_solution/public/resolver/types.ts
@@ -45,10 +45,6 @@ export interface ResolverUIState {
* The entity_id of the process for the resolver's currently selected descendant.
*/
readonly processEntityIdOfSelectedDescendant: string | null;
- /**
- * Which panel the ui should display
- */
- readonly panelToDisplay: string | null;
}
/**
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx
index 2a2e7e87394a9..f4fe4fe520c92 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx
@@ -4,17 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, {
- memo,
- useCallback,
- useMemo,
- useContext,
- useLayoutEffect,
- useState,
- useEffect,
-} from 'react';
+import React, { memo, useCallback, useMemo, useContext, useLayoutEffect, useState } from 'react';
import { useSelector } from 'react-redux';
-import { useHistory } from 'react-router-dom';
+import { useHistory, useLocation } from 'react-router-dom';
// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';
import { EuiPanel } from '@elastic/eui';
@@ -48,7 +40,7 @@ import { CrumbInfo } from './panels/panel_content_utilities';
*/
const PanelContent = memo(function PanelContent() {
const history = useHistory();
- const urlSearch = history.location.search;
+ const urlSearch = useLocation().search;
const dispatch = useResolverDispatch();
const { timestamp } = useContext(SideEffectContext);
@@ -205,21 +197,12 @@ const PanelContent = memo(function PanelContent() {
return 'processListWithCounts';
}, [uiSelectedEvent, crumbEvent, crumbId, graphableProcessEntityIds]);
- useEffect(() => {
- // dispatch `appDisplayedDifferentPanel` to sync state with which panel gets displayed
- dispatch({
- type: 'appDisplayedDifferentPanel',
- payload: panelToShow,
- });
- }, [panelToShow, dispatch]);
-
- const currentPanelView = useSelector(selectors.currentPanelView);
const terminatedProcesses = useSelector(selectors.terminatedProcesses);
const processEntityId = uiSelectedEvent ? event.entityId(uiSelectedEvent) : undefined;
const isProcessTerminated = processEntityId ? terminatedProcesses.has(processEntityId) : false;
const panelInstance = useMemo(() => {
- if (currentPanelView === 'processDetails') {
+ if (panelToShow === 'processDetails') {
return (
sum + val, 0);
@@ -278,7 +261,7 @@ const PanelContent = memo(function PanelContent() {
crumbId,
pushToQueryParams,
relatedStatsForIdFromParams,
- currentPanelView,
+ panelToShow,
isProcessTerminated,
]);
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
index 56f88ccb13115..517b847855647 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
@@ -82,10 +82,13 @@ const invalidDateText = i18n.translate(
}
);
/**
- * @param {ConstructorParameters[0]} timestamp To be passed through Date->Intl.DateTimeFormat
* @returns {string} A nicely formatted string for a date
*/
-export function formatDate(timestamp: ConstructorParameters[0]) {
+export function formatDate(
+ /** To be passed through Date->Intl.DateTimeFormat */ timestamp: ConstructorParameters<
+ typeof Date
+ >[0]
+): string {
const date = new Date(timestamp);
if (isFinite(date.getTime())) {
return formatter.format(date);
diff --git a/x-pack/plugins/security_solution/public/sub_plugins.ts b/x-pack/plugins/security_solution/public/sub_plugins.ts
index d47aae680aa35..5e7c5e8242fde 100644
--- a/x-pack/plugins/security_solution/public/sub_plugins.ts
+++ b/x-pack/plugins/security_solution/public/sub_plugins.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Alerts } from './alerts';
+import { Detections } from './detections';
import { Cases } from './cases';
import { Hosts } from './hosts';
import { Network } from './network';
@@ -12,7 +12,7 @@ import { Overview } from './overview';
import { Timelines } from './timelines';
import { Management } from './management';
-const alertsSubPlugin = new Alerts();
+const detectionsSubPlugin = new Detections();
const casesSubPlugin = new Cases();
const hostsSubPlugin = new Hosts();
const networkSubPlugin = new Network();
@@ -21,7 +21,7 @@ const timelinesSubPlugin = new Timelines();
const managementSubPlugin = new Management();
export {
- alertsSubPlugin,
+ detectionsSubPlugin,
casesSubPlugin,
hostsSubPlugin,
networkSubPlugin,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/helpers.tsx
index f8d4a6eebcbff..195bb770312cb 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/helpers.tsx
@@ -31,22 +31,26 @@ export const operatorLabels: EuiComboBoxOptionOption[] = [
},
];
+export const EMPTY_ARRAY_RESULT = [];
+
/** Returns the names of fields in a category */
export const getFieldNames = (category: Partial): string[] =>
category.fields != null && Object.keys(category.fields).length > 0
? Object.keys(category.fields)
- : [];
+ : EMPTY_ARRAY_RESULT;
/** Returns all field names by category, for display in an `EuiComboBox` */
export const getCategorizedFieldNames = (browserFields: BrowserFields): EuiComboBoxOptionOption[] =>
- Object.keys(browserFields)
- .sort()
- .map((categoryId) => ({
- label: categoryId,
- options: getFieldNames(browserFields[categoryId]).map((fieldId) => ({
- label: fieldId,
- })),
- }));
+ !browserFields
+ ? EMPTY_ARRAY_RESULT
+ : Object.keys(browserFields)
+ .sort()
+ .map((categoryId) => ({
+ label: categoryId,
+ options: getFieldNames(browserFields[categoryId]).map((fieldId) => ({
+ label: fieldId,
+ })),
+ }));
/** Returns true if the specified field name is valid */
export const selectionsAreValid = ({
@@ -61,7 +65,7 @@ export const selectionsAreValid = ({
const fieldId = selectedField.length > 0 ? selectedField[0].label : '';
const operator = selectedOperator.length > 0 ? selectedOperator[0].label : '';
- const fieldIsValid = getAllFieldsByName(browserFields)[fieldId] != null;
+ const fieldIsValid = browserFields && getAllFieldsByName(browserFields)[fieldId] != null;
const operatorIsValid = findIndex((o) => o.label === operator, operatorLabels) !== -1;
return fieldIsValid && operatorIsValid;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.test.tsx
index 2160a05cb9da5..5d01995ac6380 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.test.tsx
@@ -9,7 +9,11 @@ import React from 'react';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
-import { IS_OPERATOR, EXISTS_OPERATOR } from '../timeline/data_providers/data_provider';
+import {
+ DataProviderType,
+ IS_OPERATOR,
+ EXISTS_OPERATOR,
+} from '../timeline/data_providers/data_provider';
import { StatefulEditDataProvider } from '.';
@@ -266,6 +270,27 @@ describe('StatefulEditDataProvider', () => {
expect(wrapper.find('[data-test-subj="value"]').exists()).toBe(false);
});
+ test('it does NOT render value when is template field', () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="value"]').exists()).toBe(false);
+ });
+
test('it does NOT disable the save button when field is valid', () => {
const wrapper = mount(
@@ -361,6 +386,7 @@ describe('StatefulEditDataProvider', () => {
field: 'client.address',
id: 'test',
operator: ':',
+ type: 'default',
providerId: 'hosts-table-hostName-test-host',
value: 'test-host',
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx
index 95f3ec3b31649..72386a2b287f1 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { noop } from 'lodash/fp';
+import { noop, startsWith, endsWith } from 'lodash/fp';
import {
EuiButton,
EuiComboBox,
@@ -17,12 +17,12 @@ import {
EuiSpacer,
EuiToolTip,
} from '@elastic/eui';
-import React, { useEffect, useState, useCallback } from 'react';
+import React, { useEffect, useMemo, useState, useCallback } from 'react';
import styled from 'styled-components';
import { BrowserFields } from '../../../common/containers/source';
import { OnDataProviderEdited } from '../timeline/events';
-import { QueryOperator } from '../timeline/data_providers/data_provider';
+import { DataProviderType, QueryOperator } from '../timeline/data_providers/data_provider';
import {
getCategorizedFieldNames,
@@ -56,6 +56,7 @@ interface Props {
providerId: string;
timelineId: string;
value: string | number;
+ type?: DataProviderType;
}
const sanatizeValue = (value: string | number): string =>
@@ -83,6 +84,7 @@ export const StatefulEditDataProvider = React.memo(
providerId,
timelineId,
value,
+ type = DataProviderType.default,
}) => {
const [updatedField, setUpdatedField] = useState([{ label: field }]);
const [updatedOperator, setUpdatedOperator] = useState(
@@ -105,11 +107,18 @@ export const StatefulEditDataProvider = React.memo(
}
};
- const onFieldSelected = useCallback((selectedField: EuiComboBoxOptionOption[]) => {
- setUpdatedField(selectedField);
+ const onFieldSelected = useCallback(
+ (selectedField: EuiComboBoxOptionOption[]) => {
+ setUpdatedField(selectedField);
- focusInput();
- }, []);
+ if (type === DataProviderType.template) {
+ setUpdatedValue(`{${selectedField[0].label}}`);
+ }
+
+ focusInput();
+ },
+ [type]
+ );
const onOperatorSelected = useCallback((operatorSelected: EuiComboBoxOptionOption[]) => {
setUpdatedOperator(operatorSelected);
@@ -139,6 +148,36 @@ export const StatefulEditDataProvider = React.memo(
window.onscroll = () => noop;
};
+ const handleSave = useCallback(() => {
+ onDataProviderEdited({
+ andProviderId,
+ excluded: getExcludedFromSelection(updatedOperator),
+ field: updatedField.length > 0 ? updatedField[0].label : '',
+ id: timelineId,
+ operator: getQueryOperatorFromSelection(updatedOperator),
+ providerId,
+ value: updatedValue,
+ type,
+ });
+ }, [
+ onDataProviderEdited,
+ andProviderId,
+ updatedOperator,
+ updatedField,
+ timelineId,
+ providerId,
+ updatedValue,
+ type,
+ ]);
+
+ const isValueFieldInvalid = useMemo(
+ () =>
+ type !== DataProviderType.template &&
+ (startsWith('{', sanatizeValue(updatedValue)) ||
+ endsWith('}', sanatizeValue(updatedValue))),
+ [type, updatedValue]
+ );
+
useEffect(() => {
disableScrolling();
focusInput();
@@ -190,7 +229,8 @@ export const StatefulEditDataProvider = React.memo(
- {updatedOperator.length > 0 &&
+ {type !== DataProviderType.template &&
+ updatedOperator.length > 0 &&
updatedOperator[0].label !== i18n.EXISTS &&
updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? (
@@ -201,6 +241,7 @@ export const StatefulEditDataProvider = React.memo(
onChange={onValueChange}
placeholder={i18n.VALUE}
value={sanatizeValue(updatedValue)}
+ isInvalid={isValueFieldInvalid}
/>
@@ -224,19 +265,9 @@ export const StatefulEditDataProvider = React.memo(
browserFields,
selectedField: updatedField,
selectedOperator: updatedOperator,
- })
+ }) || isValueFieldInvalid
}
- onClick={() => {
- onDataProviderEdited({
- andProviderId,
- excluded: getExcludedFromSelection(updatedOperator),
- field: updatedField.length > 0 ? updatedField[0].label : '',
- id: timelineId,
- operator: getQueryOperatorFromSelection(updatedOperator),
- providerId,
- value: updatedValue,
- });
- }}
+ onClick={handleSave}
size="s"
>
{i18n.SAVE}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx
index a1392ad8b8270..5896a02b82023 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx
@@ -124,12 +124,13 @@ export const FlyoutButton = React.memo(
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
index 8e34e11e85729..10f20eeacbcb0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
@@ -9,10 +9,10 @@ import { connect, ConnectedProps } from 'react-redux';
import { Dispatch } from 'redux';
import { isEmpty, get } from 'lodash/fp';
+import { TimelineType } from '../../../../../common/types/timeline';
import { History } from '../../../../common/lib/history';
import { Note } from '../../../../common/lib/note';
import { appSelectors, inputsModel, inputsSelectors, State } from '../../../../common/store';
-import { defaultHeaders } from '../../timeline/body/column_headers/default_headers';
import { Properties } from '../../timeline/properties';
import { appActions } from '../../../../common/store/app';
import { inputsActions } from '../../../../common/store/inputs';
@@ -31,7 +31,6 @@ type Props = OwnProps & PropsFromRedux;
const StatefulFlyoutHeader = React.memo(
({
associateNote,
- createTimeline,
description,
graphEventId,
isDataInTimeline,
@@ -57,7 +56,6 @@ const StatefulFlyoutHeader = React.memo(
return (
{
title = '',
noteIds = emptyNotesId,
status,
- timelineType,
+ timelineType = TimelineType.default,
} = timeline;
const history = emptyHistory; // TODO: get history from store via selector
@@ -127,14 +125,6 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({
associateNote: (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })),
- createTimeline: ({ id, show }: { id: string; show?: boolean }) =>
- dispatch(
- timelineActions.createTimeline({
- id,
- columns: defaultHeaders,
- show,
- })
- ),
updateDescription: ({ id, description }: { id: string; description: string }) =>
dispatch(timelineActions.updateDescription({ id, description })),
updateIsFavorite: ({ id, isFavorite }: { id: string; isFavorite: boolean }) =>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx
index 15c078e175355..27fda48b69598 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx
@@ -7,7 +7,7 @@
import { EuiContextMenuPanel, EuiContextMenuItem, EuiBasicTable } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
-import { TimelineStatus } from '../../../../common/types/timeline';
+import { TimelineType, TimelineStatus } from '../../../../common/types/timeline';
import * as i18n from './translations';
import { DeleteTimelines, OpenTimelineResult } from './types';
@@ -26,10 +26,12 @@ export const useEditTimelineBatchActions = ({
deleteTimelines,
selectedItems,
tableRef,
+ timelineType = TimelineType.default,
}: {
deleteTimelines?: DeleteTimelines;
selectedItems?: OpenTimelineResult[];
tableRef: React.MutableRefObject | undefined>;
+ timelineType: TimelineType | null;
}) => {
const {
enableExportTimelineDownloader,
@@ -49,8 +51,7 @@ export const useEditTimelineBatchActions = ({
disableExportTimelineDownloader();
onCloseDeleteTimelineModal();
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [disableExportTimelineDownloader, onCloseDeleteTimelineModal, tableRef.current]
+ [disableExportTimelineDownloader, onCloseDeleteTimelineModal, tableRef]
);
const selectedIds = useMemo(() => getExportedIds(selectedItems ?? []), [selectedItems]);
@@ -76,7 +77,9 @@ export const useEditTimelineBatchActions = ({
onComplete={onCompleteBatchActions.bind(null, closePopover)}
title={
selectedItems?.length !== 1
- ? i18n.SELECTED_TIMELINES(selectedItems?.length ?? 0)
+ ? timelineType === TimelineType.template
+ ? i18n.SELECTED_TEMPLATES(selectedItems?.length ?? 0)
+ : i18n.SELECTED_TIMELINES(selectedItems?.length ?? 0)
: selectedItems[0]?.title ?? ''
}
/>
@@ -106,14 +109,15 @@ export const useEditTimelineBatchActions = ({
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
+ selectedItems,
deleteTimelines,
+ selectedIds,
isEnableDownloader,
isDeleteTimelineModalOpen,
- selectedIds,
- selectedItems,
+ onCompleteBatchActions,
+ timelineType,
handleEnableExportTimelineDownloader,
handleOnOpenDeleteTimelineModal,
- onCompleteBatchActions,
]
);
return { onCompleteBatchActions, getBatchItemsPopoverContent };
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
index e841718c8119b..03a6d475b3426 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
@@ -4,11 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
+/* eslint-disable complexity */
+
import ApolloClient from 'apollo-client';
import { getOr, set, isEmpty } from 'lodash/fp';
import { Action } from 'typescript-fsa';
import uuid from 'uuid';
import { Dispatch } from 'redux';
+import deepMerge from 'deepmerge';
import { oneTimelineQuery } from '../../containers/one/index.gql_query';
import {
TimelineResult,
@@ -17,9 +20,10 @@ import {
FilterTimelineResult,
ColumnHeaderResult,
PinnedEvent,
+ DataProviderResult,
} from '../../../graphql/types';
-import { TimelineStatus, TimelineType } from '../../../../common/types/timeline';
+import { DataProviderType, TimelineStatus, TimelineType } from '../../../../common/types/timeline';
import {
addNotes as dispatchAddNotes,
@@ -47,6 +51,7 @@ import {
import { OpenTimelineResult, UpdateTimeline, DispatchUpdateTimeline } from './types';
import { getTimeRangeSettings } from '../../../common/utils/default_date_settings';
import { createNote } from '../notes/helpers';
+import { IS_OPERATOR } from '../timeline/data_providers/data_provider';
export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline';
@@ -162,15 +167,61 @@ const setPinnedEventIds = (duplicate: boolean, pinnedEventIds: string[] | null |
? pinnedEventIds.reduce((acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), {})
: {};
+const getTemplateTimelineId = (
+ timeline: TimelineResult,
+ duplicate: boolean,
+ targetTimelineType?: TimelineType
+) => {
+ if (!duplicate) {
+ return timeline.templateTimelineId;
+ }
+
+ if (
+ targetTimelineType === TimelineType.default &&
+ timeline.timelineType === TimelineType.template
+ ) {
+ return timeline.templateTimelineId;
+ }
+
+ // TODO: MOVE TO BACKEND
+ return uuid.v4();
+};
+
+const convertToDefaultField = ({ and, ...dataProvider }: DataProviderResult) =>
+ deepMerge(dataProvider, {
+ type: DataProviderType.default,
+ queryMatch: {
+ value:
+ dataProvider.queryMatch!.operator === IS_OPERATOR ? '' : dataProvider.queryMatch!.value,
+ },
+ });
+
+const getDataProviders = (
+ duplicate: boolean,
+ dataProviders: TimelineResult['dataProviders'],
+ timelineType?: TimelineType
+) => {
+ if (duplicate && dataProviders && timelineType === TimelineType.default) {
+ return dataProviders.map((dataProvider) => ({
+ ...convertToDefaultField(dataProvider),
+ and: dataProvider.and?.map(convertToDefaultField) ?? [],
+ }));
+ }
+
+ return dataProviders;
+};
+
// eslint-disable-next-line complexity
export const defaultTimelineToTimelineModel = (
timeline: TimelineResult,
- duplicate: boolean
+ duplicate: boolean,
+ timelineType?: TimelineType
): TimelineModel => {
const isTemplate = timeline.timelineType === TimelineType.template;
const timelineEntries = {
...timeline,
columns: timeline.columns != null ? timeline.columns.map(setTimelineColumn) : defaultHeaders,
+ dataProviders: getDataProviders(duplicate, timeline.dataProviders, timelineType),
eventIdToNoteIds: setEventIdToNoteIds(duplicate, timeline.eventIdToNoteIds),
filters: timeline.filters != null ? timeline.filters.map(setTimelineFilters) : [],
isFavorite: duplicate
@@ -185,8 +236,9 @@ export const defaultTimelineToTimelineModel = (
status: duplicate ? TimelineStatus.active : timeline.status,
savedObjectId: duplicate ? null : timeline.savedObjectId,
version: duplicate ? null : timeline.version,
+ timelineType: timelineType ?? timeline.timelineType,
title: duplicate ? `${timeline.title} - Duplicate` : timeline.title || '',
- templateTimelineId: duplicate && isTemplate ? uuid.v4() : timeline.templateTimelineId,
+ templateTimelineId: getTemplateTimelineId(timeline, duplicate, timelineType),
templateTimelineVersion: duplicate && isTemplate ? 1 : timeline.templateTimelineVersion,
};
return Object.entries(timelineEntries).reduce(
@@ -200,12 +252,13 @@ export const defaultTimelineToTimelineModel = (
export const formatTimelineResultToModel = (
timelineToOpen: TimelineResult,
- duplicate: boolean = false
+ duplicate: boolean = false,
+ timelineType?: TimelineType
): { notes: NoteResult[] | null | undefined; timeline: TimelineModel } => {
const { notes, ...timelineModel } = timelineToOpen;
return {
notes,
- timeline: defaultTimelineToTimelineModel(timelineModel, duplicate),
+ timeline: defaultTimelineToTimelineModel(timelineModel, duplicate, timelineType),
};
};
@@ -214,6 +267,7 @@ export interface QueryTimelineById {
duplicate?: boolean;
graphEventId?: string;
timelineId: string;
+ timelineType?: TimelineType;
onOpenTimeline?: (timeline: TimelineModel) => void;
openTimeline?: boolean;
updateIsLoading: ({
@@ -231,6 +285,7 @@ export const queryTimelineById = ({
duplicate = false,
graphEventId = '',
timelineId,
+ timelineType,
onOpenTimeline,
openTimeline = true,
updateIsLoading,
@@ -250,7 +305,11 @@ export const queryTimelineById = ({
getOr({}, 'data.getOneTimeline', result)
);
- const { timeline, notes } = formatTimelineResultToModel(timelineToOpen, duplicate);
+ const { timeline, notes } = formatTimelineResultToModel(
+ timelineToOpen,
+ duplicate,
+ timelineType
+ );
if (onOpenTimeline != null) {
onOpenTimeline(timeline);
} else if (updateTimeline) {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx
index ea63f2b7b0710..6d332c79f77cd 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx
@@ -7,11 +7,8 @@
import ApolloClient from 'apollo-client';
import React, { useEffect, useState, useCallback } from 'react';
import { connect, ConnectedProps } from 'react-redux';
-
import { Dispatch } from 'redux';
-import { disableTemplate } from '../../../../common/constants';
-
import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../../graphql/types';
import { State } from '../../../common/store';
import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model';
@@ -267,7 +264,7 @@ export const StatefulOpenTimelineComponent = React.memo(
}, []);
const openTimeline: OnOpenTimeline = useCallback(
- ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => {
+ ({ duplicate, timelineId, timelineType: timelineTypeToOpen }) => {
if (isModal && closeModalTimeline != null) {
closeModalTimeline();
}
@@ -277,6 +274,7 @@ export const StatefulOpenTimelineComponent = React.memo(
duplicate,
onOpenTimeline,
timelineId,
+ timelineType: timelineTypeToOpen,
updateIsLoading,
updateTimeline,
});
@@ -318,9 +316,9 @@ export const StatefulOpenTimelineComponent = React.memo(
selectedItems={selectedItems}
sortDirection={sortDirection}
sortField={sortField}
- templateTimelineFilter={!disableTemplate ? templateTimelineFilter : null}
+ templateTimelineFilter={templateTimelineFilter}
timelineType={timelineType}
- timelineFilter={!disableTemplate ? timelineTabs : null}
+ timelineFilter={timelineTabs}
title={title}
totalSearchResultsCount={totalCount}
/>
@@ -348,9 +346,9 @@ export const StatefulOpenTimelineComponent = React.memo(
selectedItems={selectedItems}
sortDirection={sortDirection}
sortField={sortField}
- templateTimelineFilter={!disableTemplate ? templateTimelineFilter : null}
+ templateTimelineFilter={templateTimelineFilter}
timelineType={timelineType}
- timelineFilter={!disableTemplate ? timelineFilters : null}
+ timelineFilter={timelineFilters}
title={title}
totalSearchResultsCount={totalCount}
/>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx
index 849143894efe0..60b009f59c13b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx
@@ -8,6 +8,7 @@ import { EuiPanel, EuiBasicTable, EuiCallOut, EuiSpacer } from '@elastic/eui';
import React, { useCallback, useMemo, useRef } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
+import { TimelineType } from '../../../../common/types/timeline';
import { ImportDataModal } from '../../../common/components/import_data_modal';
import {
UtilityBarGroup,
@@ -36,7 +37,6 @@ export const OpenTimeline = React.memo(
isLoading,
itemIdToExpandedNotesRowMap,
importDataModalToggle,
- onAddTimelinesToFavorites,
onDeleteSelected,
onlyFavorites,
onOpenTimeline,
@@ -54,7 +54,7 @@ export const OpenTimeline = React.memo(
sortDirection,
setImportDataModalToggle,
sortField,
- timelineType,
+ timelineType = TimelineType.default,
timelineFilter,
templateTimelineFilter,
totalSearchResultsCount,
@@ -73,8 +73,27 @@ export const OpenTimeline = React.memo(
deleteTimelines,
selectedItems,
tableRef,
+ timelineType,
});
+ const nTemplates = useMemo(
+ () => (
+
+ {query.trim().length ? `${i18n.WITH} "${query.trim()}"` : ''}
+
+ ),
+ }}
+ />
+ ),
+ [totalSearchResultsCount, query]
+ );
+
const nTimelines = useMemo(
() => (
(
}
}, [setImportDataModalToggle, refetch, searchResults, totalSearchResultsCount]);
- const actionTimelineToShow = useMemo(
- () =>
- onDeleteSelected != null && deleteTimelines != null
- ? ['delete', 'duplicate', 'export', 'selectable']
- : ['duplicate', 'export', 'selectable'],
- [onDeleteSelected, deleteTimelines]
- );
+ const actionTimelineToShow = useMemo(() => {
+ const timelineActions: ActionTimelineToShow[] = [
+ 'createFrom',
+ 'duplicate',
+ 'export',
+ 'selectable',
+ ];
+
+ if (onDeleteSelected != null && deleteTimelines != null) {
+ timelineActions.push('delete');
+ }
+
+ return timelineActions;
+ }, [onDeleteSelected, deleteTimelines]);
const SearchRowContent = useMemo(() => <>{templateTimelineFilter}>, [templateTimelineFilter]);
@@ -167,7 +193,7 @@ export const OpenTimeline = React.memo(
onQueryChange={onQueryChange}
onToggleOnlyFavorites={onToggleOnlyFavorites}
query={query}
- totalSearchResultsCount={totalSearchResultsCount}
+ timelineType={timelineType}
>
{SearchRowContent}
@@ -177,13 +203,18 @@ export const OpenTimeline = React.memo(
<>
- {i18n.SHOWING} {nTimelines}
+ {i18n.SHOWING}{' '}
+ {timelineType === TimelineType.template ? nTemplates : nTimelines}
>
- {i18n.SELECTED_TIMELINES(selectedItems.length)}
+
+ {timelineType === TimelineType.template
+ ? i18n.SELECTED_TEMPLATES(selectedItems.length)
+ : i18n.SELECTED_TIMELINES(selectedItems.length)}
+
(
totalSearchResultsCount,
}) => {
const actionsToShow = useMemo(() => {
- const actions: ActionTimelineToShow[] =
- onDeleteSelected != null && deleteTimelines != null
- ? ['delete', 'duplicate']
- : ['duplicate'];
+ const actions: ActionTimelineToShow[] = ['createFrom', 'duplicate'];
+
+ if (onDeleteSelected != null && deleteTimelines != null) {
+ actions.push('delete');
+ }
+
return actions.filter((action) => !hideActions.includes(action));
}, [onDeleteSelected, deleteTimelines, hideActions]);
@@ -84,8 +86,8 @@ export const OpenTimelineModalBody = memo(
onlyFavorites={onlyFavorites}
onQueryChange={onQueryChange}
onToggleOnlyFavorites={onToggleOnlyFavorites}
- query={query}
- totalSearchResultsCount={totalSearchResultsCount}
+ query=""
+ timelineType={timelineType}
>
{SearchRowContent}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx
index 77aa306157c92..2e6dcb85ad769 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx
@@ -10,6 +10,8 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers';
import React from 'react';
import { ThemeProvider } from 'styled-components';
+import { TimelineType } from '../../../../../common/types/timeline';
+
import { SearchRow } from '.';
import * as i18n from '../translations';
@@ -25,7 +27,7 @@ describe('SearchRow', () => {
onQueryChange={jest.fn()}
onToggleOnlyFavorites={jest.fn()}
query=""
- totalSearchResultsCount={0}
+ timelineType={TimelineType.default}
/>
);
@@ -45,7 +47,7 @@ describe('SearchRow', () => {
onQueryChange={jest.fn()}
onToggleOnlyFavorites={jest.fn()}
query=""
- totalSearchResultsCount={0}
+ timelineType={TimelineType.default}
/>
);
@@ -65,7 +67,7 @@ describe('SearchRow', () => {
onQueryChange={jest.fn()}
onToggleOnlyFavorites={onToggleOnlyFavorites}
query=""
- totalSearchResultsCount={0}
+ timelineType={TimelineType.default}
/>
);
@@ -83,7 +85,7 @@ describe('SearchRow', () => {
onQueryChange={jest.fn()}
onToggleOnlyFavorites={jest.fn()}
query=""
- totalSearchResultsCount={0}
+ timelineType={TimelineType.default}
/>
);
@@ -104,7 +106,7 @@ describe('SearchRow', () => {
onQueryChange={jest.fn()}
onToggleOnlyFavorites={jest.fn()}
query=""
- totalSearchResultsCount={0}
+ timelineType={TimelineType.default}
/>
);
@@ -129,7 +131,7 @@ describe('SearchRow', () => {
onQueryChange={onQueryChange}
onToggleOnlyFavorites={jest.fn()}
query=""
- totalSearchResultsCount={32}
+ timelineType={TimelineType.default}
/>
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx
index 6f9178664ccf0..5b927db3c37a9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx
@@ -12,9 +12,10 @@ import {
// @ts-ignore
EuiSearchBar,
} from '@elastic/eui';
-import React from 'react';
+import React, { useMemo } from 'react';
import styled from 'styled-components';
+import { TimelineType } from '../../../../../common/types/timeline';
import * as i18n from '../translations';
import { OpenTimelineProps } from '../types';
@@ -39,14 +40,9 @@ type Props = Pick<
| 'onQueryChange'
| 'onToggleOnlyFavorites'
| 'query'
- | 'totalSearchResultsCount'
+ | 'timelineType'
> & { children?: JSX.Element | null };
-const searchBox = {
- placeholder: i18n.SEARCH_PLACEHOLDER,
- incremental: false,
-};
-
/**
* Renders the row containing the search input and Only Favorites filter
*/
@@ -56,10 +52,20 @@ export const SearchRow = React.memo(
onlyFavorites,
onQueryChange,
onToggleOnlyFavorites,
- query,
- totalSearchResultsCount,
children,
+ timelineType,
}) => {
+ const searchBox = useMemo(
+ () => ({
+ placeholder:
+ timelineType === TimelineType.default
+ ? i18n.SEARCH_PLACEHOLDER
+ : i18n.SEARCH_TEMPLATE_PLACEHOLDER,
+ incremental: false,
+ }),
+ [timelineType]
+ );
+
return (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx
index 5b8eb8fd0365c..aa4bb3f1e0467 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx
@@ -16,7 +16,7 @@ import {
TimelineActionsOverflowColumns,
} from '../types';
import * as i18n from '../translations';
-import { TimelineStatus } from '../../../../../common/types/timeline';
+import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
/**
* Returns the action columns (e.g. delete, open duplicate timeline)
@@ -34,6 +34,42 @@ export const getActionsColumns = ({
onOpenDeleteTimelineModal?: OnOpenDeleteTimelineModal;
onOpenTimeline: OnOpenTimeline;
}): [TimelineActionsOverflowColumns] => {
+ const createTimelineFromTemplate = {
+ name: i18n.CREATE_TIMELINE_FROM_TEMPLATE,
+ icon: 'timeline',
+ onClick: ({ savedObjectId }: OpenTimelineResult) => {
+ onOpenTimeline({
+ duplicate: true,
+ timelineType: TimelineType.default,
+ timelineId: savedObjectId!,
+ });
+ },
+ type: 'icon',
+ enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null,
+ description: i18n.CREATE_TIMELINE_FROM_TEMPLATE,
+ 'data-test-subj': 'create-from-template',
+ available: (item: OpenTimelineResult) =>
+ item.timelineType === TimelineType.template && actionTimelineToShow.includes('createFrom'),
+ };
+
+ const createTemplateFromTimeline = {
+ name: i18n.CREATE_TEMPLATE_FROM_TIMELINE,
+ icon: 'visText',
+ onClick: ({ savedObjectId }: OpenTimelineResult) => {
+ onOpenTimeline({
+ duplicate: true,
+ timelineType: TimelineType.template,
+ timelineId: savedObjectId!,
+ });
+ },
+ type: 'icon',
+ enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null,
+ description: i18n.CREATE_TEMPLATE_FROM_TIMELINE,
+ 'data-test-subj': 'create-template-from-timeline',
+ available: (item: OpenTimelineResult) =>
+ item.timelineType !== TimelineType.template && actionTimelineToShow.includes('createFrom'),
+ };
+
const openAsDuplicateColumn = {
name: i18n.OPEN_AS_DUPLICATE,
icon: 'copy',
@@ -47,6 +83,25 @@ export const getActionsColumns = ({
enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null,
description: i18n.OPEN_AS_DUPLICATE,
'data-test-subj': 'open-duplicate',
+ available: (item: OpenTimelineResult) =>
+ item.timelineType !== TimelineType.template && actionTimelineToShow.includes('duplicate'),
+ };
+
+ const openAsDuplicateTemplateColumn = {
+ name: i18n.OPEN_AS_DUPLICATE_TEMPLATE,
+ icon: 'copy',
+ onClick: ({ savedObjectId }: OpenTimelineResult) => {
+ onOpenTimeline({
+ duplicate: true,
+ timelineId: savedObjectId ?? '',
+ });
+ },
+ type: 'icon',
+ enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null,
+ description: i18n.OPEN_AS_DUPLICATE_TEMPLATE,
+ 'data-test-subj': 'open-duplicate-template',
+ available: (item: OpenTimelineResult) =>
+ item.timelineType === TimelineType.template && actionTimelineToShow.includes('duplicate'),
};
const exportTimelineAction = {
@@ -60,6 +115,7 @@ export const getActionsColumns = ({
},
description: i18n.EXPORT_SELECTED,
'data-test-subj': 'export-timeline',
+ available: () => actionTimelineToShow.includes('export'),
};
const deleteTimelineColumn = {
@@ -72,18 +128,20 @@ export const getActionsColumns = ({
savedObjectId != null && status !== TimelineStatus.immutable,
description: i18n.DELETE_SELECTED,
'data-test-subj': 'delete-timeline',
+ available: () => actionTimelineToShow.includes('delete') && deleteTimelines != null,
};
return [
{
- width: '40px',
+ width: '80px',
actions: [
- actionTimelineToShow.includes('duplicate') ? openAsDuplicateColumn : null,
- actionTimelineToShow.includes('export') ? exportTimelineAction : null,
- actionTimelineToShow.includes('delete') && deleteTimelines != null
- ? deleteTimelineColumn
- : null,
- ].filter((action) => action != null),
+ createTimelineFromTemplate,
+ createTemplateFromTimeline,
+ openAsDuplicateColumn,
+ openAsDuplicateTemplateColumn,
+ exportTimelineAction,
+ deleteTimelineColumn,
+ ],
},
];
};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx
index e0c7ab68f6bf5..eb9ddcce112d3 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx
@@ -17,6 +17,7 @@ import * as i18n from '../translations';
import { OnOpenTimeline, OnToggleShowNotes, OpenTimelineResult } from '../types';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
+import { TimelineType } from '../../../../../common/types/timeline';
/**
* Returns the column definitions (passed as the `columns` prop to
@@ -27,10 +28,12 @@ export const getCommonColumns = ({
itemIdToExpandedNotesRowMap,
onOpenTimeline,
onToggleShowNotes,
+ timelineType,
}: {
onOpenTimeline: OnOpenTimeline;
onToggleShowNotes: OnToggleShowNotes;
itemIdToExpandedNotesRowMap: Record;
+ timelineType: TimelineType | null;
}) => [
{
isExpander: true,
@@ -55,7 +58,7 @@ export const getCommonColumns = ({
{
dataType: 'string',
field: 'title',
- name: i18n.TIMELINE_NAME,
+ name: timelineType === TimelineType.default ? i18n.TIMELINE_NAME : i18n.TIMELINE_TEMPLATE_NAME,
render: (title: string, timelineResult: OpenTimelineResult) =>
timelineResult.savedObjectId != null ? (
[
- {
- dataType: 'string',
- field: 'updatedBy',
- name: i18n.MODIFIED_BY,
- render: (updatedBy: OpenTimelineResult['updatedBy']) => (
- {defaultToEmptyTag(updatedBy)}
- ),
- sortable: false,
- },
-];
+export const getExtendedColumns = (showExtendedColumns: boolean) => {
+ if (!showExtendedColumns) return [];
+
+ return [
+ {
+ dataType: 'string',
+ field: 'updatedBy',
+ name: i18n.MODIFIED_BY,
+ render: (updatedBy: OpenTimelineResult['updatedBy']) => (
+ {defaultToEmptyTag(updatedBy)}
+ ),
+ sortable: false,
+ },
+ ];
+};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx
index fdba3247afb38..2c55edb9034b5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx
@@ -5,7 +5,7 @@
*/
import { EuiBasicTable as _EuiBasicTable } from '@elastic/eui';
-import React from 'react';
+import React, { useMemo } from 'react';
import styled from 'styled-components';
import * as i18n from '../translations';
@@ -40,9 +40,6 @@ const BasicTable = styled(EuiBasicTable)`
`;
BasicTable.displayName = 'BasicTable';
-const getExtendedColumnsIfEnabled = (showExtendedColumns: boolean) =>
- showExtendedColumns ? [...getExtendedColumns()] : [];
-
/**
* Returns the column definitions (passed as the `columns` prop to
* `EuiBasicTable`) that are displayed in the compact `Open Timeline` modal
@@ -77,8 +74,9 @@ export const getTimelinesTableColumns = ({
itemIdToExpandedNotesRowMap,
onOpenTimeline,
onToggleShowNotes,
+ timelineType,
}),
- ...getExtendedColumnsIfEnabled(showExtendedColumns),
+ ...getExtendedColumns(showExtendedColumns),
...getIconHeaderColumns({ timelineType }),
...getActionsColumns({
actionTimelineToShow,
@@ -167,9 +165,10 @@ export const TimelinesTable = React.memo(
onSelectionChange,
};
const basicTableProps = tableRef != null ? { ref: tableRef } : {};
- return (
-
+ getTimelinesTableColumns({
actionTimelineToShow,
deleteTimelines,
itemIdToExpandedNotesRowMap,
@@ -180,7 +179,24 @@ export const TimelinesTable = React.memo(
onToggleShowNotes,
showExtendedColumns,
timelineType,
- })}
+ }),
+ [
+ actionTimelineToShow,
+ deleteTimelines,
+ itemIdToExpandedNotesRowMap,
+ enableExportTimelineDownloader,
+ onOpenDeleteTimelineModal,
+ onOpenTimeline,
+ onSelectionChange,
+ onToggleShowNotes,
+ showExtendedColumns,
+ timelineType,
+ ]
+ );
+
+ return (
+
+ i18n.translate('xpack.securitySolution.open.timeline.selectedTemplatesTitle', {
+ values: { selectedTemplates },
+ defaultMessage:
+ 'Selected {selectedTemplates} {selectedTemplates, plural, =1 {template} other {templates}}',
+ });
+
export const SELECTED_TIMELINES = (selectedTimelines: number) =>
i18n.translate('xpack.securitySolution.open.timeline.selectedTimelinesTitle', {
values: { selectedTimelines },
@@ -298,6 +368,7 @@ export const IMPORT_FAILED_DETAILED = (id: string, statusCode: number, message:
export const TEMPLATE_CALL_OUT_MESSAGE = i18n.translate(
'xpack.securitySolution.timelines.components.templateCallOutMessageTitle',
{
- defaultMessage: 'Now you can add timeline templates and link it to rules.',
+ defaultMessage:
+ 'Prebuit detection rules are now packaged with Timeline templates. You can also create your own Timeline templates and associate them with any rule.',
}
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
index 8811d5452e039..c21edaa916588 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
@@ -54,7 +54,7 @@ export interface OpenTimelineResult {
status?: TimelineStatus | null;
title?: string | null;
templateTimelineId?: string | null;
- type?: TimelineTypeLiteral;
+ timelineType?: TimelineTypeLiteral;
updated?: number | null;
updatedBy?: string | null;
}
@@ -82,9 +82,11 @@ export type OnDeleteOneTimeline = (timelineIds: string[]) => void;
export type OnOpenTimeline = ({
duplicate,
timelineId,
+ timelineType,
}: {
duplicate: boolean;
timelineId: string;
+ timelineType?: TimelineTypeLiteral;
}) => void;
export type OnOpenDeleteTimelineModal = (selectedItem: OpenTimelineResult) => void;
@@ -117,7 +119,7 @@ export interface OnTableChangeParams {
/** Invoked by the EUI table implementation when the user interacts with the table */
export type OnTableChange = (tableChange: OnTableChangeParams) => void;
-export type ActionTimelineToShow = 'duplicate' | 'delete' | 'export' | 'selectable';
+export type ActionTimelineToShow = 'createFrom' | 'duplicate' | 'delete' | 'export' | 'selectable';
export interface OpenTimelineProps {
/** Invoked when the user clicks the delete (trash) icon on an individual timeline */
@@ -172,7 +174,7 @@ export interface OpenTimelineProps {
timelineType: TimelineTypeLiteralWithNull;
/** when timelineType === template, templatetimelineFilter is a JSX.Element */
templateTimelineFilter: JSX.Element[] | null;
- /** timeline / template timeline */
+ /** timeline / timeline template */
timelineFilter?: JSX.Element | JSX.Element[] | null;
/** The title of the Open Timeline component */
title: string;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx
index f17f6aebaddf6..c321caed46f22 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx
@@ -17,7 +17,6 @@ import {
import * as i18n from './translations';
import { TemplateTimelineFilter } from './types';
-import { disableTemplate } from '../../../../common/constants';
export const useTimelineStatus = ({
timelineType,
@@ -33,16 +32,16 @@ export const useTimelineStatus = ({
templateTimelineFilter: JSX.Element[] | null;
} => {
const [selectedTab, setSelectedTab] = useState(
- disableTemplate ? null : TemplateTimelineType.elastic
+ TemplateTimelineType.elastic
);
const isTemplateFilterEnabled = useMemo(() => timelineType === TimelineType.template, [
timelineType,
]);
- const templateTimelineType = useMemo(
- () => (disableTemplate || !isTemplateFilterEnabled ? null : selectedTab),
- [selectedTab, isTemplateFilterEnabled]
- );
+ const templateTimelineType = useMemo(() => (!isTemplateFilterEnabled ? null : selectedTab), [
+ selectedTab,
+ isTemplateFilterEnabled,
+ ]);
const timelineStatus = useMemo(
() =>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap
index 7baefaa6ab951..e38f6ad022d78 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap
@@ -901,6 +901,7 @@ In other use cases the message field can be used to concatenate different values
}
indexToAdd={Array []}
isLive={false}
+ isSaving={false}
itemsPerPage={5}
itemsPerPageOptions={
Array [
@@ -918,6 +919,7 @@ In other use cases the message field can be used to concatenate different values
onDataProviderRemoved={[MockFunction]}
onToggleDataProviderEnabled={[MockFunction]}
onToggleDataProviderExcluded={[MockFunction]}
+ onToggleDataProviderType={[MockFunction]}
show={true}
showCallOutUnauthorizedMsg={false}
sort={
@@ -928,6 +930,7 @@ In other use cases the message field can be used to concatenate different values
}
start={1521830963132}
status="active"
+ timelineType="default"
toggleColumn={[MockFunction]}
usersViewing={
Array [
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx
index e8074c2f6f381..445f2d8e62c82 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx
@@ -10,4 +10,6 @@ export const IP_FIELD_TYPE = 'ip';
export const MESSAGE_FIELD_NAME = 'message';
export const EVENT_MODULE_FIELD_NAME = 'event.module';
export const RULE_REFERENCE_FIELD_NAME = 'rule.reference';
+export const REFERENCE_URL_FIELD_NAME = 'reference.url';
+export const EVENT_URL_FIELD_NAME = 'event.url';
export const SIGNAL_RULE_NAME_FIELD_NAME = 'signal.rule.name';
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 b2588e19800a6..ab9e47f5ae3f5 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
@@ -29,8 +29,10 @@ import {
EVENT_MODULE_FIELD_NAME,
RULE_REFERENCE_FIELD_NAME,
SIGNAL_RULE_NAME_FIELD_NAME,
+ REFERENCE_URL_FIELD_NAME,
+ EVENT_URL_FIELD_NAME,
} from './constants';
-import { RenderRuleName, renderEventModule, renderRulReference } from './formatted_field_helpers';
+import { RenderRuleName, renderEventModule, renderUrl } from './formatted_field_helpers';
// simple black-list to prevent dragging and dropping fields such as message name
const columnNamesNotDraggable = [MESSAGE_FIELD_NAME];
@@ -107,8 +109,10 @@ const FormattedFieldValueComponent: React.FC<{
);
} else if (fieldName === EVENT_MODULE_FIELD_NAME) {
return renderEventModule({ contextId, eventId, fieldName, linkValue, truncate, value });
- } else if (fieldName === RULE_REFERENCE_FIELD_NAME) {
- return renderRulReference({ contextId, eventId, fieldName, linkValue, truncate, value });
+ } else if (
+ [RULE_REFERENCE_FIELD_NAME, REFERENCE_URL_FIELD_NAME, EVENT_URL_FIELD_NAME].includes(fieldName)
+ ) {
+ return renderUrl({ contextId, eventId, fieldName, linkValue, truncate, value });
} else if (columnNamesNotDraggable.includes(fieldName)) {
return truncate && !isEmpty(value) ? (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx
index 81820e2253fc9..8e64b484ffd2d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx
@@ -47,14 +47,14 @@ export const RenderRuleName: React.FC = ({
}) => {
const ruleName = `${value}`;
const ruleId = linkValue;
- const { search } = useFormatUrl(SecurityPageName.alerts);
+ const { search } = useFormatUrl(SecurityPageName.detections);
const { navigateToApp, getUrlForApp } = useKibana().services.application;
const content = truncate ? {value} : value;
const goToRuleDetails = useCallback(
(ev) => {
ev.preventDefault();
- navigateToApp(`${APP_ID}:${SecurityPageName.alerts}`, {
+ navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, {
path: getRuleDetailsUrl(ruleId ?? '', search),
});
},
@@ -70,7 +70,7 @@ export const RenderRuleName: React.FC = ({
>
@@ -150,7 +150,7 @@ export const renderEventModule = ({
);
};
-export const renderRulReference = ({
+export const renderUrl = ({
contextId,
eventId,
fieldName,
@@ -165,23 +165,23 @@ export const renderRulReference = ({
truncate?: boolean;
value: string | number | null | undefined;
}) => {
- const referenceUrlName = `${value}`;
+ const urlName = `${value}`;
const content = truncate ? {value} : value;
- return isString(value) && referenceUrlName.length > 0 ? (
+ return isString(value) && urlName.length > 0 ? (
- {!isUrlInvalid(referenceUrlName) && (
-
+ {!isUrlInvalid(urlName) && (
+
{content}
)}
- {isUrlInvalid(referenceUrlName) && <>{content}>}
+ {isUrlInvalid(urlName) && <>{content}>}
) : (
getEmptyTagValue()
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap
index 46a6970720def..14304b99263ac 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap
@@ -144,11 +144,12 @@ exports[`DataProviders rendering renders correctly against snapshot 1`] = `
},
]
}
- id="foo"
onDataProviderEdited={[MockFunction]}
onDataProviderRemoved={[MockFunction]}
onToggleDataProviderEnabled={[MockFunction]}
onToggleDataProviderExcluded={[MockFunction]}
+ onToggleDataProviderType={[MockFunction]}
+ timelineId="foo"
/>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap
index dac95c302af27..006da47460012 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap
@@ -20,8 +20,6 @@ exports[`Empty rendering renders correctly against snapshot 1`] = `
highlighted
-
-
+
`;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap
index 16094c585911b..d589a9aa33f06 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap
@@ -11,6 +11,8 @@ exports[`Provider rendering renders correctly against snapshot 1`] = `
providerId="id-Provider 1"
toggleEnabledProvider={[Function]}
toggleExcludedProvider={[Function]}
+ toggleTypeProvider={[Function]}
+ type="default"
val="Provider 1"
/>
`;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap
index d0d12a135e3dc..a227f39494b61 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap
@@ -5,26 +5,24 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
-
-
+
-
(
-
+
@@ -42,37 +40,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
-
)
-
+
+
-
+
-
(
-
+
@@ -90,37 +88,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
-
)
-
+
+
-
+
-
(
-
+
@@ -138,37 +136,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
-
)
-
+
+
-
+
-
(
-
+
@@ -186,37 +184,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
-
)
-
+
+
-
+
-
(
-
+
@@ -234,37 +232,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
-
)
-
+
+
-
+
-
(
-
+
@@ -282,37 +280,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
-
)
-
+
+
-
+
-
(
-
+
@@ -330,37 +328,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
-
)
-
+
+
-
+
-
(
-
+
@@ -378,37 +376,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
-
)
-
+
+
-
+
-
(
-
+
@@ -426,37 +424,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
-
)
-
+
+
-
+
-
(
-
+
@@ -474,37 +472,37 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
-
)
-
+
+
-
+
-
(
-
+
@@ -522,13 +520,13 @@ exports[`Providers rendering renders correctly against snapshot 1`] = `
-
)
-
+
`;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx
new file mode 100644
index 0000000000000..8e1c02bad50a3
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx
@@ -0,0 +1,198 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useCallback, useMemo, useState } from 'react';
+import {
+ EuiButton,
+ EuiContextMenu,
+ EuiText,
+ EuiPopover,
+ EuiIcon,
+ EuiContextMenuPanelItemDescriptor,
+} from '@elastic/eui';
+import uuid from 'uuid';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { BrowserFields } from '../../../../common/containers/source';
+import { TimelineType } from '../../../../../common/types/timeline';
+import { StatefulEditDataProvider } from '../../edit_data_provider';
+import { addContentToTimeline } from './helpers';
+import { DataProviderType } from './data_provider';
+import { timelineSelectors } from '../../../store/timeline';
+import { ADD_FIELD_LABEL, ADD_TEMPLATE_FIELD_LABEL } from './translations';
+
+interface AddDataProviderPopoverProps {
+ browserFields: BrowserFields;
+ timelineId: string;
+}
+
+const AddDataProviderPopoverComponent: React.FC = ({
+ browserFields,
+ timelineId,
+}) => {
+ const dispatch = useDispatch();
+ const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false);
+ const timelineById = useSelector(timelineSelectors.timelineByIdSelector);
+ const { dataProviders, timelineType } = timelineById[timelineId] ?? {};
+
+ const handleOpenPopover = useCallback(() => setIsAddFilterPopoverOpen(true), [
+ setIsAddFilterPopoverOpen,
+ ]);
+
+ const handleClosePopover = useCallback(() => setIsAddFilterPopoverOpen(false), [
+ setIsAddFilterPopoverOpen,
+ ]);
+
+ const handleDataProviderEdited = useCallback(
+ ({ andProviderId, excluded, field, id, operator, providerId, value, type }) => {
+ addContentToTimeline({
+ dataProviders,
+ destination: {
+ droppableId: `droppableId.timelineProviders.${timelineId}.group.${dataProviders.length}`,
+ index: 0,
+ },
+ dispatch,
+ onAddedToTimeline: handleClosePopover,
+ providerToAdd: {
+ id: providerId,
+ name: value,
+ enabled: true,
+ excluded,
+ kqlQuery: '',
+ type,
+ queryMatch: {
+ displayField: undefined,
+ displayValue: undefined,
+ field,
+ value,
+ operator,
+ },
+ and: [],
+ },
+ timelineId,
+ });
+ },
+ [dataProviders, timelineId, dispatch, handleClosePopover]
+ );
+
+ const panels = useMemo(
+ () => [
+ {
+ id: 0,
+ width: 400,
+ items: [
+ {
+ name: ADD_FIELD_LABEL,
+ icon: ,
+ panel: 1,
+ },
+ timelineType === TimelineType.template
+ ? {
+ disabled: timelineType !== TimelineType.template,
+ name: ADD_TEMPLATE_FIELD_LABEL,
+ icon: ,
+ panel: 2,
+ }
+ : null,
+ ].filter((item) => item !== null) as EuiContextMenuPanelItemDescriptor[],
+ },
+ {
+ id: 1,
+ title: ADD_FIELD_LABEL,
+ width: 400,
+ content: (
+
+ ),
+ },
+ {
+ id: 2,
+ title: ADD_TEMPLATE_FIELD_LABEL,
+ width: 400,
+ content: (
+
+ ),
+ },
+ ],
+ [browserFields, handleDataProviderEdited, timelineId, timelineType]
+ );
+
+ const button = useMemo(
+ () => (
+
+ {ADD_FIELD_LABEL}
+
+ ),
+ [handleOpenPopover]
+ );
+
+ const content = useMemo(() => {
+ if (timelineType === TimelineType.template) {
+ return ;
+ }
+
+ return (
+
+ );
+ }, [browserFields, handleDataProviderEdited, panels, timelineId, timelineType]);
+
+ return (
+
+ {content}
+
+ );
+};
+
+AddDataProviderPopoverComponent.displayName = 'AddDataProviderPopoverComponent';
+
+export const AddDataProviderPopover = React.memo(AddDataProviderPopoverComponent);
+
+AddDataProviderPopover.displayName = 'AddDataProviderPopover';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_provider.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_provider.ts
index a6fd8a0ceabbe..7fe0255132bc9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_provider.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_provider.ts
@@ -15,6 +15,11 @@ export const EXISTS_OPERATOR = ':*';
/** The operator applied to a field */
export type QueryOperator = ':' | ':*';
+export enum DataProviderType {
+ default = 'default',
+ template = 'template',
+}
+
export interface QueryMatch {
field: string;
displayField?: string;
@@ -39,7 +44,7 @@ export interface DataProvider {
*/
excluded: boolean;
/**
- * Return the KQL query who have been added by user
+ * Returns the KQL query who have been added by user
*/
kqlQuery: string;
/**
@@ -50,6 +55,10 @@ export interface DataProvider {
* Additional query clauses that are ANDed with this query to narrow results
*/
and: DataProvidersAnd[];
+ /**
+ * Returns a DataProviderType
+ */
+ type?: DataProviderType.default | DataProviderType.template;
}
export type DataProvidersAnd = Pick>;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx
index 3a8c0d8831217..754d7f9c47edf 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx
@@ -37,13 +37,14 @@ describe('DataProviders', () => {
@@ -58,12 +59,13 @@ describe('DataProviders', () => {
);
@@ -76,12 +78,13 @@ describe('DataProviders', () => {
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.test.tsx
index 598d9233cb01d..e1fad47e4204e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.test.tsx
@@ -13,7 +13,7 @@ import { TestProviders } from '../../../../common/mock/test_providers';
describe('Empty', () => {
describe('rendering', () => {
test('renders correctly against snapshot', () => {
- const wrapper = shallow( );
+ const wrapper = shallow( );
expect(wrapper).toMatchSnapshot();
});
@@ -22,7 +22,7 @@ describe('Empty', () => {
test('it renders the expected message', () => {
const wrapper = mount(
-
+
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.tsx
index 691c919029261..a6e70791d1ec7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/empty.tsx
@@ -8,7 +8,9 @@ import { EuiBadge, EuiText } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
+import { BrowserFields } from '../../../../common/containers/source';
import { AndOrBadge } from '../../../../common/components/and_or_badge';
+import { AddDataProviderPopover } from './add_data_provider_popover';
import * as i18n from './translations';
@@ -42,7 +44,7 @@ const EmptyContainer = styled.div<{ showSmallMsg: boolean }>`
width: ${(props) => (props.showSmallMsg ? '60px' : 'auto')};
align-items: center;
display: flex;
- flex-direction: row;
+ flex-direction: column;
flex-wrap: wrap;
justify-content: center;
user-select: none;
@@ -72,12 +74,14 @@ const NoWrap = styled.div`
NoWrap.displayName = 'NoWrap';
interface Props {
+ browserFields: BrowserFields;
showSmallMsg?: boolean;
+ timelineId: string;
}
/**
* Prompts the user to drop anything with a facet count into the data providers section.
*/
-export const Empty = React.memo(({ showSmallMsg = false }) => (
+export const Empty = React.memo(({ showSmallMsg = false, browserFields, timelineId }) => (
(({ showSmallMsg = false }) => (
{i18n.HIGHLIGHTED}
-
-
-
{i18n.HERE_TO_BUILD_AN}
@@ -105,6 +106,8 @@ export const Empty = React.memo(({ showSmallMsg = false }) => (
{i18n.QUERY}
+
+
>
)}
{showSmallMsg && }
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.tsx
index 9dc66a930ccc0..923ef86c0bbc0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.tsx
@@ -281,6 +281,7 @@ export const addProviderToGroup = ({
}
const destinationGroupIndex = getGroupIndexFromDroppableId(destination.droppableId);
+
if (
indexIsValid({
index: destinationGroupIndex,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx
index 90411f975da0b..c9e06f89af41c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx
@@ -19,6 +19,7 @@ import {
OnDataProviderRemoved,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
+ OnToggleDataProviderType,
} from '../events';
import { DataProvider } from './data_provider';
@@ -28,12 +29,13 @@ import { useManageTimeline } from '../../manage_timeline';
interface Props {
browserFields: BrowserFields;
- id: string;
+ timelineId: string;
dataProviders: DataProvider[];
onDataProviderEdited: OnDataProviderEdited;
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
+ onToggleDataProviderType: OnToggleDataProviderType;
}
const DropTargetDataProvidersContainer = styled.div`
@@ -61,6 +63,7 @@ const DropTargetDataProviders = styled.div`
position: relative;
border: 0.2rem dashed ${(props) => props.theme.eui.euiColorMediumShade};
border-radius: 5px;
+ padding: 5px 0;
margin: 2px 0 2px 0;
min-height: 100px;
overflow-y: auto;
@@ -91,17 +94,18 @@ const getDroppableId = (id: string): string => `${droppableTimelineProvidersPref
export const DataProviders = React.memo(
({
browserFields,
- id,
dataProviders,
+ timelineId,
onDataProviderEdited,
onDataProviderRemoved,
onToggleDataProviderEnabled,
onToggleDataProviderExcluded,
+ onToggleDataProviderType,
}) => {
const { getManageTimelineById } = useManageTimeline();
- const isLoading = useMemo(() => getManageTimelineById(id).isLoading, [
+ const isLoading = useMemo(() => getManageTimelineById(timelineId).isLoading, [
getManageTimelineById,
- id,
+ timelineId,
]);
return (
@@ -112,16 +116,17 @@ export const DataProviders = React.memo(
{dataProviders != null && dataProviders.length ? (
) : (
-
-
+
+
)}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider.tsx
index 8fd164eb8a3e2..2b598c7cf04f0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider.tsx
@@ -7,7 +7,7 @@
import { noop } from 'lodash/fp';
import React from 'react';
-import { DataProvider, IS_OPERATOR } from './data_provider';
+import { DataProvider, DataProviderType, IS_OPERATOR } from './data_provider';
import { ProviderItemBadge } from './provider_item_badge';
interface OwnProps {
@@ -24,8 +24,10 @@ export const Provider = React.memo(({ dataProvider }) => (
providerId={dataProvider.id}
toggleExcludedProvider={noop}
toggleEnabledProvider={noop}
+ toggleTypeProvider={noop}
val={dataProvider.queryMatch.displayValue || dataProvider.queryMatch.value}
operator={dataProvider.queryMatch.operator || IS_OPERATOR}
+ type={dataProvider.type || DataProviderType.default}
/>
));
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_badge.tsx
index b3682c0d55147..af63957d35075 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_badge.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_badge.tsx
@@ -10,14 +10,20 @@ import { isString } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
+import { TimelineType } from '../../../../../common/types/timeline';
import { getEmptyString } from '../../../../common/components/empty_value';
import { ProviderContainer } from '../../../../common/components/drag_and_drop/provider_container';
-import { EXISTS_OPERATOR, QueryOperator } from './data_provider';
+import { DataProviderType, EXISTS_OPERATOR, QueryOperator } from './data_provider';
import * as i18n from './translations';
-const ProviderBadgeStyled = (styled(EuiBadge)`
+type ProviderBadgeStyledType = typeof EuiBadge & {
+ // https://styled-components.com/docs/api#transient-props
+ $timelineType: TimelineType;
+};
+
+const ProviderBadgeStyled = styled(EuiBadge)`
.euiToolTipAnchor {
&::after {
font-style: normal;
@@ -25,17 +31,29 @@ const ProviderBadgeStyled = (styled(EuiBadge)`
padding: 0px 3px;
}
}
+
&.globalFilterItem {
white-space: nowrap;
+ min-width: ${({ $timelineType }) =>
+ $timelineType === TimelineType.template ? '140px' : 'none'};
+ display: flex;
+
&.globalFilterItem-isDisabled {
text-decoration: line-through;
font-weight: 400;
font-style: italic;
}
+
+ &.globalFilterItem-isError {
+ box-shadow: 0 1px 1px -1px rgba(152, 162, 179, 0.2), 0 3px 2px -2px rgba(152, 162, 179, 0.2),
+ inset 0 0 0 1px #bd271e;
+ }
}
+
.euiBadge.euiBadge--iconLeft &.euiBadge.euiBadge--iconRight .euiBadge__content {
flex-direction: row;
}
+
.euiBadge.euiBadge--iconLeft
&.euiBadge.euiBadge--iconRight
.euiBadge__content
@@ -43,10 +61,46 @@ const ProviderBadgeStyled = (styled(EuiBadge)`
margin-right: 0;
margin-left: 4px;
}
-` as unknown) as typeof EuiBadge;
+`;
ProviderBadgeStyled.displayName = 'ProviderBadgeStyled';
+const ProviderFieldBadge = styled.div`
+ display: block;
+ color: #fff;
+ padding: 6px 8px;
+ font-size: 0.6em;
+`;
+
+const StyledTemplateFieldBadge = styled(ProviderFieldBadge)`
+ background: ${({ theme }) => theme.eui.euiColorVis3_behindText};
+ text-transform: uppercase;
+`;
+
+interface TemplateFieldBadgeProps {
+ type: DataProviderType;
+ toggleType: () => void;
+}
+
+const ConvertFieldBadge = styled(ProviderFieldBadge)`
+ background: ${({ theme }) => theme.eui.euiColorDarkShade};
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+`;
+
+const TemplateFieldBadge: React.FC = ({ type, toggleType }) => {
+ if (type === DataProviderType.default) {
+ return (
+ {i18n.CONVERT_TO_TEMPLATE_FIELD}
+ );
+ }
+
+ return {i18n.TEMPLATE_FIELD_LABEL} ;
+};
+
interface ProviderBadgeProps {
deleteProvider: () => void;
field: string;
@@ -55,8 +109,11 @@ interface ProviderBadgeProps {
isExcluded: boolean;
providerId: string;
togglePopover: () => void;
+ toggleType: () => void;
val: string | number;
operator: QueryOperator;
+ type: DataProviderType;
+ timelineType: TimelineType;
}
const closeButtonProps = {
@@ -66,7 +123,19 @@ const closeButtonProps = {
};
export const ProviderBadge = React.memo(
- ({ deleteProvider, field, isEnabled, isExcluded, operator, providerId, togglePopover, val }) => {
+ ({
+ deleteProvider,
+ field,
+ isEnabled,
+ isExcluded,
+ operator,
+ providerId,
+ togglePopover,
+ toggleType,
+ val,
+ type,
+ timelineType,
+ }) => {
const deleteFilter: React.MouseEventHandler = useCallback(
(event: React.MouseEvent) => {
// Make sure it doesn't also trigger the onclick for the whole badge
@@ -93,34 +162,46 @@ export const ProviderBadge = React.memo(
const prefix = useMemo(() => (isExcluded ? {i18n.NOT} : null), [isExcluded]);
- return (
-
-
+ const content = useMemo(
+ () => (
+ <>
{prefix}
{operator !== EXISTS_OPERATOR ? (
- <>
- {`${field}: `}
- {`"${formattedValue}"`}
- >
+ {`${field}: "${formattedValue}"`}
) : (
{field} {i18n.EXISTS_LABEL}
)}
-
+ >
+ ),
+ [field, formattedValue, operator, prefix]
+ );
+
+ return (
+
+ <>
+
+ {content}
+
+
+ {timelineType === TimelineType.template && (
+
+ )}
+ >
);
}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_actions.tsx
index 540b1b80259a0..7aa782c05c0dd 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_actions.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_actions.tsx
@@ -12,9 +12,11 @@ import {
import React, { FunctionComponent } from 'react';
import styled from 'styled-components';
+import { TimelineType } from '../../../../../common/types/timeline';
import { BrowserFields } from '../../../../common/containers/source';
+
import { OnDataProviderEdited } from '../events';
-import { QueryOperator, EXISTS_OPERATOR } from './data_provider';
+import { DataProviderType, QueryOperator, EXISTS_OPERATOR } from './data_provider';
import { StatefulEditDataProvider } from '../../edit_data_provider';
import * as i18n from './translations';
@@ -23,6 +25,7 @@ export const EDIT_CLASS_NAME = 'edit-data-provider';
export const EXCLUDE_CLASS_NAME = 'exclude-data-provider';
export const ENABLE_CLASS_NAME = 'enable-data-provider';
export const FILTER_FOR_FIELD_PRESENT_CLASS_NAME = 'filter-for-field-present-data-provider';
+export const CONVERT_TO_FIELD_CLASS_NAME = 'convert-to-field-data-provider';
export const DELETE_CLASS_NAME = 'delete-data-provider';
interface OwnProps {
@@ -41,9 +44,12 @@ interface OwnProps {
operator: QueryOperator;
providerId: string;
timelineId?: string;
+ timelineType?: TimelineType;
toggleEnabledProvider: () => void;
toggleExcludedProvider: () => void;
+ toggleTypeProvider: () => void;
value: string | number;
+ type: DataProviderType;
}
const MyEuiPopover = styled((EuiPopover as unknown) as FunctionComponent)<
@@ -57,6 +63,27 @@ const MyEuiPopover = styled((EuiPopover as unknown) as FunctionComponent)<
MyEuiPopover.displayName = 'MyEuiPopover';
+interface GetProviderActionsProps {
+ andProviderId?: string;
+ browserFields?: BrowserFields;
+ deleteItem: () => void;
+ field: string;
+ isEnabled: boolean;
+ isExcluded: boolean;
+ isLoading: boolean;
+ onDataProviderEdited?: OnDataProviderEdited;
+ onFilterForFieldPresent: () => void;
+ operator: QueryOperator;
+ providerId: string;
+ timelineId?: string;
+ timelineType?: TimelineType;
+ toggleEnabled: () => void;
+ toggleExcluded: () => void;
+ toggleType: () => void;
+ value: string | number;
+ type: DataProviderType;
+}
+
export const getProviderActions = ({
andProviderId,
browserFields,
@@ -70,26 +97,13 @@ export const getProviderActions = ({
onFilterForFieldPresent,
providerId,
timelineId,
+ timelineType,
toggleEnabled,
toggleExcluded,
+ toggleType,
+ type,
value,
-}: {
- andProviderId?: string;
- browserFields?: BrowserFields;
- deleteItem: () => void;
- field: string;
- isEnabled: boolean;
- isExcluded: boolean;
- isLoading: boolean;
- onDataProviderEdited?: OnDataProviderEdited;
- onFilterForFieldPresent: () => void;
- operator: QueryOperator;
- providerId: string;
- timelineId?: string;
- toggleEnabled: () => void;
- toggleExcluded: () => void;
- value: string | number;
-}): EuiContextMenuPanelDescriptor[] => [
+}: GetProviderActionsProps): EuiContextMenuPanelDescriptor[] => [
{
id: 0,
items: [
@@ -121,6 +135,18 @@ export const getProviderActions = ({
name: i18n.FILTER_FOR_FIELD_PRESENT,
onClick: onFilterForFieldPresent,
},
+ timelineType === TimelineType.template
+ ? {
+ className: CONVERT_TO_FIELD_CLASS_NAME,
+ disabled: isLoading,
+ icon: 'visText',
+ name:
+ type === DataProviderType.template
+ ? i18n.CONVERT_TO_FIELD
+ : i18n.CONVERT_TO_TEMPLATE_FIELD,
+ onClick: toggleType,
+ }
+ : { name: null },
{
className: DELETE_CLASS_NAME,
disabled: isLoading,
@@ -128,7 +154,7 @@ export const getProviderActions = ({
name: i18n.DELETE_DATA_PROVIDER,
onClick: deleteItem,
},
- ],
+ ].filter((item) => item.name != null),
},
{
content:
@@ -143,6 +169,7 @@ export const getProviderActions = ({
providerId={providerId}
timelineId={timelineId}
value={value}
+ type={type}
/>
) : null,
id: 1,
@@ -167,9 +194,12 @@ export class ProviderItemActions extends React.PureComponent {
operator,
providerId,
timelineId,
+ timelineType,
toggleEnabledProvider,
toggleExcludedProvider,
+ toggleTypeProvider,
value,
+ type,
} = this.props;
const panelTree = getProviderActions({
@@ -185,9 +215,12 @@ export class ProviderItemActions extends React.PureComponent {
operator,
providerId,
timelineId,
+ timelineType,
toggleEnabled: toggleEnabledProvider,
toggleExcluded: toggleExcludedProvider,
+ toggleType: toggleTypeProvider,
value,
+ type,
});
return (
@@ -214,6 +247,7 @@ export class ProviderItemActions extends React.PureComponent {
operator,
providerId,
value,
+ type,
}) => {
if (this.props.onDataProviderEdited != null) {
this.props.onDataProviderEdited({
@@ -224,6 +258,7 @@ export class ProviderItemActions extends React.PureComponent {
operator,
providerId,
value,
+ type,
});
}
@@ -231,7 +266,7 @@ export class ProviderItemActions extends React.PureComponent {
};
private onFilterForFieldPresent = () => {
- const { andProviderId, field, timelineId, providerId, value } = this.props;
+ const { andProviderId, field, timelineId, providerId, value, type } = this.props;
if (this.props.onDataProviderEdited != null) {
this.props.onDataProviderEdited({
@@ -242,6 +277,7 @@ export class ProviderItemActions extends React.PureComponent {
operator: EXISTS_OPERATOR,
providerId,
value,
+ type,
});
}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx
index 1f6fe998a44e9..bc7c313553f1e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx
@@ -6,14 +6,16 @@
import { noop } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { useDispatch } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
+import { TimelineType } from '../../../../../common/types/timeline';
import { BrowserFields } from '../../../../common/containers/source';
+import { timelineSelectors } from '../../../store/timeline';
import { OnDataProviderEdited } from '../events';
import { ProviderBadge } from './provider_badge';
import { ProviderItemActions } from './provider_item_actions';
-import { DataProvidersAnd, QueryOperator } from './data_provider';
+import { DataProvidersAnd, DataProviderType, QueryOperator } from './data_provider';
import { dragAndDropActions } from '../../../../common/store/drag_and_drop';
import { useManageTimeline } from '../../manage_timeline';
@@ -32,7 +34,9 @@ interface ProviderItemBadgeProps {
timelineId?: string;
toggleEnabledProvider: () => void;
toggleExcludedProvider: () => void;
+ toggleTypeProvider: () => void;
val: string | number;
+ type?: DataProviderType;
}
export const ProviderItemBadge = React.memo(
@@ -51,8 +55,12 @@ export const ProviderItemBadge = React.memo(
timelineId,
toggleEnabledProvider,
toggleExcludedProvider,
+ toggleTypeProvider,
val,
+ type = DataProviderType.default,
}) => {
+ const timelineById = useSelector(timelineSelectors.timelineByIdSelector);
+ const timelineType = timelineId ? timelineById[timelineId]?.timelineType : TimelineType.default;
const { getManageTimelineById } = useManageTimeline();
const isLoading = useMemo(() => getManageTimelineById(timelineId ?? '').isLoading, [
getManageTimelineById,
@@ -71,14 +79,17 @@ export const ProviderItemBadge = React.memo(
const onToggleEnabledProvider = useCallback(() => {
toggleEnabledProvider();
closePopover();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [toggleEnabledProvider]);
+ }, [closePopover, toggleEnabledProvider]);
const onToggleExcludedProvider = useCallback(() => {
toggleExcludedProvider();
closePopover();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [toggleExcludedProvider]);
+ }, [toggleExcludedProvider, closePopover]);
+
+ const onToggleTypeProvider = useCallback(() => {
+ toggleTypeProvider();
+ closePopover();
+ }, [toggleTypeProvider, closePopover]);
const [providerRegistered, setProviderRegistered] = useState(false);
@@ -102,27 +113,31 @@ export const ProviderItemBadge = React.memo(
() => () => {
unRegisterProvider();
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- []
+ [unRegisterProvider]
+ );
+
+ const button = (
+
);
return (
- }
+ button={button}
closePopover={closePopover}
deleteProvider={deleteProvider}
field={field}
@@ -135,9 +150,12 @@ export const ProviderItemBadge = React.memo(
operator={operator}
providerId={providerId}
timelineId={timelineId}
+ timelineType={timelineType}
toggleEnabledProvider={onToggleEnabledProvider}
toggleExcludedProvider={onToggleExcludedProvider}
+ toggleTypeProvider={onToggleTypeProvider}
value={val}
+ type={type}
/>
);
}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx
index 9dc0b76224458..b788f70cb2e4a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx
@@ -38,11 +38,12 @@ describe('Providers', () => {
);
expect(wrapper).toMatchSnapshot();
@@ -55,11 +56,12 @@ describe('Providers', () => {
@@ -82,11 +84,12 @@ describe('Providers', () => {
@@ -107,11 +110,12 @@ describe('Providers', () => {
@@ -134,11 +138,12 @@ describe('Providers', () => {
@@ -163,11 +168,12 @@ describe('Providers', () => {
@@ -195,11 +201,12 @@ describe('Providers', () => {
@@ -227,11 +234,12 @@ describe('Providers', () => {
@@ -260,11 +268,12 @@ describe('Providers', () => {
@@ -295,11 +304,12 @@ describe('Providers', () => {
@@ -330,11 +340,12 @@ describe('Providers', () => {
@@ -344,9 +355,9 @@ describe('Providers', () => {
'[data-test-subj="providerBadge"] .euiBadge__content span.field-value'
);
const andProviderBadgesText = andProviderBadges.map((node) => node.text()).join(' ');
- expect(andProviderBadges.length).toEqual(6);
+ expect(andProviderBadges.length).toEqual(3);
expect(andProviderBadgesText).toEqual(
- 'name: "Provider 1" name: "Provider 2" name: "Provider 3"'
+ 'name: "Provider 1" name: "Provider 2" name: "Provider 3"'
);
});
@@ -361,11 +372,12 @@ describe('Providers', () => {
@@ -395,11 +407,12 @@ describe('Providers', () => {
@@ -429,11 +442,12 @@ describe('Providers', () => {
@@ -472,11 +486,12 @@ describe('Providers', () => {
@@ -511,11 +526,12 @@ describe('Providers', () => {
@@ -554,11 +570,12 @@ describe('Providers', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx
index b5d44cf854458..c9dd906cee59b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx
@@ -4,13 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiFormHelpText } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiFormHelpText, EuiSpacer } from '@elastic/eui';
import { rgba } from 'polished';
-import React, { useMemo } from 'react';
+import React, { Fragment, useMemo } from 'react';
import { Draggable, DraggingStyle, Droppable, NotDraggingStyle } from 'react-beautiful-dnd';
import styled, { css } from 'styled-components';
import { AndOrBadge } from '../../../../common/components/and_or_badge';
+import { AddDataProviderPopover } from './add_data_provider_popover';
import { BrowserFields } from '../../../../common/containers/source';
import {
getTimelineProviderDroppableId,
@@ -22,9 +23,10 @@ import {
OnDataProviderRemoved,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
+ OnToggleDataProviderType,
} from '../events';
-import { DataProvider, DataProvidersAnd, IS_OPERATOR } from './data_provider';
+import { DataProvider, DataProviderType, DataProvidersAnd, IS_OPERATOR } from './data_provider';
import { EMPTY_GROUP, flattenIntoAndGroups } from './helpers';
import { ProviderItemBadge } from './provider_item_badge';
@@ -32,12 +34,13 @@ export const EMPTY_PROVIDERS_GROUP_CLASS_NAME = 'empty-providers-group';
interface Props {
browserFields: BrowserFields;
- id: string;
+ timelineId: string;
dataProviders: DataProvider[];
onDataProviderEdited: OnDataProviderEdited;
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
+ onToggleDataProviderType: OnToggleDataProviderType;
}
/**
@@ -62,7 +65,8 @@ const getItemStyle = (
});
const DroppableContainer = styled.div`
- height: ${ROW_OF_DATA_PROVIDERS_HEIGHT}px;
+ min-height: ${ROW_OF_DATA_PROVIDERS_HEIGHT}px;
+ height: auto !important;
.${IS_DRAGGING_CLASS_NAME} &:hover {
background-color: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.2)} !important;
@@ -78,10 +82,10 @@ const Parens = styled.span`
`}
`;
-const AndOrBadgeContainer = styled.div<{ hideBadge: boolean }>`
- span {
- visibility: ${({ hideBadge }) => (hideBadge ? 'hidden' : 'inherit')};
- }
+const AndOrBadgeContainer = styled.div`
+ width: 121px;
+ display: flex;
+ justify-content: flex-end;
`;
const LastAndOrBadgeInGroup = styled.div`
@@ -105,6 +109,17 @@ const TimelineEuiFormHelpText = styled(EuiFormHelpText)`
TimelineEuiFormHelpText.displayName = 'TimelineEuiFormHelpText';
+const ParensContainer = styled(EuiFlexItem)`
+ align-self: center;
+`;
+
+const AddDataProviderContainer = styled.div`
+ padding-right: 9px;
+`;
+
+const getDataProviderValue = (dataProvider: DataProvidersAnd) =>
+ dataProvider.queryMatch.displayValue ?? dataProvider.queryMatch.value;
+
/**
* Renders an interactive card representation of the data providers. It also
* affords uniform UI controls for the following actions:
@@ -115,148 +130,178 @@ TimelineEuiFormHelpText.displayName = 'TimelineEuiFormHelpText';
export const Providers = React.memo(
({
browserFields,
- id,
+ timelineId,
dataProviders,
onDataProviderEdited,
onDataProviderRemoved,
onToggleDataProviderEnabled,
onToggleDataProviderExcluded,
+ onToggleDataProviderType,
}) => {
// Transform the dataProviders into flattened groups, and append an empty group
const dataProviderGroups: DataProvidersAnd[][] = useMemo(
() => [...flattenIntoAndGroups(dataProviders), ...EMPTY_GROUP],
[dataProviders]
);
+
return (
{dataProviderGroups.map((group, groupIndex) => (
-
-
-
-
-
-
-
- {'('}
-
-
-
- {(droppableProvided) => (
-
- {group.map((dataProvider, index) => (
-
- {(provided, snapshot) => (
-
-
-
- 0 ? dataProvider.id : undefined}
- browserFields={browserFields}
- deleteProvider={() =>
- index > 0
- ? onDataProviderRemoved(group[0].id, dataProvider.id)
- : onDataProviderRemoved(dataProvider.id)
- }
- field={
- index > 0
- ? dataProvider.queryMatch.displayField ??
- dataProvider.queryMatch.field
- : group[0].queryMatch.displayField ??
- group[0].queryMatch.field
- }
- kqlQuery={index > 0 ? dataProvider.kqlQuery : group[0].kqlQuery}
- isEnabled={index > 0 ? dataProvider.enabled : group[0].enabled}
- isExcluded={index > 0 ? dataProvider.excluded : group[0].excluded}
- onDataProviderEdited={onDataProviderEdited}
- operator={
- index > 0
- ? dataProvider.queryMatch.operator ?? IS_OPERATOR
- : group[0].queryMatch.operator ?? IS_OPERATOR
- }
- register={dataProvider}
- providerId={index > 0 ? group[0].id : dataProvider.id}
- timelineId={id}
- toggleEnabledProvider={() =>
- index > 0
- ? onToggleDataProviderEnabled({
- providerId: group[0].id,
- enabled: !dataProvider.enabled,
- andProviderId: dataProvider.id,
- })
- : onToggleDataProviderEnabled({
- providerId: dataProvider.id,
- enabled: !dataProvider.enabled,
- })
- }
- toggleExcludedProvider={() =>
- index > 0
- ? onToggleDataProviderExcluded({
- providerId: group[0].id,
- excluded: !dataProvider.excluded,
- andProviderId: dataProvider.id,
- })
- : onToggleDataProviderExcluded({
- providerId: dataProvider.id,
- excluded: !dataProvider.excluded,
- })
- }
- val={
- dataProvider.queryMatch.displayValue ??
- dataProvider.queryMatch.value
- }
- />
-
-
- {!snapshot.isDragging &&
- (index < group.length - 1 ? (
-
- ) : (
-
-
-
- ))}
-
-
-
- )}
-
- ))}
- {droppableProvided.placeholder}
-
+
+ {groupIndex !== 0 && }
+
+
+
+ {groupIndex === 0 ? (
+
+
+
+ ) : (
+
+
+
)}
-
-
-
- {')'}
-
-
+
+
+ {'('}
+
+
+
+ {(droppableProvided) => (
+
+ {group.map((dataProvider, index) => (
+
+ {(provided, snapshot) => (
+
+
+
+ 0 ? dataProvider.id : undefined}
+ browserFields={browserFields}
+ deleteProvider={() =>
+ index > 0
+ ? onDataProviderRemoved(group[0].id, dataProvider.id)
+ : onDataProviderRemoved(dataProvider.id)
+ }
+ field={
+ index > 0
+ ? dataProvider.queryMatch.displayField ??
+ dataProvider.queryMatch.field
+ : group[0].queryMatch.displayField ??
+ group[0].queryMatch.field
+ }
+ kqlQuery={index > 0 ? dataProvider.kqlQuery : group[0].kqlQuery}
+ isEnabled={index > 0 ? dataProvider.enabled : group[0].enabled}
+ isExcluded={
+ index > 0 ? dataProvider.excluded : group[0].excluded
+ }
+ onDataProviderEdited={onDataProviderEdited}
+ operator={
+ index > 0
+ ? dataProvider.queryMatch.operator ?? IS_OPERATOR
+ : group[0].queryMatch.operator ?? IS_OPERATOR
+ }
+ register={dataProvider}
+ providerId={index > 0 ? group[0].id : dataProvider.id}
+ timelineId={timelineId}
+ toggleEnabledProvider={() =>
+ index > 0
+ ? onToggleDataProviderEnabled({
+ providerId: group[0].id,
+ enabled: !dataProvider.enabled,
+ andProviderId: dataProvider.id,
+ })
+ : onToggleDataProviderEnabled({
+ providerId: dataProvider.id,
+ enabled: !dataProvider.enabled,
+ })
+ }
+ toggleExcludedProvider={() =>
+ index > 0
+ ? onToggleDataProviderExcluded({
+ providerId: group[0].id,
+ excluded: !dataProvider.excluded,
+ andProviderId: dataProvider.id,
+ })
+ : onToggleDataProviderExcluded({
+ providerId: dataProvider.id,
+ excluded: !dataProvider.excluded,
+ })
+ }
+ toggleTypeProvider={() =>
+ index > 0
+ ? onToggleDataProviderType({
+ providerId: group[0].id,
+ type:
+ dataProvider.type === DataProviderType.template
+ ? DataProviderType.default
+ : DataProviderType.template,
+ andProviderId: dataProvider.id,
+ })
+ : onToggleDataProviderType({
+ providerId: dataProvider.id,
+ type:
+ dataProvider.type === DataProviderType.template
+ ? DataProviderType.default
+ : DataProviderType.template,
+ })
+ }
+ val={getDataProviderValue(dataProvider)}
+ type={dataProvider.type}
+ />
+
+
+ {!snapshot.isDragging &&
+ (index < group.length - 1 ? (
+
+ ) : (
+
+
+
+ ))}
+
+
+
+ )}
+
+ ))}
+ {droppableProvided.placeholder}
+
+ )}
+
+
+
+ {')'}
+
+
+
))}
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/translations.ts
index 104ff44cb9b7c..48f1f4e2218d2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/translations.ts
@@ -72,6 +72,20 @@ export const FILTER_FOR_FIELD_PRESENT = i18n.translate(
}
);
+export const CONVERT_TO_FIELD = i18n.translate(
+ 'xpack.securitySolution.dataProviders.convertToFieldLabel',
+ {
+ defaultMessage: 'Convert to field',
+ }
+);
+
+export const CONVERT_TO_TEMPLATE_FIELD = i18n.translate(
+ 'xpack.securitySolution.dataProviders.convertToTemplateFieldLabel',
+ {
+ defaultMessage: 'Convert to template field',
+ }
+);
+
export const HIGHLIGHTED = i18n.translate('xpack.securitySolution.dataProviders.highlighted', {
defaultMessage: 'highlighted',
});
@@ -148,3 +162,24 @@ export const VALUE_ARIA_LABEL = i18n.translate(
defaultMessage: 'value',
}
);
+
+export const ADD_FIELD_LABEL = i18n.translate(
+ 'xpack.securitySolution.dataProviders.addFieldPopoverButtonLabel',
+ {
+ defaultMessage: 'Add field',
+ }
+);
+
+export const ADD_TEMPLATE_FIELD_LABEL = i18n.translate(
+ 'xpack.securitySolution.dataProviders.addTemplateFieldPopoverButtonLabel',
+ {
+ defaultMessage: 'Add template field',
+ }
+);
+
+export const TEMPLATE_FIELD_LABEL = i18n.translate(
+ 'xpack.securitySolution.dataProviders.templateFieldLabel',
+ {
+ defaultMessage: 'Template field',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts
index 6c9a9b8b89679..4653880739c6d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts
@@ -7,7 +7,7 @@
import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { ColumnId } from './body/column_id';
import { SortDirection } from './body/sort';
-import { QueryOperator } from './data_providers/data_provider';
+import { DataProvider, DataProviderType, QueryOperator } from './data_providers/data_provider';
/** Invoked when a user clicks the close button to remove a data provider */
export type OnDataProviderRemoved = (providerId: string, andProviderId?: string) => void;
@@ -26,6 +26,13 @@ export type OnToggleDataProviderExcluded = (excluded: {
andProviderId?: string;
}) => void;
+/** Invoked when a user toggles type (can "default" or "template") of a data provider */
+export type OnToggleDataProviderType = (type: {
+ providerId: string;
+ type: DataProviderType;
+ andProviderId?: string;
+}) => void;
+
/** Invoked when a user edits the properties of a data provider */
export type OnDataProviderEdited = ({
andProviderId,
@@ -35,6 +42,7 @@ export type OnDataProviderEdited = ({
operator,
providerId,
value,
+ type,
}: {
andProviderId?: string;
excluded: boolean;
@@ -43,6 +51,7 @@ export type OnDataProviderEdited = ({
operator: QueryOperator;
providerId: string;
value: string | number;
+ type: DataProvider['type'];
}) => void;
/** Invoked when a user change the kql query of our data provider */
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap
index b3b39236150ec..f94c30c5a102d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap
@@ -138,11 +138,12 @@ exports[`Header rendering renders correctly against snapshot 1`] = `
},
]
}
- id="foo"
onDataProviderEdited={[MockFunction]}
onDataProviderRemoved={[MockFunction]}
onToggleDataProviderEnabled={[MockFunction]}
onToggleDataProviderExcluded={[MockFunction]}
+ onToggleDataProviderType={[MockFunction]}
+ timelineId="foo"
/>
{
browserFields: {},
dataProviders: mockDataProviders,
filterManager: new FilterManager(mockUiSettingsForFilterManager),
- id: 'foo',
indexPattern,
onDataProviderEdited: jest.fn(),
onDataProviderRemoved: jest.fn(),
onToggleDataProviderEnabled: jest.fn(),
onToggleDataProviderExcluded: jest.fn(),
+ onToggleDataProviderType: jest.fn(),
show: true,
showCallOutUnauthorizedMsg: false,
status: TimelineStatus.active,
+ timelineId: 'foo',
+ timelineType: TimelineType.default,
};
describe('rendering', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx
index 0541dee4b1e52..93af374b15b56 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx
@@ -17,6 +17,7 @@ import {
OnDataProviderRemoved,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
+ OnToggleDataProviderType,
} from '../events';
import { StatefulSearchOrFilter } from '../search_or_filter';
import { BrowserFields } from '../../../../common/containers/source';
@@ -32,20 +33,20 @@ interface Props {
dataProviders: DataProvider[];
filterManager: FilterManager;
graphEventId?: string;
- id: string;
indexPattern: IIndexPattern;
onDataProviderEdited: OnDataProviderEdited;
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
+ onToggleDataProviderType: OnToggleDataProviderType;
show: boolean;
showCallOutUnauthorizedMsg: boolean;
status: TimelineStatusLiteralWithNull;
+ timelineId: string;
}
const TimelineHeaderComponent: React.FC = ({
browserFields,
- id,
indexPattern,
dataProviders,
filterManager,
@@ -54,9 +55,11 @@ const TimelineHeaderComponent: React.FC = ({
onDataProviderRemoved,
onToggleDataProviderEnabled,
onToggleDataProviderExcluded,
+ onToggleDataProviderType,
show,
showCallOutUnauthorizedMsg,
status,
+ timelineId,
}) => (
<>
{showCallOutUnauthorizedMsg && (
@@ -81,19 +84,20 @@ const TimelineHeaderComponent: React.FC = ({
<>
>
)}
@@ -104,7 +108,6 @@ export const TimelineHeader = React.memo(
TimelineHeaderComponent,
(prevProps, nextProps) =>
deepEqual(prevProps.browserFields, nextProps.browserFields) &&
- prevProps.id === nextProps.id &&
deepEqual(prevProps.indexPattern, nextProps.indexPattern) &&
deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
prevProps.filterManager === nextProps.filterManager &&
@@ -113,7 +116,9 @@ export const TimelineHeader = React.memo(
prevProps.onDataProviderRemoved === nextProps.onDataProviderRemoved &&
prevProps.onToggleDataProviderEnabled === nextProps.onToggleDataProviderEnabled &&
prevProps.onToggleDataProviderExcluded === nextProps.onToggleDataProviderExcluded &&
+ prevProps.onToggleDataProviderType === nextProps.onToggleDataProviderType &&
prevProps.show === nextProps.show &&
prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg &&
- prevProps.status === nextProps.status
+ prevProps.status === nextProps.status &&
+ prevProps.timelineId === nextProps.timelineId
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx
index 1038ac4b69587..391d367ad3dc3 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx
@@ -7,6 +7,7 @@
import { cloneDeep } from 'lodash/fp';
import { mockIndexPattern } from '../../../common/mock';
+import { DataProviderType } from './data_providers/data_provider';
import { mockDataProviders } from './data_providers/mock/mock_data_providers';
import { buildGlobalQuery, combineQueries } from './helpers';
import { mockBrowserFields } from '../../../common/containers/source/mock';
@@ -23,6 +24,20 @@ describe('Build KQL Query', () => {
expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"');
});
+ test('Build KQL query with one template data provider', () => {
+ const dataProviders = cloneDeep(mockDataProviders.slice(0, 1));
+ dataProviders[0].type = DataProviderType.template;
+ const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields);
+ expect(cleanUpKqlQuery(kqlQuery)).toEqual('name :*');
+ });
+
+ test('Build KQL query with one disabled data provider', () => {
+ const dataProviders = cloneDeep(mockDataProviders.slice(0, 1));
+ dataProviders[0].enabled = false;
+ const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields);
+ expect(cleanUpKqlQuery(kqlQuery)).toEqual('');
+ });
+
test('Build KQL query with one data provider as timestamp (string input)', () => {
const dataProviders = cloneDeep(mockDataProviders.slice(0, 1));
dataProviders[0].queryMatch.field = '@timestamp';
@@ -75,6 +90,20 @@ describe('Build KQL Query', () => {
expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"');
});
+ test('Build KQL query with two data provider (first is template)', () => {
+ const dataProviders = cloneDeep(mockDataProviders.slice(0, 2));
+ dataProviders[0].type = DataProviderType.template;
+ const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields);
+ expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name :*) or (name : "Provider 2")');
+ });
+
+ test('Build KQL query with two data provider (second is template)', () => {
+ const dataProviders = cloneDeep(mockDataProviders.slice(0, 2));
+ dataProviders[1].type = DataProviderType.template;
+ const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields);
+ expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name :*)');
+ });
+
test('Build KQL query with one data provider and one and', () => {
const dataProviders = cloneDeep(mockDataProviders.slice(0, 1));
dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2));
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
index a3fc692c3a8a8..a0087ab638dbf 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
@@ -9,7 +9,12 @@ import memoizeOne from 'memoize-one';
import { escapeQueryValue, convertToBuildEsQuery } from '../../../common/lib/keury';
-import { DataProvider, DataProvidersAnd, EXISTS_OPERATOR } from './data_providers/data_provider';
+import {
+ DataProvider,
+ DataProviderType,
+ DataProvidersAnd,
+ EXISTS_OPERATOR,
+} from './data_providers/data_provider';
import { BrowserFields } from '../../../common/containers/source';
import {
IIndexPattern,
@@ -52,7 +57,8 @@ const buildQueryMatch = (
browserFields: BrowserFields
) =>
`${dataProvider.excluded ? 'NOT ' : ''}${
- dataProvider.queryMatch.operator !== EXISTS_OPERATOR
+ dataProvider.queryMatch.operator !== EXISTS_OPERATOR &&
+ dataProvider.type !== DataProviderType.template
? checkIfFieldTypeIsDate(dataProvider.queryMatch.field, browserFields)
? convertDateFieldToQuery(dataProvider.queryMatch.field, dataProvider.queryMatch.value)
: `${dataProvider.queryMatch.field} : ${
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx
index 296b24cff43ad..50a7782012b76 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx
@@ -13,7 +13,7 @@ import useResizeObserver from 'use-resize-observer/polyfilled';
import {
useSignalIndex,
ReturnSignalIndex,
-} from '../../../alerts/containers/detection_engine/alerts/use_signal_index';
+} from '../../../detections/containers/detection_engine/alerts/use_signal_index';
import { mocksSource } from '../../../common/containers/source/mock';
import { wait } from '../../../common/lib/helpers';
import { defaultHeaders, mockTimelineData, TestProviders } from '../../../common/mock';
@@ -40,7 +40,7 @@ jest.mock('use-resize-observer/polyfilled');
mockUseResizeObserver.mockImplementation(() => ({}));
const mockUseSignalIndex: jest.Mock = useSignalIndex as jest.Mock;
-jest.mock('../../../alerts/containers/detection_engine/alerts/use_signal_index');
+jest.mock('../../../detections/containers/detection_engine/alerts/use_signal_index');
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@@ -76,6 +76,7 @@ describe('StatefulTimeline', () => {
graphEventId: undefined,
id: 'foo',
isLive: false,
+ isSaving: false,
isTimelineExists: false,
itemsPerPage: 5,
itemsPerPageOptions: [5, 10, 20],
@@ -95,6 +96,7 @@ describe('StatefulTimeline', () => {
updateDataProviderEnabled: timelineActions.updateDataProviderEnabled,
updateDataProviderExcluded: timelineActions.updateDataProviderExcluded,
updateDataProviderKqlQuery: timelineActions.updateDataProviderKqlQuery,
+ updateDataProviderType: timelineActions.updateDataProviderType,
updateHighlightedDropAndProviderId: timelineActions.updateHighlightedDropAndProviderId,
updateItemsPerPage: timelineActions.updateItemsPerPage,
updateItemsPerPageOptions: timelineActions.updateItemsPerPageOptions,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
index 35622eddc359c..5265efc8109a4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
@@ -4,13 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { isEmpty } from 'lodash/fp';
import React, { useEffect, useCallback, useMemo } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { NO_ALERT_INDEX } from '../../../../common/constants';
import { useWithSource } from '../../../common/containers/source';
-import { useSignalIndex } from '../../../alerts/containers/detection_engine/alerts/use_signal_index';
+import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
import { inputsModel, inputsSelectors, State } from '../../../common/store';
import { timelineActions, timelineSelectors } from '../../store/timeline';
import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model';
@@ -22,6 +23,7 @@ import {
OnDataProviderEdited,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
+ OnToggleDataProviderType,
} from './events';
import { Timeline } from './timeline';
@@ -44,6 +46,7 @@ const StatefulTimelineComponent = React.memo(
graphEventId,
id,
isLive,
+ isSaving,
isTimelineExists,
itemsPerPage,
itemsPerPageOptions,
@@ -61,6 +64,7 @@ const StatefulTimelineComponent = React.memo(
timelineType,
updateDataProviderEnabled,
updateDataProviderExcluded,
+ updateDataProviderType,
updateItemsPerPage,
upsertColumn,
usersViewing,
@@ -82,8 +86,7 @@ const StatefulTimelineComponent = React.memo(
const onDataProviderRemoved: OnDataProviderRemoved = useCallback(
(providerId: string, andProviderId?: string) =>
removeProvider!({ id, providerId, andProviderId }),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [id]
+ [id, removeProvider]
);
const onToggleDataProviderEnabled: OnToggleDataProviderEnabled = useCallback(
@@ -94,8 +97,7 @@ const StatefulTimelineComponent = React.memo(
providerId,
andProviderId,
}),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [id]
+ [id, updateDataProviderEnabled]
);
const onToggleDataProviderExcluded: OnToggleDataProviderExcluded = useCallback(
@@ -106,8 +108,18 @@ const StatefulTimelineComponent = React.memo(
providerId,
andProviderId,
}),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [id]
+ [id, updateDataProviderExcluded]
+ );
+
+ const onToggleDataProviderType: OnToggleDataProviderType = useCallback(
+ ({ providerId, type, andProviderId }) =>
+ updateDataProviderType!({
+ id,
+ type,
+ providerId,
+ andProviderId,
+ }),
+ [id, updateDataProviderType]
);
const onDataProviderEditedLocal: OnDataProviderEdited = useCallback(
@@ -121,14 +133,12 @@ const StatefulTimelineComponent = React.memo(
providerId,
value,
}),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [id]
+ [id, onDataProviderEdited]
);
const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback(
(itemsChangedPerPage) => updateItemsPerPage!({ id, itemsPerPage: itemsChangedPerPage }),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [id]
+ [id, updateItemsPerPage]
);
const toggleColumn = useCallback(
@@ -176,6 +186,7 @@ const StatefulTimelineComponent = React.memo(
indexPattern={indexPattern}
indexToAdd={indexToAdd}
isLive={isLive}
+ isSaving={isSaving}
itemsPerPage={itemsPerPage!}
itemsPerPageOptions={itemsPerPageOptions!}
kqlMode={kqlMode}
@@ -187,12 +198,14 @@ const StatefulTimelineComponent = React.memo(
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
+ onToggleDataProviderType={onToggleDataProviderType}
show={show!}
showCallOutUnauthorizedMsg={showCallOutUnauthorizedMsg}
sort={sort!}
start={start}
status={status}
toggleColumn={toggleColumn}
+ timelineType={timelineType}
usersViewing={usersViewing}
/>
);
@@ -204,6 +217,7 @@ const StatefulTimelineComponent = React.memo(
prevProps.graphEventId === nextProps.graphEventId &&
prevProps.id === nextProps.id &&
prevProps.isLive === nextProps.isLive &&
+ prevProps.isSaving === nextProps.isSaving &&
prevProps.itemsPerPage === nextProps.itemsPerPage &&
prevProps.kqlMode === nextProps.kqlMode &&
prevProps.kqlQueryExpression === nextProps.kqlQueryExpression &&
@@ -240,15 +254,19 @@ const makeMapStateToProps = () => {
graphEventId,
itemsPerPage,
itemsPerPageOptions,
+ isSaving,
kqlMode,
show,
sort,
status,
timelineType,
} = timeline;
- const kqlQueryExpression = getKqlQueryTimeline(state, id)!;
-
+ const kqlQueryTimeline = getKqlQueryTimeline(state, id)!;
const timelineFilter = kqlMode === 'filter' ? filters || [] : [];
+
+ // return events on empty search
+ const kqlQueryExpression =
+ isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) ? ' ' : kqlQueryTimeline;
return {
columns,
dataProviders,
@@ -258,6 +276,7 @@ const makeMapStateToProps = () => {
graphEventId,
id,
isLive: input.policy.kind === 'interval',
+ isSaving,
isTimelineExists: getTimeline(state, id) != null,
itemsPerPage,
itemsPerPageOptions,
@@ -284,6 +303,7 @@ const mapDispatchToProps = {
updateDataProviderEnabled: timelineActions.updateDataProviderEnabled,
updateDataProviderExcluded: timelineActions.updateDataProviderExcluded,
updateDataProviderKqlQuery: timelineActions.updateDataProviderKqlQuery,
+ updateDataProviderType: timelineActions.updateDataProviderType,
updateHighlightedDropAndProviderId: timelineActions.updateHighlightedDropAndProviderId,
updateItemsPerPage: timelineActions.updateItemsPerPage,
updateItemsPerPageOptions: timelineActions.updateItemsPerPageOptions,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
index 7b5e9c0c4c949..452808e51c096 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
@@ -119,22 +119,32 @@ Description.displayName = 'Description';
interface NameProps {
timelineId: string;
+ timelineType: TimelineType;
title: string;
updateTitle: UpdateTitle;
}
-export const Name = React.memo(({ timelineId, title, updateTitle }) => (
-
- updateTitle({ id: timelineId, title: e.target.value })}
- placeholder={i18n.UNTITLED_TIMELINE}
- spellCheck={true}
- value={title}
- />
-
-));
+export const Name = React.memo(({ timelineId, timelineType, title, updateTitle }) => {
+ const handleChange = useCallback((e) => updateTitle({ id: timelineId, title: e.target.value }), [
+ timelineId,
+ updateTitle,
+ ]);
+
+ return (
+
+
+
+ );
+});
Name.displayName = 'Name';
interface NewCaseProps {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
index 3a28c26a16c9a..ce99304c676ee 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
@@ -6,6 +6,7 @@
import { mount } from 'enzyme';
import React from 'react';
+
import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
import {
mockGlobalState,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
index b3567151c74b3..6de40725f461c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
@@ -27,15 +27,6 @@ import { useKibana } from '../../../../common/lib/kibana';
import { APP_ID } from '../../../../../common/constants';
import { getCaseDetailsUrl } from '../../../../common/components/link_to';
-type CreateTimeline = ({
- id,
- show,
- timelineType,
-}: {
- id: string;
- show?: boolean;
- timelineType?: TimelineTypeLiteral;
-}) => void;
type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void;
type UpdateTitle = ({ id, title }: { id: string; title: string }) => void;
type UpdateDescription = ({ id, description }: { id: string; description: string }) => void;
@@ -43,7 +34,6 @@ type ToggleLock = ({ linkToId }: { linkToId: InputsModelId }) => void;
interface Props {
associateNote: AssociateNote;
- createTimeline: CreateTimeline;
description: string;
getNotesByIds: (noteIds: string[]) => Note[];
graphEventId?: string;
@@ -78,7 +68,6 @@ const settingsWidth = 55;
export const Properties = React.memo(
({
associateNote,
- createTimeline,
description,
getNotesByIds,
graphEventId,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx
index 4673ba662b2e9..a3cd8802c36bc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx
@@ -13,7 +13,6 @@ import { AssociateNote, UpdateNote } from '../../notes/helpers';
import { Note } from '../../../../common/lib/note';
import { SuperDatePicker } from '../../../../common/components/super_date_picker';
-
import { TimelineTypeLiteral, TimelineStatusLiteral } from '../../../../../common/types/timeline';
import * as i18n from './translations';
@@ -106,7 +105,12 @@ export const PropertiesLeft = React.memo(
/>
-
+
{showDescription ? (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx
index a36e841f3f871..3f02772b46bb3 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx
@@ -10,7 +10,6 @@ import React from 'react';
import { PropertiesRight } from './properties_right';
import { useKibana } from '../../../../common/lib/kibana';
import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
-import { disableTemplate } from '../../../../../common/constants';
jest.mock('../../../../common/lib/kibana', () => {
return {
@@ -97,20 +96,10 @@ describe('Properties Right', () => {
expect(wrapper.find('[data-test-subj="settings-gear"]').exists()).toBeTruthy();
});
- test('it renders create timelin btn', () => {
+ test('it renders create timeline btn', () => {
expect(wrapper.find('[data-test-subj="create-default-btn"]').exists()).toBeTruthy();
});
- /*
- * CreateTemplateTimelineBtn
- * Remove the comment here to enable CreateTemplateTimelineBtn
- */
- test('it renders no create template timelin btn', () => {
- expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toEqual(
- !disableTemplate
- );
- });
-
test('it renders create attach timeline to a case btn', () => {
expect(wrapper.find('[data-test-subj="NewCase"]').exists()).toBeTruthy();
});
@@ -208,14 +197,8 @@ describe('Properties Right', () => {
expect(wrapper.find('[data-test-subj="settings-gear"]').exists()).toBeTruthy();
});
- test('it renders no create timelin btn', () => {
- expect(wrapper.find('[data-test-subj="create-default-btn"]').exists()).not.toBeTruthy();
- });
-
- test('it renders create template timelin btn if it is enabled', () => {
- expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toEqual(
- !disableTemplate
- );
+ test('it renders create timeline template btn', () => {
+ expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toEqual(true);
});
test('it renders create attach timeline to a case btn', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx
index 8a1bf0a842cb0..70257c97a6887 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx
@@ -16,9 +16,11 @@ import {
} from '@elastic/eui';
import { NewTimeline, Description, NotesButton, NewCase, ExistingCase } from './helpers';
-import { disableTemplate } from '../../../../../common/constants';
-import { TimelineStatusLiteral, TimelineTypeLiteral } from '../../../../../common/types/timeline';
-
+import {
+ TimelineStatusLiteral,
+ TimelineTypeLiteral,
+ TimelineType,
+} from '../../../../../common/types/timeline';
import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect';
import { useKibana } from '../../../../common/lib/kibana';
import { Note } from '../../../../common/lib/note';
@@ -151,41 +153,39 @@ const PropertiesRightComponent: React.FC = ({
)}
- {/*
- * CreateTemplateTimelineBtn
- * Remove the comment here to enable CreateTemplateTimelineBtn
- */}
- {!disableTemplate && (
-
-
-
- )}
-
-
-
-
-
-
+
-
+
+ {timelineType === TimelineType.default && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
{i18n.ALERT_EVENT},
+ inputDisplay: {i18n.DETECTION_ALERTS_EVENT} ,
},
];
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx
index 388085d1361f3..4d90bd875efcc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx
@@ -5,7 +5,7 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/eui';
-import React from 'react';
+import React, { useCallback } from 'react';
import styled, { createGlobalStyle } from 'styled-components';
import {
@@ -117,57 +117,64 @@ export const SearchOrFilter = React.memo(
updateEventType,
updateKqlMode,
updateReduxTime,
- }) => (
- <>
-
-
-
-
- updateKqlMode({ id: timelineId, kqlMode: mode })}
- options={options}
- popoverClassName={searchOrFilterPopoverClassName}
- valueOfSelected={kqlMode}
+ }) => {
+ const handleChange = useCallback(
+ (mode: KqlMode) => updateKqlMode({ id: timelineId, kqlMode: mode }),
+ [timelineId, updateKqlMode]
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- >
- )
+
+
+
+
+
+
+
+ >
+ );
+ }
);
SearchOrFilter.displayName = 'SearchOrFilter';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts
index 7271c599302c5..7fa520a2d8df4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts
@@ -84,9 +84,9 @@ export const RAW_EVENT = i18n.translate(
}
);
-export const ALERT_EVENT = i18n.translate(
- 'xpack.securitySolution.timeline.searchOrFilter.eventTypeAlertEvent',
+export const DETECTION_ALERTS_EVENT = i18n.translate(
+ 'xpack.securitySolution.timeline.searchOrFilter.eventTypeDetectionAlertsEvent',
{
- defaultMessage: 'Alert events',
+ defaultMessage: 'Detection Alerts',
}
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_super_select/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_super_select/index.tsx
index b549fdab8ea4a..825d4fe3b29b1 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_super_select/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_super_select/index.tsx
@@ -52,7 +52,7 @@ const SearchTimelineSuperSelectComponent: React.FC {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx
index 0ff4c0a70fff2..6bea5a7b7635e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx
@@ -60,7 +60,7 @@ describe('SelectableTimeline', () => {
});
});
- describe('template timeline', () => {
+ describe('timeline template', () => {
const templateTimelineProps = { ...props, timelineType: TimelineType.template };
beforeAll(() => {
wrapper = shallow( );
@@ -74,7 +74,7 @@ describe('SelectableTimeline', () => {
const searchProps: SearchProps = wrapper
.find('[data-test-subj="selectable-input"]')
.prop('searchProps');
- expect(searchProps.placeholder).toEqual('e.g. Template timeline name or description');
+ expect(searchProps.placeholder).toEqual('e.g. Timeline template name or description');
});
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx
index dacaf325130d7..ae8bf53090789 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx
@@ -33,7 +33,6 @@ import * as i18nTimeline from '../../open_timeline/translations';
import { OpenTimelineResult } from '../../open_timeline/types';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import * as i18n from '../translations';
-import { useTimelineStatus } from '../../open_timeline/use_timeline_status';
const MyEuiFlexItem = styled(EuiFlexItem)`
display: inline-block;
@@ -119,7 +118,6 @@ const SelectableTimelineComponent: React.FC = ({
const [onlyFavorites, setOnlyFavorites] = useState(false);
const [searchRef, setSearchRef] = useState(null);
const { fetchAllTimeline, timelines, loading, totalCount: timelineCount } = useGetAllTimeline();
- const { timelineStatus, templateTimelineType } = useTimelineStatus({ timelineType });
const onSearchTimeline = useCallback((val) => {
setSearchTimelineValue(val);
@@ -263,19 +261,11 @@ const SelectableTimelineComponent: React.FC = ({
sortOrder: Direction.desc,
},
onlyUserFavorite: onlyFavorites,
- status: timelineStatus,
+ status: null,
timelineType,
- templateTimelineType,
+ templateTimelineType: null,
});
- }, [
- fetchAllTimeline,
- onlyFavorites,
- pageSize,
- searchTimelineValue,
- timelineType,
- timelineStatus,
- templateTimelineType,
- ]);
+ }, [fetchAllTimeline, onlyFavorites, pageSize, searchTimelineValue, timelineType]);
return (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx
index b58505546c341..360737ce41d2d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx
@@ -24,7 +24,7 @@ import { TimelineComponent, Props as TimelineComponentProps } from './timeline';
import { Sort } from './body/sort';
import { mockDataProviders } from './data_providers/mock/mock_data_providers';
import { useMountAppended } from '../../../common/utils/use_mount_appended';
-import { TimelineStatus } from '../../../../common/types/timeline';
+import { TimelineStatus, TimelineType } from '../../../../common/types/timeline';
jest.mock('../../../common/lib/kibana');
jest.mock('./properties/properties_right');
@@ -82,6 +82,7 @@ describe('Timeline', () => {
indexPattern,
indexToAdd: [],
isLive: false,
+ isSaving: false,
itemsPerPage: 5,
itemsPerPageOptions: [5, 10, 20],
kqlMode: 'search' as TimelineComponentProps['kqlMode'],
@@ -93,6 +94,7 @@ describe('Timeline', () => {
onDataProviderRemoved: jest.fn(),
onToggleDataProviderEnabled: jest.fn(),
onToggleDataProviderExcluded: jest.fn(),
+ onToggleDataProviderType: jest.fn(),
show: true,
showCallOutUnauthorizedMsg: false,
start: startDate,
@@ -100,6 +102,7 @@ describe('Timeline', () => {
status: TimelineStatus.active,
toggleColumn: jest.fn(),
usersViewing: ['elastic'],
+ timelineType: TimelineType.default,
};
});
@@ -298,9 +301,9 @@ describe('Timeline', () => {
);
const andProviderBadgesText = andProviderBadges.map((node) => node.text()).join(' ');
- expect(andProviderBadges.length).toEqual(6);
+ expect(andProviderBadges.length).toEqual(3);
expect(andProviderBadgesText).toEqual(
- 'name: "Provider 1" name: "Provider 2" name: "Provider 3"'
+ 'name: "Provider 1" name: "Provider 2" name: "Provider 3"'
);
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx
index b930325c3d35d..ee48f97164b86 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui';
+import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter, EuiProgress } from '@elastic/eui';
import { getOr, isEmpty } from 'lodash/fp';
import React, { useState, useMemo, useEffect } from 'react';
import { useDispatch } from 'react-redux';
@@ -27,12 +27,14 @@ import {
OnDataProviderEdited,
OnToggleDataProviderEnabled,
OnToggleDataProviderExcluded,
+ OnToggleDataProviderType,
} from './events';
import { TimelineKqlFetch } from './fetch_kql_timeline';
import { Footer, footerHeight } from './footer';
import { TimelineHeader } from './header';
import { combineQueries } from './helpers';
import { TimelineRefetch } from './refetch_timeline';
+import { TIMELINE_TEMPLATE } from './translations';
import {
esQuery,
Filter,
@@ -40,12 +42,13 @@ import {
IIndexPattern,
} from '../../../../../../../src/plugins/data/public';
import { useManageTimeline } from '../manage_timeline';
-import { TimelineStatusLiteral } from '../../../../common/types/timeline';
+import { TimelineType, TimelineStatusLiteral } from '../../../../common/types/timeline';
const TimelineContainer = styled.div`
height: 100%;
display: flex;
flex-direction: column;
+ position: relative;
`;
const TimelineHeaderContainer = styled.div`
@@ -84,6 +87,13 @@ const StyledEuiFlyoutFooter = styled(EuiFlyoutFooter)`
padding: 0 10px 5px 12px;
`;
+const TimelineTemplateBadge = styled.div`
+ background: ${({ theme }) => theme.eui.euiColorVis3_behindText};
+ color: #fff;
+ padding: 10px 15px;
+ font-size: 0.8em;
+`;
+
export interface Props {
browserFields: BrowserFields;
columns: ColumnHeaderOptions[];
@@ -96,6 +106,7 @@ export interface Props {
indexPattern: IIndexPattern;
indexToAdd: string[];
isLive: boolean;
+ isSaving: boolean;
itemsPerPage: number;
itemsPerPageOptions: number[];
kqlMode: KqlMode;
@@ -107,6 +118,7 @@ export interface Props {
onDataProviderRemoved: OnDataProviderRemoved;
onToggleDataProviderEnabled: OnToggleDataProviderEnabled;
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
+ onToggleDataProviderType: OnToggleDataProviderType;
show: boolean;
showCallOutUnauthorizedMsg: boolean;
start: number;
@@ -114,6 +126,7 @@ export interface Props {
status: TimelineStatusLiteral;
toggleColumn: (column: ColumnHeaderOptions) => void;
usersViewing: string[];
+ timelineType: TimelineType;
}
/** The parent Timeline component */
@@ -129,6 +142,7 @@ export const TimelineComponent: React.FC = ({
indexPattern,
indexToAdd,
isLive,
+ isSaving,
itemsPerPage,
itemsPerPageOptions,
kqlMode,
@@ -140,11 +154,13 @@ export const TimelineComponent: React.FC = ({
onDataProviderRemoved,
onToggleDataProviderEnabled,
onToggleDataProviderExcluded,
+ onToggleDataProviderType,
show,
showCallOutUnauthorizedMsg,
start,
status,
sort,
+ timelineType,
toggleColumn,
usersViewing,
}) => {
@@ -182,6 +198,7 @@ export const TimelineComponent: React.FC = ({
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+
useEffect(() => {
setIsTimelineLoading({ id, isLoading: isQueryLoading || loadingIndexName });
}, [loadingIndexName, id, isQueryLoading, setIsTimelineLoading]);
@@ -192,6 +209,10 @@ export const TimelineComponent: React.FC = ({
return (
+ {isSaving && }
+ {timelineType === TimelineType.template && (
+ {TIMELINE_TEMPLATE}
+ )}
= ({
= ({
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
+ onToggleDataProviderType={onToggleDataProviderType}
show={show}
showCallOutUnauthorizedMsg={showCallOutUnauthorizedMsg}
+ timelineId={id}
status={status}
/>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/translations.ts
index ebd27f9bffa5e..f8c38b3527d7a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/translations.ts
@@ -23,7 +23,7 @@ export const DEFAULT_TIMELINE_DESCRIPTION = i18n.translate(
export const SEARCH_BOX_TIMELINE_PLACEHOLDER = (timelineType: TimelineTypeLiteral) =>
i18n.translate('xpack.securitySolution.timeline.searchBoxPlaceholder', {
- values: { timeline: timelineType === TimelineType.template ? 'Template timeline' : 'Timeline' },
+ values: { timeline: timelineType === TimelineType.template ? 'Timeline template' : 'Timeline' },
defaultMessage: 'e.g. {timeline} name or description',
});
@@ -33,3 +33,10 @@ export const INSERT_TIMELINE = i18n.translate(
defaultMessage: 'Insert timeline link',
}
);
+
+export const TIMELINE_TEMPLATE = i18n.translate(
+ 'xpack.securitySolution.timeline.flyoutTimelineTemplateLabel',
+ {
+ defaultMessage: 'Timeline template',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx
index 17cc0f64de039..4ecabeef16dff 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx
@@ -23,6 +23,7 @@ import { useApolloClient } from '../../../common/utils/apollo_context';
import { allTimelinesQuery } from './index.gql_query';
import * as i18n from '../../pages/translations';
import {
+ TimelineType,
TimelineTypeLiteralWithNull,
TimelineStatusLiteralWithNull,
TemplateTimelineTypeLiteralWithNull,
@@ -92,6 +93,7 @@ export const getAllTimeline = memoizeOne(
title: timeline.title,
updated: timeline.updated,
updatedBy: timeline.updatedBy,
+ timelineType: timeline.timelineType ?? TimelineType.default,
}))
);
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts
index 089a428f7dfaf..42c01da7e23c9 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts
@@ -7,7 +7,7 @@ import * as api from './api';
import { KibanaServices } from '../../common/lib/kibana';
import { TimelineType, TimelineStatus } from '../../../common/types/timeline';
import { TIMELINE_DRAFT_URL, TIMELINE_URL } from '../../../common/constants';
-import { ImportDataProps } from '../../alerts/containers/detection_engine/rules/types';
+import { ImportDataProps } from '../../detections/containers/detection_engine/rules/types';
jest.mock('../../common/lib/kibana', () => {
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts
index ff252ea93039d..72e1f1d4de32d 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts
+++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts
@@ -30,7 +30,7 @@ import { createToasterPlainError } from '../../cases/containers/utils';
import {
ImportDataProps,
ImportDataResponse,
-} from '../../alerts/containers/detection_engine/rules';
+} from '../../detections/containers/detection_engine/rules';
interface RequestPostTimeline {
timeline: TimelineInput;
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx
index 6a6d74cc91508..164d34db16d87 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx
@@ -29,7 +29,7 @@ import { EventType } from '../../timelines/store/timeline/model';
import { timelineQuery } from './index.gql_query';
import { timelineActions } from '../../timelines/store/timeline';
-const timelineIds = [TimelineId.alertsPage, TimelineId.alertsRulesDetailsPage];
+const timelineIds = [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage];
export interface TimelineArgs {
events: TimelineItem[];
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts
index 47e80b005fb99..24beed0801aa6 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts
+++ b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts
@@ -28,6 +28,7 @@ export const oneTimelineQuery = gql`
enabled
excluded
kqlQuery
+ type
queryMatch {
field
displayField
diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx
index 1bd5874394df3..2e59dbb72233f 100644
--- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx
@@ -9,12 +9,23 @@ import React from 'react';
import { useKibana } from '../../common/lib/kibana';
import { TimelinesPageComponent } from './timelines_page';
-import { disableTemplate } from '../../../common/constants';
-jest.mock('../../overview/components/events_by_dataset');
+jest.mock('react-router-dom', () => {
+ const originalModule = jest.requireActual('react-router-dom');
+ return {
+ ...originalModule,
+ useParams: jest.fn().mockReturnValue({
+ tabName: 'default',
+ }),
+ };
+});
+jest.mock('../../overview/components/events_by_dataset');
jest.mock('../../common/lib/kibana', () => {
+ const originalModule = jest.requireActual('../../common/lib/kibana');
+
return {
+ ...originalModule,
useKibana: jest.fn(),
};
});
@@ -59,22 +70,16 @@ describe('TimelinesPageComponent', () => {
).toEqual(true);
});
- test('it renders create timelin btn', () => {
+ test('it renders create timeline btn', () => {
expect(wrapper.find('[data-test-subj="create-default-btn"]').exists()).toBeTruthy();
});
- /*
- * CreateTemplateTimelineBtn
- * Remove the comment here to enable CreateTemplateTimelineBtn
- */
- test('it renders no create template timelin btn', () => {
- expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toEqual(
- !disableTemplate
- );
+ test('it renders no create timeline template btn', () => {
+ expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toBeFalsy();
});
});
- describe('If the user is not authorised', () => {
+ describe('If the user is not authorized', () => {
beforeAll(() => {
((useKibana as unknown) as jest.Mock).mockReturnValue({
services: {
diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
index 089a928403b0b..56aff3ec8aaac 100644
--- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
@@ -7,9 +7,9 @@
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useCallback, useState } from 'react';
import styled from 'styled-components';
+import { useParams } from 'react-router-dom';
-import { disableTemplate } from '../../../common/constants';
-
+import { TimelineType } from '../../../common/types/timeline';
import { HeaderPage } from '../../common/components/header_page';
import { WrapperPage } from '../../common/components/wrapper_page';
import { useKibana } from '../../common/lib/kibana';
@@ -31,6 +31,7 @@ const TimelinesContainer = styled.div`
export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10;
export const TimelinesPageComponent: React.FC = () => {
+ const { tabName } = useParams();
const [importDataModalToggle, setImportDataModalToggle] = useState(false);
const onImportTimelineBtnClick = useCallback(() => {
setImportDataModalToggle(true);
@@ -56,20 +57,17 @@ export const TimelinesPageComponent: React.FC = () => {
)}
-
- {capabilitiesCanUserCRUD && (
-
- )}
-
- {/**
- * CreateTemplateTimelineBtn
- * Remove the comment here to enable CreateTemplateTimelineBtn
- */}
- {!disableTemplate && (
+ {tabName === TimelineType.default ? (
+
+ {capabilitiesCanUserCRUD && (
+
+ )}
+
+ ) : (
('PROVIDER_EDIT_KQL_QUERY');
+export const updateDataProviderType = actionCreator<{
+ andProviderId?: string;
+ id: string;
+ type: DataProviderType;
+ providerId: string;
+}>('UPDATE_PROVIDER_TYPE');
+
export const updateHighlightedDropAndProviderId = actionCreator<{
id: string;
providerId: string;
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
index 94acb9d92075b..605700cb71a2a 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
@@ -58,6 +58,7 @@ import {
updateDataProviderEnabled,
updateDataProviderExcluded,
updateDataProviderKqlQuery,
+ updateDataProviderType,
updateDescription,
updateKqlMode,
updateProviders,
@@ -96,6 +97,7 @@ const timelineActionsType = [
updateDataProviderEnabled.type,
updateDataProviderExcluded.type,
updateDataProviderKqlQuery.type,
+ updateDataProviderType.type,
updateDescription.type,
updateEventType.type,
updateKqlMode.type,
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx
index 388869194085c..7d65181db65fd 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx
@@ -39,7 +39,7 @@ import { Direction } from '../../../graphql/types';
import { addTimelineInStorage } from '../../containers/local_storage';
import { isPageTimeline } from './epic_local_storage';
-import { TimelineStatus } from '../../../../common/types/timeline';
+import { TimelineStatus, TimelineType } from '../../../../common/types/timeline';
jest.mock('../../containers/local_storage');
@@ -89,6 +89,7 @@ describe('epicLocalStorage', () => {
indexPattern,
indexToAdd: [],
isLive: false,
+ isSaving: false,
itemsPerPage: 5,
itemsPerPageOptions: [5, 10, 20],
kqlMode: 'search' as TimelineComponentProps['kqlMode'],
@@ -100,11 +101,13 @@ describe('epicLocalStorage', () => {
onDataProviderRemoved: jest.fn(),
onToggleDataProviderEnabled: jest.fn(),
onToggleDataProviderExcluded: jest.fn(),
+ onToggleDataProviderType: jest.fn(),
show: true,
showCallOutUnauthorizedMsg: false,
start: startDate,
status: TimelineStatus.active,
sort,
+ timelineType: TimelineType.default,
toggleColumn: jest.fn(),
usersViewing: ['elastic'],
};
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
index 33770aacde6bb..a347d3e41e206 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
@@ -9,14 +9,15 @@ import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp';
import uuid from 'uuid';
import { Filter } from '../../../../../../../src/plugins/data/public';
-import { disableTemplate } from '../../../../common/constants';
-
import { getColumnWidthFromType } from '../../../timelines/components/timeline/body/column_headers/helpers';
import { Sort } from '../../../timelines/components/timeline/body/sort';
import {
DataProvider,
QueryOperator,
QueryMatch,
+ DataProviderType,
+ IS_OPERATOR,
+ EXISTS_OPERATOR,
} from '../../../timelines/components/timeline/data_providers/data_provider';
import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model';
import { TimelineNonEcsData } from '../../../graphql/types';
@@ -161,7 +162,7 @@ export const addNewTimeline = ({
timelineType,
}: AddNewTimelineParams): TimelineById => {
const templateTimelineInfo =
- !disableTemplate && timelineType === TimelineType.template
+ timelineType === TimelineType.template
? {
templateTimelineId: uuid.v4(),
templateTimelineVersion: 1,
@@ -186,7 +187,7 @@ export const addNewTimeline = ({
isLoading: false,
showCheckboxes,
showRowRenderers,
- timelineType: !disableTemplate ? timelineType : timelineDefaults.timelineType,
+ timelineType,
...templateTimelineInfo,
},
};
@@ -1046,6 +1047,92 @@ export const updateTimelineProviderKqlQuery = ({
};
};
+interface UpdateTimelineProviderTypeParams {
+ andProviderId?: string;
+ id: string;
+ providerId: string;
+ type: DataProviderType;
+ timelineById: TimelineById;
+}
+
+const updateTypeAndProvider = (
+ andProviderId: string,
+ type: DataProviderType,
+ providerId: string,
+ timeline: TimelineModel
+) =>
+ timeline.dataProviders.map((provider) =>
+ provider.id === providerId
+ ? {
+ ...provider,
+ and: provider.and.map((andProvider) =>
+ andProvider.id === andProviderId
+ ? {
+ ...andProvider,
+ type,
+ name: type === DataProviderType.template ? `${andProvider.queryMatch.field}` : '',
+ queryMatch: {
+ ...andProvider.queryMatch,
+ displayField: undefined,
+ displayValue: undefined,
+ value:
+ type === DataProviderType.template ? `{${andProvider.queryMatch.field}}` : '',
+ operator: (type === DataProviderType.template
+ ? IS_OPERATOR
+ : EXISTS_OPERATOR) as QueryOperator,
+ },
+ }
+ : andProvider
+ ),
+ }
+ : provider
+ );
+
+const updateTypeProvider = (type: DataProviderType, providerId: string, timeline: TimelineModel) =>
+ timeline.dataProviders.map((provider) =>
+ provider.id === providerId
+ ? {
+ ...provider,
+ type,
+ name: type === DataProviderType.template ? `${provider.queryMatch.field}` : '',
+ queryMatch: {
+ ...provider.queryMatch,
+ displayField: undefined,
+ displayValue: undefined,
+ value: type === DataProviderType.template ? `{${provider.queryMatch.field}}` : '',
+ operator: (type === DataProviderType.template
+ ? IS_OPERATOR
+ : EXISTS_OPERATOR) as QueryOperator,
+ },
+ }
+ : provider
+ );
+
+export const updateTimelineProviderType = ({
+ andProviderId,
+ id,
+ providerId,
+ type,
+ timelineById,
+}: UpdateTimelineProviderTypeParams): TimelineById => {
+ const timeline = timelineById[id];
+
+ if (timeline.timelineType !== TimelineType.template && type === DataProviderType.template) {
+ // Not supported, timeline template cannot have template type providers
+ return timelineById;
+ }
+
+ return {
+ ...timelineById,
+ [id]: {
+ ...timeline,
+ dataProviders: andProviderId
+ ? updateTypeAndProvider(andProviderId, type, providerId, timeline)
+ : updateTypeProvider(type, providerId, timeline),
+ },
+ };
+};
+
interface UpdateTimelineItemsPerPageParams {
id: string;
itemsPerPage: number;
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
index 57895fea8f8ff..a78fbc41ac430 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
@@ -87,9 +87,9 @@ export interface TimelineModel {
title: string;
/** timelineType: default | template */
timelineType: TimelineType;
- /** an unique id for template timeline */
+ /** an unique id for timeline template */
templateTimelineId: string | null;
- /** null for default timeline, number for template timeline */
+ /** null for default timeline, number for timeline template */
templateTimelineVersion: number | null;
/** Notes added to the timeline itself. Notes added to events are stored (separately) in `eventIdToNote` */
noteIds: string[];
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
index 6e7a36079a0c3..b8bdb4f2ad7f0 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
@@ -11,6 +11,7 @@ import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'
import {
IS_OPERATOR,
DataProvider,
+ DataProviderType,
DataProvidersAnd,
} from '../../../timelines/components/timeline/data_providers/data_provider';
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
@@ -35,6 +36,7 @@ import {
updateTimelinePerPageOptions,
updateTimelineProviderEnabled,
updateTimelineProviderExcluded,
+ updateTimelineProviderType,
updateTimelineProviders,
updateTimelineRange,
updateTimelineShowTimeline,
@@ -107,6 +109,14 @@ const timelineByIdMock: TimelineById = {
},
};
+const timelineByIdTemplateMock: TimelineById = {
+ ...timelineByIdMock,
+ foo: {
+ ...timelineByIdMock.foo,
+ timelineType: TimelineType.template,
+ },
+};
+
const columnsMock: ColumnHeaderOptions[] = [
defaultHeaders[0],
defaultHeaders[1],
@@ -1547,6 +1557,211 @@ describe('Timeline', () => {
});
});
+ describe('#updateTimelineProviderType', () => {
+ test('should return the same reference if run on timelineType default', () => {
+ const update = updateTimelineProviderType({
+ id: 'foo',
+ providerId: '123',
+ type: DataProviderType.template, // value we are updating from default to template
+ timelineById: timelineByIdMock,
+ });
+ expect(update).toBe(timelineByIdMock);
+ });
+
+ test('should return a new reference and not the same reference', () => {
+ const update = updateTimelineProviderType({
+ id: 'foo',
+ providerId: '123',
+ type: DataProviderType.template, // value we are updating from default to template
+ timelineById: timelineByIdTemplateMock,
+ });
+ expect(update).not.toBe(timelineByIdTemplateMock);
+ });
+
+ test('should return a new reference for data provider and not the same reference of data provider', () => {
+ const update = updateTimelineProviderType({
+ id: 'foo',
+ providerId: '123',
+ type: DataProviderType.template, // value we are updating from default to template
+ timelineById: timelineByIdTemplateMock,
+ });
+ expect(update.foo.dataProviders).not.toBe(timelineByIdTemplateMock.foo.dataProviders);
+ });
+
+ test('should update the timeline provider type from default to template', () => {
+ const update = updateTimelineProviderType({
+ id: 'foo',
+ providerId: '123',
+ type: DataProviderType.template, // value we are updating from default to template
+ timelineById: timelineByIdTemplateMock,
+ });
+ const expected: TimelineById = {
+ foo: {
+ id: 'foo',
+ savedObjectId: null,
+ columns: [],
+ dataProviders: [
+ {
+ and: [],
+ id: '123',
+ name: '', // This value changed
+ enabled: true,
+ excluded: false,
+ kqlQuery: '',
+ type: DataProviderType.template, // value we are updating from default to template
+ queryMatch: {
+ field: '',
+ value: '{}', // This value changed
+ operator: IS_OPERATOR,
+ },
+ },
+ ],
+ description: '',
+ deletedEventIds: [],
+ eventIdToNoteIds: {},
+ highlightedDropAndProviderId: '',
+ historyIds: [],
+ isFavorite: false,
+ isLive: false,
+ isSelectAllChecked: false,
+ isLoading: false,
+ kqlMode: 'filter',
+ kqlQuery: { filterQuery: null, filterQueryDraft: null },
+ loadingEventIds: [],
+ title: '',
+ timelineType: TimelineType.template,
+ templateTimelineVersion: null,
+ templateTimelineId: null,
+ noteIds: [],
+ dateRange: {
+ start: 0,
+ end: 0,
+ },
+ selectedEventIds: {},
+ show: true,
+ showRowRenderers: true,
+ showCheckboxes: false,
+ sort: {
+ columnId: '@timestamp',
+ sortDirection: Direction.desc,
+ },
+ status: TimelineStatus.active,
+ pinnedEventIds: {},
+ pinnedEventsSaveObject: {},
+ itemsPerPage: 25,
+ itemsPerPageOptions: [10, 25, 50],
+ width: DEFAULT_TIMELINE_WIDTH,
+ isSaving: false,
+ version: null,
+ },
+ };
+ expect(update).toEqual(expected);
+ });
+
+ test('should update only one data provider and not two data providers', () => {
+ const multiDataProvider = timelineByIdTemplateMock.foo.dataProviders.concat({
+ and: [],
+ id: '456',
+ name: 'data provider 1',
+ enabled: true,
+ excluded: false,
+ type: DataProviderType.template,
+ kqlQuery: '',
+ queryMatch: {
+ field: '',
+ value: '',
+ operator: IS_OPERATOR,
+ },
+ });
+ const multiDataProviderMock = set(
+ 'foo.dataProviders',
+ multiDataProvider,
+ timelineByIdTemplateMock
+ );
+ const update = updateTimelineProviderType({
+ id: 'foo',
+ providerId: '123',
+ type: DataProviderType.template, // value we are updating from default to template
+ timelineById: multiDataProviderMock,
+ });
+ const expected: TimelineById = {
+ foo: {
+ id: 'foo',
+ savedObjectId: null,
+ columns: [],
+ dataProviders: [
+ {
+ and: [],
+ id: '123',
+ name: '',
+ enabled: true,
+ excluded: false,
+ type: DataProviderType.template, // value we are updating from default to template
+ kqlQuery: '',
+ queryMatch: {
+ field: '',
+ value: '{}',
+ operator: IS_OPERATOR,
+ },
+ },
+ {
+ and: [],
+ id: '456',
+ name: 'data provider 1',
+ enabled: true,
+ excluded: false,
+ kqlQuery: '',
+ queryMatch: {
+ field: '',
+ value: '',
+ operator: IS_OPERATOR,
+ },
+ type: DataProviderType.template,
+ },
+ ],
+ description: '',
+ deletedEventIds: [],
+ eventIdToNoteIds: {},
+ highlightedDropAndProviderId: '',
+ historyIds: [],
+ isFavorite: false,
+ isLive: false,
+ isSelectAllChecked: false,
+ isLoading: false,
+ kqlMode: 'filter',
+ kqlQuery: { filterQuery: null, filterQueryDraft: null },
+ loadingEventIds: [],
+ title: '',
+ timelineType: TimelineType.template,
+ templateTimelineId: null,
+ templateTimelineVersion: null,
+ noteIds: [],
+ dateRange: {
+ start: 0,
+ end: 0,
+ },
+ selectedEventIds: {},
+ show: true,
+ showRowRenderers: true,
+ showCheckboxes: false,
+ sort: {
+ columnId: '@timestamp',
+ sortDirection: Direction.desc,
+ },
+ status: TimelineStatus.active,
+ pinnedEventIds: {},
+ pinnedEventsSaveObject: {},
+ itemsPerPage: 25,
+ itemsPerPageOptions: [10, 25, 50],
+ width: DEFAULT_TIMELINE_WIDTH,
+ isSaving: false,
+ version: null,
+ },
+ };
+ expect(update).toEqual(expected);
+ });
+ });
+
describe('#updateTimelineAndProviderExcluded', () => {
let timelineByIdwithAndMock: TimelineById = timelineByIdMock;
beforeEach(() => {
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
index 4072b4ac2f78b..6bb546c16b617 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
@@ -39,6 +39,7 @@ import {
updateDataProviderEnabled,
updateDataProviderExcluded,
updateDataProviderKqlQuery,
+ updateDataProviderType,
updateDescription,
updateEventType,
updateHighlightedDropAndProviderId,
@@ -88,6 +89,7 @@ import {
updateTimelineProviderExcluded,
updateTimelineProviderProperties,
updateTimelineProviderKqlQuery,
+ updateTimelineProviderType,
updateTimelineProviders,
updateTimelineRange,
updateTimelineShowTimeline,
@@ -427,7 +429,16 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
}),
})
)
-
+ .case(updateDataProviderType, (state, { id, type, providerId, andProviderId }) => ({
+ ...state,
+ timelineById: updateTimelineProviderType({
+ id,
+ type,
+ providerId,
+ timelineById: state.timelineById,
+ andProviderId,
+ }),
+ }))
.case(updateDataProviderKqlQuery, (state, { id, kqlQuery, providerId }) => ({
...state,
timelineById: updateTimelineProviderKqlQuery({
diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts
index 2daf259941cbf..7642db23812e1 100644
--- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts
@@ -8,14 +8,14 @@ import { httpServerMock } from '../../../../../src/core/server/mocks';
import { EndpointAppContextService } from './endpoint_app_context_services';
describe('test endpoint app context services', () => {
- it('should throw error on getAgentService if start is not called', async () => {
- const endpointAppContextService = new EndpointAppContextService();
- expect(() => endpointAppContextService.getAgentService()).toThrow(Error);
- });
- it('should return undefined on getManifestManager if start is not called', async () => {
- const endpointAppContextService = new EndpointAppContextService();
- expect(endpointAppContextService.getManifestManager()).toEqual(undefined);
- });
+ // it('should return undefined on getAgentService if dependencies are not enabled', async () => {
+ // const endpointAppContextService = new EndpointAppContextService();
+ // expect(endpointAppContextService.getAgentService()).toEqual(undefined);
+ // });
+ // it('should return undefined on getManifestManager if dependencies are not enabled', async () => {
+ // const endpointAppContextService = new EndpointAppContextService();
+ // expect(endpointAppContextService.getManifestManager()).toEqual(undefined);
+ // });
it('should throw error on getScopedSavedObjectsClient if start is not called', async () => {
const endpointAppContextService = new EndpointAppContextService();
expect(() =>
diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
index 97a82049634c4..f51e8c6be1040 100644
--- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
@@ -4,20 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
- SavedObjectsServiceStart,
KibanaRequest,
+ Logger,
+ SavedObjectsServiceStart,
SavedObjectsClientContract,
} from 'src/core/server';
import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server';
import { getPackageConfigCreateCallback } from './ingest_integration';
import { ManifestManager } from './services/artifacts';
-export type EndpointAppContextServiceStartContract = Pick<
- IngestManagerStartContract,
- 'agentService'
+export type EndpointAppContextServiceStartContract = Partial<
+ Pick
> & {
- manifestManager?: ManifestManager | undefined;
- registerIngestCallback: IngestManagerStartContract['registerExternalCallback'];
+ logger: Logger;
+ manifestManager?: ManifestManager;
+ registerIngestCallback?: IngestManagerStartContract['registerExternalCallback'];
savedObjectsStart: SavedObjectsServiceStart;
};
@@ -35,20 +36,17 @@ export class EndpointAppContextService {
this.manifestManager = dependencies.manifestManager;
this.savedObjectsStart = dependencies.savedObjectsStart;
- if (this.manifestManager !== undefined) {
+ if (this.manifestManager && dependencies.registerIngestCallback) {
dependencies.registerIngestCallback(
'packageConfigCreate',
- getPackageConfigCreateCallback(this.manifestManager)
+ getPackageConfigCreateCallback(dependencies.logger, this.manifestManager)
);
}
}
public stop() {}
- public getAgentService(): AgentService {
- if (!this.agentService) {
- throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`);
- }
+ public getAgentService(): AgentService | undefined {
return this.agentService;
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts
index ace5aec77ed2c..1acec1e7c53ac 100644
--- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { Logger } from '../../../../../src/core/server';
import { NewPackageConfig } from '../../../ingest_manager/common/types/models';
import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config';
import { NewPolicyData } from '../../common/endpoint/types';
@@ -13,6 +14,7 @@ import { ManifestManager } from './services/artifacts';
* Callback to handle creation of PackageConfigs in Ingest Manager
*/
export const getPackageConfigCreateCallback = (
+ logger: Logger,
manifestManager: ManifestManager
): ((newPackageConfig: NewPackageConfig) => Promise) => {
const handlePackageConfigCreate = async (
@@ -27,8 +29,19 @@ export const getPackageConfigCreateCallback = (
// follow the types/schema expected
let updatedPackageConfig = newPackageConfig as NewPolicyData;
- const wrappedManifest = await manifestManager.refresh({ initialize: true });
- if (wrappedManifest !== null) {
+ // get snapshot based on exception-list-agnostic SOs
+ // with diffs from last dispatched manifest, if it exists
+ const snapshot = await manifestManager.getSnapshot({ initialize: true });
+
+ if (snapshot === null) {
+ logger.warn('No manifest snapshot available.');
+ return updatedPackageConfig;
+ }
+
+ if (snapshot.diffs.length > 0) {
+ // create new artifacts
+ await manifestManager.syncArtifacts(snapshot, 'add');
+
// Until we get the Default Policy Configuration in the Endpoint package,
// we will add it here manually at creation time.
// @ts-ignore
@@ -42,7 +55,7 @@ export const getPackageConfigCreateCallback = (
streams: [],
config: {
artifact_manifest: {
- value: wrappedManifest.manifest.toEndpointFormat(),
+ value: snapshot.manifest.toEndpointFormat(),
},
policy: {
value: policyConfigFactory(),
@@ -57,9 +70,18 @@ export const getPackageConfigCreateCallback = (
try {
return updatedPackageConfig;
} finally {
- // TODO: confirm creation of package config
- // then commit.
- await manifestManager.commit(wrappedManifest);
+ if (snapshot.diffs.length > 0) {
+ // TODO: let's revisit the way this callback happens... use promises?
+ // only commit when we know the package config was created
+ try {
+ await manifestManager.commit(snapshot.manifest);
+
+ // clean up old artifacts
+ await manifestManager.syncArtifacts(snapshot, 'delete');
+ } catch (err) {
+ logger.error(err);
+ }
+ }
}
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts
index 5a0fb91345552..00c764d0b912e 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts
@@ -8,6 +8,7 @@ import { ExceptionsCache } from './cache';
describe('ExceptionsCache tests', () => {
let cache: ExceptionsCache;
+ const body = Buffer.from('body');
beforeEach(() => {
jest.clearAllMocks();
@@ -15,29 +16,33 @@ describe('ExceptionsCache tests', () => {
});
test('it should cache', async () => {
- cache.set('test', 'body');
+ cache.set('test', body);
const cacheResp = cache.get('test');
- expect(cacheResp).toEqual('body');
+ expect(cacheResp).toEqual(body);
});
test('it should handle cache miss', async () => {
- cache.set('test', 'body');
+ cache.set('test', body);
const cacheResp = cache.get('not test');
expect(cacheResp).toEqual(undefined);
});
test('it should handle cache eviction', async () => {
- cache.set('1', 'a');
- cache.set('2', 'b');
- cache.set('3', 'c');
+ const a = Buffer.from('a');
+ const b = Buffer.from('b');
+ const c = Buffer.from('c');
+ const d = Buffer.from('d');
+ cache.set('1', a);
+ cache.set('2', b);
+ cache.set('3', c);
const cacheResp = cache.get('1');
- expect(cacheResp).toEqual('a');
+ expect(cacheResp).toEqual(a);
- cache.set('4', 'd');
+ cache.set('4', d);
const secondResp = cache.get('1');
expect(secondResp).toEqual(undefined);
- expect(cache.get('2')).toEqual('b');
- expect(cache.get('3')).toEqual('c');
- expect(cache.get('4')).toEqual('d');
+ expect(cache.get('2')).toEqual(b);
+ expect(cache.get('3')).toEqual(c);
+ expect(cache.get('4')).toEqual(d);
});
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts
index b7a4c2feb6bf8..b9d3bae4e6ef9 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts
@@ -10,7 +10,7 @@ const DEFAULT_MAX_SIZE = 10;
* FIFO cache implementation for artifact downloads.
*/
export class ExceptionsCache {
- private cache: Map;
+ private cache: Map;
private queue: string[];
private maxSize: number;
@@ -20,7 +20,7 @@ export class ExceptionsCache {
this.maxSize = maxSize || DEFAULT_MAX_SIZE;
}
- set(id: string, body: string) {
+ set(id: string, body: Buffer) {
if (this.queue.length + 1 > this.maxSize) {
const entry = this.queue.shift();
if (entry !== undefined) {
@@ -31,7 +31,7 @@ export class ExceptionsCache {
this.cache.set(id, body);
}
- get(id: string): string | undefined {
+ get(id: string): Buffer | undefined {
return this.cache.get(id);
}
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts
index cf38147522083..9ad4554b30203 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts
@@ -8,10 +8,11 @@ export const ArtifactConstants = {
GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist',
SAVED_OBJECT_TYPE: 'endpoint:user-artifact:v2',
SUPPORTED_OPERATING_SYSTEMS: ['linux', 'macos', 'windows'],
- SCHEMA_VERSION: '1.0.0',
+ SCHEMA_VERSION: 'v1',
};
export const ManifestConstants = {
SAVED_OBJECT_TYPE: 'endpoint:user-artifact-manifest:v2',
- SCHEMA_VERSION: '1.0.0',
+ SCHEMA_VERSION: 'v1',
+ INITIAL_VERSION: 'WzAsMF0=',
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts
index 0a1cd556e6e91..acde455f77cb4 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts
@@ -46,7 +46,7 @@ describe('buildEventTypeSignal', () => {
const first = getFoundExceptionListItemSchemaMock();
mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
- const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0');
+ const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
expect(resp).toEqual({
entries: [expectedEndpointExceptions],
});
@@ -87,7 +87,7 @@ describe('buildEventTypeSignal', () => {
first.data[0].entries = testEntries;
mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
- const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0');
+ const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
expect(resp).toEqual({
entries: [expectedEndpointExceptions],
});
@@ -133,7 +133,7 @@ describe('buildEventTypeSignal', () => {
first.data[0].entries = testEntries;
mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
- const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0');
+ const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
expect(resp).toEqual({
entries: [expectedEndpointExceptions],
});
@@ -171,7 +171,7 @@ describe('buildEventTypeSignal', () => {
first.data[0].entries = testEntries;
mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
- const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0');
+ const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
expect(resp).toEqual({
entries: [expectedEndpointExceptions],
});
@@ -193,7 +193,7 @@ describe('buildEventTypeSignal', () => {
.mockReturnValueOnce(first)
.mockReturnValueOnce(second)
.mockReturnValueOnce(third);
- const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0');
+ const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
expect(resp.entries.length).toEqual(3);
});
@@ -202,7 +202,7 @@ describe('buildEventTypeSignal', () => {
exceptionsResponse.data = [];
exceptionsResponse.total = 0;
mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(exceptionsResponse);
- const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0');
+ const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
expect(resp.entries.length).toEqual(0);
});
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
index a13781519b508..556405adff62f 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
@@ -5,6 +5,7 @@
*/
import { createHash } from 'crypto';
+import { deflate } from 'zlib';
import { ExceptionListItemSchema } from '../../../../../lists/common/schemas';
import { validate } from '../../../../common/validate';
@@ -34,6 +35,7 @@ export async function buildArtifact(
const exceptionsBuffer = Buffer.from(JSON.stringify(exceptions));
const sha256 = createHash('sha256').update(exceptionsBuffer.toString()).digest('hex');
+ // Keep compression info empty in case its a duplicate. Lazily compress before committing if needed.
return {
identifier: `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-${os}-${schemaVersion}`,
compressionAlgorithm: 'none',
@@ -95,7 +97,7 @@ export function translateToEndpointExceptions(
exc: FoundExceptionListItemSchema,
schemaVersion: string
): TranslatedExceptionListItem[] {
- if (schemaVersion === '1.0.0') {
+ if (schemaVersion === 'v1') {
return exc.data.map((item) => {
return translateItem(schemaVersion, item);
});
@@ -180,3 +182,15 @@ function translateEntry(
}
}
}
+
+export async function compressExceptionList(buffer: Buffer): Promise {
+ return new Promise((resolve, reject) => {
+ deflate(buffer, function (err, buf) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(buf);
+ }
+ });
+ });
+}
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts
index 3e5fdbf9484ca..e1f6bac2620ea 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts
@@ -10,6 +10,7 @@ import {
getInternalArtifactMock,
getInternalArtifactMockWithDiffs,
} from '../../schemas/artifacts/saved_objects.mock';
+import { ManifestConstants } from './common';
import { Manifest } from './manifest';
describe('manifest', () => {
@@ -20,41 +21,45 @@ describe('manifest', () => {
let manifest2: Manifest;
beforeAll(async () => {
- const artifactLinux = await getInternalArtifactMock('linux', '1.0.0');
- const artifactMacos = await getInternalArtifactMock('macos', '1.0.0');
- const artifactWindows = await getInternalArtifactMock('windows', '1.0.0');
+ const artifactLinux = await getInternalArtifactMock('linux', 'v1');
+ const artifactMacos = await getInternalArtifactMock('macos', 'v1');
+ const artifactWindows = await getInternalArtifactMock('windows', 'v1');
artifacts.push(artifactLinux);
artifacts.push(artifactMacos);
artifacts.push(artifactWindows);
- manifest1 = new Manifest(now, '1.0.0', 'v0');
+ manifest1 = new Manifest(now, 'v1', ManifestConstants.INITIAL_VERSION);
manifest1.addEntry(artifactLinux);
manifest1.addEntry(artifactMacos);
manifest1.addEntry(artifactWindows);
manifest1.setVersion('abcd');
- const newArtifactLinux = await getInternalArtifactMockWithDiffs('linux', '1.0.0');
- manifest2 = new Manifest(new Date(), '1.0.0', 'v0');
+ const newArtifactLinux = await getInternalArtifactMockWithDiffs('linux', 'v1');
+ manifest2 = new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION);
manifest2.addEntry(newArtifactLinux);
manifest2.addEntry(artifactMacos);
manifest2.addEntry(artifactWindows);
});
test('Can create manifest with valid schema version', () => {
- const manifest = new Manifest(new Date(), '1.0.0', 'v0');
+ const manifest = new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION);
expect(manifest).toBeInstanceOf(Manifest);
});
test('Cannot create manifest with invalid schema version', () => {
expect(() => {
- new Manifest(new Date(), 'abcd' as ManifestSchemaVersion, 'v0');
+ new Manifest(
+ new Date(),
+ 'abcd' as ManifestSchemaVersion,
+ ManifestConstants.INITIAL_VERSION
+ );
}).toThrow();
});
test('Manifest transforms correctly to expected endpoint format', async () => {
expect(manifest1.toEndpointFormat()).toStrictEqual({
artifacts: {
- 'endpoint-exceptionlist-linux-1.0.0': {
+ 'endpoint-exceptionlist-linux-v1': {
compression_algorithm: 'none',
encryption_algorithm: 'none',
decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
@@ -62,9 +67,9 @@ describe('manifest', () => {
decoded_size: 430,
encoded_size: 430,
relative_url:
- '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-1.0.0/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
+ '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
},
- 'endpoint-exceptionlist-macos-1.0.0': {
+ 'endpoint-exceptionlist-macos-v1': {
compression_algorithm: 'none',
encryption_algorithm: 'none',
decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
@@ -72,9 +77,9 @@ describe('manifest', () => {
decoded_size: 430,
encoded_size: 430,
relative_url:
- '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-1.0.0/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
+ '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
},
- 'endpoint-exceptionlist-windows-1.0.0': {
+ 'endpoint-exceptionlist-windows-v1': {
compression_algorithm: 'none',
encryption_algorithm: 'none',
decoded_sha256: '5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
@@ -82,11 +87,11 @@ describe('manifest', () => {
decoded_size: 430,
encoded_size: 430,
relative_url:
- '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-1.0.0/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
+ '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
},
},
manifest_version: 'abcd',
- schema_version: '1.0.0',
+ schema_version: 'v1',
});
});
@@ -94,9 +99,9 @@ describe('manifest', () => {
expect(manifest1.toSavedObject()).toStrictEqual({
created: now.getTime(),
ids: [
- 'endpoint-exceptionlist-linux-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
- 'endpoint-exceptionlist-macos-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
- 'endpoint-exceptionlist-windows-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
+ 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
+ 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
+ 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
],
});
});
@@ -106,12 +111,12 @@ describe('manifest', () => {
expect(diffs).toEqual([
{
id:
- 'endpoint-exceptionlist-linux-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
+ 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
type: 'delete',
},
{
id:
- 'endpoint-exceptionlist-linux-1.0.0-3d3546e94f70493021ee845be32c66e36ea7a720c64b4d608d8029fe949f7e51',
+ 'endpoint-exceptionlist-linux-v1-3d3546e94f70493021ee845be32c66e36ea7a720c64b4d608d8029fe949f7e51',
type: 'add',
},
]);
@@ -119,7 +124,7 @@ describe('manifest', () => {
test('Manifest returns data for given artifact', async () => {
const artifact = artifacts[0];
- const returned = manifest1.getArtifact(`${artifact.identifier}-${artifact.encodedSha256}`);
+ const returned = manifest1.getArtifact(`${artifact.identifier}-${artifact.decodedSha256}`);
expect(returned).toEqual(artifact);
});
@@ -127,34 +132,39 @@ describe('manifest', () => {
const entries = manifest1.getEntries();
const keys = Object.keys(entries);
expect(keys).toEqual([
- 'endpoint-exceptionlist-linux-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
- 'endpoint-exceptionlist-macos-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
- 'endpoint-exceptionlist-windows-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
+ 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
+ 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
+ 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
]);
});
test('Manifest returns true if contains artifact', async () => {
const found = manifest1.contains(
- 'endpoint-exceptionlist-macos-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735'
+ 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735'
);
expect(found).toEqual(true);
});
test('Manifest can be created from list of artifacts', async () => {
- const manifest = Manifest.fromArtifacts(artifacts, '1.0.0', 'v0');
+ const oldManifest = new Manifest(
+ new Date(),
+ ManifestConstants.SCHEMA_VERSION,
+ ManifestConstants.INITIAL_VERSION
+ );
+ const manifest = Manifest.fromArtifacts(artifacts, 'v1', oldManifest);
expect(
manifest.contains(
- 'endpoint-exceptionlist-linux-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735'
+ 'endpoint-exceptionlist-linux-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735'
)
).toEqual(true);
expect(
manifest.contains(
- 'endpoint-exceptionlist-macos-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735'
+ 'endpoint-exceptionlist-macos-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735'
)
).toEqual(true);
expect(
manifest.contains(
- 'endpoint-exceptionlist-windows-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735'
+ 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735'
)
).toEqual(true);
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts
index c343568226e22..576ecb08d6923 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts
@@ -11,6 +11,7 @@ import {
ManifestSchemaVersion,
} from '../../../../common/endpoint/schema/common';
import { ManifestSchema, manifestSchema } from '../../../../common/endpoint/schema/manifest';
+import { ManifestConstants } from './common';
import { ManifestEntry } from './manifest_entry';
export interface ManifestDiff {
@@ -46,11 +47,17 @@ export class Manifest {
public static fromArtifacts(
artifacts: InternalArtifactSchema[],
schemaVersion: string,
- version: string
+ oldManifest: Manifest
): Manifest {
- const manifest = new Manifest(new Date(), schemaVersion, version);
+ const manifest = new Manifest(new Date(), schemaVersion, oldManifest.getVersion());
artifacts.forEach((artifact) => {
- manifest.addEntry(artifact);
+ const id = `${artifact.identifier}-${artifact.decodedSha256}`;
+ const existingArtifact = oldManifest.getArtifact(id);
+ if (existingArtifact) {
+ manifest.addEntry(existingArtifact);
+ } else {
+ manifest.addEntry(artifact);
+ }
});
return manifest;
}
@@ -80,8 +87,8 @@ export class Manifest {
return this.entries;
}
- public getArtifact(artifactId: string): InternalArtifactSchema {
- return this.entries[artifactId].getArtifact();
+ public getArtifact(artifactId: string): InternalArtifactSchema | undefined {
+ return this.entries[artifactId]?.getArtifact();
}
public diff(manifest: Manifest): ManifestDiff[] {
@@ -104,7 +111,7 @@ export class Manifest {
public toEndpointFormat(): ManifestSchema {
const manifestObj: ManifestSchema = {
- manifest_version: this.version ?? 'v0',
+ manifest_version: this.version ?? ManifestConstants.INITIAL_VERSION,
schema_version: this.schemaVersion,
artifacts: {},
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts
index a52114ad90258..7ea2a07210c55 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts
@@ -14,7 +14,7 @@ describe('manifest_entry', () => {
let manifestEntry: ManifestEntry;
beforeAll(async () => {
- artifact = await getInternalArtifactMock('windows', '1.0.0');
+ artifact = await getInternalArtifactMock('windows', 'v1');
manifestEntry = new ManifestEntry(artifact);
});
@@ -24,12 +24,12 @@ describe('manifest_entry', () => {
test('Correct doc_id is returned', () => {
expect(manifestEntry.getDocId()).toEqual(
- 'endpoint-exceptionlist-windows-1.0.0-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735'
+ 'endpoint-exceptionlist-windows-v1-5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735'
);
});
test('Correct identifier is returned', () => {
- expect(manifestEntry.getIdentifier()).toEqual('endpoint-exceptionlist-windows-1.0.0');
+ expect(manifestEntry.getIdentifier()).toEqual('endpoint-exceptionlist-windows-v1');
});
test('Correct sha256 is returned', () => {
@@ -48,7 +48,7 @@ describe('manifest_entry', () => {
test('Correct url is returned', () => {
expect(manifestEntry.getUrl()).toEqual(
- '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-1.0.0/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735'
+ '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735'
);
});
@@ -65,8 +65,10 @@ describe('manifest_entry', () => {
decoded_size: 430,
encoded_size: 430,
relative_url:
- '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-1.0.0/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
+ '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/5f16e5e338c53e77cfa945c17c11b175c3967bf109aa87131de41fb93b149735',
});
});
+
+ // TODO: add test for entry with compression
});
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts
index c23258c4c3ba4..b35e0c2b9ad6e 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts
@@ -5,6 +5,7 @@
*/
import { InternalArtifactSchema } from '../../schemas/artifacts';
+import { CompressionAlgorithm } from '../../../../common/endpoint/schema/common';
import { ManifestEntrySchema } from '../../../../common/endpoint/schema/manifest';
export class ManifestEntry {
@@ -15,13 +16,17 @@ export class ManifestEntry {
}
public getDocId(): string {
- return `${this.getIdentifier()}-${this.getEncodedSha256()}`;
+ return `${this.getIdentifier()}-${this.getDecodedSha256()}`;
}
public getIdentifier(): string {
return this.artifact.identifier;
}
+ public getCompressionAlgorithm(): CompressionAlgorithm {
+ return this.artifact.compressionAlgorithm;
+ }
+
public getEncodedSha256(): string {
return this.artifact.encodedSha256;
}
@@ -39,7 +44,7 @@ export class ManifestEntry {
}
public getUrl(): string {
- return `/api/endpoint/artifacts/download/${this.getIdentifier()}/${this.getEncodedSha256()}`;
+ return `/api/endpoint/artifacts/download/${this.getIdentifier()}/${this.getDecodedSha256()}`;
}
public getArtifact(): InternalArtifactSchema {
@@ -48,7 +53,7 @@ export class ManifestEntry {
public getRecord(): ManifestEntrySchema {
return {
- compression_algorithm: 'none',
+ compression_algorithm: this.getCompressionAlgorithm(),
encryption_algorithm: 'none',
decoded_sha256: this.getDecodedSha256(),
decoded_size: this.getDecodedSize(),
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts
index 78b60e9e61f3e..aa7f56e815d58 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts
@@ -88,20 +88,22 @@ export class ManifestTask {
return;
}
- manifestManager
- .refresh()
- .then((wrappedManifest) => {
- if (wrappedManifest) {
- return manifestManager.dispatch(wrappedManifest);
- }
- })
- .then((wrappedManifest) => {
- if (wrappedManifest) {
- return manifestManager.commit(wrappedManifest);
- }
- })
- .catch((err) => {
- this.logger.error(err);
- });
+ try {
+ // get snapshot based on exception-list-agnostic SOs
+ // with diffs from last dispatched manifest
+ const snapshot = await manifestManager.getSnapshot();
+ if (snapshot && snapshot.diffs.length > 0) {
+ // create new artifacts
+ await manifestManager.syncArtifacts(snapshot, 'add');
+ // write to ingest-manager package config
+ await manifestManager.dispatch(snapshot.manifest);
+ // commit latest manifest state to user-artifact-manifest SO
+ await manifestManager.commit(snapshot.manifest);
+ // clean up old artifacts
+ await manifestManager.syncArtifacts(snapshot, 'delete');
+ }
+ } catch (err) {
+ this.logger.error(err);
+ }
};
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts
index fbcd3bd130dfd..8c6faee7f7a5d 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts
@@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import { deflateSync, inflateSync } from 'zlib';
import {
ILegacyClusterClient,
IRouter,
@@ -29,7 +30,7 @@ import { createMockEndpointAppContextServiceStartContract } from '../../mocks';
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
import { WrappedTranslatedExceptionList } from '../../schemas/artifacts/lists';
-const mockArtifactName = `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-windows-1.0.0`;
+const mockArtifactName = `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-windows-v1`;
const expectedEndpointExceptions: WrappedTranslatedExceptionList = {
entries: [
{
@@ -93,7 +94,6 @@ describe('test alerts route', () => {
let mockScopedClient: jest.Mocked;
let mockSavedObjectClient: jest.Mocked;
let mockResponse: jest.Mocked;
- // @ts-ignore
let routeConfig: RouteConfig;
let routeHandler: RequestHandler;
let endpointAppContextService: EndpointAppContextService;
@@ -114,8 +114,9 @@ describe('test alerts route', () => {
// The authentication with the Fleet Plugin needs a separate scoped SO Client
ingestSavedObjectClient = savedObjectsClientMock.create();
ingestSavedObjectClient.find.mockReturnValue(Promise.resolve(mockIngestSOResponse));
- // @ts-ignore
- startContract.savedObjectsStart.getScopedClient.mockReturnValue(ingestSavedObjectClient);
+ (startContract.savedObjectsStart.getScopedClient as jest.Mock).mockReturnValue(
+ ingestSavedObjectClient
+ );
endpointAppContextService.start(startContract);
registerDownloadExceptionListRoute(
@@ -146,11 +147,11 @@ describe('test alerts route', () => {
references: [],
attributes: {
identifier: mockArtifactName,
- schemaVersion: '1.0.0',
+ schemaVersion: 'v1',
sha256: '123456',
encoding: 'application/json',
created: Date.now(),
- body: Buffer.from(JSON.stringify(expectedEndpointExceptions)).toString('base64'),
+ body: deflateSync(JSON.stringify(expectedEndpointExceptions)).toString('base64'),
size: 100,
},
};
@@ -163,6 +164,8 @@ describe('test alerts route', () => {
path.startsWith('/api/endpoint/artifacts/download')
)!;
+ expect(routeConfig.options).toEqual(undefined);
+
await routeHandler(
({
core: {
@@ -176,14 +179,16 @@ describe('test alerts route', () => {
);
const expectedHeaders = {
- 'content-encoding': 'application/json',
- 'content-disposition': `attachment; filename=${mockArtifactName}.json`,
+ 'content-encoding': 'identity',
+ 'content-disposition': `attachment; filename=${mockArtifactName}.zz`,
};
expect(mockResponse.ok).toBeCalled();
expect(mockResponse.ok.mock.calls[0][0]?.headers).toEqual(expectedHeaders);
- const artifact = mockResponse.ok.mock.calls[0][0]?.body;
- expect(artifact).toEqual(Buffer.from(mockArtifact.attributes.body, 'base64').toString());
+ const artifact = inflateSync(mockResponse.ok.mock.calls[0][0]?.body as Buffer).toString();
+ expect(artifact).toEqual(
+ inflateSync(Buffer.from(mockArtifact.attributes.body, 'base64')).toString()
+ );
});
it('should handle fetching a non-existent artifact', async () => {
@@ -233,7 +238,7 @@ describe('test alerts route', () => {
// Add to the download cache
const mockArtifact = expectedEndpointExceptions;
const cacheKey = `${mockArtifactName}-${mockSha}`;
- cache.set(cacheKey, JSON.stringify(mockArtifact));
+ cache.set(cacheKey, Buffer.from(JSON.stringify(mockArtifact))); // TODO: add compression here
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/artifacts/download')
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts
index 337393e768a8f..1b364a04a4272 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts
@@ -43,9 +43,7 @@ export function registerDownloadExceptionListRoute(
DownloadArtifactRequestParamsSchema
>(downloadArtifactRequestParamsSchema),
},
- options: { tags: [] },
},
- // @ts-ignore
async (context, req, res) => {
let scopedSOClient: SavedObjectsClientContract;
const logger = endpointContext.logFactory.get('download_exception_list');
@@ -55,19 +53,19 @@ export function registerDownloadExceptionListRoute(
scopedSOClient = endpointContext.service.getScopedSavedObjectsClient(req);
await authenticateAgentWithAccessToken(scopedSOClient, req);
} catch (err) {
- if (err.output.statusCode === 401) {
+ if ((err.isBoom ? err.output.statusCode : err.statusCode) === 401) {
return res.unauthorized();
} else {
return res.notFound();
}
}
- const buildAndValidateResponse = (artName: string, body: string): IKibanaResponse => {
+ const buildAndValidateResponse = (artName: string, body: Buffer): IKibanaResponse => {
const artifact: HttpResponseOptions = {
body,
headers: {
- 'content-encoding': 'application/json',
- 'content-disposition': `attachment; filename=${artName}.json`,
+ 'content-encoding': 'identity',
+ 'content-disposition': `attachment; filename=${artName}.zz`,
},
};
@@ -90,7 +88,7 @@ export function registerDownloadExceptionListRoute(
return scopedSOClient
.get(ArtifactConstants.SAVED_OBJECT_TYPE, id)
.then((artifact: SavedObject) => {
- const body = Buffer.from(artifact.attributes.body, 'base64').toString();
+ const body = Buffer.from(artifact.attributes.body, 'base64');
cache.set(id, body);
return buildAndValidateResponse(artifact.attributes.identifier, body);
})
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts
index 235e7152b83cf..4b2eb3ea1ddb0 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts
@@ -18,6 +18,7 @@ import {
HostStatus,
} from '../../../../common/endpoint/types';
import { EndpointAppContext } from '../../types';
+import { AgentService } from '../../../../../ingest_manager/server';
import { Agent, AgentStatus } from '../../../../../ingest_manager/common/types/models';
import { findAllUnenrolledAgentIds } from './support/unenroll';
@@ -26,8 +27,9 @@ interface HitSource {
}
interface MetadataRequestContext {
+ agentService: AgentService;
+ logger: Logger;
requestHandlerContext: RequestHandlerContext;
- endpointAppContext: EndpointAppContext;
}
const HOST_STATUS_MAPPING = new Map([
@@ -35,8 +37,12 @@ const HOST_STATUS_MAPPING = new Map([
['offline', HostStatus.OFFLINE],
]);
+const getLogger = (endpointAppContext: EndpointAppContext): Logger => {
+ return endpointAppContext.logFactory.get('metadata');
+};
+
export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) {
- const logger = endpointAppContext.logFactory.get('metadata');
+ const logger = getLogger(endpointAppContext);
router.post(
{
path: '/api/endpoint/metadata',
@@ -66,12 +72,23 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
})
),
},
- options: { authRequired: true },
+ options: { authRequired: true, tags: ['access:securitySolution'] },
},
async (context, req, res) => {
try {
+ const agentService = endpointAppContext.service.getAgentService();
+ if (agentService === undefined) {
+ throw new Error('agentService not available');
+ }
+
+ const metadataRequestContext: MetadataRequestContext = {
+ agentService,
+ logger,
+ requestHandlerContext: context,
+ };
+
const unenrolledAgentIds = await findAllUnenrolledAgentIds(
- endpointAppContext.service.getAgentService(),
+ agentService,
context.core.savedObjects.client
);
@@ -88,11 +105,9 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
'search',
queryParams
)) as SearchResponse;
+
return res.ok({
- body: await mapToHostResultList(queryParams, response, {
- endpointAppContext,
- requestHandlerContext: context,
- }),
+ body: await mapToHostResultList(queryParams, response, metadataRequestContext),
});
} catch (err) {
logger.warn(JSON.stringify(err, null, 2));
@@ -107,17 +122,22 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
validate: {
params: schema.object({ id: schema.string() }),
},
- options: { authRequired: true },
+ options: { authRequired: true, tags: ['access:securitySolution'] },
},
async (context, req, res) => {
+ const agentService = endpointAppContext.service.getAgentService();
+ if (agentService === undefined) {
+ return res.internalError({ body: 'agentService not available' });
+ }
+
+ const metadataRequestContext: MetadataRequestContext = {
+ agentService,
+ logger,
+ requestHandlerContext: context,
+ };
+
try {
- const doc = await getHostData(
- {
- endpointAppContext,
- requestHandlerContext: context,
- },
- req.params.id
- );
+ const doc = await getHostData(metadataRequestContext, req.params.id);
if (doc) {
return res.ok({ body: doc });
}
@@ -164,17 +184,16 @@ async function findAgent(
metadataRequestContext: MetadataRequestContext,
hostMetadata: HostMetadata
): Promise {
- const logger = metadataRequestContext.endpointAppContext.logFactory.get('metadata');
try {
- return await metadataRequestContext.endpointAppContext.service
- .getAgentService()
- .getAgent(
- metadataRequestContext.requestHandlerContext.core.savedObjects.client,
- hostMetadata.elastic.agent.id
- );
+ return await metadataRequestContext.agentService.getAgent(
+ metadataRequestContext.requestHandlerContext.core.savedObjects.client,
+ hostMetadata.elastic.agent.id
+ );
} catch (e) {
if (e.isBoom && e.output.statusCode === 404) {
- logger.warn(`agent with id ${hostMetadata.elastic.agent.id} not found`);
+ metadataRequestContext.logger.warn(
+ `agent with id ${hostMetadata.elastic.agent.id} not found`
+ );
return undefined;
} else {
throw e;
@@ -217,7 +236,7 @@ async function enrichHostMetadata(
): Promise {
let hostStatus = HostStatus.ERROR;
let elasticAgentId = hostMetadata?.elastic?.agent?.id;
- const log = logger(metadataRequestContext.endpointAppContext);
+ const log = metadataRequestContext.logger;
try {
/**
* Get agent status by elastic agent id if available or use the host id.
@@ -228,12 +247,10 @@ async function enrichHostMetadata(
log.warn(`Missing elastic agent id, using host id instead ${elasticAgentId}`);
}
- const status = await metadataRequestContext.endpointAppContext.service
- .getAgentService()
- .getAgentStatusById(
- metadataRequestContext.requestHandlerContext.core.savedObjects.client,
- elasticAgentId
- );
+ const status = await metadataRequestContext.agentService.getAgentStatusById(
+ metadataRequestContext.requestHandlerContext.core.savedObjects.client,
+ elasticAgentId
+ );
hostStatus = HOST_STATUS_MAPPING.get(status) || HostStatus.ERROR;
} catch (e) {
if (e.isBoom && e.output.statusCode === 404) {
@@ -248,7 +265,3 @@ async function enrichHostMetadata(
host_status: hostStatus,
};
}
-
-const logger = (endpointAppContext: EndpointAppContext): Logger => {
- return endpointAppContext.logFactory.get('metadata');
-};
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts
index 42cce382ec20c..81027b42eb64f 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts
@@ -47,8 +47,9 @@ describe('test endpoint route', () => {
let routeHandler: RequestHandler;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let routeConfig: RouteConfig;
- let mockAgentService: ReturnType<
- typeof createMockEndpointAppContextServiceStartContract
+ // tests assume that ingestManager is enabled, and thus agentService is available
+ let mockAgentService: Required<
+ ReturnType
>['agentService'];
let endpointAppContextService: EndpointAppContextService;
const noUnenrolledAgent = {
@@ -70,7 +71,7 @@ describe('test endpoint route', () => {
endpointAppContextService = new EndpointAppContextService();
const startContract = createMockEndpointAppContextServiceStartContract();
endpointAppContextService.start(startContract);
- mockAgentService = startContract.agentService;
+ mockAgentService = startContract.agentService!;
registerEndpointRoutes(routerMock, {
logFactory: loggingSystemMock.create(),
@@ -97,7 +98,7 @@ describe('test endpoint route', () => {
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
- expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] });
expect(mockResponse.ok).toBeCalled();
const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList;
expect(endpointResultList.hosts.length).toEqual(1);
@@ -139,7 +140,7 @@ describe('test endpoint route', () => {
expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query).toEqual({
match_all: {},
});
- expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] });
expect(mockResponse.ok).toBeCalled();
const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList;
expect(endpointResultList.hosts.length).toEqual(1);
@@ -202,7 +203,7 @@ describe('test endpoint route', () => {
],
},
});
- expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] });
expect(mockResponse.ok).toBeCalled();
const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList;
expect(endpointResultList.hosts.length).toEqual(1);
@@ -234,7 +235,10 @@ describe('test endpoint route', () => {
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
- expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(routeConfig.options).toEqual({
+ authRequired: true,
+ tags: ['access:securitySolution'],
+ });
expect(mockResponse.notFound).toBeCalled();
const message = mockResponse.notFound.mock.calls[0][0]?.body;
expect(message).toEqual('Endpoint Not Found');
@@ -263,7 +267,10 @@ describe('test endpoint route', () => {
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
- expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(routeConfig.options).toEqual({
+ authRequired: true,
+ tags: ['access:securitySolution'],
+ });
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result).toHaveProperty('metadata.Endpoint');
@@ -298,7 +305,10 @@ describe('test endpoint route', () => {
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
- expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(routeConfig.options).toEqual({
+ authRequired: true,
+ tags: ['access:securitySolution'],
+ });
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result.host_status).toEqual(HostStatus.ERROR);
@@ -328,7 +338,10 @@ describe('test endpoint route', () => {
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
- expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(routeConfig.options).toEqual({
+ authRequired: true,
+ tags: ['access:securitySolution'],
+ });
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result.host_status).toEqual(HostStatus.ERROR);
diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/common.ts
index 3c066e150288a..d5a30951e9398 100644
--- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/common.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/common.ts
@@ -6,14 +6,19 @@
import * as t from 'io-ts';
-export const body = t.string;
+export const buffer = new t.Type(
+ 'buffer',
+ (input: unknown): input is Buffer => Buffer.isBuffer(input),
+ (input, context) => (Buffer.isBuffer(input) ? t.success(input) : t.failure(input, context)),
+ t.identity
+);
-export const created = t.number; // TODO: Make this into an ISO Date string check
+export const created = t.number;
export const encoding = t.keyof({
- 'application/json': null,
+ identity: null,
});
export const schemaVersion = t.keyof({
- '1.0.0': null,
+ v1: null,
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts
index 537f7707889e4..3705062449c60 100644
--- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts
@@ -5,9 +5,8 @@
*/
import * as t from 'io-ts';
-import { encoding } from '../common';
+import { buffer, encoding } from '../common';
-const body = t.string;
const headers = t.exact(
t.type({
'content-encoding': encoding,
@@ -17,7 +16,7 @@ const headers = t.exact(
export const downloadArtifactResponseSchema = t.exact(
t.type({
- body,
+ body: buffer,
headers,
})
);
diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts
index e4cd7f48a2901..aa11f4409269a 100644
--- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts
@@ -12,7 +12,9 @@ import {
sha256,
size,
} from '../../../../common/endpoint/schema/common';
-import { body, created } from './common';
+import { created } from './common';
+
+export const body = t.string; // base64
export const internalArtifactSchema = t.exact(
t.type({
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts
index 08e29b5c6b82b..3e3b12c04d65c 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts
@@ -27,7 +27,7 @@ describe('artifact_client', () => {
test('can create artifact', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
const artifactClient = getArtifactClientMock(savedObjectsClient);
- const artifact = await getInternalArtifactMock('linux', '1.0.0');
+ const artifact = await getInternalArtifactMock('linux', 'v1');
await artifactClient.createArtifact(artifact);
expect(savedObjectsClient.create).toHaveBeenCalledWith(
ArtifactConstants.SAVED_OBJECT_TYPE,
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts
index e899905602c8d..ca53a891c4d6b 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts
@@ -16,7 +16,7 @@ export class ArtifactClient {
}
public getArtifactId(artifact: InternalArtifactSchema) {
- return `${artifact.identifier}-${artifact.encodedSha256}`;
+ return `${artifact.identifier}-${artifact.decodedSha256}`;
}
public async getArtifact(id: string): Promise> {
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.mock.ts
index bfeacbcedf2cb..d869ed9493abc 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.mock.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.mock.ts
@@ -12,7 +12,7 @@ export const getManifestClientMock = (
savedObjectsClient?: SavedObjectsClientContract
): ManifestClient => {
if (savedObjectsClient !== undefined) {
- return new ManifestClient(savedObjectsClient, '1.0.0');
+ return new ManifestClient(savedObjectsClient, 'v1');
}
- return new ManifestClient(savedObjectsClientMock.create(), '1.0.0');
+ return new ManifestClient(savedObjectsClientMock.create(), 'v1');
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts
index 5780c6279ee6a..fe3f193bc8ff5 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts
@@ -14,7 +14,7 @@ import { ManifestClient } from './manifest_client';
describe('manifest_client', () => {
describe('ManifestClient sanity checks', () => {
test('can create ManifestClient', () => {
- const manifestClient = new ManifestClient(savedObjectsClientMock.create(), '1.0.0');
+ const manifestClient = new ManifestClient(savedObjectsClientMock.create(), 'v1');
expect(manifestClient).toBeInstanceOf(ManifestClient);
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts
index 483b3434d63f2..dfbe2572076d0 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts
@@ -15,6 +15,7 @@ import {
buildArtifact,
getFullEndpointExceptionList,
} from '../../../lib/artifacts';
+import { ManifestConstants } from '../../../lib/artifacts/common';
import { InternalArtifactSchema } from '../../../schemas/artifacts';
import { getArtifactClientMock } from '../artifact_client.mock';
import { getManifestClientMock } from '../manifest_client.mock';
@@ -69,13 +70,13 @@ async function mockBuildExceptionListArtifacts(
export class ManifestManagerMock extends ManifestManager {
// @ts-ignore
private buildExceptionListArtifacts = async () => {
- return mockBuildExceptionListArtifacts('linux', '1.0.0');
+ return mockBuildExceptionListArtifacts('linux', 'v1');
};
// @ts-ignore
private getLastDispatchedManifest = jest
.fn()
- .mockResolvedValue(new Manifest(new Date(), '1.0.0', 'v0'));
+ .mockResolvedValue(new Manifest(new Date(), 'v1', ManifestConstants.INITIAL_VERSION));
// @ts-ignore
private getManifestClient = jest
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts
index 1d6dffadde61a..b1cbc41459f15 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { inflateSync } from 'zlib';
import { savedObjectsClientMock } from 'src/core/server/mocks';
import {
ArtifactConstants,
@@ -15,32 +16,33 @@ import { getPackageConfigServiceMock, getManifestManagerMock } from './manifest_
describe('manifest_manager', () => {
describe('ManifestManager sanity checks', () => {
- test('ManifestManager can refresh manifest', async () => {
+ test('ManifestManager can snapshot manifest', async () => {
const manifestManager = getManifestManagerMock();
- const manifestWrapper = await manifestManager.refresh();
- expect(manifestWrapper!.diffs).toEqual([
+ const snapshot = await manifestManager.getSnapshot();
+ expect(snapshot!.diffs).toEqual([
{
id:
- 'endpoint-exceptionlist-linux-1.0.0-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc',
+ 'endpoint-exceptionlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc',
type: 'add',
},
]);
- expect(manifestWrapper!.manifest).toBeInstanceOf(Manifest);
+ expect(snapshot!.manifest).toBeInstanceOf(Manifest);
});
test('ManifestManager populates cache properly', async () => {
const cache = new ExceptionsCache(5);
const manifestManager = getManifestManagerMock({ cache });
- const manifestWrapper = await manifestManager.refresh();
- expect(manifestWrapper!.diffs).toEqual([
+ const snapshot = await manifestManager.getSnapshot();
+ expect(snapshot!.diffs).toEqual([
{
id:
- 'endpoint-exceptionlist-linux-1.0.0-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc',
+ 'endpoint-exceptionlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc',
type: 'add',
},
]);
- const diff = manifestWrapper!.diffs[0];
- const entry = JSON.parse(cache.get(diff!.id)!);
+ await manifestManager.syncArtifacts(snapshot!, 'add');
+ const diff = snapshot!.diffs[0];
+ const entry = JSON.parse(inflateSync(cache.get(diff!.id)! as Buffer).toString());
expect(entry).toEqual({
entries: [
{
@@ -73,16 +75,16 @@ describe('manifest_manager', () => {
test('ManifestManager can dispatch manifest', async () => {
const packageConfigService = getPackageConfigServiceMock();
const manifestManager = getManifestManagerMock({ packageConfigService });
- const manifestWrapperRefresh = await manifestManager.refresh();
- const manifestWrapperDispatch = await manifestManager.dispatch(manifestWrapperRefresh);
- expect(manifestWrapperRefresh).toEqual(manifestWrapperDispatch);
- const entries = manifestWrapperDispatch!.manifest.getEntries();
+ const snapshot = await manifestManager.getSnapshot();
+ const dispatched = await manifestManager.dispatch(snapshot!.manifest);
+ expect(dispatched).toEqual(true);
+ const entries = snapshot!.manifest.getEntries();
const artifact = Object.values(entries)[0].getArtifact();
expect(
packageConfigService.update.mock.calls[0][2].inputs[0].config.artifact_manifest.value
).toEqual({
- manifest_version: 'v0',
- schema_version: '1.0.0',
+ manifest_version: ManifestConstants.INITIAL_VERSION,
+ schema_version: 'v1',
artifacts: {
[artifact.identifier]: {
compression_algorithm: 'none',
@@ -91,7 +93,7 @@ describe('manifest_manager', () => {
encoded_sha256: artifact.encodedSha256,
decoded_size: artifact.decodedSize,
encoded_size: artifact.encodedSize,
- relative_url: `/api/endpoint/artifacts/download/${artifact.identifier}/${artifact.encodedSha256}`,
+ relative_url: `/api/endpoint/artifacts/download/${artifact.identifier}/${artifact.decodedSha256}`,
},
},
});
@@ -103,15 +105,21 @@ describe('manifest_manager', () => {
savedObjectsClient,
});
- const manifestWrapperRefresh = await manifestManager.refresh();
- const manifestWrapperDispatch = await manifestManager.dispatch(manifestWrapperRefresh);
+ const snapshot = await manifestManager.getSnapshot();
+ await manifestManager.syncArtifacts(snapshot!, 'add');
+
const diff = {
id: 'abcd',
type: 'delete',
};
- manifestWrapperDispatch!.diffs.push(diff);
+ snapshot!.diffs.push(diff);
+
+ const dispatched = await manifestManager.dispatch(snapshot!.manifest);
+ expect(dispatched).toEqual(true);
+
+ await manifestManager.commit(snapshot!.manifest);
- await manifestManager.commit(manifestWrapperDispatch);
+ await manifestManager.syncArtifacts(snapshot!, 'delete');
// created new artifact
expect(savedObjectsClient.create.mock.calls[0][0]).toEqual(
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
index f7bc711d4bd05..b9e289cee62af 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Logger, SavedObjectsClientContract, SavedObject } from 'src/core/server';
+import { Logger, SavedObjectsClientContract } from 'src/core/server';
+import { createHash } from 'crypto';
import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server';
import { ExceptionListClient } from '../../../../../../lists/server';
import { ManifestSchemaVersion } from '../../../../../common/endpoint/schema/common';
@@ -17,9 +18,10 @@ import {
ExceptionsCache,
ManifestDiff,
} from '../../../lib/artifacts';
-import { InternalArtifactSchema, InternalManifestSchema } from '../../../schemas/artifacts';
+import { InternalArtifactSchema } from '../../../schemas/artifacts';
import { ArtifactClient } from '../artifact_client';
import { ManifestClient } from '../manifest_client';
+import { compressExceptionList } from '../../../lib/artifacts/lists';
export interface ManifestManagerContext {
savedObjectsClient: SavedObjectsClientContract;
@@ -30,11 +32,11 @@ export interface ManifestManagerContext {
cache: ExceptionsCache;
}
-export interface ManifestRefreshOpts {
+export interface ManifestSnapshotOpts {
initialize?: boolean;
}
-export interface WrappedManifest {
+export interface ManifestSnapshot {
manifest: Manifest;
diffs: ManifestDiff[];
}
@@ -56,215 +58,264 @@ export class ManifestManager {
this.cache = context.cache;
}
- private getManifestClient(schemaVersion: string): ManifestClient {
+ /**
+ * Gets a ManifestClient for the provided schemaVersion.
+ *
+ * @param schemaVersion
+ */
+ private getManifestClient(schemaVersion: string) {
return new ManifestClient(this.savedObjectsClient, schemaVersion as ManifestSchemaVersion);
}
- private async buildExceptionListArtifacts(
- schemaVersion: string
- ): Promise {
- const artifacts: InternalArtifactSchema[] = [];
+ /**
+ * Builds an array of artifacts (one per supported OS) based on the current
+ * state of exception-list-agnostic SO's.
+ *
+ * @param schemaVersion
+ */
+ private async buildExceptionListArtifacts(schemaVersion: string) {
+ return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.reduce(
+ async (acc: Promise, os) => {
+ const exceptionList = await getFullEndpointExceptionList(
+ this.exceptionListClient,
+ os,
+ schemaVersion
+ );
+ const artifacts = await acc;
+ const artifact = await buildArtifact(exceptionList, os, schemaVersion);
+ artifacts.push(artifact);
+ return Promise.resolve(artifacts);
+ },
+ Promise.resolve([])
+ );
+ }
+
+ /**
+ * Returns the last dispatched manifest based on the current state of the
+ * user-artifact-manifest SO.
+ *
+ * @param schemaVersion
+ */
+ private async getLastDispatchedManifest(schemaVersion: string) {
+ try {
+ const manifestClient = this.getManifestClient(schemaVersion);
+ const manifestSo = await manifestClient.getManifest();
- for (const os of ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS) {
- const exceptionList = await getFullEndpointExceptionList(
- this.exceptionListClient,
- os,
- schemaVersion
+ if (manifestSo.version === undefined) {
+ throw new Error('No version returned for manifest.');
+ }
+
+ const manifest = new Manifest(
+ new Date(manifestSo.attributes.created),
+ schemaVersion,
+ manifestSo.version
);
- const artifact = await buildArtifact(exceptionList, os, schemaVersion);
- artifacts.push(artifact);
+ for (const id of manifestSo.attributes.ids) {
+ const artifactSo = await this.artifactClient.getArtifact(id);
+ manifest.addEntry(artifactSo.attributes);
+ }
+ return manifest;
+ } catch (err) {
+ if (err.output.statusCode !== 404) {
+ throw err;
+ }
+ return null;
}
-
- return artifacts;
}
- private async getLastDispatchedManifest(schemaVersion: string): Promise {
- return this.getManifestClient(schemaVersion)
- .getManifest()
- .then(async (manifestSo: SavedObject) => {
- if (manifestSo.version === undefined) {
- throw new Error('No version returned for manifest.');
- }
- const manifest = new Manifest(
- new Date(manifestSo.attributes.created),
- schemaVersion,
- manifestSo.version
- );
-
- for (const id of manifestSo.attributes.ids) {
- const artifactSo = await this.artifactClient.getArtifact(id);
- manifest.addEntry(artifactSo.attributes);
- }
-
- return manifest;
- })
- .catch((err) => {
- if (err.output.statusCode !== 404) {
- throw err;
- }
+ /**
+ * Snapshots a manifest based on current state of exception-list-agnostic SOs.
+ *
+ * @param opts TODO
+ */
+ public async getSnapshot(opts?: ManifestSnapshotOpts) {
+ try {
+ let oldManifest: Manifest | null;
+
+ // Get the last-dispatched manifest
+ oldManifest = await this.getLastDispatchedManifest(ManifestConstants.SCHEMA_VERSION);
+
+ if (oldManifest === null && opts !== undefined && opts.initialize) {
+ oldManifest = new Manifest(
+ new Date(),
+ ManifestConstants.SCHEMA_VERSION,
+ ManifestConstants.INITIAL_VERSION
+ ); // create empty manifest
+ } else if (oldManifest == null) {
+ this.logger.debug('Manifest does not exist yet. Waiting...');
return null;
- });
- }
+ }
+
+ // Build new exception list artifacts
+ const artifacts = await this.buildExceptionListArtifacts(ArtifactConstants.SCHEMA_VERSION);
- public async refresh(opts?: ManifestRefreshOpts): Promise {
- let oldManifest: Manifest | null;
+ // Build new manifest
+ const newManifest = Manifest.fromArtifacts(
+ artifacts,
+ ManifestConstants.SCHEMA_VERSION,
+ oldManifest
+ );
- // Get the last-dispatched manifest
- oldManifest = await this.getLastDispatchedManifest(ManifestConstants.SCHEMA_VERSION);
+ // Get diffs
+ const diffs = newManifest.diff(oldManifest);
- if (oldManifest === null && opts !== undefined && opts.initialize) {
- oldManifest = new Manifest(new Date(), ManifestConstants.SCHEMA_VERSION, 'v0'); // create empty manifest
- } else if (oldManifest == null) {
- this.logger.debug('Manifest does not exist yet. Waiting...');
+ return {
+ manifest: newManifest,
+ diffs,
+ };
+ } catch (err) {
+ this.logger.error(err);
return null;
}
+ }
- // Build new exception list artifacts
- const artifacts = await this.buildExceptionListArtifacts(ArtifactConstants.SCHEMA_VERSION);
-
- // Build new manifest
- const newManifest = Manifest.fromArtifacts(
- artifacts,
- ManifestConstants.SCHEMA_VERSION,
- oldManifest.getVersion()
- );
+ /**
+ * Syncs artifacts based on provided snapshot.
+ *
+ * Creates artifacts that do not yet exist and cleans up old artifacts that have been
+ * superceded by this snapshot.
+ *
+ * Can be filtered to apply one or both operations.
+ *
+ * @param snapshot
+ * @param diffType
+ */
+ public async syncArtifacts(snapshot: ManifestSnapshot, diffType?: 'add' | 'delete') {
+ const filteredDiffs = snapshot.diffs.reduce((diffs: ManifestDiff[], diff) => {
+ if (diff.type === diffType || diffType === undefined) {
+ diffs.push(diff);
+ } else if (!['add', 'delete'].includes(diff.type)) {
+ // TODO: replace with io-ts schema
+ throw new Error(`Unsupported diff type: ${diff.type}`);
+ }
+ return diffs;
+ }, []);
+
+ const adds = filteredDiffs.filter((diff) => {
+ return diff.type === 'add';
+ });
+
+ const deletes = filteredDiffs.filter((diff) => {
+ return diff.type === 'delete';
+ });
+
+ for (const diff of adds) {
+ const artifact = snapshot.manifest.getArtifact(diff.id);
+ if (artifact === undefined) {
+ throw new Error(
+ `Corrupted manifest detected. Diff contained artifact ${diff.id} not in manifest.`
+ );
+ }
+ const compressedArtifact = await compressExceptionList(Buffer.from(artifact.body, 'base64'));
+ artifact.body = compressedArtifact.toString('base64');
+ artifact.encodedSize = compressedArtifact.byteLength;
+ artifact.compressionAlgorithm = 'zlib';
+ artifact.encodedSha256 = createHash('sha256').update(compressedArtifact).digest('hex');
- // Get diffs
- const diffs = newManifest.diff(oldManifest);
-
- // Create new artifacts
- for (const diff of diffs) {
- if (diff.type === 'add') {
- const artifact = newManifest.getArtifact(diff.id);
- try {
- await this.artifactClient.createArtifact(artifact);
-
- // Cache the body of the artifact
- this.cache.set(diff.id, Buffer.from(artifact.body, 'base64').toString());
- } catch (err) {
- if (err.status === 409) {
- // This artifact already existed...
- this.logger.debug(`Tried to create artifact ${diff.id}, but it already exists.`);
- } else {
- throw err;
- }
+ try {
+ await this.artifactClient.createArtifact(artifact);
+ } catch (err) {
+ if (err.status === 409) {
+ this.logger.debug(`Tried to create artifact ${diff.id}, but it already exists.`);
+ } else {
+ throw err;
}
}
+ // Cache the body of the artifact
+ this.cache.set(diff.id, Buffer.from(artifact.body, 'base64'));
}
- return {
- manifest: newManifest,
- diffs,
- };
+ for (const diff of deletes) {
+ await this.artifactClient.deleteArtifact(diff.id);
+ // TODO: should we delete the cache entry here?
+ this.logger.info(`Cleaned up artifact ${diff.id}`);
+ }
}
/**
- * Dispatches the manifest by writing it to the endpoint packageConfig.
+ * Dispatches the manifest by writing it to the endpoint package config.
*
- * @return {WrappedManifest | null} WrappedManifest if all dispatched, else null
*/
- public async dispatch(wrappedManifest: WrappedManifest | null): Promise {
- if (wrappedManifest === null) {
- this.logger.debug('wrappedManifest was null, aborting dispatch');
- return null;
- }
-
- function showDiffs(diffs: ManifestDiff[]) {
- return diffs.map((diff) => {
- const op = diff.type === 'add' ? '(+)' : '(-)';
- return `${op}${diff.id}`;
+ public async dispatch(manifest: Manifest) {
+ let paging = true;
+ let page = 1;
+ let success = true;
+
+ while (paging) {
+ const { items, total } = await this.packageConfigService.list(this.savedObjectsClient, {
+ page,
+ perPage: 20,
+ kuery: 'ingest-package-configs.package.name:endpoint',
});
- }
- if (wrappedManifest.diffs.length > 0) {
- this.logger.info(`Dispatching new manifest with diffs: ${showDiffs(wrappedManifest.diffs)}`);
-
- let paging = true;
- let page = 1;
- let success = true;
-
- while (paging) {
- const { items, total } = await this.packageConfigService.list(this.savedObjectsClient, {
- page,
- perPage: 20,
- kuery: 'ingest-package-configs.package.name:endpoint',
- });
-
- for (const packageConfig of items) {
- const { id, revision, updated_at, updated_by, ...newPackageConfig } = packageConfig;
-
- if (
- newPackageConfig.inputs.length > 0 &&
- newPackageConfig.inputs[0].config !== undefined
- ) {
- const artifactManifest = newPackageConfig.inputs[0].config.artifact_manifest ?? {
- value: {},
- };
- artifactManifest.value = wrappedManifest.manifest.toEndpointFormat();
- newPackageConfig.inputs[0].config.artifact_manifest = artifactManifest;
-
- await this.packageConfigService
- .update(this.savedObjectsClient, id, newPackageConfig)
- .then((response) => {
- this.logger.debug(`Updated package config ${id}`);
- })
- .catch((err) => {
- success = false;
- this.logger.debug(`Error updating package config ${id}`);
- this.logger.error(err);
- });
- } else {
+ for (const packageConfig of items) {
+ const { id, revision, updated_at, updated_by, ...newPackageConfig } = packageConfig;
+ if (newPackageConfig.inputs.length > 0 && newPackageConfig.inputs[0].config !== undefined) {
+ const artifactManifest = newPackageConfig.inputs[0].config.artifact_manifest ?? {
+ value: {},
+ };
+ artifactManifest.value = manifest.toEndpointFormat();
+ newPackageConfig.inputs[0].config.artifact_manifest = artifactManifest;
+
+ try {
+ await this.packageConfigService.update(this.savedObjectsClient, id, newPackageConfig);
+ this.logger.debug(
+ `Updated package config ${id} with manifest version ${manifest.getVersion()}`
+ );
+ } catch (err) {
success = false;
- this.logger.debug(`Package config ${id} has no config.`);
+ this.logger.debug(`Error updating package config ${id}`);
+ this.logger.error(err);
}
+ } else {
+ success = false;
+ this.logger.debug(`Package config ${id} has no config.`);
}
-
- paging = page * items.length < total;
- page++;
}
- return success ? wrappedManifest : null;
- } else {
- this.logger.debug('No manifest diffs [no-op]');
+ paging = page * items.length < total;
+ page++;
}
- return null;
+ // TODO: revisit success logic
+ return success;
}
- public async commit(wrappedManifest: WrappedManifest | null) {
- if (wrappedManifest === null) {
- this.logger.debug('wrappedManifest was null, aborting commit');
- return;
- }
-
- const manifestClient = this.getManifestClient(wrappedManifest.manifest.getSchemaVersion());
+ /**
+ * Commits a manifest to indicate that it has been dispatched.
+ *
+ * @param manifest
+ */
+ public async commit(manifest: Manifest) {
+ const manifestClient = this.getManifestClient(manifest.getSchemaVersion());
// Commit the new manifest
- if (wrappedManifest.manifest.getVersion() === 'v0') {
- await manifestClient.createManifest(wrappedManifest.manifest.toSavedObject());
+ if (manifest.getVersion() === ManifestConstants.INITIAL_VERSION) {
+ await manifestClient.createManifest(manifest.toSavedObject());
} else {
- const version = wrappedManifest.manifest.getVersion();
- if (version === 'v0') {
+ const version = manifest.getVersion();
+ if (version === ManifestConstants.INITIAL_VERSION) {
throw new Error('Updating existing manifest with baseline version. Bad state.');
}
- await manifestClient.updateManifest(wrappedManifest.manifest.toSavedObject(), {
+ await manifestClient.updateManifest(manifest.toSavedObject(), {
version,
});
}
- this.logger.info(`Commited manifest ${wrappedManifest.manifest.getVersion()}`);
+ this.logger.info(`Committed manifest ${manifest.getVersion()}`);
+ }
- // Clean up old artifacts
- for (const diff of wrappedManifest.diffs) {
- try {
- if (diff.type === 'delete') {
- await this.artifactClient.deleteArtifact(diff.id);
- this.logger.info(`Cleaned up artifact ${diff.id}`);
- }
- } catch (err) {
- this.logger.error(err);
- }
- }
+ /**
+ * Confirms that a packageConfig exists with provided name.
+ */
+ public async confirmPackageConfigExists(name: string) {
+ // TODO: what if there are multiple results? uh oh.
+ const { total } = await this.packageConfigService.list(this.savedObjectsClient, {
+ page: 1,
+ perPage: 20,
+ kuery: `ingest-package-configs.name:${name}`,
+ });
+ return total > 0;
}
}
diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts
index a9d07389797db..e46d3be44dbd1 100644
--- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts
+++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts
@@ -84,6 +84,12 @@ export const timelineSchema = gql`
kqlQuery: String
queryMatch: QueryMatchInput
and: [DataProviderInput!]
+ type: DataProviderType
+ }
+
+ enum DataProviderType {
+ default
+ template
}
input KueryFilterQueryInput {
@@ -194,6 +200,7 @@ export const timelineSchema = gql`
excluded: Boolean
kqlQuery: String
queryMatch: QueryMatchResult
+ type: DataProviderType
and: [DataProviderResult!]
}
diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts
index a702b1a72f0a9..52bb4a9862160 100644
--- a/x-pack/plugins/security_solution/server/graphql/types.ts
+++ b/x-pack/plugins/security_solution/server/graphql/types.ts
@@ -187,6 +187,8 @@ export interface DataProviderInput {
queryMatch?: Maybe;
and?: Maybe;
+
+ type?: Maybe;
}
export interface QueryMatchInput {
@@ -344,6 +346,11 @@ export enum TlsFields {
_id = '_id',
}
+export enum DataProviderType {
+ default = 'default',
+ template = 'template',
+}
+
export enum TimelineStatus {
active = 'active',
draft = 'draft',
@@ -2032,6 +2039,8 @@ export interface DataProviderResult {
queryMatch?: Maybe;
+ type?: Maybe;
+
and?: Maybe;
}
@@ -8368,6 +8377,8 @@ export namespace DataProviderResultResolvers {
queryMatch?: QueryMatchResolver, TypeParent, TContext>;
+ type?: TypeResolver, TypeParent, TContext>;
+
and?: AndResolver, TypeParent, TContext>;
}
@@ -8401,6 +8412,11 @@ export namespace DataProviderResultResolvers {
Parent = DataProviderResult,
TContext = SiemContext
> = Resolver;
+ export type TypeResolver<
+ R = Maybe,
+ Parent = DataProviderResult,
+ TContext = SiemContext
+ > = Resolver;
export type AndResolver<
R = Maybe,
Parent = DataProviderResult,
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts
index 68e7f8d5e6fe1..eb8f6f5022985 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts
@@ -35,6 +35,7 @@ export const pickSavedTimeline = (
if (savedTimeline.timelineType === TimelineType.default) {
savedTimeline.timelineType = savedTimeline.timelineType ?? TimelineType.default;
+ savedTimeline.status = savedTimeline.status ?? TimelineStatus.active;
savedTimeline.templateTimelineId = null;
savedTimeline.templateTimelineVersion = null;
}
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts
index f5345c3dce222..0286ef558810e 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts
@@ -28,7 +28,7 @@ import {
import {
CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
CREATE_TIMELINE_ERROR_MESSAGE,
-} from './utils/create_timelines';
+} from './utils/failure_cases';
describe('create timelines', () => {
let server: ReturnType;
@@ -167,8 +167,8 @@ describe('create timelines', () => {
});
});
- describe('Manipulate template timeline', () => {
- describe('Create a new template timeline', () => {
+ describe('Manipulate timeline template', () => {
+ describe('Create a new timeline template', () => {
beforeEach(async () => {
jest.doMock('../saved_object', () => {
return {
@@ -199,19 +199,19 @@ describe('create timelines', () => {
await server.inject(mockRequest, context);
});
- test('should Create a new template timeline savedObject', async () => {
+ test('should Create a new timeline template savedObject', async () => {
expect(mockPersistTimeline).toHaveBeenCalled();
});
- test('should Create a new template timeline savedObject without timelineId', async () => {
+ test('should Create a new timeline template savedObject without timelineId', async () => {
expect(mockPersistTimeline.mock.calls[0][1]).toBeNull();
});
- test('should Create a new template timeline savedObject without template timeline version', async () => {
+ test('should Create a new timeline template savedObject without timeline template version', async () => {
expect(mockPersistTimeline.mock.calls[0][2]).toBeNull();
});
- test('should Create a new template timeline savedObject witn given template timeline', async () => {
+ test('should Create a new timeline template savedObject witn given timeline template', async () => {
expect(mockPersistTimeline.mock.calls[0][3]).toEqual(
createTemplateTimelineWithTimelineId.timeline
);
@@ -234,7 +234,7 @@ describe('create timelines', () => {
});
});
- describe('Create a template timeline already exist', () => {
+ describe('Create a timeline template already exist', () => {
beforeEach(() => {
jest.doMock('../saved_object', () => {
return {
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts
index 15fb8f3411cfa..248bf358064c0 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts
@@ -409,7 +409,7 @@ describe('import timelines', () => {
});
});
-describe('import template timelines', () => {
+describe('import timeline templates', () => {
let server: ReturnType;
let request: ReturnType;
let securitySetup: SecurityPluginSetup;
@@ -473,7 +473,7 @@ describe('import template timelines', () => {
}));
});
- describe('Import a new template timeline', () => {
+ describe('Import a new timeline template', () => {
beforeEach(() => {
jest.doMock('../saved_object', () => {
return {
@@ -596,7 +596,7 @@ describe('import template timelines', () => {
});
});
- describe('Import a template timeline already exist', () => {
+ describe('Import a timeline template already exist', () => {
beforeEach(() => {
jest.doMock('../saved_object', () => {
return {
@@ -704,7 +704,7 @@ describe('import template timelines', () => {
expect(response.status).toEqual(200);
});
- test('should throw error if with given template timeline version conflict', async () => {
+ test('should throw error if with given timeline template version conflict', async () => {
mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue([
mockDuplicateIdErrors,
[
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts
index fb4991d7d1e7d..56e4e81b4214b 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts
@@ -46,7 +46,7 @@ import { createTimelines } from './utils/create_timelines';
import { TimelineStatus } from '../../../../common/types/timeline';
const CHUNK_PARSED_OBJECT_SIZE = 10;
-const DEFAULT_IMPORT_ERROR = `Something went wrong, there's something we didn't handle properly, please help us improve by providing the file you try to import on https://discuss.elastic.co/c/security/siem`;
+const DEFAULT_IMPORT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`;
export const importTimelinesRoute = (
router: IRouter,
@@ -158,7 +158,7 @@ export const importTimelinesRoute = (
await compareTimelinesStatus.init();
const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline;
if (compareTimelinesStatus.isCreatableViaImport) {
- // create timeline / template timeline
+ // create timeline / timeline template
newTimeline = await createTimelines({
frameworkRequest,
timeline: {
@@ -199,7 +199,7 @@ export const importTimelinesRoute = (
);
} else {
if (compareTimelinesStatus.isUpdatableViaImport) {
- // update template timeline
+ // update timeline template
newTimeline = await createTimelines({
frameworkRequest,
timeline: parsedTimelineObject,
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts
index 3cedb925649a2..17e6e8a84ef22 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts
@@ -168,8 +168,8 @@ describe('update timelines', () => {
});
});
- describe('Manipulate template timeline', () => {
- describe('Update an existing template timeline', () => {
+ describe('Manipulate timeline template', () => {
+ describe('Update an existing timeline template', () => {
beforeEach(async () => {
jest.doMock('../saved_object', () => {
return {
@@ -209,25 +209,25 @@ describe('update timelines', () => {
);
});
- test('should Update existing template timeline with template timelineId', async () => {
+ test('should Update existing timeline template with timeline templateId', async () => {
expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual(
updateTemplateTimelineWithTimelineId.timeline.templateTimelineId
);
});
- test('should Update existing template timeline with timelineId', async () => {
+ test('should Update existing timeline template with timelineId', async () => {
expect(mockPersistTimeline.mock.calls[0][1]).toEqual(
updateTemplateTimelineWithTimelineId.timelineId
);
});
- test('should Update existing template timeline with timeline version', async () => {
+ test('should Update existing timeline template with timeline version', async () => {
expect(mockPersistTimeline.mock.calls[0][2]).toEqual(
updateTemplateTimelineWithTimelineId.version
);
});
- test('should Update existing template timeline witn given timeline', async () => {
+ test('should Update existing timeline template witn given timeline', async () => {
expect(mockPersistTimeline.mock.calls[0][3]).toEqual(
updateTemplateTimelineWithTimelineId.timeline
);
@@ -241,7 +241,7 @@ describe('update timelines', () => {
expect(mockPersistNote).not.toBeCalled();
});
- test('returns 200 when create template timeline successfully', async () => {
+ test('returns 200 when create timeline template successfully', async () => {
const response = await server.inject(
getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId),
context
@@ -250,7 +250,7 @@ describe('update timelines', () => {
});
});
- describe("Update a template timeline that doesn't exist", () => {
+ describe("Update a timeline template that doesn't exist", () => {
beforeEach(() => {
jest.doMock('../saved_object', () => {
return {
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts
index a6d379e534bc2..6e3e3a420963f 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts
@@ -179,8 +179,8 @@ describe('CompareTimelinesStatus', () => {
});
});
- describe('template timeline', () => {
- describe('given template timeline exists', () => {
+ describe('timeline template', () => {
+ describe('given timeline template exists', () => {
const mockGetTimeline: jest.Mock = jest.fn();
const mockGetTemplateTimeline: jest.Mock = jest.fn();
@@ -249,12 +249,12 @@ describe('CompareTimelinesStatus', () => {
expect(timelineObj.isUpdatableViaImport).toEqual(true);
});
- test('should indicate we are handling a template timeline', () => {
+ test('should indicate we are handling a timeline template', () => {
expect(timelineObj.isHandlingTemplateTimeline).toEqual(true);
});
});
- describe('given template timeline does NOT exists', () => {
+ describe('given timeline template does NOT exists', () => {
const mockGetTimeline: jest.Mock = jest.fn();
const mockGetTemplateTimeline: jest.Mock = jest.fn();
@@ -339,7 +339,7 @@ describe('CompareTimelinesStatus', () => {
expect(error?.body).toEqual(UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE);
});
- test('should indicate we are handling a template timeline', () => {
+ test('should indicate we are handling a timeline template', () => {
expect(timelineObj.isHandlingTemplateTimeline).toEqual(true);
});
});
@@ -427,7 +427,7 @@ describe('CompareTimelinesStatus', () => {
});
});
- describe('template timeline', () => {
+ describe('timeline template', () => {
const mockGetTimeline: jest.Mock = jest.fn();
const mockGetTemplateTimeline: jest.Mock = jest.fn();
let timelineObj: TimelinesStatusType;
@@ -589,7 +589,7 @@ describe('CompareTimelinesStatus', () => {
});
});
- describe('immutable template timeline', () => {
+ describe('immutable timeline template', () => {
const mockGetTimeline: jest.Mock = jest.fn();
const mockGetTemplateTimeline: jest.Mock = jest.fn();
let timelineObj: TimelinesStatusType;
@@ -662,7 +662,7 @@ describe('CompareTimelinesStatus', () => {
});
});
- describe('If create template timeline without template timeline id', () => {
+ describe('If create timeline template without timeline template id', () => {
const mockGetTimeline: jest.Mock = jest.fn();
const mockGetTemplateTimeline: jest.Mock = jest.fn();
@@ -724,7 +724,7 @@ describe('CompareTimelinesStatus', () => {
});
});
- describe('Throw error if template timeline version is conflict when update via import', () => {
+ describe('Throw error if timeline template version is conflict when update via import', () => {
const mockGetTimeline: jest.Mock = jest.fn();
const mockGetTemplateTimeline: jest.Mock = jest.fn();
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts
index abe298566341c..67965469e1a9f 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts
@@ -13,11 +13,6 @@ import { SavedTimeline, TimelineSavedObject } from '../../../../../common/types/
import { SavedNote } from '../../../../../common/types/timeline/note';
import { NoteResult, ResponseTimeline } from '../../../../graphql/types';
-export const CREATE_TIMELINE_ERROR_MESSAGE =
- 'UPDATE timeline with POST is not allowed, please use PATCH instead';
-export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE =
- 'UPDATE template timeline with POST is not allowed, please use PATCH instead';
-
export const saveTimelines = (
frameworkRequest: FrameworkRequest,
timeline: SavedTimeline,
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts
index 60ba5389280c4..d41e8fc190983 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts
@@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+
import { isEmpty } from 'lodash/fp';
import {
TimelineSavedObject,
@@ -11,27 +12,31 @@ import {
} from '../../../../../common/types/timeline';
export const UPDATE_TIMELINE_ERROR_MESSAGE =
- 'CREATE timeline with PATCH is not allowed, please use POST instead';
+ 'You cannot create new timelines with PATCH. Use POST instead.';
export const UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE =
- "CREATE template timeline with PATCH is not allowed, please use POST instead (Given template timeline doesn't exist)";
+ 'You cannot create new Timeline templates with PATCH. Use POST instead (templateTimelineId does not exist).';
export const NO_MATCH_VERSION_ERROR_MESSAGE =
- 'TimelineVersion conflict: The given version doesn not match with existing timeline';
+ 'Timeline template version conflict. The provided templateTimelineVersion does not match the current template.';
export const NO_MATCH_ID_ERROR_MESSAGE =
- "Timeline id doesn't match with existing template timeline";
-export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE = 'Template timelineVersion conflict';
+ 'There are no Timeline templates that match the provided templateTimelineId.';
+export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE =
+ 'To update existing Timeline templates, you must increment the templateTimelineVersion value.';
export const CREATE_TIMELINE_ERROR_MESSAGE =
- 'UPDATE timeline with POST is not allowed, please use PATCH instead';
+ 'You cannot update timelines with POST. Use PATCH instead.';
export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE =
- 'UPDATE template timeline with POST is not allowed, please use PATCH instead';
-export const EMPTY_TITLE_ERROR_MESSAGE = 'Title cannot be empty';
-export const UPDATE_STATUS_ERROR_MESSAGE = 'Update an immutable timeline is is not allowed';
+ 'You cannot update Timeline templates with POST. Use PATCH instead.';
+export const EMPTY_TITLE_ERROR_MESSAGE = 'The title field cannot be empty.';
+export const UPDATE_STATUS_ERROR_MESSAGE =
+ 'You are not allowed to set the status field value to immutable.';
export const CREATE_TEMPLATE_TIMELINE_WITHOUT_VERSION_ERROR_MESSAGE =
- 'Create template timeline without a valid templateTimelineVersion is not allowed. Please start from 1 to create a new template timeline';
-export const CREATE_WITH_INVALID_STATUS_ERROR_MESSAGE = 'Cannot create a draft timeline';
-export const NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE = 'Update status is not allowed';
-export const NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE = 'Update timelineType is not allowed';
+ 'You must provide a valid templateTimelineVersion value. Use 1 for new Timeline templates.';
+export const CREATE_WITH_INVALID_STATUS_ERROR_MESSAGE =
+ 'You are not allowed to set the status field value to draft.';
+export const NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE = 'You are not allowed to set the status field.';
+export const NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE =
+ 'You cannot convert a Timeline template to a timeline, or a timeline to a Timeline template.';
export const UPDAT_TIMELINE_VIA_IMPORT_NOT_ALLOWED_ERROR_MESSAGE =
- 'Update timeline via import is not allowed';
+ 'You cannot update a timeline via imports. Use the UI to modify existing timelines.';
const isUpdatingStatus = (
isHandlingTemplateTimeline: boolean,
@@ -81,8 +86,8 @@ const commonUpdateTemplateTimelineCheck = (
}
if (existTemplateTimeline == null && templateTimelineVersion != null) {
- // template timeline !exists
- // Throw error to create template timeline in patch
+ // timeline template !exists
+ // Throw error to create timeline template in patch
return {
body: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
statusCode: 405,
@@ -94,7 +99,7 @@ const commonUpdateTemplateTimelineCheck = (
existTemplateTimeline != null &&
existTimeline.savedObjectId !== existTemplateTimeline.savedObjectId
) {
- // Throw error you can not have a no matching between your timeline and your template timeline during an update
+ // Throw error you can not have a no matching between your timeline and your timeline template during an update
return {
body: NO_MATCH_ID_ERROR_MESSAGE,
statusCode: 409,
@@ -191,7 +196,7 @@ const createTemplateTimelineCheck = (
existTemplateTimeline: TimelineSavedObject | null
) => {
if (isHandlingTemplateTimeline && existTemplateTimeline != null) {
- // Throw error to create template timeline in patch
+ // Throw error to create timeline template in patch
return {
body: CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
statusCode: 405,
@@ -264,7 +269,7 @@ export const checkIsUpdateViaImportFailureCases = (
existTemplateTimeline.templateTimelineVersion != null &&
existTemplateTimeline.templateTimelineVersion >= templateTimelineVersion
) {
- // Throw error you can not update a template timeline version with an old version
+ // Throw error you can not update a timeline template version with an old version
return {
body: TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE,
statusCode: 409,
@@ -365,7 +370,7 @@ export const checkIsCreateViaImportFailureCases = (
}
} else {
if (existTemplateTimeline != null) {
- // Throw error to create template timeline in patch
+ // Throw error to create timeline template in patch
return {
body: getImportExistingTimelineError(existTemplateTimeline.savedObjectId),
statusCode: 405,
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts
index ec90fc6d8e071..f4dbd2db3329c 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts
@@ -7,11 +7,7 @@
import { getOr } from 'lodash/fp';
import { SavedObjectsFindOptions } from '../../../../../../src/core/server';
-import {
- UNAUTHENTICATED_USER,
- disableTemplate,
- enableElasticFilter,
-} from '../../../common/constants';
+import { UNAUTHENTICATED_USER, enableElasticFilter } from '../../../common/constants';
import { NoteSavedObject } from '../../../common/types/timeline/note';
import { PinnedEventSavedObject } from '../../../common/types/timeline/pinned_event';
import {
@@ -158,10 +154,9 @@ const getTimelineTypeFilter = (
? `siem-ui-timeline.attributes.createdBy: "Elsatic"`
: `not siem-ui-timeline.attributes.createdBy: "Elastic"`;
- const filters =
- !disableTemplate && enableElasticFilter
- ? [typeFilter, draftFilter, immutableFilter, templateTimelineTypeFilter]
- : [typeFilter, draftFilter, immutableFilter];
+ const filters = enableElasticFilter
+ ? [typeFilter, draftFilter, immutableFilter, templateTimelineTypeFilter]
+ : [typeFilter, draftFilter, immutableFilter];
return filters.filter((f) => f != null).join(' and ');
};
@@ -183,16 +178,7 @@ export const getAllTimeline = async (
searchFields: onlyUserFavorite
? ['title', 'description', 'favorite.keySearch']
: ['title', 'description'],
- /**
- * CreateTemplateTimelineBtn
- * Remove the comment here to enable template timeline and apply the change below
- * filter: getTimelineTypeFilter(timelineType, templateTimelineType, false)
- */
- filter: getTimelineTypeFilter(
- disableTemplate ? TimelineType.default : timelineType,
- disableTemplate ? null : templateTimelineType,
- disableTemplate ? null : status
- ),
+ filter: getTimelineTypeFilter(timelineType, templateTimelineType, status),
sortField: sort != null ? sort.sortField : undefined,
sortOrder: sort != null ? sort.sortOrder : undefined,
};
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts
index 51bff033b8791..22b98930f3181 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts
@@ -64,6 +64,9 @@ export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = {
kqlQuery: {
type: 'text',
},
+ type: {
+ type: 'text',
+ },
queryMatch: {
properties: {
field: {
@@ -100,6 +103,9 @@ export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = {
kqlQuery: {
type: 'text',
},
+ type: {
+ type: 'text',
+ },
queryMatch: {
properties: {
field: {
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index 568aa0e85de93..d4935f1aabc1c 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -24,7 +24,7 @@ import { ListPluginSetup } from '../../lists/server';
import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../encrypted_saved_objects/server';
import { SpacesPluginSetup as SpacesSetup } from '../../spaces/server';
import { LicensingPluginSetup } from '../../licensing/server';
-import { IngestManagerStartContract } from '../../ingest_manager/server';
+import { IngestManagerStartContract, ExternalCallback } from '../../ingest_manager/server';
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
import { initServer } from './init_server';
import { compose } from './lib/compose/kibana';
@@ -54,14 +54,14 @@ export interface SetupPlugins {
licensing: LicensingPluginSetup;
security?: SecuritySetup;
spaces?: SpacesSetup;
- taskManager: TaskManagerSetupContract;
+ taskManager?: TaskManagerSetupContract;
ml?: MlSetup;
lists?: ListPluginSetup;
}
export interface StartPlugins {
- ingestManager: IngestManagerStartContract;
- taskManager: TaskManagerStartContract;
+ ingestManager?: IngestManagerStartContract;
+ taskManager?: TaskManagerStartContract;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -72,7 +72,7 @@ export interface PluginStart {}
const securitySubPlugins = [
APP_ID,
`${APP_ID}:${SecurityPageName.overview}`,
- `${APP_ID}:${SecurityPageName.alerts}`,
+ `${APP_ID}:${SecurityPageName.detections}`,
`${APP_ID}:${SecurityPageName.hosts}`,
`${APP_ID}:${SecurityPageName.network}`,
`${APP_ID}:${SecurityPageName.timelines}`,
@@ -227,11 +227,15 @@ export class Plugin implements IPlugin {
+ return plugins.taskManager && plugins.lists;
+ };
+
+ if (exceptionListsSetupEnabled()) {
this.lists = plugins.lists;
this.manifestTask = new ManifestTask({
endpointAppContext: endpointContext,
- taskManager: plugins.taskManager,
+ taskManager: plugins.taskManager!,
});
}
@@ -245,32 +249,41 @@ export class Plugin implements IPlugin void) | undefined;
+
+ const exceptionListsStartEnabled = () => {
+ return this.lists && plugins.taskManager && plugins.ingestManager;
+ };
+
+ if (exceptionListsStartEnabled()) {
+ const exceptionListClient = this.lists!.getExceptionListClient(savedObjectsClient, 'kibana');
const artifactClient = new ArtifactClient(savedObjectsClient);
+
+ registerIngestCallback = plugins.ingestManager!.registerExternalCallback;
manifestManager = new ManifestManager({
savedObjectsClient,
artifactClient,
exceptionListClient,
- packageConfigService: plugins.ingestManager.packageConfigService,
+ packageConfigService: plugins.ingestManager!.packageConfigService,
logger: this.logger,
cache: this.exceptionsCache,
});
}
this.endpointAppContextService.start({
- agentService: plugins.ingestManager.agentService,
+ agentService: plugins.ingestManager?.agentService,
+ logger: this.logger,
manifestManager,
- registerIngestCallback: plugins.ingestManager.registerExternalCallback,
+ registerIngestCallback,
savedObjectsStart: core.savedObjects,
});
- if (this.manifestTask) {
+ if (exceptionListsStartEnabled() && this.manifestTask) {
this.manifestTask.start({
- taskManager: plugins.taskManager,
+ taskManager: plugins.taskManager!,
});
} else {
- this.logger.debug('Manifest task not available.');
+ this.logger.debug('User artifacts task not available.');
}
return {};
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 72d21400540fd..dee92c4fbad58 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -87,6 +87,7 @@
"advancedSettings.categoryNames.notificationsLabel": "通知",
"advancedSettings.categoryNames.reportingLabel": "レポート",
"advancedSettings.categoryNames.searchLabel": "検索",
+ "advancedSettings.categoryNames.securitySolutionLabel": "Security Solution",
"advancedSettings.categoryNames.timelionLabel": "Timelion",
"advancedSettings.categoryNames.visualizationsLabel": "可視化",
"advancedSettings.categorySearchLabel": "カテゴリー",
@@ -123,119 +124,6 @@
"advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません",
"advancedSettings.searchBarAriaLabel": "高度な設定を検索",
"advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other {# オプション}}があります。",
- "apmOss.tutorial.apmAgents.statusCheck.btnLabel": "エージェントステータスを確認",
- "apmOss.tutorial.apmAgents.statusCheck.errorMessage": "エージェントからまだデータを受け取っていません",
- "apmOss.tutorial.apmAgents.statusCheck.successMessage": "1 つまたは複数のエージェントからデータを受け取りました",
- "apmOss.tutorial.apmAgents.statusCheck.text": "アプリケーションが実行されていてエージェントがデータを送信していることを確認してください。",
- "apmOss.tutorial.apmAgents.statusCheck.title": "エージェントステータス",
- "apmOss.tutorial.apmAgents.title": "APM エージェント",
- "apmOss.tutorial.apmServer.callOut.message": "ご使用の APM Server を 7.0 以上に更新してあることを確認してください。 Kibana の管理セクションにある移行アシスタントで 6.x データを移行することもできます。",
- "apmOss.tutorial.apmServer.callOut.title": "重要:7.0 以上に更新中",
- "apmOss.tutorial.apmServer.statusCheck.btnLabel": "APM Server ステータスを確認",
- "apmOss.tutorial.apmServer.statusCheck.errorMessage": "APM Server が検出されました。7.0 以上に更新され、動作中であることを確認してください。",
- "apmOss.tutorial.apmServer.statusCheck.successMessage": "APM Server が正しくセットアップされました",
- "apmOss.tutorial.apmServer.statusCheck.text": "APM エージェントの導入を開始する前に、APM Server が動作していることを確認してください。",
- "apmOss.tutorial.apmServer.statusCheck.title": "APM Server ステータス",
- "apmOss.tutorial.apmServer.title": "APM Server",
- "apmOss.tutorial.djangoClient.configure.commands.addAgentComment": "インストールされたアプリにエージェントを追加します",
- "apmOss.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment": "パフォーマンスメトリックを送信するには、追跡ミドルウェアを追加します。",
- "apmOss.tutorial.djangoClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース",
- "apmOss.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})",
- "apmOss.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment": "必要なサーバー名を設定します。使用できる文字:",
- "apmOss.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server にトークンが必要な場合に使います",
- "apmOss.tutorial.djangoClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。",
- "apmOss.tutorial.djangoClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。",
- "apmOss.tutorial.djangoClient.configure.title": "エージェントの構成",
- "apmOss.tutorial.djangoClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。",
- "apmOss.tutorial.djangoClient.install.title": "APM エージェントのインストール",
- "apmOss.tutorial.dotNetClient.configureAgent.textPost": "エージェントに「IConfiguration」インスタンスが渡されていない場合、(例: 非 ASP.NET Core アプリケーションの場合)、エージェントを環境変数で構成することもできます。\n 高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。",
- "apmOss.tutorial.dotNetClient.configureAgent.title": "appsettings.json ファイルの例:",
- "apmOss.tutorial.dotNetClient.configureApplication.textPost": "「IConfiguration」インスタンスを渡すのは任意であり、これにより、エージェントはこの「IConfiguration」インスタンス (例: 「appsettings.json」ファイル) から構成を読み込みます。",
- "apmOss.tutorial.dotNetClient.configureApplication.textPre": "「Elastic.Apm.NetCoreAll」パッケージの ASP.NET Core の場合、「Startup.cs」ファイル内の「Configure」メソドの「UseElasticApm」メソドを呼び出します。",
- "apmOss.tutorial.dotNetClient.configureApplication.title": "エージェントをアプリケーションに追加",
- "apmOss.tutorial.dotNetClient.download.textPre": "[NuGet]({allNuGetPackagesLink}) から .NET アプリケーションにエージェントパッケージを追加してください。用途の異なる複数の NuGet パッケージがあります。\n\nEntity Framework Core の ASP.NET Core アプリケーションの場合は、[Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}) パッケージをダウンロードしてください。このパッケージは、自動的にすべてのエージェントコンポーネントをアプリケーションに追加します。\n\n 依存性を最低限に抑えたい場合、ASP.NET Core の監視のみに [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) パッケージ、または Entity Framework Core の監視のみに [Elastic.Apm.EfCore]({efCorePackageLink}) パッケージを使用することができます。\n\n 手動インストルメンテーションのみにパブリック Agent API を使用する場合は、[Elastic.Apm]({elasticApmPackageLink}) パッケージを使用してください。",
- "apmOss.tutorial.dotNetClient.download.title": "APM エージェントのダウンロード",
- "apmOss.tutorial.downloadServer.title": "APM Server をダウンロードして展開します",
- "apmOss.tutorial.downloadServerRpm": "32 ビットパッケージをお探しですか?[ダウンロードページ]({downloadPageLink}) をご覧ください。",
- "apmOss.tutorial.downloadServerTitle": "32 ビットパッケージをお探しですか?[ダウンロードページ]({downloadPageLink}) をご覧ください。",
- "apmOss.tutorial.editConfig.textPre": "Elastic Stack の X-Pack セキュアバージョンをご使用の場合、「apm-server.yml」構成ファイルで認証情報を指定する必要があります。",
- "apmOss.tutorial.editConfig.title": "構成を編集する",
- "apmOss.tutorial.flaskClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース",
- "apmOss.tutorial.flaskClient.configure.commands.configureElasticApmComment": "またはアプリケーションの設定で ELASTIC_APM を使用するよう構成します。",
- "apmOss.tutorial.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します",
- "apmOss.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})",
- "apmOss.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment": "必要なサーバー名を設定します。使用できる文字:",
- "apmOss.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server にトークンが必要な場合に使います",
- "apmOss.tutorial.flaskClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。",
- "apmOss.tutorial.flaskClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。",
- "apmOss.tutorial.flaskClient.configure.title": "エージェントの構成",
- "apmOss.tutorial.flaskClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。",
- "apmOss.tutorial.flaskClient.install.title": "APM エージェントのインストール",
- "apmOss.tutorial.goClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します:",
- "apmOss.tutorial.goClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})",
- "apmOss.tutorial.goClient.configure.commands.setServiceNameComment": "サービス名を設定します。使用できる文字は # a-z、A-Z、0-9、-、_、スペースです。",
- "apmOss.tutorial.goClient.configure.commands.usedExecutableNameComment": "ELASTIC_APM_SERVICE_NAME が指定されていない場合、実行可能な名前が使用されます。",
- "apmOss.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment": "APM Server にトークンが必要な場合に使います",
- "apmOss.tutorial.goClient.configure.textPost": "高度な構成に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。",
- "apmOss.tutorial.goClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは実行ファイル名または「ELASTIC_APM_SERVICE_NAME」環境変数に基づいてプログラムで作成されます。",
- "apmOss.tutorial.goClient.configure.title": "エージェントの構成",
- "apmOss.tutorial.goClient.install.textPre": "Go の APM エージェントパッケージをインストールします。",
- "apmOss.tutorial.goClient.install.title": "APM エージェントのインストール",
- "apmOss.tutorial.goClient.instrument.textPost": "Go のソースコードのインストルメンテーションの詳細ガイドは、[ドキュメンテーション]({documentationLink}) をご覧ください。",
- "apmOss.tutorial.goClient.instrument.textPre": "提供されたインストルメンテーションモジュールの 1 つ、またはトレーサー API を直接使用して、Go アプリケーションにインストルメンテーションを設定します。",
- "apmOss.tutorial.goClient.instrument.title": "アプリケーションのインストルメンテーション",
- "apmOss.tutorial.introduction": "アプリケーション内から詳細なパフォーマンスメトリックやエラーを収集します。",
- "apmOss.tutorial.javaClient.download.textPre": "[Maven Central]({mavenCentralLink}) からエージェントをダウンロードします。アプリケーションにエージェントを依存関係として「追加しない」でください。",
- "apmOss.tutorial.javaClient.download.title": "APM エージェントのダウンロード",
- "apmOss.tutorial.javaClient.startApplication.textPost": "構成オプションと高度な用途に関しては、[ドキュメンテーション]({documentationLink}) をご覧ください。",
- "apmOss.tutorial.javaClient.startApplication.textPre": "「-javaagent」フラグを追加してエージェントをシステムプロパティで構成します。\n\n * 必要なサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです)\n * カスタム APM Server URL (デフォルト: {customApmServerUrl})\n * アプリケーションのベースパッケージを設定します",
- "apmOss.tutorial.javaClient.startApplication.title": "javaagent フラグでアプリケーションを起動",
- "apmOss.tutorial.jsClient.enableRealUserMonitoring.textPre": "デフォルトでは、APM Server を実行すると RUM サポートは無効になります。RUM サポートを有効にする手順については、[ドキュメンテーション]({documentationLink}) をご覧ください。",
- "apmOss.tutorial.jsClient.enableRealUserMonitoring.title": "APMサーバーのリアルユーザー監視サポートを有効にする",
- "apmOss.tutorial.jsClient.installDependency.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})",
- "apmOss.tutorial.jsClient.installDependency.commands.setRequiredServiceNameComment": "必要なサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです)",
- "apmOss.tutorial.jsClient.installDependency.commands.setServiceVersionComment": "サービスバージョンを設定します (ソースマップ機能に必要)",
- "apmOss.tutorial.jsClient.installDependency.textPost": "React や Angular などのフレームワーク統合には、カスタム依存関係があります。詳細は [統合ドキュメント]({docLink}) をご覧ください。",
- "apmOss.tutorial.jsClient.installDependency.textPre": "「npm install @elastic/apm-rum --save」でエージェントをアプリケーションへの依存関係としてインストールできます。\n\nその後で以下のようにアプリケーションでエージェントを初期化して構成できます。",
- "apmOss.tutorial.jsClient.installDependency.title": "エージェントを依存関係としてセットアップ",
- "apmOss.tutorial.jsClient.scriptTags.textPre": "または、スクリプトタグを使用してエージェントのセットアップと構成ができます。` を追加